diff --git a/Cargo.lock b/Cargo.lock index ccbfb24..992637e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,9 +99,12 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "assert-json-diff" @@ -130,9 +133,9 @@ dependencies = [ [[package]] name = "assertables" -version = "9.8.2" +version = "9.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59051ec02907378a67b0ba1b8631121f5388c8dbbb3cec8c749d8f93c2c3c211" +checksum = "cbada39b42413d4db3d9460f6e791702490c40f72924378a1b6fc1a4181188fd" [[package]] name = "async-trait" @@ -142,7 +145,16 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", ] [[package]] @@ -169,7 +181,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -187,27 +199,57 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -230,9 +272,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "bytes" @@ -246,12 +294,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "castaway" version = "0.2.4" @@ -263,9 +305,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -294,14 +336,14 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -309,9 +351,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -322,9 +364,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.61" +version = "4.5.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" +checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" dependencies = [ "clap", ] @@ -338,7 +380,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -364,9 +406,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", "cfg-if", @@ -397,10 +439,19 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width", "windows-sys 0.59.0", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -417,13 +468,22 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -433,6 +493,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.3", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -442,6 +520,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "ctrlc" version = "3.5.1" @@ -449,7 +547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ "dispatch2", - "nix", + "nix 0.30.1", "windows-sys 0.61.2", ] @@ -459,8 +557,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -474,7 +582,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", ] [[package]] @@ -483,11 +604,28 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", "quote", - "syn 2.0.111", + "syn 2.0.114", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "deranged" version = "0.5.5" @@ -510,23 +648,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn 2.0.114", "unicode-xid", ] @@ -536,10 +675,10 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -566,6 +705,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories" version = "5.0.1" @@ -614,7 +763,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags", + "bitflags 2.10.0", "block2", "libc", "objc2", @@ -628,7 +777,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", ] [[package]] @@ -664,18 +822,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583f1f514d2754010ff71ed6853068cacbe43cc142cc076aa1b871d9754efc48" dependencies = [ - "darling", + "darling 0.20.11", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "enum_display_style_derive" version = "0.6.1" dependencies = [ - "darling", + "darling 0.20.11", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -694,6 +852,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -701,10 +878,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "find-msvc-tools" -version = "0.1.5" +name = "filedescriptor" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" @@ -714,9 +914,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foreign-types" @@ -748,21 +948,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -770,7 +955,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -790,12 +974,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - [[package]] name = "futures-macro" version = "0.3.31" @@ -804,7 +982,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -831,18 +1009,24 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -880,9 +1064,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -899,27 +1083,27 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -967,16 +1151,16 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human-panic" -version = "2.0.4" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a07a0957cd4a3cad4a1e4ca7cd5ea07fcacef6ebe2e5d0c7935bfc95120d8" +checksum = "075e8747af11abcff07d55d98297c9c6c70eb5d6365b25e7b12f02e484935191" dependencies = [ "anstream", "anstyle", "backtrace", - "os_info", "serde", "serde_derive", + "sysinfo", "toml", "uuid", ] @@ -1080,7 +1264,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1202,12 +1386,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown", ] [[package]] @@ -1219,7 +1403,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width", "web-time", ] @@ -1234,26 +1418,27 @@ dependencies = [ [[package]] name = "insta" -version = "1.44.3" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" +checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5" dependencies = [ "console", "once_cell", "similar", + "tempfile", ] [[package]] name = "instability" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1264,9 +1449,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1298,9 +1483,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -1313,21 +1498,53 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.178" +name = "kasuari" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown", + "portable-atomic", + "thiserror 2.0.17", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.179" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1346,6 +1563,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1387,7 +1610,7 @@ dependencies = [ "log-mdc", "mock_instant", "parking_lot", - "rand", + "rand 0.9.2", "serde", "serde-value", "serde_json", @@ -1401,11 +1624,21 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.5" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.5", + "hashbrown", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", ] [[package]] @@ -1424,7 +1657,7 @@ dependencies = [ "clap_complete", "colored", "confy", - "crossterm", + "crossterm 0.28.1", "ctrlc", "derivative", "derive_setters", @@ -1452,8 +1685,8 @@ dependencies = [ "serde_json", "serde_yaml", "serial_test", - "strum", - "strum_macros", + "strum 0.26.3", + "strum_macros 0.26.4", "tokio", "tokio-util", "urlencoding", @@ -1463,12 +1696,12 @@ dependencies = [ [[package]] name = "managarr-tree-widget" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae8e5f28f9581dcddb67e4741a96231752dafb997224cae6d42c75db29eb5af" +checksum = "b1d56114c2ca4bb81c0e68f1d1c71ad3d4dd839f851ce1873d6256817d1d7d1b" dependencies = [ "ratatui", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -1477,12 +1710,33 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1533,7 +1787,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1553,7 +1807,7 @@ dependencies = [ "hyper-util", "log", "pin-project-lite", - "rand", + "rand 0.9.2", "regex", "serde_json", "serde_urlencoded", @@ -1578,24 +1832,67 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +dependencies = [ + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1629,81 +1926,13 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", + "bitflags 2.10.0", ] [[package]] @@ -1713,72 +1942,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] -name = "objc2-foundation" +name = "objc2-io-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ - "bitflags", - "block2", "libc", - "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "object" version = "0.37.3" @@ -1806,7 +1978,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1823,7 +1995,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1870,19 +2042,12 @@ dependencies = [ ] [[package]] -name = "os_info" -version = "3.14.0" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "android_system_properties", - "log", - "nix", - "objc2", - "objc2-foundation", - "objc2-ui-kit", - "serde", - "windows-sys 0.61.2", + "num-traits", ] [[package]] @@ -1905,21 +2070,110 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1940,9 +2194,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "potential_utf" @@ -2016,9 +2270,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -2029,11 +2283,11 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ - "bit-set", - "bit-vec", - "bitflags", + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.10.0", "num-traits", - "rand", + "rand 0.9.2", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -2050,9 +2304,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -2063,6 +2317,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" @@ -2070,7 +2333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -2080,9 +2343,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rand_core" version = "0.9.3" @@ -2098,29 +2367,92 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core", + "rand_core 0.9.3", ] [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" dependencies = [ - "bitflags", - "cassowary", - "compact_str", - "crossterm", - "indoc", "instability", - "itertools 0.13.0", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.10.0", + "compact_str", + "hashbrown", + "indoc", + "itertools 0.14.0", + "kasuari", "lru", - "paste", - "strum", - "time", + "strum 0.27.2", + "thiserror 2.0.17", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.10.0", + "hashbrown", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "strum 0.27.2", + "time", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -2129,7 +2461,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -2180,9 +2512,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.25" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2258,7 +2590,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.111", + "syn 2.0.114", "unicode-ident", ] @@ -2283,7 +2615,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2292,11 +2624,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -2305,9 +2637,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "rustls-pki-types", @@ -2318,9 +2650,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "zeroize", ] @@ -2356,9 +2688,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "scc" @@ -2396,7 +2728,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -2435,7 +2767,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "ordered-float", + "ordered-float 2.10.1", "serde", ] @@ -2456,27 +2788,27 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -2508,11 +2840,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -2522,13 +2855,24 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -2560,10 +2904,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2573,6 +2918,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -2619,7 +2970,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -2632,7 +2992,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -2654,9 +3026,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2680,7 +3052,21 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", ] [[package]] @@ -2689,7 +3075,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] @@ -2706,14 +3092,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -2723,16 +3109,79 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.60.2", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.10.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float 4.6.0", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2759,7 +3208,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2770,7 +3219,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2816,9 +3265,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -2839,7 +3288,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2864,9 +3313,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2877,9 +3326,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "serde_core", "serde_spanned", @@ -2889,18 +3338,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", @@ -2910,18 +3359,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" @@ -2944,7 +3393,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2970,9 +3419,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2980,9 +3429,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -3002,6 +3451,18 @@ dependencies = [ "unsafe-any-ors", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unarray" version = "0.1.4" @@ -3022,21 +3483,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.0" @@ -3072,9 +3527,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3106,7 +3561,10 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ + "atomic", "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -3116,7 +3574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40630259c022600210096da9538abcb992b801e30b464cb9d19f19ef0e0d09b9" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3125,7 +3583,7 @@ version = "0.6.1" dependencies = [ "log", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3152,7 +3610,22 @@ checksum = "5b2d5567b6fbd34e8f0488d56b648e67c0d999535f4af2060d14f9074b43e833" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", ] [[package]] @@ -3233,7 +3706,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -3266,6 +3739,78 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float 4.6.0", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3288,6 +3833,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3296,9 +3876,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -3309,7 +3900,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3320,24 +3911,49 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3346,7 +3962,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3355,7 +3980,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3400,7 +4025,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3440,7 +4065,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -3451,6 +4076,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3635,28 +4269,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3676,7 +4310,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] @@ -3716,5 +4350,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/Cargo.toml b/Cargo.toml index 8f4aea4..f83eb0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ strum = { version = "0.26.3", features = ["derive"] } strum_macros = "0.26.4" tokio = { version = "1.44.2", features = ["full"] } tokio-util = "0.7.8" -ratatui = { version = "0.29.0", features = [ +ratatui = { version = "0.30.0", features = [ "all-widgets", "unstable-widget-ref", ] } @@ -59,7 +59,7 @@ ctrlc = "3.4.5" colored = "3.0.0" async-trait = "0.1.83" dirs-next = "2.0.0" -managarr-tree-widget = "0.24.0" +managarr-tree-widget = "0.25.0" indicatif = "0.17.9" derive_setters = "0.1.6" deunicode = "1.6.0" diff --git a/README.md b/README.md index 6716b09..61b1c65 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,16 @@ ![Docker pulls](https://img.shields.io/docker/pulls/darkalex17/managarr?label=Docker%20downloads) [![Matrix](https://img.shields.io/matrix/managarr-room%3Amatrix.org?logo=matrix&server_fqdn=matrix.org&fetchMode=guest&style=social&label=Managarr%20Matrix%20Space&link=https%3A%2F%2Fmatrix.to%2F%23%2F%23managarr%3Amatrix.org)](https://matrix.to/#/#managarr:matrix.org) - Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust! -![library](screenshots/sonarr/sonarr_library.png) +![library](screenshots/lidarr/lidarr_library.png) ## What Servarrs are supported? - [x] ![radarr_logo](logos/radarr.png) [Radarr](https://wiki.servarr.com/radarr) - [x] ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr) +- [x] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr) - [ ] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr) -- [ ] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr) - [ ] ![prowlarr_logo](logos/prowlarr.png) [Prowlarr](https://wiki.servarr.com/en/prowlarr) - [ ] ![whisparr_logo](logos/whisparr.png) [Whisparr](https://wiki.servarr.com/whisparr) - [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/) @@ -96,7 +95,7 @@ of Chocolatey packages take quite some time, and thus the package may not be ava choco install managarr # Some newer releases may require a version number, so you can specify it like so: -choco install managarr --version=0.5.0 +choco install managarr --version=0.7.0 ``` To upgrade to the latest and greatest version of Managarr: @@ -104,7 +103,7 @@ To upgrade to the latest and greatest version of Managarr: choco upgrade managarr # To upgrade to a specific version: -choco upgrade managarr --version=0.5.0 +choco upgrade managarr --version=0.7.0 ``` ### Manual @@ -182,14 +181,30 @@ Key: | ✅ | ✅ | View and browse logs, tasks, events queues, and updates | | ✅ | ✅ | Manually trigger scheduled tasks | +### Lidarr + +| TUI | CLI | Feature | +|-----|-----|----------------------------------------------------------------------------------------------------------------| +| ✅ | ✅ | View your library, downloads, blocklist, tracks | +| ✅ | ✅ | View details of a specific artists, albums, or tracks including description, history, downloaded file info | +| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | +| ✅ | ✅ | Search your library | +| ✅ | ✅ | Add artists to your library | +| ✅ | ✅ | Delete artists, downloads, indexers, root folders, and track files | +| ✅ | ✅ | Trigger automatic searches for artists or albums | +| ✅ | ✅ | Trigger refresh and disk scan for artists and downloads | +| ✅ | ✅ | Manually search for full artist discographies or albums | +| ✅ | ✅ | Edit your artists and indexers | +| ✅ | ✅ | Manage your tags | +| ✅ | ✅ | Manage your root folders | +| ✅ | ✅ | Manage your blocklist | +| ✅ | ✅ | View and browse logs, tasks, events queues, and updates | +| ✅ | ✅ | Manually trigger scheduled tasks | + ### Readarr - [ ] Support for Readarr -### Lidarr - -- [ ] Support for Lidarr - ### Whisparr - [ ] Support for Whisparr @@ -231,7 +246,7 @@ To see all available commands, simply run `managarr --help`: ```shell $ managarr --help -managarr 0.5.1 +managarr 0.7.0 Alex Clarke A TUI and CLI to manage your Servarrs @@ -241,20 +256,24 @@ Usage: managarr [OPTIONS] [COMMAND] Commands: radarr Commands for manging your Radarr instance sonarr Commands for manging your Sonarr instance + lidarr Commands for manging your Lidarr instance completions Generate shell completions for the Managarr CLI tail-logs Tail Managarr logs help Print this message or the help of the given subcommand(s) Options: + -h, --help Print help + -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. - 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. - -h, --help Print help - -V, --version Print version + + 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: @@ -283,12 +302,21 @@ Commands: test-all-indexers Test all Sonarr indexers toggle-episode-monitoring Toggle monitoring for the specified episode toggle-season-monitoring Toggle monitoring for the specified season that corresponds to the specified series ID + toggle-series-monitoring Toggle monitoring for the specified series corresponding to the given series ID help Print this message or the help of the given subcommand(s) Options: - --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] - --config The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] - -h, --help Print help + -h, --help Print help + +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. + + 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. ``` **Pro Tip:** The CLI is even more powerful and useful when used in conjunction with the `jq` CLI tool. This allows you to parse the JSON response from the Managarr CLI and use it in your scripts; For example, to extract the `movieId` of the movie "Ad Astra", you would run: @@ -428,9 +456,6 @@ Managarr supports using environment variables on startup so you don't have to al | `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` | | `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` | -## Track What I'm Currently Working On -To see what feature(s) I'm currently working on, check out my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr). - ## Screenshots ### Radarr @@ -446,6 +471,13 @@ To see what feature(s) I'm currently working on, check out my [Wekan Board](http ![season_details](screenshots/sonarr/season_details.png) ![manual_episode_search](screenshots/sonarr/manual_episode_search.png) +### Lidarr +![lidarr_library](screenshots/lidarr/lidarr_library.png) +![artist_details](screenshots/lidarr/artist_details.png) +![album_details](screenshots/lidarr/album_details.png) +![artist_discography_search](screenshots/lidarr/artist_discography_search.png) +![manual_album_search](screenshots/lidarr/manual_album_search.png) + ### General ![logs](screenshots/radarr/logs.png) ![indexers](screenshots/radarr/indexers.png) @@ -461,8 +493,8 @@ To see what feature(s) I'm currently working on, check out my [Wekan Board](http ## Servarr Requirements * [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/) * [Sonarr >= v4](https://sonarr.tv/docs/api/) -* [Readarr v1](https://readarr.com/docs/api/) * [Lidarr v1](https://lidarr.audio/docs/api/) +* [Readarr v1](https://readarr.com/docs/api/) * [Whisparr >= v3](https://whisparr.com/docs/api/) * [Prowlarr v1](https://prowlarr.com/docs/api/) * [Bazarr v1.1.4](http://localhost:6767/api) diff --git a/justfile b/justfile index c64cf55..1705435 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,5 @@ VERSION := "latest" IMG_NAME := "darkalex17/managarr" -IMAGE := "{{IMG_NAME}}:{{VERSION}}" - # List all recipes default: @@ -88,4 +86,4 @@ build build_type='debug': # Build the docker image [group: 'build'] build-docker: - @DOCKER_BUILDKIT=1 docker build --rm -t {{IMAGE}} + @DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{VERSION}} . diff --git a/screenshots/lidarr/album_details.png b/screenshots/lidarr/album_details.png new file mode 100644 index 0000000..57821fb Binary files /dev/null and b/screenshots/lidarr/album_details.png differ diff --git a/screenshots/lidarr/artist_details.png b/screenshots/lidarr/artist_details.png new file mode 100644 index 0000000..aee1a42 Binary files /dev/null and b/screenshots/lidarr/artist_details.png differ diff --git a/screenshots/lidarr/artist_discography_search.png b/screenshots/lidarr/artist_discography_search.png new file mode 100644 index 0000000..2a95911 Binary files /dev/null and b/screenshots/lidarr/artist_discography_search.png differ diff --git a/screenshots/lidarr/lidarr_library.png b/screenshots/lidarr/lidarr_library.png new file mode 100644 index 0000000..e1f7c60 Binary files /dev/null and b/screenshots/lidarr/lidarr_library.png differ diff --git a/screenshots/lidarr/manual_album_search.png b/screenshots/lidarr/manual_album_search.png new file mode 100644 index 0000000..e84009d Binary files /dev/null and b/screenshots/lidarr/manual_album_search.png differ diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index 5b85e37..c47bcf5 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -8,6 +8,7 @@ mod tests { use tokio::sync::mpsc; use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars}; + use crate::models::servarr_data::lidarr::lidarr_data::LidarrData; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::{HorizontallyScrollableText, TabRoute}; @@ -35,6 +36,7 @@ mod tests { theme: None, radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]), sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]), + lidarr: None, }; let expected_tab_routes = vec![ TabRoute { @@ -78,7 +80,7 @@ mod tests { assert_eq!(app.server_tabs.index, 0); assert_eq!(app.server_tabs.tabs, expected_tab_routes); assert_eq!(app.tick_until_poll, 400); - assert_eq!(app.ticks_until_scroll, 4); + assert_eq!(app.ticks_until_scroll, 64); assert_eq!(app.tick_count, 0); assert_eq!(app.ui_scroll_tick_count, 0); assert!(!app.is_loading); @@ -99,7 +101,7 @@ mod tests { assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.server_tabs.index, 0); assert_eq!(app.tick_until_poll, 400); - assert_eq!(app.ticks_until_scroll, 4); + assert_eq!(app.ticks_until_scroll, 64); assert_eq!(app.tick_count, 0); assert!(!app.is_loading); assert!(!app.is_routing); @@ -184,6 +186,7 @@ mod tests { ..SonarrData::default() }; let data = Data { + lidarr_data: LidarrData::default(), radarr_data, sonarr_data, }; diff --git a/src/app/context_clues.rs b/src/app/context_clues.rs index f58f901..d7ec1b2 100644 --- a/src/app/context_clues.rs +++ b/src/app/context_clues.rs @@ -1,5 +1,6 @@ use crate::app::App; use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding}; +use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider; use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider; use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider; use crate::models::Route; @@ -21,6 +22,7 @@ impl ContextClueProvider for ServarrContextClueProvider { match app.get_current_route() { Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app), Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app), + Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app), _ => None, } } @@ -100,6 +102,18 @@ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [ ), ]; +pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.esc, "cancel filter"), +]; + pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [ (DEFAULT_KEYBINDINGS.tasks, "open tasks"), (DEFAULT_KEYBINDINGS.events, "open events"), @@ -110,3 +124,8 @@ pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [ DEFAULT_KEYBINDINGS.refresh.desc, ), ]; + +pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "start task"), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; diff --git a/src/app/context_clues_tests.rs b/src/app/context_clues_tests.rs index ff435ad..a10881e 100644 --- a/src/app/context_clues_tests.rs +++ b/src/app/context_clues_tests.rs @@ -2,9 +2,9 @@ mod test { use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, - ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, - ServarrContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, ServarrContextClueProvider, }; use crate::app::{App, key_binding::DEFAULT_KEYBINDINGS}; use crate::models::servarr_data::ActiveKeybindingBlock; @@ -204,6 +204,40 @@ mod test { assert_none!(indexers_context_clues_iter.next()); } + #[test] + fn test_history_context_clues() { + let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter") + ); + assert_none!(history_context_clues_iter.next()); + } + #[test] fn test_system_context_clues() { let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter(); @@ -234,6 +268,21 @@ mod test { assert_none!(system_context_clues_iter.next()); } + #[test] + fn test_system_tasks_context_clues() { + let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + system_tasks_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "start task") + ); + assert_some_eq_x!( + system_tasks_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(system_tasks_context_clues_iter.next()); + } + #[test] fn test_servarr_context_clue_provider_delegates_to_radarr_provider() { let mut app = App::test_default(); @@ -241,10 +290,7 @@ mod test { let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); - assert_some_eq_x!( - context_clues, - &crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES, - ); + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,); } #[test] @@ -254,10 +300,7 @@ mod test { let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); - assert_some_eq_x!( - context_clues, - &crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES, - ); + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES,); } #[test] diff --git a/src/app/lidarr/lidarr_context_clues.rs b/src/app/lidarr/lidarr_context_clues.rs new file mode 100644 index 0000000..2b734f4 --- /dev/null +++ b/src/app/lidarr/lidarr_context_clues.rs @@ -0,0 +1,225 @@ +use crate::app::App; +use crate::app::context_clues::{ + BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, +}; +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, + ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, + TRACK_DETAILS_BLOCKS, +}; + +#[cfg(test)] +#[path = "lidarr_context_clues_tests.rs"] +mod lidarr_context_clues_tests; + +pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 10] = [ + (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.update, "update all"), + (DEFAULT_KEYBINDINGS.esc, "cancel filter"), +]; + +pub static ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "edit search"), +]; + +pub static ARTIST_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, "edit artist"), + (DEFAULT_KEYBINDINGS.delete, "delete album"), + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + "toggle album monitoring", + ), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static ARTIST_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, "edit artist"), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub static MANUAL_ARTIST_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, "edit artist"), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static ALBUM_DETAILS_CONTEXT_CLUES: [ContextClue; 6] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), + (DEFAULT_KEYBINDINGS.submit, "track details"), + (DEFAULT_KEYBINDINGS.delete, "delete track"), +]; + +pub static ALBUM_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub static MANUAL_ALBUM_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static TRACK_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static TRACK_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub(in crate::app) struct LidarrContextClueProvider; + +impl ContextClueProvider for LidarrContextClueProvider { + fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> { + let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() else { + panic!("LidarrContextClueProvider::get_context_clues called with non-Lidarr route"); + }; + + match active_lidarr_block { + _ if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) => app + .data + .lidarr_data + .artist_info_tabs + .get_active_route_contextual_help(), + _ if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) => app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal is empty") + .album_details_tabs + .get_active_route_contextual_help(), + _ if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) => app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal is empty") + .track_details_modal + .as_ref() + .expect("track_details_modal is empty") + .track_details_tabs + .get_active_route_contextual_help(), + ActiveLidarrBlock::AddArtistSearchInput + | ActiveLidarrBlock::AddArtistEmptySearchResults + | ActiveLidarrBlock::TestAllIndexers + | ActiveLidarrBlock::SystemLogs + | ActiveLidarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES), + _ if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) + || EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) + || INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) + || ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) => + { + Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES) + } + ActiveLidarrBlock::AddArtistPrompt + | ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder + | ActiveLidarrBlock::AddArtistTagsInput + | ActiveLidarrBlock::AddArtistAlreadyInLibrary => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES), + _ if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) => { + Some(&ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES) + } + ActiveLidarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES), + _ => app + .data + .lidarr_data + .main_tabs + .get_active_route_contextual_help(), + } + } +} diff --git a/src/app/lidarr/lidarr_context_clues_tests.rs b/src/app/lidarr/lidarr_context_clues_tests.rs new file mode 100644 index 0000000..92ef2e6 --- /dev/null +++ b/src/app/lidarr/lidarr_context_clues_tests.rs @@ -0,0 +1,589 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::context_clues::{ + BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, + }; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::lidarr::lidarr_context_clues::{ + ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES, ALBUM_DETAILS_CONTEXT_CLUES, + ALBUM_HISTORY_CONTEXT_CLUES, ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, + ARTISTS_CONTEXT_CLUES, LidarrContextClueProvider, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES, + MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES, + }; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_INDEXER_BLOCKS, + INDEXER_SETTINGS_BLOCKS, LidarrData, + }; + use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal}; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use rstest::rstest; + + #[test] + fn test_artists_context_clues() { + let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc + ) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, "update all") + ); + assert_some_eq_x!( + artists_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter") + ); + assert_none!(artists_context_clues_iter.next()); + } + + #[test] + fn test_artist_details_context_clues() { + let mut artist_details_context_clues_iter = ARTIST_DETAILS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ) + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, "edit artist") + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.delete, "delete album") + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.toggle_monitoring, + "toggle album monitoring", + ) + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc) + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ) + ); + assert_some_eq_x!( + artist_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(artist_details_context_clues_iter.next()); + } + + #[test] + fn test_add_artist_search_results_context_clues() { + let mut add_artist_search_results_context_clues_iter = + ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + add_artist_search_results_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + add_artist_search_results_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "edit search") + ); + assert_none!(add_artist_search_results_context_clues_iter.next()); + } + + #[test] + #[should_panic( + expected = "LidarrContextClueProvider::get_context_clues called with non-Lidarr route" + )] + fn test_lidarr_context_clue_provider_get_context_clues_non_lidarr_route() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + + LidarrContextClueProvider::get_context_clues(&mut app); + } + + #[test] + fn test_artist_history_context_clues() { + let mut artist_history_context_clues_iter = ARTIST_HISTORY_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, "edit artist") + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc) + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + artist_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter/close") + ); + assert_none!(artist_history_context_clues_iter.next()); + } + + #[test] + fn test_manual_artist_search_context_clues() { + let mut manual_artist_search_context_clues_iter = MANUAL_ARTIST_SEARCH_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.edit, "edit artist") + ); + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc) + ); + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + manual_artist_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(manual_artist_search_context_clues_iter.next()); + } + + #[test] + fn test_album_details_context_clues() { + let mut album_details_context_clues_iter = ALBUM_DETAILS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + album_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + album_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + album_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + album_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_some_eq_x!( + album_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "track details") + ); + assert_some_eq_x!( + album_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.delete, "delete track") + ); + assert_none!(album_details_context_clues_iter.next()); + } + + #[test] + fn test_album_history_context_clues() { + let mut album_history_context_clues_iter = ALBUM_HISTORY_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + album_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter/close") + ); + assert_none!(album_history_context_clues_iter.next()); + } + + #[test] + fn test_manual_album_search_context_clues() { + let mut manual_album_search_context_clues_iter = MANUAL_ALBUM_SEARCH_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + manual_album_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + manual_album_search_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc + ) + ); + assert_some_eq_x!( + manual_album_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + manual_album_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + manual_album_search_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(manual_album_search_context_clues_iter.next()); + } + + #[test] + fn test_track_details_context_clues() { + let mut track_details_context_clues_iter = TRACK_DETAILS_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + track_details_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + track_details_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc) + ); + assert_none!(track_details_context_clues_iter.next()); + } + + #[test] + fn test_track_history_context_clues() { + let mut track_history_context_clues_iter = TRACK_HISTORY_CONTEXT_CLUES.iter(); + + assert_some_eq_x!( + track_history_context_clues_iter.next(), + &( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc + ) + ); + assert_some_eq_x!( + track_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) + ); + assert_some_eq_x!( + track_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) + ); + assert_some_eq_x!( + track_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) + ); + assert_some_eq_x!( + track_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.submit, "details") + ); + assert_some_eq_x!( + track_history_context_clues_iter.next(), + &(DEFAULT_KEYBINDINGS.esc, "cancel filter/close") + ); + assert_none!(track_history_context_clues_iter.next()); + } + + #[rstest] + #[case(0, ActiveLidarrBlock::ArtistDetails, &ARTIST_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveLidarrBlock::ArtistHistory, &ARTIST_HISTORY_CONTEXT_CLUES)] + #[case(2, ActiveLidarrBlock::ManualArtistSearch, &MANUAL_ARTIST_SEARCH_CONTEXT_CLUES)] + fn test_lidarr_context_clue_provider_artist_info_tabs( + #[case] index: usize, + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + app.data.lidarr_data = LidarrData::default(); + app.data.lidarr_data.artist_info_tabs.set_index(index); + app.push_navigation_stack(active_lidarr_block.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, expected_context_clues); + } + + #[rstest] + #[case(0, ActiveLidarrBlock::AlbumDetails, &ALBUM_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveLidarrBlock::AlbumHistory, &ALBUM_HISTORY_CONTEXT_CLUES)] + #[case(2, ActiveLidarrBlock::ManualAlbumSearch, &MANUAL_ALBUM_SEARCH_CONTEXT_CLUES)] + fn test_lidarr_context_clue_provider_album_details_tabs( + #[case] index: usize, + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + let mut album_details_modal = AlbumDetailsModal::default(); + album_details_modal.album_details_tabs.set_index(index); + let lidarr_data = LidarrData { + album_details_modal: Some(album_details_modal), + ..LidarrData::default() + }; + app.data.lidarr_data = lidarr_data; + app.push_navigation_stack(active_lidarr_block.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, expected_context_clues); + } + + #[rstest] + #[case(0, ActiveLidarrBlock::TrackDetails, &TRACK_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveLidarrBlock::TrackHistory, &TRACK_HISTORY_CONTEXT_CLUES)] + fn test_lidarr_context_clue_provider_track_details_tabs( + #[case] index: usize, + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + let mut track_details_modal = TrackDetailsModal::default(); + track_details_modal.track_details_tabs.set_index(index); + let album_details_modal = AlbumDetailsModal { + track_details_modal: Some(track_details_modal), + ..AlbumDetailsModal::default() + }; + let lidarr_data = LidarrData { + album_details_modal: Some(album_details_modal), + ..LidarrData::default() + }; + app.data.lidarr_data = lidarr_data; + app.push_navigation_stack(active_lidarr_block.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, expected_context_clues); + } + + #[test] + fn test_lidarr_context_clue_provider_artists_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES); + } + + #[test] + fn test_lidarr_context_clue_provider_artists_sort_prompt_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistsSortPrompt.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES); + } + + #[test] + fn test_lidarr_context_clue_provider_search_artists_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::SearchArtists.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES); + } + + #[test] + fn test_lidarr_context_clue_provider_filter_artists_block() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::FilterArtists.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES); + } + + #[rstest] + fn test_lidarr_context_clue_provider_bare_popup_context_clues( + #[values( + ActiveLidarrBlock::AddArtistSearchInput, + ActiveLidarrBlock::AddArtistEmptySearchResults, + ActiveLidarrBlock::TestAllIndexers + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_lidarr_block.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &BARE_POPUP_CONTEXT_CLUES); + } + + #[test] + fn test_lidarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks() { + let mut blocks = EDIT_ARTIST_BLOCKS.to_vec(); + blocks.extend(ADD_ROOT_FOLDER_BLOCKS); + blocks.extend(INDEXER_SETTINGS_BLOCKS); + blocks.extend(EDIT_INDEXER_BLOCKS); + + for active_lidarr_block in blocks { + let mut app = App::test_default(); + app.push_navigation_stack(active_lidarr_block.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + } + + #[test] + fn test_lidarr_context_clue_provider_add_artist_search_results_context_clues() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &ADD_ARTIST_SEARCH_RESULTS_CONTEXT_CLUES); + } + + #[rstest] + fn test_lidarr_context_clue_provider_confirmation_prompt_context_clues_add_artist_blocks( + #[values( + ActiveLidarrBlock::AddArtistPrompt, + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistSelectRootFolder, + ActiveLidarrBlock::AddArtistTagsInput, + ActiveLidarrBlock::AddArtistAlreadyInLibrary + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_lidarr_block.into()); + + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + + #[test] + fn test_lidarr_context_clue_provider_system_tasks_clues() { + let mut app = App::test_default(); + + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + let context_clues = LidarrContextClueProvider::get_context_clues(&mut app); + + assert_some_eq_x!(context_clues, &SYSTEM_TASKS_CONTEXT_CLUES); + } +} diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs new file mode 100644 index 0000000..4b53013 --- /dev/null +++ b/src/app/lidarr/lidarr_tests.rs @@ -0,0 +1,783 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::models::lidarr_models::{Album, Artist, LidarrRelease}; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal; + use crate::models::servarr_models::Indexer; + use crate::network::NetworkEvent; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + album, artist, track, + }; + use pretty_assertions::{assert_eq, assert_str_eq}; + use tokio::sync::mpsc; + + #[tokio::test] + async fn test_dispatch_by_lidarr_block_artists() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_lidarr_block_artist_details() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.artists.set_items(vec![artist()]); + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistDetails) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetAlbums(1).into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_blocklist_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.artists.set_items(vec![artist()]); + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::Blocklist) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetBlocklist.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_artist_history_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ArtistHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetArtistHistory(1).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_artist_search_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDiscographyReleases(1).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_artist_search_block_discography_releases_non_empty() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .discography_releases + .set_items(vec![LidarrRelease::default()]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualArtistSearch) + .await; + + assert!(!app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_album_details_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + app.data.lidarr_data.albums.set_items(vec![Album { + id: 1, + ..Album::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumDetails) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetTracks(1, 1).into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetTrackFiles(1).into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_album_history_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + app.data.lidarr_data.albums.set_items(vec![Album { + id: 1, + ..Album::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetAlbumHistory(1, 1).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_album_history_block_no_op_when_albums_table_is_empty() { + let (tx, _) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::AlbumHistory) + .await; + + assert!(!app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_album_search_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + app.data.lidarr_data.albums.set_items(vec![Album { + id: 1, + ..Album::default() + }]); + app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetAlbumReleases(1, 1).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_album_search_block_is_loading() { + let mut app = App { + is_loading: true, + ..App::test_default() + }; + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch) + .await; + + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_album_search_block_album_releases_non_empty() { + let mut app = App::test_default(); + let mut album_details_modal = AlbumDetailsModal::default(); + album_details_modal + .album_releases + .set_items(vec![LidarrRelease::default()]); + app.data.lidarr_data.album_details_modal = Some(album_details_modal); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::ManualAlbumSearch) + .await; + + assert!(!app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_downloads_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::Downloads) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_lidarr_block_add_artist_search_results() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.add_artist_search = Some("test artist".into()); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::AddArtistSearchResults) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::SearchNewArtist("test artist".to_owned()).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_history_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::History) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetHistory(500).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_root_folders_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::RootFolders) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_indexers_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::Indexers) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetIndexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_all_indexer_settings_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::AllIndexerSettingsPrompt) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetAllIndexerSettings.into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_indexer_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::TestIndexer) + .await; + + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::TestIndexer(1).into()); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_all_indexers_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::TestAllIndexers) + .await; + + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::TestAllIndexers.into() + ); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::System) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTasks.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQueuedEvents.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetLogs(500).into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_updates_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::SystemUpdates) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetUpdates.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_track_details_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackDetails) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetTrackDetails(1).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_track_history_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::TrackHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetTrackHistory(1, 1, 1).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_check_for_lidarr_prompt_action_no_prompt_confirm() { + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = false; + + app.check_for_lidarr_prompt_action().await; + + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.should_refresh); + } + + #[tokio::test] + async fn test_check_for_lidarr_prompt_action() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::GetStatus); + + app.check_for_lidarr_prompt_action().await; + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into()); + assert!(app.should_refresh); + assert_eq!(app.data.lidarr_data.prompt_confirm_action, None); + } + + #[tokio::test] + async fn test_lidarr_refresh_metadata() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_routing = true; + + app.refresh_lidarr_metadata().await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into()); + assert!(app.is_loading); + } + + #[tokio::test] + async fn test_lidarr_on_tick_first_render() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_first_render = true; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetDiskSpace.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetStatus.into()); + assert!(app.is_loading); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.is_first_render); + } + + #[tokio::test] + async fn test_lidarr_on_tick_routing() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_routing = true; + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_lidarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { + let (tx, _) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_routing = true; + app.should_refresh = false; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert!(app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_lidarr_on_tick_should_refresh() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(app.should_refresh); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_lidarr_on_tick_should_refresh_does_not_cancel_prompt_requests() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.is_loading = true; + app.is_routing = true; + app.should_refresh = true; + app.is_first_render = false; + app.tick_count = 1; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(app.is_loading); + assert!(app.should_refresh); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_lidarr_on_tick_network_tick_frequency() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + app.tick_count = 2; + app.tick_until_poll = 2; + + app.lidarr_on_tick(ActiveLidarrBlock::Downloads).await; + + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetMetadataProfiles.into() + ); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into()); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(app.is_loading); + } + + #[tokio::test] + async fn test_extract_add_new_artist_search_query() { + let app = App::test_default_fully_populated(); + + let query = app.extract_add_new_artist_search_query().await; + + assert_str_eq!(query, "Test Artist"); + } + + #[tokio::test] + #[should_panic(expected = "Add artist search is empty")] + async fn test_extract_add_new_artist_search_query_panics_when_the_query_is_not_set() { + let app = App::test_default(); + + app.extract_add_new_artist_search_query().await; + } + + #[tokio::test] + async fn test_extract_artist_id() { + let mut app = App::test_default(); + app.data.lidarr_data.artists.set_items(vec![artist()]); + + assert_eq!(app.extract_artist_id().await, 1); + } + + #[tokio::test] + async fn test_extract_album_id() { + let mut app = App::test_default(); + app.data.lidarr_data.albums.set_items(vec![album()]); + + assert_eq!(app.extract_album_id().await, 1); + } + + #[tokio::test] + async fn test_extract_track_id() { + let mut app = App::test_default(); + let mut album_details_modal = AlbumDetailsModal::default(); + album_details_modal.tracks.set_items(vec![track()]); + app.data.lidarr_data.album_details_modal = Some(album_details_modal); + + assert_eq!(app.extract_track_id().await, 1); + } + + #[tokio::test] + #[should_panic(expected = "album_details_modal is empty")] + async fn test_extract_track_id_panics_when_album_details_modal_is_not_set() { + let app = App::test_default(); + + app.extract_track_id().await; + } + + #[tokio::test] + async fn test_extract_lidarr_indexer_id() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + id: 1, + ..Indexer::default() + }]); + + assert_eq!(app.extract_lidarr_indexer_id().await, 1); + } +} diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs new file mode 100644 index 0000000..a58234c --- /dev/null +++ b/src/app/lidarr/mod.rs @@ -0,0 +1,279 @@ +use super::App; +use crate::{ + models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, + network::lidarr_network::LidarrEvent, +}; + +pub mod lidarr_context_clues; + +#[cfg(test)] +#[path = "lidarr_tests.rs"] +mod lidarr_tests; + +impl App<'_> { + pub(super) async fn dispatch_by_lidarr_block(&mut self, active_lidarr_block: &ActiveLidarrBlock) { + match active_lidarr_block { + ActiveLidarrBlock::Artists => { + self + .dispatch_network_event(LidarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetMetadataProfiles.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(LidarrEvent::ListArtists.into()) + .await; + } + ActiveLidarrBlock::Blocklist => { + self + .dispatch_network_event(LidarrEvent::GetBlocklist.into()) + .await; + } + ActiveLidarrBlock::Downloads => { + self + .dispatch_network_event(LidarrEvent::GetDownloads(500).into()) + .await; + } + ActiveLidarrBlock::ArtistDetails => { + self + .dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into()) + .await; + } + ActiveLidarrBlock::ArtistHistory => { + self + .dispatch_network_event( + LidarrEvent::GetArtistHistory(self.extract_artist_id().await).into(), + ) + .await; + } + ActiveLidarrBlock::ManualArtistSearch => { + if self.data.lidarr_data.discography_releases.is_empty() { + self + .dispatch_network_event( + LidarrEvent::GetDiscographyReleases(self.extract_artist_id().await).into(), + ) + .await; + } + } + ActiveLidarrBlock::AlbumDetails => { + let artist_id = self.extract_artist_id().await; + let album_id = self.extract_album_id().await; + self + .dispatch_network_event(LidarrEvent::GetTracks(artist_id, album_id).into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetTrackFiles(album_id).into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetDownloads(500).into()) + .await; + } + ActiveLidarrBlock::AlbumHistory => { + if !self.data.lidarr_data.albums.is_empty() { + self + .dispatch_network_event( + LidarrEvent::GetAlbumHistory( + self.extract_artist_id().await, + self.extract_album_id().await, + ) + .into(), + ) + .await; + } + } + ActiveLidarrBlock::ManualAlbumSearch => { + match self.data.lidarr_data.album_details_modal.as_ref() { + Some(album_details_modal) if album_details_modal.album_releases.is_empty() => { + self + .dispatch_network_event( + LidarrEvent::GetAlbumReleases( + self.extract_artist_id().await, + self.extract_album_id().await, + ) + .into(), + ) + .await; + } + _ => (), + } + } + ActiveLidarrBlock::AddArtistSearchResults => { + self + .dispatch_network_event( + LidarrEvent::SearchNewArtist(self.extract_add_new_artist_search_query().await).into(), + ) + .await; + } + ActiveLidarrBlock::History => { + self + .dispatch_network_event(LidarrEvent::GetHistory(500).into()) + .await + } + ActiveLidarrBlock::RootFolders => { + self + .dispatch_network_event(LidarrEvent::GetRootFolders.into()) + .await; + } + ActiveLidarrBlock::Indexers => { + self + .dispatch_network_event(LidarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetIndexers.into()) + .await; + } + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self + .dispatch_network_event(LidarrEvent::GetAllIndexerSettings.into()) + .await; + } + ActiveLidarrBlock::TestIndexer => { + self + .dispatch_network_event( + LidarrEvent::TestIndexer(self.extract_lidarr_indexer_id().await).into(), + ) + .await; + } + ActiveLidarrBlock::TestAllIndexers => { + self + .dispatch_network_event(LidarrEvent::TestAllIndexers.into()) + .await; + } + ActiveLidarrBlock::System => { + self + .dispatch_network_event(LidarrEvent::GetTasks.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetQueuedEvents.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetLogs(500).into()) + .await; + } + ActiveLidarrBlock::SystemUpdates => { + self + .dispatch_network_event(LidarrEvent::GetUpdates.into()) + .await; + } + ActiveLidarrBlock::TrackDetails => { + self + .dispatch_network_event( + LidarrEvent::GetTrackDetails(self.extract_track_id().await).into(), + ) + .await; + } + ActiveLidarrBlock::TrackHistory => { + let artist_id = self.extract_artist_id().await; + let album_id = self.extract_album_id().await; + let track_id = self.extract_track_id().await; + self + .dispatch_network_event( + LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into(), + ) + .await; + } + _ => (), + } + + self.check_for_lidarr_prompt_action().await; + self.reset_tick_count(); + } + + async fn extract_add_new_artist_search_query(&self) -> String { + self + .data + .lidarr_data + .add_artist_search + .as_ref() + .expect("Add artist search is empty") + .text + .clone() + } + + async fn extract_artist_id(&self) -> i64 { + self.data.lidarr_data.artists.current_selection().id + } + + async fn extract_album_id(&self) -> i64 { + self.data.lidarr_data.albums.current_selection().id + } + + async fn extract_track_id(&self) -> i64 { + self + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal is empty") + .tracks + .current_selection() + .id + } + + async fn extract_lidarr_indexer_id(&self) -> i64 { + self.data.lidarr_data.indexers.current_selection().id + } + + async fn check_for_lidarr_prompt_action(&mut self) { + if self.data.lidarr_data.prompt_confirm { + self.data.lidarr_data.prompt_confirm = false; + if let Some(lidarr_event) = self.data.lidarr_data.prompt_confirm_action.take() { + self.dispatch_network_event(lidarr_event.into()).await; + self.should_refresh = true; + } + } + } + + pub(super) async fn lidarr_on_tick(&mut self, active_lidarr_block: ActiveLidarrBlock) { + if self.is_first_render { + self.refresh_lidarr_metadata().await; + self.dispatch_by_lidarr_block(&active_lidarr_block).await; + self.is_first_render = false; + return; + } + + if self.should_refresh { + self.dispatch_by_lidarr_block(&active_lidarr_block).await; + self.refresh_lidarr_metadata().await; + } + + if self.is_routing { + if !self.should_refresh { + self.cancellation_token.cancel(); + } else { + self.dispatch_by_lidarr_block(&active_lidarr_block).await; + } + } + + if self.tick_count.is_multiple_of(self.tick_until_poll) { + self.refresh_lidarr_metadata().await; + } + } + + async fn refresh_lidarr_metadata(&mut self) { + self + .dispatch_network_event(LidarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetMetadataProfiles.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetRootFolders.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetDownloads(500).into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetDiskSpace.into()) + .await; + self + .dispatch_network_event(LidarrEvent::GetStatus.into()) + .await; + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index a8de430..9524980 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::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}; use crate::models::servarr_models::KeybindingItem; @@ -25,6 +26,7 @@ mod app_tests; pub mod context_clues; pub mod key_binding; mod key_binding_tests; +pub mod lidarr; pub mod radarr; pub mod sonarr; @@ -96,6 +98,26 @@ impl App<'_> { server_tabs.extend(sonarr_tabs); } + if let Some(lidarr_configs) = config.lidarr { + let mut unnamed_idx = 0; + let lidarr_tabs = lidarr_configs.into_iter().map(|lidarr_config| { + let name = if let Some(name) = lidarr_config.name.clone() { + name + } else { + unnamed_idx += 1; + format!("Lidarr {unnamed_idx}") + }; + + TabRoute { + title: name, + route: ActiveLidarrBlock::Artists.into(), + contextual_help: None, + config: Some(lidarr_config), + } + }); + server_tabs.extend(lidarr_tabs); + } + let weight_sorted_tabs = server_tabs .into_iter() .sorted_by(|tab1, tab2| { @@ -176,6 +198,7 @@ impl App<'_> { match self.get_current_route() { Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await, Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await, + Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await, _ => (), } @@ -234,7 +257,7 @@ impl Default for App<'_> { is_first_render: true, server_tabs: TabState::new(Vec::new()), tick_until_poll: 400, - ticks_until_scroll: 4, + ticks_until_scroll: 64, tick_count: 0, ui_scroll_tick_count: 0, is_loading: false, @@ -264,6 +287,12 @@ impl App<'_> { contextual_help: None, config: Some(ServarrConfig::default()), }, + TabRoute { + title: "Lidarr".to_owned(), + route: ActiveLidarrBlock::Artists.into(), + contextual_help: None, + config: Some(ServarrConfig::default()), + }, ]), ..App::default() } @@ -272,6 +301,7 @@ impl App<'_> { pub fn test_default_fully_populated() -> Self { App { data: Data { + lidarr_data: LidarrData::test_default_fully_populated(), radarr_data: RadarrData::test_default_fully_populated(), sonarr_data: SonarrData::test_default_fully_populated(), }, @@ -288,6 +318,12 @@ impl App<'_> { contextual_help: None, config: Some(ServarrConfig::default()), }, + TabRoute { + title: "Lidarr".to_owned(), + route: ActiveLidarrBlock::Artists.into(), + contextual_help: None, + config: Some(ServarrConfig::default()), + }, ]), ..App::default() } @@ -296,6 +332,7 @@ impl App<'_> { #[derive(Default)] pub struct Data<'a> { + pub lidarr_data: LidarrData<'a>, pub radarr_data: RadarrData<'a>, pub sonarr_data: SonarrData<'a>, } @@ -303,13 +340,14 @@ pub struct Data<'a> { #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct AppConfig { pub theme: Option, + pub lidarr: Option>, pub radarr: Option>, pub sonarr: Option>, } impl AppConfig { pub fn validate(&self) { - if 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( "No Servarr configuration provided in the specified configuration file".to_owned(), ); @@ -323,6 +361,10 @@ impl AppConfig { if let Some(sonarr_configs) = &self.sonarr { sonarr_configs.iter().for_each(|config| config.validate()); } + + if let Some(lidarr_configs) = &self.lidarr { + lidarr_configs.iter().for_each(|config| config.validate()); + } } pub fn verify_config_present_for_cli(&self, command: &Command) { @@ -340,6 +382,10 @@ impl AppConfig { msg("Sonarr"); process::exit(1); } + Command::Lidarr(_) if self.lidarr.is_none() => { + msg("Lidarr"); + process::exit(1); + } _ => (), } } @@ -356,6 +402,12 @@ impl AppConfig { sonarr_config.post_process_initialization(); } } + + if let Some(lidarr_configs) = self.lidarr.as_mut() { + for lidarr_config in lidarr_configs { + lidarr_config.post_process_initialization(); + } + } } } diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 3e939ef..f15a20a 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -62,6 +62,11 @@ impl App<'_> { .dispatch_network_event(RadarrEvent::GetDownloads(500).into()) .await; } + ActiveRadarrBlock::History => { + self + .dispatch_network_event(RadarrEvent::GetHistory(500).into()) + .await; + } ActiveRadarrBlock::Indexers => { self .dispatch_network_event(RadarrEvent::GetTags.into()) diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index 04f17aa..3222187 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -1,6 +1,7 @@ use crate::app::App; use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClue, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::models::Route; @@ -82,11 +83,6 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.esc, "edit search"), ]; -pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.submit, "start task"), - (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), -]; - pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ (DEFAULT_KEYBINDINGS.submit, "show overview/add movie"), (DEFAULT_KEYBINDINGS.edit, "edit collection"), diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index d5fbca9..a4b4f10 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -3,14 +3,15 @@ mod tests { use crate::app::App; use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, - ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::radarr_context_clues::{ ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, - MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, SYSTEM_TASKS_CONTEXT_CLUES, + MOVIE_DETAILS_CONTEXT_CLUES, RadarrContextClueProvider, }; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; @@ -459,9 +460,10 @@ mod tests { #[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)] #[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)] #[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)] - #[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] - #[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] - #[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)] + #[case(4, ActiveRadarrBlock::History, &HISTORY_CONTEXT_CLUES)] + #[case(5, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] + #[case(6, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] + #[case(7, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)] fn test_radarr_context_clue_provider_radarr_blocks_context_clues( #[case] index: usize, #[case] active_radarr_block: ActiveRadarrBlock, diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index e4d1f9b..e42b347 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -6,7 +6,8 @@ mod tests { use crate::app::App; use crate::app::radarr::ActiveRadarrBlock; use crate::models::radarr_models::{ - AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, Movie, RadarrRelease, + AddMovieBody, AddMovieOptions, Collection, CollectionMovie, Credit, MinimumAvailability, Movie, + MovieMonitor, RadarrRelease, }; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_models::Indexer; @@ -88,13 +89,13 @@ mod tests { tmdb_id: 1234, title: "Test".to_owned(), root_folder_path: "/nfs2".to_owned(), - minimum_availability: "announced".to_owned(), + minimum_availability: MinimumAvailability::Announced, monitored: true, quality_profile_id: 2222, tags: vec![1, 2], tag_input_string: None, add_options: AddMovieOptions { - monitor: "movieOnly".to_owned(), + monitor: MovieMonitor::MovieOnly, search_for_movie: true, }, }; @@ -146,6 +147,23 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::History) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetHistory(500).into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_root_folders_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index e9c3de7..8a7cc2c 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -58,21 +58,19 @@ impl App<'_> { } ActiveSonarrBlock::SeasonHistory => { if !self.data.sonarr_data.seasons.is_empty() { + let (series_id, season_number) = self.extract_series_id_season_number_tuple().await; self - .dispatch_network_event( - SonarrEvent::GetSeasonHistory(self.extract_series_id_season_number_tuple().await) - .into(), - ) + .dispatch_network_event(SonarrEvent::GetSeasonHistory(series_id, season_number).into()) .await; } } ActiveSonarrBlock::ManualSeasonSearch => { match self.data.sonarr_data.season_details_modal.as_ref() { Some(season_details_modal) if season_details_modal.season_releases.is_empty() => { + let (series_id, season_number) = self.extract_series_id_season_number_tuple().await; self .dispatch_network_event( - SonarrEvent::GetSeasonReleases(self.extract_series_id_season_number_tuple().await) - .into(), + SonarrEvent::GetSeasonReleases(series_id, season_number).into(), ) .await; } diff --git a/src/app/sonarr/sonarr_context_clues.rs b/src/app/sonarr/sonarr_context_clues.rs index 2a58a81..90d8aad 100644 --- a/src/app/sonarr/sonarr_context_clues.rs +++ b/src/app/sonarr/sonarr_context_clues.rs @@ -1,5 +1,6 @@ use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::{App, context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS}; use crate::models::Route; @@ -57,18 +58,6 @@ pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [ (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ - (DEFAULT_KEYBINDINGS.submit, "details"), - (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), - (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), - (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), - (DEFAULT_KEYBINDINGS.esc, "cancel filter"), -]; - pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ ( DEFAULT_KEYBINDINGS.refresh, @@ -175,11 +164,6 @@ pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [ (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.submit, "start task"), - (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), -]; - pub(in crate::app) struct SonarrContextClueProvider; impl ContextClueProvider for SonarrContextClueProvider { diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs index 30b953d..4f02fc3 100644 --- a/src/app/sonarr/sonarr_context_clues_tests.rs +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -2,8 +2,9 @@ mod tests { use crate::app::context_clues::{ BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, - ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + ContextClue, ContextClueProvider, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + SYSTEM_TASKS_CONTEXT_CLUES, }; use crate::app::sonarr::sonarr_context_clues::{ SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, SonarrContextClueProvider, @@ -13,10 +14,9 @@ mod tests { key_binding::DEFAULT_KEYBINDINGS, sonarr::sonarr_context_clues::{ ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, - HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, - MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, - SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, - SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, + MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, + SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, + SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, }, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; @@ -146,40 +146,6 @@ mod tests { assert_none!(series_history_context_clues_iter.next()); } - #[test] - fn test_history_context_clues() { - let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter(); - - assert_some_eq_x!( - history_context_clues_iter.next(), - &(DEFAULT_KEYBINDINGS.submit, "details") - ); - assert_some_eq_x!( - history_context_clues_iter.next(), - &(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc) - ); - assert_some_eq_x!( - history_context_clues_iter.next(), - &(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc) - ); - assert_some_eq_x!( - history_context_clues_iter.next(), - &(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc) - ); - assert_some_eq_x!( - history_context_clues_iter.next(), - &( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc - ) - ); - assert_some_eq_x!( - history_context_clues_iter.next(), - &(DEFAULT_KEYBINDINGS.esc, "cancel filter") - ); - assert_none!(history_context_clues_iter.next()); - } - #[test] fn test_series_details_context_clues() { let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter(); @@ -455,7 +421,6 @@ mod tests { let mut app = App::test_default(); app.push_navigation_stack(ActiveRadarrBlock::default().into()); - // This should panic because the route is not a Sonarr route SonarrContextClueProvider::get_context_clues(&mut app); } diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index bbf008e..4962b15 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -132,7 +132,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetSeasonHistory((1, 1)).into() + SonarrEvent::GetSeasonHistory(1, 1).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); @@ -175,7 +175,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetSeasonReleases((1, 1)).into() + SonarrEvent::GetSeasonReleases(1, 1).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/cli/cli_tests.rs b/src/cli/cli_tests.rs index 7232497..08fb287 100644 --- a/src/cli/cli_tests.rs +++ b/src/cli/cli_tests.rs @@ -8,12 +8,18 @@ mod tests { use serde_json::json; use tokio::sync::Mutex; + use crate::cli::lidarr::LidarrCommand; + use crate::network::lidarr_network::LidarrEvent; use crate::{ Cli, app::App, cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand}, models::{ Serdeable, + lidarr_models::{ + BlocklistItem as LidarrBlocklistItem, BlocklistResponse as LidarrBlocklistResponse, + LidarrSerdeable, + }, radarr_models::{ BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse, RadarrSerdeable, @@ -55,6 +61,13 @@ mod tests { assert_ok!(&result); } + #[test] + fn test_lidarr_subcommand_delegates_to_lidarr() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]); + + assert_ok!(&result); + } + #[test] fn test_completions_requires_argument() { let result = Cli::command().try_get_matches_from(["managarr", "completions"]); @@ -174,4 +187,35 @@ mod tests { assert_ok!(&result); } + + #[tokio::test] + async fn test_cli_handler_delegates_lidarr_commands_to_the_lidarr_cli_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetBlocklist.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::BlocklistResponse( + LidarrBlocklistResponse { + records: vec![LidarrBlocklistItem::default()], + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::ClearBlocklist.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let clear_blocklist_command = LidarrCommand::ClearBlocklist.into(); + + let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await; + + assert_ok!(&result); + } } diff --git a/src/cli/lidarr/add_command_handler.rs b/src/cli/lidarr/add_command_handler.rs new file mode 100644 index 0000000..b52d13f --- /dev/null +++ b/src/cli/lidarr/add_command_handler.rs @@ -0,0 +1,223 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::{ArgAction, Subcommand, arg}; +use tokio::sync::Mutex; + +use super::LidarrCommand; +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + models::lidarr_models::{ + AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, MonitorType, NewItemMonitorType, + }, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +#[cfg(test)] +#[path = "add_command_handler_tests.rs"] +mod add_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrAddCommand { + #[command(about = "Add a new artist to your Lidarr library")] + Artist { + #[arg( + long, + help = "The MusicBrainz foreign artist ID of the artist you wish to add to your library", + required = true + )] + foreign_artist_id: String, + #[arg(long, help = "The name of the artist", required = true)] + artist_name: String, + #[arg( + long, + help = "The root folder path where all artist data and metadata should live", + required = true + )] + root_folder_path: String, + #[arg( + long, + help = "The ID of the quality profile to use for this artist", + required = true + )] + quality_profile_id: i64, + #[arg( + long, + help = "The ID of the metadata profile to use for this artist", + required = true + )] + metadata_profile_id: i64, + #[arg(long, help = "Disable monitoring for this artist")] + disable_monitoring: bool, + #[arg( + long, + help = "Tag IDs to tag the artist with", + value_parser, + action = ArgAction::Append + )] + tag: Vec, + #[arg( + long, + help = "What Lidarr should monitor for this artist", + value_enum, + default_value_t = MonitorType::default() + )] + monitor: MonitorType, + #[arg( + long, + help = "How Lidarr should monitor new items for this artist", + value_enum, + default_value_t = NewItemMonitorType::default() + )] + monitor_new_items: NewItemMonitorType, + #[arg( + long, + help = "Tell Lidarr to not start a search for missing albums once the artist is added to your library" + )] + no_search_for_missing_albums: bool, + }, + #[command(about = "Add a new root folder")] + RootFolder { + #[arg(long, help = "The name of the root folder", required = true)] + name: String, + #[arg(long, help = "The path of the new root folder", required = true)] + root_folder_path: String, + #[arg( + long, + help = "The ID of the default quality profile for artists in this root folder", + required = true + )] + quality_profile_id: i64, + #[arg( + long, + help = "The ID of the default metadata profile for artists in this root folder", + required = true + )] + metadata_profile_id: i64, + #[arg( + long, + help = "The default monitor option for artists in this root folder", + value_enum, + default_value_t = MonitorType::default() + )] + monitor: MonitorType, + #[arg( + long, + help = "The default monitor new items option for artists in this root folder", + value_enum, + default_value_t = NewItemMonitorType::default() + )] + monitor_new_items: NewItemMonitorType, + #[arg( + long, + help = "Default tag IDs for artists in this root folder", + value_parser, + action = ArgAction::Append + )] + tag: Vec, + }, + #[command(about = "Add new tag")] + Tag { + #[arg(long, help = "The name of the tag to be added", required = true)] + name: String, + }, +} + +impl From for Command { + fn from(value: LidarrAddCommand) -> Self { + Command::Lidarr(LidarrCommand::Add(value)) + } +} + +pub(super) struct LidarrAddCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrAddCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + command: LidarrAddCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrAddCommandHandler { + _app: app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrAddCommand::Artist { + foreign_artist_id, + artist_name, + root_folder_path, + quality_profile_id, + metadata_profile_id, + disable_monitoring, + tag: tags, + monitor, + monitor_new_items, + no_search_for_missing_albums, + } => { + let body = AddArtistBody { + foreign_artist_id, + artist_name, + monitored: !disable_monitoring, + root_folder_path, + quality_profile_id, + metadata_profile_id, + tags, + tag_input_string: None, + add_options: AddArtistOptions { + monitor, + monitor_new_items, + search_for_missing_albums: !no_search_for_missing_albums, + }, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::AddArtist(body).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrAddCommand::RootFolder { + name, + root_folder_path, + quality_profile_id, + metadata_profile_id, + monitor, + monitor_new_items, + tag: tags, + } => { + let add_root_folder_body = AddLidarrRootFolderBody { + name, + path: root_folder_path, + default_quality_profile_id: quality_profile_id, + default_metadata_profile_id: metadata_profile_id, + default_monitor_option: monitor, + default_new_item_monitor_option: monitor_new_items, + default_tags: tags, + tag_input_string: None, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::AddRootFolder(add_root_folder_body).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrAddCommand::Tag { name } => { + let resp = self + .network + .handle_network_event(LidarrEvent::AddTag(name).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/add_command_handler_tests.rs b/src/cli/lidarr/add_command_handler_tests.rs new file mode 100644 index 0000000..b96d257 --- /dev/null +++ b/src/cli/lidarr/add_command_handler_tests.rs @@ -0,0 +1,561 @@ +#[cfg(test)] +mod tests { + use clap::{CommandFactory, Parser, error::ErrorKind}; + + use crate::{ + Cli, + cli::{ + Command, + lidarr::{LidarrCommand, add_command_handler::LidarrAddCommand}, + }, + models::lidarr_models::{MonitorType, NewItemMonitorType}, + }; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_add_command_from() { + let command = LidarrAddCommand::Tag { + name: String::new(), + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Add(command))); + } + + mod cli { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_add_root_folder_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "root-folder"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_root_folder_success() { + let expected_args = LidarrAddCommand::RootFolder { + name: "Music".to_owned(), + root_folder_path: "/nfs/test".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + tag: vec![], + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "root-folder", + "--name", + "Music", + "--root-folder-path", + "/nfs/test", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(add_command, expected_args); + } + + #[test] + fn test_add_tag_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "tag"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_tag_success() { + let expected_args = LidarrAddCommand::Tag { + name: "test".to_owned(), + }; + + let result = Cli::try_parse_from(["managarr", "lidarr", "add", "tag", "--name", "test"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } + + #[test] + fn test_add_artist_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_foreign_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--artist-name", + "Test", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_artist_name() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_root_folder_path() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_quality_profile_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test", + "--root-folder-path", + "/music", + "--metadata-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_requires_metadata_profile_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_artist_success_with_required_args_only() { + let expected_args = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + disable_monitoring: false, + tag: vec![], + monitor: MonitorType::default(), + monitor_new_items: NewItemMonitorType::default(), + no_search_for_missing_albums: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } + + #[test] + fn test_add_artist_success_with_all_args() { + let expected_args = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 2, + disable_monitoring: true, + tag: vec![1, 2], + monitor: MonitorType::Future, + monitor_new_items: NewItemMonitorType::New, + no_search_for_missing_albums: true, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--disable-monitoring", + "--tag", + "1", + "--tag", + "2", + "--monitor", + "future", + "--monitor-new-items", + "new", + "--no-search-for-missing-albums", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } + + #[test] + fn test_add_artist_monitor_type_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--monitor", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_add_artist_new_item_monitor_type_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--monitor-new-items", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_add_artist_tags_is_repeatable() { + let expected_args = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 2, + disable_monitoring: false, + tag: vec![1, 2], + monitor: MonitorType::default(), + monitor_new_items: NewItemMonitorType::default(), + no_search_for_missing_albums: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "artist", + "--foreign-artist-id", + "test-id", + "--artist-name", + "Test Artist", + "--root-folder-path", + "/music", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "2", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type") + }; + assert_eq!(add_command, expected_args); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::cli::CliCommandHandler; + use crate::cli::lidarr::add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler}; + use crate::models::Serdeable; + use crate::models::lidarr_models::{ + AddArtistBody, AddArtistOptions, AddLidarrRootFolderBody, LidarrSerdeable, MonitorType, + NewItemMonitorType, + }; + use crate::network::lidarr_network::LidarrEvent; + use crate::{ + app::App, + network::{MockNetworkTrait, NetworkEvent}, + }; + + #[tokio::test] + async fn test_handle_add_root_folder_command() { + let expected_root_folder_path = "/nfs/test".to_owned(); + let expected_add_root_folder_body = AddLidarrRootFolderBody { + name: "Music".to_owned(), + path: expected_root_folder_path.clone(), + default_quality_profile_id: 1, + default_metadata_profile_id: 1, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: vec![1, 2], + tag_input_string: None, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let add_root_folder_command = LidarrAddCommand::RootFolder { + name: "Music".to_owned(), + root_folder_path: expected_root_folder_path, + quality_profile_id: 1, + metadata_profile_id: 1, + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + tag: vec![1, 2], + }; + + let result = + LidarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_add_tag_command() { + let expected_tag_name = "test".to_owned(); + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::AddTag(expected_tag_name.clone()).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let add_tag_command = LidarrAddCommand::Tag { + name: expected_tag_name, + }; + + let result = LidarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_add_artist_command() { + let expected_body = AddArtistBody { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + monitored: false, + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: vec![1, 2], + tag_input_string: None, + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: false, + }, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::AddArtist(expected_body).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let add_artist_command = LidarrAddCommand::Artist { + foreign_artist_id: "test-id".to_owned(), + artist_name: "Test Artist".to_owned(), + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + disable_monitoring: true, + tag: vec![1, 2], + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + no_search_for_missing_albums: true, + }; + + let result = LidarrAddCommandHandler::with(&app_arc, add_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs new file mode 100644 index 0000000..bd6a660 --- /dev/null +++ b/src/cli/lidarr/delete_command_handler.rs @@ -0,0 +1,181 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + models::lidarr_models::DeleteParams, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +use super::LidarrCommand; + +#[cfg(test)] +#[path = "delete_command_handler_tests.rs"] +mod delete_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrDeleteCommand { + #[command(about = "Delete an album from your Lidarr library")] + Album { + #[arg(long, help = "The ID of the album to delete", required = true)] + album_id: i64, + #[arg(long, help = "Delete the album files from disk as well")] + delete_files_from_disk: bool, + #[arg(long, help = "Add a list exclusion for this album")] + add_list_exclusion: bool, + }, + #[command(about = "Delete the specified item from the Lidarr blocklist")] + BlocklistItem { + #[arg( + long, + help = "The ID of the blocklist item to remove from the blocklist", + required = true + )] + blocklist_item_id: i64, + }, + #[command(about = "Delete the specified track file from disk")] + TrackFile { + #[arg(long, help = "The ID of the track file to delete", required = true)] + track_file_id: i64, + }, + #[command(about = "Delete an artist from your Lidarr library")] + Artist { + #[arg(long, help = "The ID of the artist to delete", required = true)] + artist_id: i64, + #[arg(long, help = "Delete the artist files from disk as well")] + delete_files_from_disk: bool, + #[arg(long, help = "Add a list exclusion for this artist")] + add_list_exclusion: bool, + }, + #[command(about = "Delete the specified download")] + Download { + #[arg(long, help = "The ID of the download to delete", required = true)] + download_id: i64, + }, + #[command(about = "Delete the indexer with the given ID")] + Indexer { + #[arg(long, help = "The ID of the indexer to delete", required = true)] + indexer_id: i64, + }, + #[command(about = "Delete the root folder with the given ID")] + RootFolder { + #[arg(long, help = "The ID of the root folder to delete", required = true)] + root_folder_id: i64, + }, + #[command(about = "Delete the tag with the specified ID")] + Tag { + #[arg(long, help = "The ID of the tag to delete", required = true)] + tag_id: i64, + }, +} + +impl From for Command { + fn from(value: LidarrDeleteCommand) -> Self { + Command::Lidarr(LidarrCommand::Delete(value)) + } +} + +pub(super) struct LidarrDeleteCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrDeleteCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: LidarrDeleteCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrDeleteCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrDeleteCommand::Album { + album_id, + delete_files_from_disk, + add_list_exclusion, + } => { + let delete_album_params = DeleteParams { + id: album_id, + delete_files: delete_files_from_disk, + add_import_list_exclusion: add_list_exclusion, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteAlbum(delete_album_params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::BlocklistItem { blocklist_item_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteBlocklistItem(blocklist_item_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::TrackFile { track_file_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteTrackFile(track_file_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::Artist { + artist_id, + delete_files_from_disk, + add_list_exclusion, + } => { + let delete_artist_params = DeleteParams { + id: artist_id, + delete_files: delete_files_from_disk, + add_import_list_exclusion: add_list_exclusion, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteArtist(delete_artist_params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::Download { download_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteDownload(download_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::Indexer { indexer_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteIndexer(indexer_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::RootFolder { root_folder_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteRootFolder(root_folder_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrDeleteCommand::Tag { tag_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteTag(tag_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs new file mode 100644 index 0000000..0d8c462 --- /dev/null +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -0,0 +1,595 @@ +#[cfg(test)] +mod tests { + use crate::{ + Cli, + cli::{ + Command, + lidarr::{LidarrCommand, delete_command_handler::LidarrDeleteCommand}, + }, + }; + use clap::{CommandFactory, Parser, error::ErrorKind}; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_delete_command_from() { + let command = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: false, + add_list_exclusion: false, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Delete(command))); + } + + mod cli { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_delete_album_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "album"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_album_defaults() { + let expected_args = LidarrDeleteCommand::Album { + album_id: 1, + delete_files_from_disk: false, + add_list_exclusion: false, + }; + + let result = + Cli::try_parse_from(["managarr", "lidarr", "delete", "album", "--album-id", "1"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_album_all_args_defined() { + let expected_args = LidarrDeleteCommand::Album { + album_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "album", + "--album-id", + "1", + "--delete-files-from-disk", + "--add-list-exclusion", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_blocklist_item_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "blocklist-item"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_blocklist_item_success() { + let expected_args = LidarrDeleteCommand::BlocklistItem { + blocklist_item_id: 1, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "blocklist-item", + "--blocklist-item-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_track_file_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "track-file"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_track_file_success() { + let expected_args = LidarrDeleteCommand::TrackFile { track_file_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "track-file", + "--track-file-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_artist_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_artist_defaults() { + let expected_args = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: false, + add_list_exclusion: false, + }; + + let result = + Cli::try_parse_from(["managarr", "lidarr", "delete", "artist", "--artist-id", "1"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_artist_all_args_defined() { + let expected_args = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "artist", + "--artist-id", + "1", + "--delete-files-from-disk", + "--add-list-exclusion", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_download_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_download_success() { + let expected_args = LidarrDeleteCommand::Download { download_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "download", + "--download-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_indexer_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "indexer"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_indexer_success() { + let expected_args = LidarrDeleteCommand::Indexer { indexer_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "indexer", + "--indexer-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_root_folder_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "root-folder"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_root_folder_success() { + let expected_args = LidarrDeleteCommand::RootFolder { root_folder_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "root-folder", + "--root-folder-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + + #[test] + fn test_delete_tag_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_tag_success() { + let expected_args = LidarrDeleteCommand::Tag { tag_id: 1 }; + + let result = Cli::try_parse_from(["managarr", "lidarr", "delete", "tag", "--tag-id", "1"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + CliCommandHandler, + lidarr::delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}, + }, + models::{ + Serdeable, + lidarr_models::{DeleteParams, LidarrSerdeable}, + }, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_handle_delete_album_command() { + let expected_delete_album_params = DeleteParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteAlbum(expected_delete_album_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_album_command = LidarrDeleteCommand::Album { + album_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_album_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_blocklist_item_command() { + let expected_blocklist_item_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_blocklist_item_command = LidarrDeleteCommand::BlocklistItem { + blocklist_item_id: 1, + }; + + let result = LidarrDeleteCommandHandler::with( + &app_arc, + delete_blocklist_item_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_track_file_command() { + let expected_track_file_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteTrackFile(expected_track_file_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_track_file_command = LidarrDeleteCommand::TrackFile { track_file_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_track_file_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_artist_command() { + let expected_delete_artist_params = DeleteParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteArtist(expected_delete_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_artist_command = LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_download_command() { + let expected_download_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteDownload(expected_download_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_download_command = LidarrDeleteCommand::Download { download_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_indexer_command() { + let expected_indexer_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteIndexer(expected_indexer_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_indexer_command = LidarrDeleteCommand::Indexer { indexer_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_root_folder_command() { + let expected_root_folder_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteRootFolder(expected_root_folder_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_root_folder_command = LidarrDeleteCommand::RootFolder { root_folder_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_delete_tag_command() { + let expected_tag_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteTag(expected_tag_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_tag_command = LidarrDeleteCommand::Tag { tag_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/edit_command_handler.rs b/src/cli/lidarr/edit_command_handler.rs new file mode 100644 index 0000000..50d052f --- /dev/null +++ b/src/cli/lidarr/edit_command_handler.rs @@ -0,0 +1,343 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::{ArgAction, ArgGroup, Subcommand}; +use tokio::sync::Mutex; + +use super::LidarrCommand; +use crate::models::Serdeable; +use crate::models::lidarr_models::LidarrSerdeable; +use crate::models::servarr_models::{EditIndexerParams, IndexerSettings}; +use crate::{ + app::App, + cli::{CliCommandHandler, Command, mutex_flags_or_option}, + models::lidarr_models::{EditArtistParams, NewItemMonitorType}, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +#[cfg(test)] +#[path = "edit_command_handler_tests.rs"] +mod edit_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrEditCommand { + #[command( + about = "Edit and indexer settings that apply to all indexers", + group( + ArgGroup::new("edit_settings") + .args([ + "maximum_size", + "minimum_age", + "retention", + "rss_sync_interval", + ]).required(true) + .multiple(true)) + )] + AllIndexerSettings { + #[arg( + long, + help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited" + )] + maximum_size: Option, + #[arg( + long, + help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider." + )] + minimum_age: Option, + #[arg( + long, + help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention" + )] + retention: Option, + #[arg( + long, + help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)" + )] + rss_sync_interval: Option, + }, + #[command( + about = "Edit preferences for the specified artist", + group( + ArgGroup::new("edit_artist") + .args([ + "enable_monitoring", + "disable_monitoring", + "monitor_new_items", + "quality_profile_id", + "metadata_profile_id", + "root_folder_path", + "tag", + "clear_tags" + ]).required(true) + .multiple(true)) + )] + Artist { + #[arg( + long, + help = "The ID of the artist whose settings you want to edit", + required = true + )] + artist_id: i64, + #[arg( + long, + help = "Enable monitoring of this artist in Lidarr so Lidarr will automatically download releases from this artist if they are available", + conflicts_with = "disable_monitoring" + )] + enable_monitoring: bool, + #[arg( + long, + help = "Disable monitoring of this artist so Lidarr does not automatically download releases from this artist if they are available", + conflicts_with = "enable_monitoring" + )] + disable_monitoring: bool, + #[arg( + long, + help = "How Lidarr should monitor new albums from this artist", + value_enum + )] + monitor_new_items: Option, + #[arg(long, help = "The ID of the quality profile to use for this artist")] + quality_profile_id: Option, + #[arg(long, help = "The ID of the metadata profile to use for this artist")] + metadata_profile_id: Option, + #[arg( + long, + help = "The root folder path where all artist data and metadata should live" + )] + root_folder_path: Option, + #[arg( + long, + help = "Tag IDs to tag this artist with", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg(long, help = "Clear all tags on this artist", conflicts_with = "tag")] + clear_tags: bool, + }, + #[command( + about = "Edit preferences for the specified indexer", + group( + ArgGroup::new("edit_indexer") + .args([ + "name", + "enable_rss", + "disable_rss", + "enable_automatic_search", + "disable_automatic_search", + "enable_interactive_search", + "disable_automatic_search", + "url", + "api_key", + "seed_ratio", + "tag", + "priority", + "clear_tags" + ]).required(true) + .multiple(true)) + )] + Indexer { + #[arg( + long, + help = "The ID of the indexer whose settings you wish to edit", + required = true + )] + indexer_id: i64, + #[arg(long, help = "The name of the indexer")] + name: Option, + #[arg( + long, + help = "Indicate to Lidarr that this indexer should be used when Lidarr periodically looks for releases via RSS Sync", + conflicts_with = "disable_rss" + )] + enable_rss: bool, + #[arg( + long, + help = "Disable using this indexer when Lidarr periodically looks for releases via RSS Sync", + conflicts_with = "enable_rss" + )] + disable_rss: bool, + #[arg( + long, + help = "Indicate to Lidarr that this indexer should be used when automatic searches are performed via the UI or by Lidarr", + conflicts_with = "disable_automatic_search" + )] + enable_automatic_search: bool, + #[arg( + long, + help = "Disable using this indexer whenever automatic searches are performed via the UI or by Lidarr", + conflicts_with = "enable_automatic_search" + )] + disable_automatic_search: bool, + #[arg( + long, + help = "Indicate to Lidarr that this indexer should be used when an interactive search is used", + conflicts_with = "disable_interactive_search" + )] + enable_interactive_search: bool, + #[arg( + long, + help = "Disable using this indexer whenever an interactive search is performed", + conflicts_with = "enable_interactive_search" + )] + disable_interactive_search: bool, + #[arg(long, help = "The URL of the indexer")] + url: Option, + #[arg(long, help = "The API key used to access the indexer's API")] + api_key: Option, + #[arg( + long, + help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules" + )] + seed_ratio: Option, + #[arg( + long, + help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.", + value_parser, + action = ArgAction::Append, + conflicts_with = "clear_tags" + )] + tag: Option>, + #[arg( + long, + help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Lidarr will still use all enabled indexers for RSS Sync and Searching" + )] + priority: Option, + #[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")] + clear_tags: bool, + }, +} + +impl From for Command { + fn from(value: LidarrEditCommand) -> Self { + Command::Lidarr(LidarrCommand::Edit(value)) + } +} + +pub(super) struct LidarrEditCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrEditCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrEditCommand> for LidarrEditCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: LidarrEditCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrEditCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrEditCommand::AllIndexerSettings { + maximum_size, + minimum_age, + retention, + rss_sync_interval, + } => { + if let Serdeable::Lidarr(LidarrSerdeable::IndexerSettings(previous_indexer_settings)) = self + .network + .handle_network_event(LidarrEvent::GetAllIndexerSettings.into()) + .await? + { + let params = IndexerSettings { + id: 1, + maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size), + minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age), + retention: retention.unwrap_or(previous_indexer_settings.retention), + rss_sync_interval: rss_sync_interval + .unwrap_or(previous_indexer_settings.rss_sync_interval), + }; + self + .network + .handle_network_event(LidarrEvent::EditAllIndexerSettings(params).into()) + .await?; + "All indexer settings updated".to_owned() + } else { + String::new() + } + } + LidarrEditCommand::Artist { + artist_id, + enable_monitoring, + disable_monitoring, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tag, + clear_tags, + } => { + let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring); + let edit_artist_params = EditArtistParams { + artist_id, + monitored: monitored_value, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tags: tag, + tag_input_string: None, + clear_tags, + }; + + self + .network + .handle_network_event(LidarrEvent::EditArtist(edit_artist_params).into()) + .await?; + "Artist Updated".to_owned() + } + LidarrEditCommand::Indexer { + indexer_id, + name, + enable_rss, + disable_rss, + enable_automatic_search, + disable_automatic_search, + enable_interactive_search, + disable_interactive_search, + url, + api_key, + seed_ratio, + tag, + priority, + clear_tags, + } => { + let rss_value = mutex_flags_or_option(enable_rss, disable_rss); + let automatic_search_value = + mutex_flags_or_option(enable_automatic_search, disable_automatic_search); + let interactive_search_value = + mutex_flags_or_option(enable_interactive_search, disable_interactive_search); + let edit_indexer_params = EditIndexerParams { + indexer_id, + name, + enable_rss: rss_value, + enable_automatic_search: automatic_search_value, + enable_interactive_search: interactive_search_value, + url, + api_key, + seed_ratio, + tags: tag, + tag_input_string: None, + priority, + clear_tags, + }; + + self + .network + .handle_network_event(LidarrEvent::EditIndexer(edit_indexer_params).into()) + .await?; + "Indexer updated".to_owned() + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/edit_command_handler_tests.rs b/src/cli/lidarr/edit_command_handler_tests.rs new file mode 100644 index 0000000..ab44b31 --- /dev/null +++ b/src/cli/lidarr/edit_command_handler_tests.rs @@ -0,0 +1,858 @@ +#[cfg(test)] +mod tests { + use crate::cli::{ + Command, + lidarr::{LidarrCommand, edit_command_handler::LidarrEditCommand}, + }; + + #[test] + fn test_lidarr_edit_command_from() { + let command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: None, + quality_profile_id: None, + metadata_profile_id: None, + root_folder_path: None, + tag: None, + clear_tags: false, + }; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Edit(command))); + } + + mod cli { + use crate::{Cli, models::lidarr_models::NewItemMonitorType}; + + use super::*; + use clap::{CommandFactory, Parser, error::ErrorKind}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_edit_all_indexer_settings_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "all-indexer-settings"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[rstest] + fn test_edit_all_indexer_settings_assert_argument_flags_require_args( + #[values( + "--maximum-size", + "--minimum-age", + "--retention", + "--rss-sync-interval" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "all-indexer-settings", + flag, + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() { + let expected_args = LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: None, + retention: None, + rss_sync_interval: None, + }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "all-indexer-settings", + "--maximum-size", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_all_indexer_settings_all_arguments_defined() { + let expected_args = LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "all-indexer-settings", + "--maximum-size", + "1", + "--minimum-age", + "1", + "--retention", + "1", + "--rss-sync-interval", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_artist_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_artist_with_artist_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_artist_monitoring_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--enable-monitoring", + "--disable-monitoring", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_artist_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_artist_assert_argument_flags_require_args( + #[values( + "--monitor-new-items", + "--quality-profile-id", + "--metadata-profile-id", + "--root-folder-path", + "--tag" + )] + flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + flag, + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_artist_monitor_new_items_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--monitor-new-items", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_artist_only_requires_at_least_one_argument_plus_artist_id() { + let expected_args = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: None, + quality_profile_id: None, + metadata_profile_id: None, + root_folder_path: Some("/nfs/test".to_owned()), + tag: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--root-folder-path", + "/nfs/test", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_artist_tag_argument_is_repeatable() { + let expected_args = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: None, + quality_profile_id: None, + metadata_profile_id: None, + root_folder_path: None, + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_artist_all_arguments_defined() { + let expected_args = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: true, + disable_monitoring: false, + monitor_new_items: Some(NewItemMonitorType::New), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "artist", + "--artist-id", + "1", + "--enable-monitoring", + "--monitor-new-items", + "new", + "--quality-profile-id", + "1", + "--metadata-profile-id", + "1", + "--root-folder-path", + "/nfs/test", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_indexer_requires_arguments() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "edit", "indexer"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_indexer_with_indexer_id_still_requires_arguments() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_edit_indexer_rss_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-rss", + "--disable-rss", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_automatic_search_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-automatic-search", + "--disable-automatic-search", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_interactive_search_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--enable-interactive-search", + "--disable-interactive-search", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn test_edit_indexer_tag_flags_conflict() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--clear-tags", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict); + } + + #[rstest] + fn test_edit_indexer_assert_argument_flags_require_args( + #[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str, + ) { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + flag, + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() { + let expected_args = LidarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: false, + disable_rss: false, + enable_automatic_search: false, + disable_automatic_search: false, + enable_interactive_search: false, + disable_interactive_search: false, + url: None, + api_key: None, + seed_ratio: None, + tag: None, + priority: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_indexer_tag_argument_is_repeatable() { + let expected_args = LidarrEditCommand::Indexer { + indexer_id: 1, + name: None, + enable_rss: false, + disable_rss: false, + enable_automatic_search: false, + disable_automatic_search: false, + enable_interactive_search: false, + disable_interactive_search: false, + url: None, + api_key: None, + seed_ratio: None, + tag: Some(vec![1, 2]), + priority: None, + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--tag", + "1", + "--tag", + "2", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + + #[test] + fn test_edit_indexer_all_arguments_defined() { + let expected_args = LidarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: true, + disable_rss: false, + enable_automatic_search: true, + disable_automatic_search: false, + enable_interactive_search: true, + disable_interactive_search: false, + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tag: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "edit", + "indexer", + "--indexer-id", + "1", + "--name", + "Test", + "--enable-rss", + "--enable-automatic-search", + "--enable-interactive-search", + "--url", + "http://test.com", + "--api-key", + "testKey", + "--seed-ratio", + "1.2", + "--tag", + "1", + "--tag", + "2", + "--priority", + "25", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Edit(edit_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(edit_command, expected_args); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::models::servarr_models::{EditIndexerParams, IndexerSettings}; + use crate::{ + app::App, + cli::{ + CliCommandHandler, + lidarr::edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler}, + }, + models::{ + Serdeable, + lidarr_models::{EditArtistParams, LidarrSerdeable, NewItemMonitorType}, + }, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_command() { + let expected_edit_all_indexer_settings = IndexerSettings { + id: 1, + maximum_size: 1, + minimum_age: 1, + retention: 1, + rss_sync_interval: 1, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings( + IndexerSettings { + id: 1, + maximum_size: 2, + minimum_age: 2, + retention: 2, + rss_sync_interval: 2, + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_all_indexer_settings_command = LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }; + + let result = LidarrEditCommandHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_edit_artist_command() { + let expected_edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: Some(true), + monitor_new_items: Some(NewItemMonitorType::New), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditArtist(expected_edit_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_artist_command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: true, + disable_monitoring: false, + monitor_new_items: Some(NewItemMonitorType::New), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_edit_artist_command_handles_disable_monitoring_flag_properly() { + let expected_edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::None), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditArtist(expected_edit_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_artist_command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: true, + monitor_new_items: Some(NewItemMonitorType::None), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_edit_artist_command_no_monitoring_boolean_flags_returns_none_value() { + let expected_edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: None, + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditArtist(expected_edit_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_artist_command = LidarrEditCommand::Artist { + artist_id: 1, + enable_monitoring: false, + disable_monitoring: false, + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1), + metadata_profile_id: Some(1), + root_folder_path: Some("/nfs/test".to_owned()), + tag: Some(vec![1, 2]), + clear_tags: false, + }; + + let result = LidarrEditCommandHandler::with(&app_arc, edit_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_edit_indexer_command() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tags: Some(vec![1, 2]), + tag_input_string: None, + priority: Some(25), + clear_tags: false, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditIndexer(expected_edit_indexer_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_indexer_command = LidarrEditCommand::Indexer { + indexer_id: 1, + name: Some("Test".to_owned()), + enable_rss: true, + disable_rss: false, + enable_automatic_search: true, + disable_automatic_search: false, + enable_interactive_search: true, + disable_interactive_search: false, + url: Some("http://test.com".to_owned()), + api_key: Some("testKey".to_owned()), + seed_ratio: Some("1.2".to_owned()), + tag: Some(vec![1, 2]), + priority: Some(25), + clear_tags: false, + }; + + let result = + LidarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/get_command_handler.rs b/src/cli/lidarr/get_command_handler.rs new file mode 100644 index 0000000..8654724 --- /dev/null +++ b/src/cli/lidarr/get_command_handler.rs @@ -0,0 +1,138 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +use super::LidarrCommand; + +#[cfg(test)] +#[path = "get_command_handler_tests.rs"] +mod get_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrGetCommand { + #[command(about = "Get detailed information for the album with the given ID")] + AlbumDetails { + #[arg( + long, + help = "The Lidarr ID of the album whose details you wish to fetch", + required = true + )] + album_id: i64, + }, + #[command(about = "Get the shared settings for all indexers")] + AllIndexerSettings, + #[command(about = "Get detailed information for the artist with the given ID")] + ArtistDetails { + #[arg( + long, + help = "The Lidarr ID of the artist whose details you wish to fetch", + required = true + )] + artist_id: i64, + }, + #[command(about = "Fetch the host config for your Lidarr instance")] + HostConfig, + #[command(about = "Fetch the security config for your Lidarr instance")] + SecurityConfig, + #[command(about = "Get the system status")] + SystemStatus, + #[command(about = "Get detailed information for the track with the given ID")] + TrackDetails { + #[arg( + long, + help = "The Lidarr ID of the track whose details you wish to fetch", + required = true + )] + track_id: i64, + }, +} + +impl From for Command { + fn from(value: LidarrGetCommand) -> Self { + Command::Lidarr(LidarrCommand::Get(value)) + } +} + +pub(super) struct LidarrGetCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrGetCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrGetCommand> for LidarrGetCommandHandler<'a, 'b> { + fn with( + _app: &'a Arc>>, + command: LidarrGetCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrGetCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrGetCommand::AlbumDetails { album_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetAlbumDetails(album_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrGetCommand::AllIndexerSettings => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetAllIndexerSettings.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrGetCommand::ArtistDetails { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetArtistDetails(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrGetCommand::HostConfig => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetHostConfig.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrGetCommand::SecurityConfig => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetSecurityConfig.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrGetCommand::SystemStatus => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetStatus.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrGetCommand::TrackDetails { track_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetTrackDetails(track_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/get_command_handler_tests.rs b/src/cli/lidarr/get_command_handler_tests.rs new file mode 100644 index 0000000..c9ea9d4 --- /dev/null +++ b/src/cli/lidarr/get_command_handler_tests.rs @@ -0,0 +1,329 @@ +#[cfg(test)] +mod tests { + use crate::Cli; + use crate::cli::{ + Command, + lidarr::{LidarrCommand, get_command_handler::LidarrGetCommand}, + }; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_get_command_from() { + let command = LidarrGetCommand::SystemStatus; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Get(command))); + } + + mod cli { + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_album_details_requires_album_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "album-details"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_album_details_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "get", + "album-details", + "--album-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_all_indexer_settings_has_no_arg_requirements() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "all-indexer-settings"]); + + assert_ok!(&result); + } + + #[test] + fn test_artist_details_requires_artist_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "artist-details"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_artist_details_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "get", + "artist-details", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_host_config_has_no_arg_requirements() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "host-config"]); + + assert_ok!(&result); + } + + #[test] + fn test_security_config_has_no_arg_requirements() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "security-config"]); + + assert_ok!(&result); + } + + #[test] + fn test_system_status_has_no_arg_requirements() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "system-status"]); + + assert_ok!(&result); + } + + #[test] + fn test_track_details_requires_track_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "get", "track-details"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_track_details_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "get", + "track-details", + "--track-id", + "1", + ]); + + assert_ok!(&result); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{ + app::App, + cli::{ + CliCommandHandler, + lidarr::get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler}, + }, + models::{Serdeable, lidarr_models::LidarrSerdeable}, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_handle_get_album_details_command() { + let expected_album_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAlbumDetails(expected_album_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_album_details_command = LidarrGetCommand::AlbumDetails { album_id: 1 }; + + let result = + LidarrGetCommandHandler::with(&app_arc, get_album_details_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_get_all_indexer_settings_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_all_indexer_settings_command = LidarrGetCommand::AllIndexerSettings; + + let result = LidarrGetCommandHandler::with( + &app_arc, + get_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_get_artist_details_command() { + let expected_artist_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetArtistDetails(expected_artist_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_artist_details_command = LidarrGetCommand::ArtistDetails { artist_id: 1 }; + + let result = + LidarrGetCommandHandler::with(&app_arc, get_artist_details_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_get_host_config_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetHostConfig.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_host_config_command = LidarrGetCommand::HostConfig; + + let result = + LidarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_get_security_config_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetSecurityConfig.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_security_config_command = LidarrGetCommand::SecurityConfig; + + let result = + LidarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_get_system_status_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetStatus.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_system_status_command = LidarrGetCommand::SystemStatus; + + let result = + LidarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_get_track_details_command() { + let expected_track_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetTrackDetails(expected_track_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_track_details_command = LidarrGetCommand::TrackDetails { track_id: 1 }; + + let result = + LidarrGetCommandHandler::with(&app_arc, get_track_details_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/lidarr_command_tests.rs b/src/cli/lidarr/lidarr_command_tests.rs new file mode 100644 index 0000000..87dc654 --- /dev/null +++ b/src/cli/lidarr/lidarr_command_tests.rs @@ -0,0 +1,775 @@ +#[cfg(test)] +mod tests { + use crate::Cli; + use crate::cli::{ + Command, + lidarr::{LidarrCommand, list_command_handler::LidarrListCommand}, + }; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_command_from() { + let command = LidarrCommand::List(LidarrListCommand::Artists); + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(command)); + } + + mod cli { + use super::*; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_commands_that_have_no_arg_requirements( + #[values("clear-blocklist", "test-all-indexers")] subcommand: &str, + ) { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", subcommand]); + + assert_ok!(&result); + } + + #[test] + fn test_list_artists_has_no_arg_requirements() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artists"]); + + assert_ok!(&result); + } + + #[test] + fn test_lidarr_list_subcommand_requires_subcommand() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list"]); + + assert_err!(&result); + } + + #[test] + fn test_lidarr_add_subcommand_requires_subcommand() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add"]); + + assert_err!(&result); + } + + #[test] + fn test_lidarr_delete_subcommand_requires_subcommand() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete"]); + + assert_err!(&result); + } + + #[test] + fn test_download_release_requires_guid() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "download-release", + "--indexer-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_download_release_requires_indexer_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "download-release", + "--guid", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_download_release_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "download-release", + "--guid", + "1", + "--indexer-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_toggle_artist_monitoring_requires_artist_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-artist-monitoring"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_artist_monitoring_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "toggle-artist-monitoring", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_toggle_album_monitoring_requires_album_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "toggle-album-monitoring"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_album_monitoring_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "toggle-album-monitoring", + "--album-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_search_new_artist_requires_query() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "search-new-artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_search_new_artist_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "search-new-artist", + "--query", + "test query", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_start_task_requires_task_name() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "start-task"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_start_task_task_name_validation() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "start-task", + "--task-name", + "test", + ]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_start_task_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "start-task", + "--task-name", + "application-update-check", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_mark_history_item_as_failed_requires_history_item_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "mark-history-item-as-failed"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_mark_history_item_as_failed_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "mark-history-item-as-failed", + "--history-item-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_test_indexer_requires_indexer_id() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "test-indexer"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_test_indexer_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "test-indexer", + "--indexer-id", + "1", + ]); + + assert_ok!(&result); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::cli::lidarr::add_command_handler::LidarrAddCommand; + use crate::cli::lidarr::edit_command_handler::LidarrEditCommand; + use crate::cli::lidarr::get_command_handler::LidarrGetCommand; + use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand; + use crate::cli::lidarr::refresh_command_handler::LidarrRefreshCommand; + use crate::cli::lidarr::trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand; + use crate::models::lidarr_models::{ + BlocklistItem, BlocklistResponse, LidarrReleaseDownloadBody, LidarrTaskName, + }; + use crate::models::servarr_models::IndexerSettings; + use crate::{ + app::App, + cli::{ + CliCommandHandler, + lidarr::{ + LidarrCliHandler, LidarrCommand, delete_command_handler::LidarrDeleteCommand, + list_command_handler::LidarrListCommand, + }, + }, + models::{ + Serdeable, + lidarr_models::{Artist, DeleteParams, LidarrSerdeable}, + }, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_add_commands_to_the_add_command_handler() { + let expected_tag_name = "test".to_owned(); + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::AddTag(expected_tag_name.clone()).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let add_tag_command = LidarrCommand::Add(LidarrAddCommand::Tag { + name: expected_tag_name, + }); + + let result = LidarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_get_commands_to_the_get_command_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetStatus.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let get_system_status_command = LidarrCommand::Get(LidarrGetCommand::SystemStatus); + + let result = LidarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() { + let expected_delete_artist_params = DeleteParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteArtist(expected_delete_artist_params).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_artist_command = LidarrCommand::Delete(LidarrDeleteCommand::Artist { + artist_id: 1, + delete_files_from_disk: true, + add_list_exclusion: true, + }); + + let result = LidarrCliHandler::with(&app_arc, delete_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() { + let expected_edit_all_indexer_settings = IndexerSettings { + id: 1, + maximum_size: 1, + minimum_age: 1, + retention: 1, + rss_sync_interval: 1, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAllIndexerSettings.into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::IndexerSettings( + IndexerSettings { + id: 1, + maximum_size: 2, + minimum_age: 2, + retention: 2, + rss_sync_interval: 2, + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let edit_all_indexer_settings_command = + LidarrCommand::Edit(LidarrEditCommand::AllIndexerSettings { + maximum_size: Some(1), + minimum_age: Some(1), + retention: Some(1), + rss_sync_interval: Some(1), + }); + + let result = LidarrCliHandler::with( + &app_arc, + edit_all_indexer_settings_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_list_commands_to_the_list_command_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::ListArtists.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Artists(vec![ + Artist::default(), + ]))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_artists_command = LidarrCommand::List(LidarrListCommand::Artists); + + let result = LidarrCliHandler::with(&app_arc, list_artists_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::UpdateAllArtists.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let refresh_artist_command = LidarrCommand::Refresh(LidarrRefreshCommand::AllArtists); + + let result = LidarrCliHandler::with(&app_arc, refresh_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler() + { + let expected_artist_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetDiscographyReleases(expected_artist_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let manual_episode_search_command = + LidarrCommand::ManualSearch(LidarrManualSearchCommand::Discography { artist_id: 1 }); + + let result = + LidarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_lidarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler() + { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::TriggerAutomaticArtistSearch(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let trigger_automatic_search_command = + LidarrCommand::TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand::Artist { + artist_id: 1, + }); + + let result = LidarrCliHandler::with( + &app_arc, + trigger_automatic_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_clear_blocklist_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetBlocklist.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::BlocklistResponse( + BlocklistResponse { + records: vec![BlocklistItem::default()], + }, + ))) + }); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::ClearBlocklist.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let claer_blocklist_command = LidarrCommand::ClearBlocklist; + + let result = LidarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_download_release_command() { + let expected_release_download_body = LidarrReleaseDownloadBody { + guid: "guid".to_owned(), + indexer_id: 1, + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DownloadRelease(expected_release_download_body).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let download_release_command = LidarrCommand::DownloadRelease { + guid: "guid".to_owned(), + indexer_id: 1, + }; + + let result = LidarrCliHandler::with(&app_arc, download_release_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_toggle_artist_monitoring_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::ToggleArtistMonitoring(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let toggle_artist_monitoring_command = LidarrCommand::ToggleArtistMonitoring { artist_id: 1 }; + + let result = LidarrCliHandler::with( + &app_arc, + toggle_artist_monitoring_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_search_new_artist_command() { + let expected_query = "test artist".to_owned(); + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::SearchNewArtist(expected_query.clone()).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let search_new_artist_command = LidarrCommand::SearchNewArtist { + query: expected_query, + }; + + let result = LidarrCliHandler::with(&app_arc, search_new_artist_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_start_task_command() { + let expected_task_name = LidarrTaskName::ApplicationUpdateCheck; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::StartTask(expected_task_name).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let start_task_command = LidarrCommand::StartTask { + task_name: LidarrTaskName::ApplicationUpdateCheck, + }; + + let result = LidarrCliHandler::with(&app_arc, start_task_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_test_indexer_command() { + let expected_indexer_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::TestIndexer(expected_indexer_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let test_indexer_command = LidarrCommand::TestIndexer { indexer_id: 1 }; + + let result = LidarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_test_all_indexers_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::TestAllIndexers.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let test_all_indexers_command = LidarrCommand::TestAllIndexers; + + let result = LidarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_mark_history_item_as_failed_command() { + let expected_history_item_id = 1i64; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let mark_history_item_as_failed_command = LidarrCommand::MarkHistoryItemAsFailed { + history_item_id: expected_history_item_id, + }; + + let result = LidarrCliHandler::with( + &app_arc, + mark_history_item_as_failed_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs new file mode 100644 index 0000000..464acb6 --- /dev/null +++ b/src/cli/lidarr/list_command_handler.rs @@ -0,0 +1,341 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::{Subcommand, arg}; +use serde_json::json; +use tokio::sync::Mutex; + +use super::LidarrCommand; +use crate::models::Serdeable; +use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable}; +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +#[cfg(test)] +#[path = "list_command_handler_tests.rs"] +mod list_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrListCommand { + #[command(about = "List all albums for the artist with the given ID")] + Albums { + #[arg( + long, + help = "The Lidarr ID of the artist whose albums you want to list", + required = true + )] + artist_id: i64, + }, + #[command( + about = "Fetch all history events for the given album corresponding to the artist with the given ID." + )] + AlbumHistory { + #[arg( + long, + help = "The Lidarr artist ID of the artist whose history you wish to fetch and list", + required = true + )] + artist_id: i64, + #[arg( + long, + help = "The Lidarr album ID to fetch history events for", + required = true + )] + album_id: i64, + }, + #[command(about = "Fetch all history events for the artist with the given ID")] + ArtistHistory { + #[arg( + long, + help = "The Lidarr ID of the artist whose history you wish to fetch", + required = true + )] + artist_id: i64, + }, + #[command(about = "List all artists in your Lidarr library")] + Artists, + #[command(about = "List all items in the Lidarr blocklist")] + Blocklist, + #[command(about = "List all active downloads in Lidarr")] + Downloads { + #[arg(long, help = "How many downloads to fetch", default_value_t = 500)] + count: u64, + }, + #[command(about = "Fetch all Lidarr history events")] + History { + #[arg(long, help = "How many history events to fetch", default_value_t = 500)] + events: u64, + }, + #[command(about = "List all Lidarr indexers")] + Indexers, + #[command(about = "Fetch Lidarr logs")] + Logs { + #[arg(long, help = "How many log events to fetch", default_value_t = 500)] + events: u64, + #[arg( + long, + help = "Output the logs in the same format as they appear in the log files" + )] + output_in_log_format: bool, + }, + #[command(about = "List all Lidarr metadata profiles")] + MetadataProfiles, + #[command(about = "List all Lidarr quality profiles")] + QualityProfiles, + #[command(about = "List all queued events")] + QueuedEvents, + #[command(about = "List all root folders in Lidarr")] + RootFolders, + #[command(about = "List all Lidarr tags")] + Tags, + #[command(about = "List all Lidarr tasks")] + Tasks, + #[command(about = "Fetch all history events for the track with the given ID")] + TrackHistory { + #[arg( + long, + help = "The artist ID that the track belongs to", + required = true + )] + artist_id: i64, + #[arg( + long, + help = "The album ID that the track is a part of", + required = true + )] + album_id: i64, + #[arg( + long, + help = "The Lidarr ID of the track whose history you wish to fetch", + required = true + )] + track_id: i64, + }, + #[command( + about = "List the tracks for the album that corresponds to the artist with the given ID" + )] + Tracks { + #[arg( + long, + help = "The Lidarr artist ID of the artist whose tracks you wish to fetch", + required = true + )] + artist_id: i64, + #[arg( + long, + help = "The Lidarr album ID whose tracks you wish to fetch", + required = true + )] + album_id: i64, + }, + #[command(about = "List the track files for the album with the given ID")] + TrackFiles { + #[arg( + long, + help = "The Lidarr ID of the album whose track files you wish to fetch", + required = true + )] + album_id: i64, + }, + #[command(about = "List all Lidarr updates")] + Updates, +} + +impl From for Command { + fn from(value: LidarrListCommand) -> Self { + Command::Lidarr(LidarrCommand::List(value)) + } +} + +pub(super) struct LidarrListCommandHandler<'a, 'b> { + app: &'a Arc>>, + command: LidarrListCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + command: LidarrListCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrListCommandHandler { + app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrListCommand::Albums { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetAlbums(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::AlbumHistory { + artist_id, + album_id, + } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetAlbumHistory(artist_id, album_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::ArtistHistory { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetArtistHistory(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Artists => { + let resp = self + .network + .handle_network_event(LidarrEvent::ListArtists.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Blocklist => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetBlocklist.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Downloads { count } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetDownloads(count).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::History { events: items } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetHistory(items).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Indexers => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetIndexers.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Logs { + events, + output_in_log_format, + } => { + let logs = self + .network + .handle_network_event(LidarrEvent::GetLogs(events).into()) + .await?; + + if output_in_log_format { + let log_lines = &self.app.lock().await.data.sonarr_data.logs.items; + + serde_json::to_string_pretty(log_lines)? + } else { + serde_json::to_string_pretty(&logs)? + } + } + LidarrListCommand::MetadataProfiles => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetMetadataProfiles.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::QualityProfiles => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetQualityProfiles.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::QueuedEvents => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetQueuedEvents.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::RootFolders => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetRootFolders.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Tags => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetTags.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Tasks => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetTasks.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::TrackHistory { + artist_id, + album_id, + track_id, + } => { + match self + .network + .handle_network_event(LidarrEvent::GetTrackHistory(artist_id, album_id, track_id).into()) + .await + { + Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems(history_vec))) => { + let history_items_vec: Vec = history_vec + .into_iter() + .filter(|it| it.track_id == track_id) + .collect(); + serde_json::to_string_pretty(&history_items_vec)? + } + Err(e) => return Err(e), + _ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?, + } + } + LidarrListCommand::Tracks { + artist_id, + album_id, + } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetTracks(artist_id, album_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::TrackFiles { album_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetTrackFiles(album_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrListCommand::Updates => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetUpdates.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs new file mode 100644 index 0000000..35883fd --- /dev/null +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -0,0 +1,726 @@ +#[cfg(test)] +mod tests { + use crate::Cli; + use crate::cli::{ + Command, + lidarr::{LidarrCommand, list_command_handler::LidarrListCommand}, + }; + use clap::CommandFactory; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_list_command_from() { + let command = LidarrListCommand::Artists; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::List(command))); + } + + mod cli { + use super::*; + use clap::{Parser, error::ErrorKind}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_list_commands_have_no_arg_requirements( + #[values( + "artists", + "blocklist", + "indexers", + "metadata-profiles", + "quality-profiles", + "queued-events", + "tags", + "tasks", + "updates", + "root-folders" + )] + subcommand: &str, + ) { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]); + + assert_ok!(&result); + } + + #[test] + fn test_list_albums_requires_artist_id() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "albums"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_albums_with_artist_id() { + let expected_args = LidarrListCommand::Albums { artist_id: 1 }; + let result = + Cli::try_parse_from(["managarr", "lidarr", "list", "albums", "--artist-id", "1"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(album_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(album_command, expected_args); + } + + #[test] + fn test_album_history_requires_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "album-history", + "--album-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_album_history_requires_album_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "album-history", + "--artist-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_album_history_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "album-history", + "--artist-id", + "1", + "--album-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_list_artist_history_requires_artist_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "artist-history"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_artist_history_success() { + let expected_args = LidarrListCommand::ArtistHistory { artist_id: 1 }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "list", + "artist-history", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(artist_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(artist_command, expected_args); + } + + #[test] + fn test_list_downloads_count_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_downloads_default_values() { + let expected_args = LidarrListCommand::Downloads { count: 500 }; + let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(downloads_command, expected_args); + } + + #[test] + fn test_list_history_events_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "history", "--events"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_history_default_values() { + let expected_args = LidarrListCommand::History { events: 500 }; + let result = Cli::try_parse_from(["managarr", "lidarr", "list", "history"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(history_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(history_command, expected_args); + } + + #[test] + fn test_list_logs_events_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "logs", "--events"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_logs_default_values() { + let expected_args = LidarrListCommand::Logs { + events: 500, + output_in_log_format: false, + }; + let result = Cli::try_parse_from(["managarr", "lidarr", "list", "logs"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(logs_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(logs_command, expected_args); + } + + #[test] + fn test_list_track_history_requires_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "track-history", + "--album-id", + "1", + "--track-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_track_history_requires_album_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "track-history", + "--artist-id", + "1", + "--track-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_track_history_requires_track_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "track-history", + "--artist-id", + "1", + "--album-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_track_history_success() { + let expected_args = LidarrListCommand::TrackHistory { + artist_id: 1, + album_id: 1, + track_id: 1, + }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "list", + "track-history", + "--artist-id", + "1", + "--album-id", + "1", + "--track-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(track_history_command))) = + result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(track_history_command, expected_args); + } + + #[test] + fn test_list_tracks_requires_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "tracks", + "--album-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_tracks_requires_album_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "list", + "tracks", + "--artist-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_tracks_success() { + let expected_args = LidarrListCommand::Tracks { + artist_id: 1, + album_id: 1, + }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "list", + "tracks", + "--artist-id", + "1", + "--album-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(tracks_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(tracks_command, expected_args); + } + + #[test] + fn test_list_track_files_requires_album_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "track-files"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_list_track_files_success() { + let expected_args = LidarrListCommand::TrackFiles { album_id: 1 }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "list", + "track-files", + "--album-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(track_files_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(track_files_command, expected_args); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use pretty_assertions::assert_str_eq; + use rstest::rstest; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::cli::CliCommandHandler; + use crate::cli::lidarr::list_command_handler::{LidarrListCommand, LidarrListCommandHandler}; + use crate::models::Serdeable; + use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item; + use crate::{ + app::App, + network::{MockNetworkTrait, NetworkEvent}, + }; + + #[rstest] + #[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)] + #[case(LidarrListCommand::Blocklist, LidarrEvent::GetBlocklist)] + #[case(LidarrListCommand::Indexers, LidarrEvent::GetIndexers)] + #[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)] + #[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)] + #[case(LidarrListCommand::QueuedEvents, LidarrEvent::GetQueuedEvents)] + #[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)] + #[case(LidarrListCommand::Tags, LidarrEvent::GetTags)] + #[case(LidarrListCommand::Tasks, LidarrEvent::GetTasks)] + #[case(LidarrListCommand::Updates, LidarrEvent::GetUpdates)] + #[tokio::test] + async fn test_handle_list_command( + #[case] list_command: LidarrListCommand, + #[case] expected_lidarr_event: LidarrEvent, + ) { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(expected_lidarr_event.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + + let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_albums_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(LidarrEvent::GetAlbums(1).into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_command = LidarrListCommand::Albums { artist_id: 1 }; + + let result = LidarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_list_album_history_command() { + let expected_artist_id = 1; + let expected_album_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAlbumHistory(expected_artist_id, expected_album_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_album_history_command = LidarrListCommand::AlbumHistory { + artist_id: 1, + album_id: 1, + }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_album_history_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_artist_history_command() { + let expected_artist_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetArtistHistory(expected_artist_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_artist_history_command = LidarrListCommand::ArtistHistory { artist_id: 1 }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_artist_history_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_downloads_command() { + let expected_count = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetDownloads(expected_count).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_downloads_command = LidarrListCommand::Downloads { count: 1000 }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_history_command() { + let expected_events = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetHistory(expected_events).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_history_command = LidarrListCommand::History { events: 1000 }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_logs_command() { + let expected_events = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetLogs(expected_events).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_logs_command = LidarrListCommand::Logs { + events: 1000, + output_in_log_format: false, + }; + + let result = LidarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_track_history_command() { + let expected_artist_id = 1; + let expected_album_id = 1; + let expected_track_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetTrackHistory(expected_artist_id, expected_album_id, expected_track_id) + .into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::LidarrHistoryItems( + vec![ + lidarr_history_item(), + LidarrHistoryItem { + track_id: 2, + ..lidarr_history_item() + }, + ], + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_track_history_command = LidarrListCommand::TrackHistory { + artist_id: expected_artist_id, + album_id: expected_album_id, + track_id: expected_track_id, + }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_track_history_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + assert_str_eq!( + result.unwrap(), + serde_json::to_string_pretty(&[lidarr_history_item()]).unwrap() + ); + } + + #[tokio::test] + async fn test_handle_list_tracks_command() { + let expected_artist_id = 1; + let expected_album_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetTracks(expected_artist_id, expected_album_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_tracks_command = LidarrListCommand::Tracks { + artist_id: 1, + album_id: 1, + }; + + let result = LidarrListCommandHandler::with(&app_arc, list_tracks_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_list_track_files_command() { + let expected_album_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetTrackFiles(expected_album_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_track_files_command = LidarrListCommand::TrackFiles { album_id: 1 }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_track_files_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/manual_search_command_handler.rs b/src/cli/lidarr/manual_search_command_handler.rs new file mode 100644 index 0000000..6e24ece --- /dev/null +++ b/src/cli/lidarr/manual_search_command_handler.rs @@ -0,0 +1,118 @@ +use crate::app::App; +use crate::cli::lidarr::LidarrCommand; +use crate::cli::{CliCommandHandler, Command}; +use crate::models::Serdeable; +use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable}; +use crate::network::NetworkTrait; +use crate::network::lidarr_network::LidarrEvent; +use anyhow::Result; +use clap::Subcommand; +use serde_json::json; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[cfg(test)] +#[path = "manual_search_command_handler_tests.rs"] +mod manual_search_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrManualSearchCommand { + #[command( + about = "Trigger a manual search of releases for the given album corresponding to the artist with the given ID" + )] + Album { + #[arg( + long, + help = "The Lidarr ID of the artist whose releases you wish to fetch and list", + required = true + )] + artist_id: i64, + #[arg(long, help = "The Lidarr album ID to search for", required = true)] + album_id: i64, + }, + #[command( + about = "Trigger a manual search of discography releases for the given artist corresponding to the artist with the given ID." + )] + Discography { + #[arg( + long, + help = "The Lidarr ID of the artist whose discography releases you wish to fetch and list", + required = true + )] + artist_id: i64, + }, +} + +impl From for Command { + fn from(value: LidarrManualSearchCommand) -> Self { + Command::Lidarr(LidarrCommand::ManualSearch(value)) + } +} + +pub(super) struct LidarrManualSearchCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrManualSearchCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrManualSearchCommand> + for LidarrManualSearchCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + command: LidarrManualSearchCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrManualSearchCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrManualSearchCommand::Album { + artist_id, + album_id, + } => { + println!("Searching for album releases. This may take a minute..."); + match self + .network + .handle_network_event(LidarrEvent::GetAlbumReleases(artist_id, album_id).into()) + .await + { + Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => { + let albums_vec: Vec = releases_vec + .into_iter() + .filter(|release| !release.discography) + .collect(); + serde_json::to_string_pretty(&albums_vec)? + } + Err(e) => return Err(e), + _ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?, + } + } + LidarrManualSearchCommand::Discography { artist_id } => { + println!("Searching for artist discography releases. This may take a minute..."); + match self + .network + .handle_network_event(LidarrEvent::GetDiscographyReleases(artist_id).into()) + .await + { + Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(releases_vec))) => { + let discography_vec: Vec = releases_vec + .into_iter() + .filter(|release| release.discography) + .collect(); + serde_json::to_string_pretty(&discography_vec)? + } + Err(e) => return Err(e), + _ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?, + } + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/manual_search_command_handler_tests.rs b/src/cli/lidarr/manual_search_command_handler_tests.rs new file mode 100644 index 0000000..41c0948 --- /dev/null +++ b/src/cli/lidarr/manual_search_command_handler_tests.rs @@ -0,0 +1,207 @@ +#[cfg(test)] +mod tests { + use crate::cli::Command; + use crate::cli::lidarr::LidarrCommand; + use crate::cli::lidarr::manual_search_command_handler::LidarrManualSearchCommand; + use pretty_assertions::assert_eq; + + #[test] + fn test_lidarr_manual_search_command_from() { + let command = LidarrManualSearchCommand::Discography { artist_id: 1 }; + + let result = Command::from(command.clone()); + + assert_eq!( + result, + Command::Lidarr(LidarrCommand::ManualSearch(command)) + ); + } + + mod cli { + use crate::Cli; + use clap::CommandFactory; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + + #[test] + fn test_manual_album_search_requires_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "manual-search", + "album", + "--album-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_manual_album_search_requires_album_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "manual-search", + "album", + "--artist-id", + "1", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_manual_album_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "manual-search", + "album", + "--artist-id", + "1", + "--album-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_manual_discography_search_requires_artist_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "manual-search", "discography"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_manual_discography_search_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "manual-search", + "discography", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + } + } + + mod handler { + use crate::app::App; + use crate::cli::CliCommandHandler; + use crate::cli::lidarr::manual_search_command_handler::{ + LidarrManualSearchCommand, LidarrManualSearchCommandHandler, + }; + use crate::models::Serdeable; + use crate::models::lidarr_models::{LidarrRelease, LidarrSerdeable}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + torrent_release, usenet_release, + }; + use crate::network::{MockNetworkTrait, NetworkEvent}; + use mockall::predicate::eq; + use pretty_assertions::assert_str_eq; + use std::sync::Arc; + use tokio::sync::Mutex; + + #[tokio::test] + async fn test_manual_album_search_command() { + let expected_releases = [torrent_release()]; + let expected_artist_id = 1; + let expected_album_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetAlbumReleases(expected_artist_id, expected_album_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![ + torrent_release(), + LidarrRelease { + discography: true, + ..usenet_release() + }, + ]))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let manual_album_search_command = LidarrManualSearchCommand::Album { + artist_id: 1, + album_id: 1, + }; + + let result = LidarrManualSearchCommandHandler::with( + &app_arc, + manual_album_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + assert_str_eq!( + result.unwrap(), + serde_json::to_string_pretty(&expected_releases).unwrap() + ); + } + + #[tokio::test] + async fn test_manual_discography_search_command() { + let expected_releases = [LidarrRelease { + discography: true, + ..usenet_release() + }]; + let expected_artist_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetDiscographyReleases(expected_artist_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Releases(vec![ + torrent_release(), + LidarrRelease { + discography: true, + ..usenet_release() + }, + ]))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let manual_discography_search_command = + LidarrManualSearchCommand::Discography { artist_id: 1 }; + + let result = LidarrManualSearchCommandHandler::with( + &app_arc, + manual_discography_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + assert_str_eq!( + result.unwrap(), + serde_json::to_string_pretty(&expected_releases).unwrap() + ); + } + } +} diff --git a/src/cli/lidarr/mod.rs b/src/cli/lidarr/mod.rs new file mode 100644 index 0000000..bbae67f --- /dev/null +++ b/src/cli/lidarr/mod.rs @@ -0,0 +1,295 @@ +use std::sync::Arc; + +use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler}; +use anyhow::Result; +use clap::{Subcommand, arg}; +use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler}; +use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler}; +use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler}; +use list_command_handler::{LidarrListCommand, LidarrListCommandHandler}; +use refresh_command_handler::{LidarrRefreshCommand, LidarrRefreshCommandHandler}; +use serde_json::json; +use tokio::sync::Mutex; +use trigger_automatic_search_command_handler::{ + LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler, +}; + +use super::{CliCommandHandler, Command}; +use crate::cli::lidarr::manual_search_command_handler::{ + LidarrManualSearchCommand, LidarrManualSearchCommandHandler, +}; +use crate::models::lidarr_models::{LidarrReleaseDownloadBody, LidarrTaskName}; +use crate::network::lidarr_network::LidarrEvent; +use crate::{app::App, network::NetworkTrait}; + +mod add_command_handler; +mod delete_command_handler; +mod edit_command_handler; +mod get_command_handler; +mod list_command_handler; +mod manual_search_command_handler; +mod refresh_command_handler; +mod trigger_automatic_search_command_handler; + +#[cfg(test)] +#[path = "lidarr_command_tests.rs"] +mod lidarr_command_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrCommand { + #[command( + subcommand, + about = "Commands to add or create new resources within your Lidarr instance" + )] + Add(LidarrAddCommand), + #[command( + subcommand, + about = "Commands to delete resources from your Lidarr instance" + )] + Delete(LidarrDeleteCommand), + #[command( + subcommand, + about = "Commands to edit resources in your Lidarr instance" + )] + Edit(LidarrEditCommand), + #[command( + subcommand, + about = "Commands to fetch details of the resources in your Lidarr instance" + )] + Get(LidarrGetCommand), + #[command( + subcommand, + about = "Commands to list attributes from your Lidarr instance" + )] + List(LidarrListCommand), + #[command( + subcommand, + about = "Commands to refresh the data in your Lidarr instance" + )] + Refresh(LidarrRefreshCommand), + #[command(subcommand, about = "Commands to manually search for releases")] + ManualSearch(LidarrManualSearchCommand), + #[command( + subcommand, + about = "Commands to trigger automatic searches for releases of different resources in your Lidarr instance" + )] + TriggerAutomaticSearch(LidarrTriggerAutomaticSearchCommand), + #[command(about = "Clear the Lidarr blocklist")] + ClearBlocklist, + #[command(about = "Manually download the given release")] + DownloadRelease { + #[arg(long, help = "The GUID of the release to download", required = true)] + guid: String, + #[arg( + long, + help = "The indexer ID to download the release from", + required = true + )] + indexer_id: i64, + }, + #[command(about = "Mark the Lidarr history item with the given ID as 'failed'")] + MarkHistoryItemAsFailed { + #[arg( + long, + help = "The Lidarr ID of the history item you wish to mark as 'failed'", + required = true + )] + history_item_id: i64, + }, + #[command(about = "Search for a new artist to add to Lidarr")] + SearchNewArtist { + #[arg( + long, + help = "The name of the artist you want to search for", + required = true + )] + query: String, + }, + #[command(about = "Start the specified Lidarr task")] + StartTask { + #[arg( + long, + help = "The name of the task to trigger", + value_enum, + required = true + )] + task_name: LidarrTaskName, + }, + #[command( + about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'" + )] + TestIndexer { + #[arg(long, help = "The ID of the indexer to test", required = true)] + indexer_id: i64, + }, + #[command(about = "Test all Lidarr indexers")] + TestAllIndexers, + #[command( + about = "Toggle monitoring for the specified album corresponding to the given album ID" + )] + ToggleAlbumMonitoring { + #[arg( + long, + help = "The Lidarr ID of the album to toggle monitoring on", + required = true + )] + album_id: i64, + }, + #[command( + about = "Toggle monitoring for the specified artist corresponding to the given artist ID" + )] + ToggleArtistMonitoring { + #[arg( + long, + help = "The Lidarr ID of the artist to toggle monitoring on", + required = true + )] + artist_id: i64, + }, +} + +impl From for Command { + fn from(lidarr_command: LidarrCommand) -> Command { + Command::Lidarr(lidarr_command) + } +} + +pub(super) struct LidarrCliHandler<'a, 'b> { + app: &'a Arc>>, + command: LidarrCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrCommand> for LidarrCliHandler<'a, 'b> { + fn with( + app: &'a Arc>>, + command: LidarrCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrCliHandler { + app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrCommand::Add(add_command) => { + LidarrAddCommandHandler::with(self.app, add_command, self.network) + .handle() + .await? + } + LidarrCommand::Delete(delete_command) => { + LidarrDeleteCommandHandler::with(self.app, delete_command, self.network) + .handle() + .await? + } + LidarrCommand::Edit(edit_command) => { + LidarrEditCommandHandler::with(self.app, edit_command, self.network) + .handle() + .await? + } + LidarrCommand::Get(get_command) => { + LidarrGetCommandHandler::with(self.app, get_command, self.network) + .handle() + .await? + } + LidarrCommand::List(list_command) => { + LidarrListCommandHandler::with(self.app, list_command, self.network) + .handle() + .await? + } + LidarrCommand::Refresh(refresh_command) => { + LidarrRefreshCommandHandler::with(self.app, refresh_command, self.network) + .handle() + .await? + } + LidarrCommand::ManualSearch(manual_search_command) => { + LidarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network) + .handle() + .await? + } + LidarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => { + LidarrTriggerAutomaticSearchCommandHandler::with( + self.app, + trigger_automatic_search_command, + self.network, + ) + .handle() + .await? + } + LidarrCommand::ClearBlocklist => { + self + .network + .handle_network_event(LidarrEvent::GetBlocklist.into()) + .await?; + let resp = self + .network + .handle_network_event(LidarrEvent::ClearBlocklist.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::DownloadRelease { guid, indexer_id } => { + let params = LidarrReleaseDownloadBody { guid, indexer_id }; + let resp = self + .network + .handle_network_event(LidarrEvent::DownloadRelease(params).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::MarkHistoryItemAsFailed { history_item_id } => { + let _ = self + .network + .handle_network_event(LidarrEvent::MarkHistoryItemAsFailed(history_item_id).into()) + .await?; + serde_json::to_string_pretty(&json!({"message": "Lidarr history item marked as 'failed'"}))? + } + LidarrCommand::SearchNewArtist { query } => { + let resp = self + .network + .handle_network_event(LidarrEvent::SearchNewArtist(query).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::StartTask { task_name } => { + let resp = self + .network + .handle_network_event(LidarrEvent::StartTask(task_name).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::TestIndexer { indexer_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::TestIndexer(indexer_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::TestAllIndexers => { + println!("Testing all Lidarr indexers. This may take a minute..."); + let resp = self + .network + .handle_network_event(LidarrEvent::TestAllIndexers.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::ToggleAlbumMonitoring { album_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::ToggleAlbumMonitoring(album_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrCommand::ToggleArtistMonitoring { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::ToggleArtistMonitoring(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/refresh_command_handler.rs b/src/cli/lidarr/refresh_command_handler.rs new file mode 100644 index 0000000..71901ca --- /dev/null +++ b/src/cli/lidarr/refresh_command_handler.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +use super::LidarrCommand; + +#[cfg(test)] +#[path = "refresh_command_handler_tests.rs"] +mod refresh_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrRefreshCommand { + #[command(about = "Refresh all artist data for all artists in your Lidarr library")] + AllArtists, + #[command(about = "Refresh artist data and scan disk for the artist with the given ID")] + Artist { + #[arg( + long, + help = "The ID of the artist to refresh information on and to scan the disk for", + required = true + )] + artist_id: i64, + }, + #[command(about = "Refresh all downloads in Lidarr")] + Downloads, +} + +impl From for Command { + fn from(value: LidarrRefreshCommand) -> Self { + Command::Lidarr(LidarrCommand::Refresh(value)) + } +} + +pub(super) struct LidarrRefreshCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrRefreshCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand> + for LidarrRefreshCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + command: LidarrRefreshCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrRefreshCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> anyhow::Result { + let result = match self.command { + LidarrRefreshCommand::AllArtists => { + let resp = self + .network + .handle_network_event(LidarrEvent::UpdateAllArtists.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrRefreshCommand::Artist { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::UpdateAndScanArtist(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrRefreshCommand::Downloads => { + let resp = self + .network + .handle_network_event(LidarrEvent::UpdateDownloads.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/refresh_command_handler_tests.rs b/src/cli/lidarr/refresh_command_handler_tests.rs new file mode 100644 index 0000000..686c2f2 --- /dev/null +++ b/src/cli/lidarr/refresh_command_handler_tests.rs @@ -0,0 +1,138 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use crate::Cli; + use crate::cli::{ + Command, + lidarr::{LidarrCommand, refresh_command_handler::LidarrRefreshCommand}, + }; + use clap::CommandFactory; + + #[test] + fn test_lidarr_refresh_command_from() { + let command = LidarrRefreshCommand::AllArtists; + + let result = Command::from(command.clone()); + + assert_eq!(result, Command::Lidarr(LidarrCommand::Refresh(command))); + } + + mod cli { + use super::*; + use clap::{Parser, error::ErrorKind}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_refresh_commands_have_no_arg_requirements( + #[values("all-artists", "downloads")] subcommand: &str, + ) { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]); + + assert_ok!(&result); + } + + #[test] + fn test_refresh_artist_requires_artist_id() { + let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "artist"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_refresh_artist_with_artist_id() { + let expected_args = LidarrRefreshCommand::Artist { artist_id: 1 }; + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "refresh", + "artist", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + let Some(Command::Lidarr(LidarrCommand::Refresh(refresh_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(refresh_command, expected_args); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use rstest::rstest; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::{app::App, cli::lidarr::refresh_command_handler::LidarrRefreshCommandHandler}; + use crate::{ + cli::{CliCommandHandler, lidarr::refresh_command_handler::LidarrRefreshCommand}, + network::lidarr_network::LidarrEvent, + }; + use crate::{ + models::{Serdeable, lidarr_models::LidarrSerdeable}, + network::{MockNetworkTrait, NetworkEvent}, + }; + + #[rstest] + #[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)] + #[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)] + #[tokio::test] + async fn test_handle_refresh_command( + #[case] refresh_command: LidarrRefreshCommand, + #[case] expected_sonarr_event: LidarrEvent, + ) { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::(expected_sonarr_event.into())) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + + let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_refresh_artist_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::UpdateAndScanArtist(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let refresh_command = LidarrRefreshCommand::Artist { artist_id: 1 }; + + let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/lidarr/trigger_automatic_search_command_handler.rs b/src/cli/lidarr/trigger_automatic_search_command_handler.rs new file mode 100644 index 0000000..dc882de --- /dev/null +++ b/src/cli/lidarr/trigger_automatic_search_command_handler.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use anyhow::Result; +use clap::Subcommand; +use tokio::sync::Mutex; + +use crate::{ + app::App, + cli::{CliCommandHandler, Command}, + network::{NetworkTrait, lidarr_network::LidarrEvent}, +}; + +use super::LidarrCommand; + +#[cfg(test)] +#[path = "trigger_automatic_search_command_handler_tests.rs"] +mod trigger_automatic_search_command_handler_tests; + +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] +pub enum LidarrTriggerAutomaticSearchCommand { + #[command(about = "Trigger an automatic search for the album with the specified ID")] + Album { + #[arg( + long, + help = "The Lidarr ID of the album you want to trigger an automatic search for", + required = true + )] + album_id: i64, + }, + #[command(about = "Trigger an automatic search for the artist with the specified ID")] + Artist { + #[arg( + long, + help = "The ID of the artist you want to trigger an automatic search for", + required = true + )] + artist_id: i64, + }, +} + +impl From for Command { + fn from(value: LidarrTriggerAutomaticSearchCommand) -> Self { + Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(value)) + } +} + +pub(super) struct LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> { + _app: &'a Arc>>, + command: LidarrTriggerAutomaticSearchCommand, + network: &'a mut dyn NetworkTrait, +} + +impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrTriggerAutomaticSearchCommand> + for LidarrTriggerAutomaticSearchCommandHandler<'a, 'b> +{ + fn with( + _app: &'a Arc>>, + command: LidarrTriggerAutomaticSearchCommand, + network: &'a mut dyn NetworkTrait, + ) -> Self { + LidarrTriggerAutomaticSearchCommandHandler { + _app, + command, + network, + } + } + + async fn handle(self) -> Result { + let result = match self.command { + LidarrTriggerAutomaticSearchCommand::Album { album_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::TriggerAutomaticAlbumSearch(album_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + LidarrTriggerAutomaticSearchCommand::Artist { artist_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::TriggerAutomaticArtistSearch(artist_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + }; + + Ok(result) + } +} diff --git a/src/cli/lidarr/trigger_automatic_search_command_handler_tests.rs b/src/cli/lidarr/trigger_automatic_search_command_handler_tests.rs new file mode 100644 index 0000000..a3b1d4d --- /dev/null +++ b/src/cli/lidarr/trigger_automatic_search_command_handler_tests.rs @@ -0,0 +1,166 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use crate::Cli; + use crate::cli::{ + Command, + lidarr::{ + LidarrCommand, trigger_automatic_search_command_handler::LidarrTriggerAutomaticSearchCommand, + }, + }; + use clap::CommandFactory; + + #[test] + fn test_lidarr_trigger_automatic_search_command_from() { + let command = LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 }; + + let result = Command::from(command.clone()); + + assert_eq!( + result, + Command::Lidarr(LidarrCommand::TriggerAutomaticSearch(command)) + ); + } + + mod cli { + use super::*; + use clap::error::ErrorKind; + use pretty_assertions::assert_eq; + + #[test] + fn test_trigger_automatic_album_search_requires_album_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "trigger-automatic-search", + "album", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_trigger_automatic_album_search_with_album_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "trigger-automatic-search", + "album", + "--album-id", + "1", + ]); + + assert_ok!(&result); + } + + #[test] + fn test_trigger_automatic_artist_search_requires_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "trigger-automatic-search", + "artist", + ]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_trigger_automatic_artist_search_with_artist_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "lidarr", + "trigger-automatic-search", + "artist", + "--artist-id", + "1", + ]); + + assert_ok!(&result); + } + } + + mod handler { + use std::sync::Arc; + + use mockall::predicate::eq; + use serde_json::json; + use tokio::sync::Mutex; + + use crate::cli::lidarr::trigger_automatic_search_command_handler::{ + LidarrTriggerAutomaticSearchCommand, LidarrTriggerAutomaticSearchCommandHandler, + }; + use crate::{app::App, cli::CliCommandHandler}; + use crate::{ + models::{Serdeable, lidarr_models::LidarrSerdeable}, + network::{MockNetworkTrait, NetworkEvent, lidarr_network::LidarrEvent}, + }; + + #[tokio::test] + async fn test_handle_trigger_automatic_album_search_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::TriggerAutomaticAlbumSearch(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let trigger_automatic_search_command = + LidarrTriggerAutomaticSearchCommand::Album { album_id: 1 }; + + let result = LidarrTriggerAutomaticSearchCommandHandler::with( + &app_arc, + trigger_automatic_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + + #[tokio::test] + async fn test_handle_trigger_automatic_artist_search_command() { + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::TriggerAutomaticArtistSearch(1).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let trigger_automatic_search_command = + LidarrTriggerAutomaticSearchCommand::Artist { artist_id: 1 }; + + let result = LidarrTriggerAutomaticSearchCommandHandler::with( + &app_arc, + trigger_automatic_search_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8d0c6d2..872a18e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,12 +3,14 @@ use std::sync::Arc; use anyhow::Result; use clap::{Subcommand, command}; use clap_complete::Shell; +use lidarr::{LidarrCliHandler, LidarrCommand}; use radarr::{RadarrCliHandler, RadarrCommand}; use sonarr::{SonarrCliHandler, SonarrCommand}; use tokio::sync::Mutex; use crate::{app::App, network::NetworkTrait}; +pub mod lidarr; pub mod radarr; pub mod sonarr; @@ -24,6 +26,9 @@ pub enum Command { #[command(subcommand, about = "Commands for manging your Sonarr instance")] Sonarr(SonarrCommand), + #[command(subcommand, about = "Commands for manging your Lidarr instance")] + Lidarr(LidarrCommand), + #[command( arg_required_else_help = true, about = "Generate shell completions for the Managarr CLI" @@ -61,6 +66,11 @@ pub(crate) async fn handle_command( .handle() .await? } + Command::Lidarr(lidarr_command) => { + LidarrCliHandler::with(app, lidarr_command, network) + .handle() + .await? + } _ => String::new(), }; diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index f0e73bf..e30bad3 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -122,12 +122,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan title: String::new(), root_folder_path, quality_profile_id, - minimum_availability: minimum_availability.to_string(), + minimum_availability, monitored: !disable_monitoring, tags, tag_input_string: None, add_options: AddMovieOptions { - monitor: monitor.to_string(), + monitor, search_for_movie: !no_search_for_movie, }, }; diff --git a/src/cli/radarr/add_command_handler_tests.rs b/src/cli/radarr/add_command_handler_tests.rs index bf18652..70eb223 100644 --- a/src/cli/radarr/add_command_handler_tests.rs +++ b/src/cli/radarr/add_command_handler_tests.rs @@ -384,12 +384,12 @@ mod tests { title: String::new(), root_folder_path: "/test".to_owned(), quality_profile_id: 1, - minimum_availability: "released".to_owned(), + minimum_availability: MinimumAvailability::Released, monitored: false, tags: vec![1, 2], tag_input_string: None, add_options: AddMovieOptions { - monitor: "movieAndCollection".to_owned(), + monitor: MovieMonitor::MovieAndCollection, search_for_movie: false, }, }; diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index 4e613c9..15dd974 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -29,6 +29,11 @@ pub enum RadarrListCommand { }, #[command(about = "List disk space details for all provisioned root folders in Radarr")] DiskSpace, + #[command(about = "Fetch all Radarr history events")] + History { + #[arg(long, help = "How many history events to fetch", default_value_t = 500)] + events: u64, + }, #[command(about = "List all Radarr indexers")] Indexers, #[command(about = "Fetch Radarr logs")] @@ -121,6 +126,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + RadarrListCommand::History { events: items } => { + let resp = self + .network + .handle_network_event(RadarrEvent::GetHistory(items).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } RadarrListCommand::Indexers => { let resp = self .network diff --git a/src/cli/radarr/list_command_handler_tests.rs b/src/cli/radarr/list_command_handler_tests.rs index ba5f810..207d484 100644 --- a/src/cli/radarr/list_command_handler_tests.rs +++ b/src/cli/radarr/list_command_handler_tests.rs @@ -111,6 +111,29 @@ mod tests { assert_eq!(refresh_command, expected_args); } + #[test] + fn test_list_history_events_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "list", "history", "--events"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_history_default_values() { + let expected_args = RadarrListCommand::History { events: 500 }; + let result = Cli::try_parse_from(["managarr", "radarr", "list", "history"]); + + assert_ok!(&result); + + let Some(Command::Radarr(RadarrCommand::List(history_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(history_command, expected_args); + } + #[test] fn test_list_logs_default_values() { let expected_args = RadarrListCommand::Logs { @@ -233,6 +256,32 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_list_history_command() { + let expected_events = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::GetHistory(expected_events).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_history_command = RadarrListCommand::History { events: 1000 }; + + let result = + RadarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_list_logs_command() { let expected_events = 1000; diff --git a/src/cli/radarr/mod.rs b/src/cli/radarr/mod.rs index f13a587..97ada3b 100644 --- a/src/cli/radarr/mod.rs +++ b/src/cli/radarr/mod.rs @@ -64,6 +64,15 @@ pub enum RadarrCommand { Refresh(RadarrRefreshCommand), #[command(about = "Clear the blocklist")] ClearBlocklist, + #[command(about = "Mark the Radarr history item with the given ID as 'failed'")] + MarkHistoryItemAsFailed { + #[arg( + long, + help = "The Radarr ID of the history item you wish to mark as 'failed'", + required = true + )] + history_item_id: i64, + }, #[command(about = "Manually download the given release for the specified movie ID")] DownloadRelease { #[arg(long, help = "The GUID of the release to download", required = true)] @@ -208,6 +217,15 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, ' .await?; serde_json::to_string_pretty(&resp)? } + RadarrCommand::MarkHistoryItemAsFailed { history_item_id } => { + let _ = self + .network + .handle_network_event(RadarrEvent::MarkHistoryItemAsFailed(history_item_id).into()) + .await?; + serde_json::to_string_pretty( + &serde_json::json!({"message": "Radarr history item marked as 'failed'"}), + )? + } RadarrCommand::DownloadRelease { guid, indexer_id, diff --git a/src/cli/radarr/radarr_command_tests.rs b/src/cli/radarr/radarr_command_tests.rs index af2f15f..25825c1 100644 --- a/src/cli/radarr/radarr_command_tests.rs +++ b/src/cli/radarr/radarr_command_tests.rs @@ -31,6 +31,31 @@ mod tests { assert_ok!(&result); } + #[test] + fn test_mark_history_item_as_failed_requires_history_item_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "radarr", "mark-history-item-as-failed"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_mark_history_item_as_failed_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "radarr", + "mark-history-item-as-failed", + "--history-item-id", + "1", + ]); + + assert_ok!(&result); + } + #[test] fn test_download_release_requires_movie_id() { let result = Cli::command().try_get_matches_from([ @@ -327,6 +352,36 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_mark_history_item_as_failed_command() { + let expected_history_item_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + RadarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Radarr(RadarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let mark_history_item_as_failed_command = + RadarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 }; + + let result = RadarrCliHandler::with( + &app_arc, + mark_history_item_as_failed_command, + &mut mock_network, + ) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_download_release_command() { let expected_release_download_body = RadarrReleaseDownloadBody { diff --git a/src/cli/sonarr/add_command_handler.rs b/src/cli/sonarr/add_command_handler.rs index 14117e9..21ffc32 100644 --- a/src/cli/sonarr/add_command_handler.rs +++ b/src/cli/sonarr/add_command_handler.rs @@ -137,12 +137,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan root_folder_path, quality_profile_id, language_profile_id, - series_type: series_type.to_string(), + series_type, season_folder: !disable_season_folders, tags, tag_input_string: None, add_options: AddSeriesOptions { - monitor: monitor.to_string(), + monitor, search_for_cutoff_unmet_episodes: !no_search_for_series, search_for_missing_episodes: !no_search_for_series, }, diff --git a/src/cli/sonarr/add_command_handler_tests.rs b/src/cli/sonarr/add_command_handler_tests.rs index 6558ea7..1f5b54d 100644 --- a/src/cli/sonarr/add_command_handler_tests.rs +++ b/src/cli/sonarr/add_command_handler_tests.rs @@ -517,13 +517,13 @@ mod tests { root_folder_path: "/test".to_owned(), quality_profile_id: 1, language_profile_id: 1, - series_type: "anime".to_owned(), + series_type: SeriesType::Anime, monitored: false, tags: vec![1, 2], tag_input_string: None, season_folder: false, add_options: AddSeriesOptions { - monitor: "future".to_owned(), + monitor: SeriesMonitor::Future, search_for_cutoff_unmet_episodes: false, search_for_missing_episodes: false, }, diff --git a/src/cli/sonarr/edit_command_handler.rs b/src/cli/sonarr/edit_command_handler.rs index 6ba16ca..43cfc07 100644 --- a/src/cli/sonarr/edit_command_handler.rs +++ b/src/cli/sonarr/edit_command_handler.rs @@ -9,8 +9,8 @@ use crate::{ cli::{CliCommandHandler, Command, mutex_flags_or_option}, models::{ Serdeable, - servarr_models::EditIndexerParams, - sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, + servarr_models::{EditIndexerParams, IndexerSettings}, + sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable}, }, network::{NetworkTrait, sonarr_network::SonarrEvent}, }; diff --git a/src/cli/sonarr/edit_command_handler_tests.rs b/src/cli/sonarr/edit_command_handler_tests.rs index 58cc2ac..6964451 100644 --- a/src/cli/sonarr/edit_command_handler_tests.rs +++ b/src/cli/sonarr/edit_command_handler_tests.rs @@ -622,8 +622,8 @@ mod tests { }, models::{ Serdeable, - servarr_models::EditIndexerParams, - sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable}, + servarr_models::{EditIndexerParams, IndexerSettings}, + sonarr_models::{EditSeriesParams, SeriesType, SonarrSerdeable}, }, network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent}, }; diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index a2dbfeb..7bafcff 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -249,7 +249,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH } => { let resp = self .network - .handle_network_event(SonarrEvent::GetSeasonHistory((series_id, season_number)).into()) + .handle_network_event(SonarrEvent::GetSeasonHistory(series_id, season_number).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index 2c51b0f..2532157 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -543,7 +543,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetSeasonHistory((expected_series_id, expected_season_number)).into(), + SonarrEvent::GetSeasonHistory(expected_series_id, expected_season_number).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/manual_search_command_handler.rs b/src/cli/sonarr/manual_search_command_handler.rs index 22dafa2..c6b72d2 100644 --- a/src/cli/sonarr/manual_search_command_handler.rs +++ b/src/cli/sonarr/manual_search_command_handler.rs @@ -2,16 +2,18 @@ use std::sync::Arc; use anyhow::Result; use clap::Subcommand; +use serde_json::json; use tokio::sync::Mutex; +use super::SonarrCommand; +use crate::models::Serdeable; +use crate::models::sonarr_models::{SonarrRelease, SonarrSerdeable}; use crate::{ app::App, cli::{CliCommandHandler, Command}, network::{NetworkTrait, sonarr_network::SonarrEvent}, }; -use super::SonarrCommand; - #[cfg(test)] #[path = "manual_search_command_handler_tests.rs"] mod manual_search_command_handler_tests; @@ -28,7 +30,7 @@ pub enum SonarrManualSearchCommand { episode_id: i64, }, #[command( - about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues" + about = "Trigger a manual search of full-season releases (full_season: true) for the given season corresponding to the series with the given ID" )] Season { #[arg( @@ -73,22 +75,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand> let result = match self.command { SonarrManualSearchCommand::Episode { episode_id } => { println!("Searching for episode releases. This may take a minute..."); - let resp = self + match self .network .handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into()) - .await?; - serde_json::to_string_pretty(&resp)? + .await + { + Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => { + let seasons_vec: Vec = releases_vec + .into_iter() + .filter(|release| !release.full_season) + .collect(); + serde_json::to_string_pretty(&seasons_vec)? + } + Err(e) => return Err(e), + _ => serde_json::to_string_pretty(&json!({"message": "Unexpected response format"}))?, + } } SonarrManualSearchCommand::Season { series_id, season_number, } => { println!("Searching for season releases. This may take a minute..."); - let resp = self + match self .network - .handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into()) - .await?; - serde_json::to_string_pretty(&resp)? + .handle_network_event(SonarrEvent::GetSeasonReleases(series_id, season_number).into()) + .await + { + Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(releases_vec))) => { + let seasons_vec: Vec = releases_vec + .into_iter() + .filter(|release| release.full_season) + .collect(); + serde_json::to_string_pretty(&seasons_vec)? + } + Err(e) => return Err(e), + _ => serde_json::to_string_pretty(&json!({"message": "Failed to parse response"}))?, + } } }; diff --git a/src/cli/sonarr/manual_search_command_handler_tests.rs b/src/cli/sonarr/manual_search_command_handler_tests.rs index ddcb3fc..d802d47 100644 --- a/src/cli/sonarr/manual_search_command_handler_tests.rs +++ b/src/cli/sonarr/manual_search_command_handler_tests.rs @@ -108,9 +108,13 @@ mod tests { use std::sync::Arc; use mockall::predicate::eq; - use serde_json::json; + use pretty_assertions::assert_str_eq; use tokio::sync::Mutex; + use crate::models::sonarr_models::SonarrRelease; + use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ + torrent_release, usenet_release, + }; use crate::{ app::App, cli::{ @@ -134,9 +138,13 @@ mod tests { )) .times(1) .returning(|_| { - Ok(Serdeable::Sonarr(SonarrSerdeable::Value( - json!({"testResponse": "response"}), - ))) + Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![ + torrent_release(), + SonarrRelease { + full_season: true, + ..usenet_release() + }, + ]))) }); let app_arc = Arc::new(Mutex::new(App::test_default())); let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 }; @@ -150,23 +158,35 @@ mod tests { .await; assert_ok!(&result); + assert_str_eq!( + result.unwrap(), + serde_json::to_string_pretty(&[torrent_release()]).unwrap() + ); } #[tokio::test] async fn test_manual_season_search_command() { + let expected_release = SonarrRelease { + full_season: true, + ..usenet_release() + }; let expected_series_id = 1; let expected_season_number = 1; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::GetSeasonReleases((expected_series_id, expected_season_number)).into(), + SonarrEvent::GetSeasonReleases(expected_series_id, expected_season_number).into(), )) .times(1) .returning(|_| { - Ok(Serdeable::Sonarr(SonarrSerdeable::Value( - json!({"testResponse": "response"}), - ))) + Ok(Serdeable::Sonarr(SonarrSerdeable::Releases(vec![ + torrent_release(), + SonarrRelease { + full_season: true, + ..usenet_release() + }, + ]))) }); let app_arc = Arc::new(Mutex::new(App::test_default())); let manual_season_search_command = SonarrManualSearchCommand::Season { @@ -183,6 +203,10 @@ mod tests { .await; assert_ok!(&result); + assert_str_eq!( + result.unwrap(), + serde_json::to_string_pretty(&[expected_release]).unwrap() + ); } } } diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 2a3c3c3..721226a 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -10,6 +10,7 @@ use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler}; use list_command_handler::{SonarrListCommand, SonarrListCommandHandler}; use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler}; use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler}; +use serde_json::json; use tokio::sync::Mutex; use trigger_automatic_search_command_handler::{ SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler, @@ -251,7 +252,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .network .handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into()) .await?; - "Sonarr history item marked as 'failed'".to_owned() + serde_json::to_string_pretty(&json!({"message": "Sonarr history item marked as 'failed'"}))? } SonarrCommand::SearchNewSeries { query } => { let resp = self @@ -296,7 +297,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' let resp = self .network .handle_network_event( - SonarrEvent::ToggleSeasonMonitoring((series_id, season_number)).into(), + SonarrEvent::ToggleSeasonMonitoring(series_id, season_number).into(), ) .await?; serde_json::to_string_pretty(&resp)? diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index ac48082..48d5325 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -266,9 +266,10 @@ mod tests { }, models::{ Serdeable, + servarr_models::IndexerSettings, sonarr_models::{ - BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody, - SonarrSerdeable, SonarrTaskName, + BlocklistItem, BlocklistResponse, Series, SonarrReleaseDownloadBody, SonarrSerdeable, + SonarrTaskName, }, }, network::{MockNetworkTrait, NetworkEvent, sonarr_network::SonarrEvent}, @@ -754,7 +755,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::ToggleSeasonMonitoring((expected_series_id, expected_season_number)).into(), + SonarrEvent::ToggleSeasonMonitoring(expected_series_id, expected_season_number).into(), )) .times(1) .returning(|_| { diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler.rs b/src/cli/sonarr/trigger_automatic_search_command_handler.rs index 89bf285..dd50589 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler.rs @@ -94,7 +94,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand> let resp = self .network .handle_network_event( - SonarrEvent::TriggerAutomaticSeasonSearch((series_id, season_number)).into(), + SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number).into(), ) .await?; serde_json::to_string_pretty(&resp)? diff --git a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs index c0054af..b50cd49 100644 --- a/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs +++ b/src/cli/sonarr/trigger_automatic_search_command_handler_tests.rs @@ -197,7 +197,7 @@ mod tests { mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::TriggerAutomaticSeasonSearch((expected_series_id, expected_season_number)) + SonarrEvent::TriggerAutomaticSeasonSearch(expected_series_id, expected_season_number) .into(), )) .times(1) diff --git a/src/handlers/handler_proptest.rs b/src/handlers/handler_proptest.rs index 83fe8d6..1e3df75 100644 --- a/src/handlers/handler_proptest.rs +++ b/src/handlers/handler_proptest.rs @@ -4,13 +4,12 @@ mod property_tests { use crate::app::App; use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*; + use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::StatefulTable; - use crate::models::radarr_models::Movie; - use crate::models::{Scrollable, Paginated}; + use crate::models::{Paginated, Scrollable}; proptest! { - /// Property test: Table never panics on index selection #[test] fn test_table_index_selection_safety( list_size in list_size(), @@ -25,19 +24,15 @@ mod property_tests { table.set_items(movies); - // Try to select an arbitrary index if index < list_size { table.select_index(Some(index)); let selected = table.current_selection(); prop_assert_eq!(selected.id, index as i64); } else { - // Out of bounds selection should be safe table.select_index(Some(index)); - // Should not panic, selection stays valid } } - /// Property test: Table state remains consistent after scroll operations #[test] fn test_table_scroll_consistency( list_size in list_size(), @@ -53,42 +48,34 @@ mod property_tests { table.set_items(movies); let initial_id = table.current_selection().id; - // Scroll down multiple times for _ in 0..scroll_amount { table.scroll_down(); } let after_down_id = table.current_selection().id; - // Position should increase (up to max) prop_assert!(after_down_id >= initial_id); prop_assert!(after_down_id < list_size as i64); - // Scroll back up for _ in 0..scroll_amount { table.scroll_up(); } - // Should return to initial position (or 0 if we hit the top) prop_assert!(table.current_selection().id <= initial_id); } - /// Property test: Empty tables handle operations gracefully #[test] fn test_empty_table_safety(_scroll_ops in 0usize..50) { let table = StatefulTable::::default(); - // Empty table operations should be safe prop_assert!(table.is_empty()); prop_assert!(table.items.is_empty()); } - /// Property test: Navigation operations maintain consistency #[test] fn test_navigation_consistency(pushes in 1usize..20) { let mut app = App::test_default(); let initial_route = app.get_current_route(); - // Push multiple routes let routes = vec![ ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections, @@ -101,34 +88,27 @@ mod property_tests { app.push_navigation_stack(route.into()); } - // Current route should be the last pushed let last_pushed = routes[(pushes - 1) % routes.len()]; prop_assert_eq!(app.get_current_route(), last_pushed.into()); - // Pop all routes for _ in 0..pushes { app.pop_navigation_stack(); } - // Should return to initial route prop_assert_eq!(app.get_current_route(), initial_route); } - /// Property test: String input handling is safe #[test] fn test_string_input_safety(input in text_input_string()) { - // String operations should never panic let _lowercase = input.to_lowercase(); let _uppercase = input.to_uppercase(); let _trimmed = input.trim(); let _len = input.len(); let _chars: Vec = input.chars().collect(); - // All operations completed without panic prop_assert!(true); } - /// Property test: Table maintains data integrity after operations #[test] fn test_table_data_integrity( list_size in 1usize..100 @@ -144,16 +124,13 @@ mod property_tests { table.set_items(movies.clone()); let original_count = table.items.len(); - // Count should remain the same after various operations prop_assert_eq!(table.items.len(), original_count); - // All original items should still be present for movie in &movies { prop_assert!(table.items.iter().any(|m| m.id == movie.id)); } } - /// Property test: Page up/down maintains bounds #[test] fn test_page_navigation_bounds( list_size in list_size(), @@ -168,7 +145,6 @@ mod property_tests { table.set_items(movies); - // Perform page operations for i in 0..page_ops { if i % 2 == 0 { table.page_down(); @@ -176,14 +152,12 @@ mod property_tests { table.page_up(); } - // Should never exceed bounds let current = table.current_selection(); prop_assert!(current.id >= 0); prop_assert!(current.id < list_size as i64); } } - /// Property test: Table filtering reduces or maintains size #[test] fn test_table_filter_size_invariant( list_size in list_size(), @@ -200,7 +174,6 @@ mod property_tests { table.set_items(movies.clone()); let original_size = table.items.len(); - // Apply filter if !filter_term.is_empty() { let filtered: Vec = movies.into_iter() .filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase())) @@ -208,10 +181,8 @@ mod property_tests { table.set_items(filtered); } - // Filtered size should be <= original prop_assert!(table.items.len() <= original_size); - // Selection should still be valid if table not empty if !table.items.is_empty() { let current = table.current_selection(); prop_assert!(current.id >= 0); diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index f3348c4..592f7c6 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -330,90 +330,7 @@ mod test_utils { #[macro_export] macro_rules! test_handler_delegation { ($handler:ident, $base:expr, $active_block:expr) => { - let mut app = App::test_default(); - app.data.sonarr_data.history.set_items(vec![ - $crate::models::sonarr_models::SonarrHistoryItem::default(), - ]); - app - .data - .sonarr_data - .root_folders - .set_items(vec![$crate::models::servarr_models::RootFolder::default()]); - app - .data - .sonarr_data - .indexers - .set_items(vec![$crate::models::servarr_models::Indexer::default()]); - app - .data - .sonarr_data - .blocklist - .set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]); - app.data.sonarr_data.add_searched_series = - Some($crate::models::stateful_table::StatefulTable::default()); - app - .data - .radarr_data - .movies - .set_items(vec![$crate::models::radarr_models::Movie::default()]); - app - .data - .radarr_data - .collections - .set_items(vec![$crate::models::radarr_models::Collection::default()]); - app.data.radarr_data.collection_movies.set_items(vec![ - $crate::models::radarr_models::CollectionMovie::default(), - ]); - app - .data - .radarr_data - .indexers - .set_items(vec![$crate::models::servarr_models::Indexer::default()]); - app - .data - .radarr_data - .root_folders - .set_items(vec![$crate::models::servarr_models::RootFolder::default()]); - app - .data - .radarr_data - .blocklist - .set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]); - app.data.radarr_data.add_searched_movies = - Some($crate::models::stateful_table::StatefulTable::default()); - let mut movie_details_modal = - $crate::models::servarr_data::radarr::modals::MovieDetailsModal::default(); - movie_details_modal.movie_history.set_items(vec![ - $crate::models::radarr_models::MovieHistoryItem::default(), - ]); - movie_details_modal - .movie_cast - .set_items(vec![$crate::models::radarr_models::Credit::default()]); - movie_details_modal - .movie_crew - .set_items(vec![$crate::models::radarr_models::Credit::default()]); - movie_details_modal - .movie_releases - .set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - let mut season_details_modal = - $crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default(); - season_details_modal.season_history.set_items(vec![ - $crate::models::sonarr_models::SonarrHistoryItem::default(), - ]); - season_details_modal.episode_details_modal = - Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default()); - app.data.sonarr_data.season_details_modal = Some(season_details_modal); - let mut series_history = $crate::models::stateful_table::StatefulTable::default(); - series_history.set_items(vec![ - $crate::models::sonarr_models::SonarrHistoryItem::default(), - ]); - app.data.sonarr_data.series_history = Some(series_history); - app - .data - .sonarr_data - .series - .set_items(vec![$crate::models::sonarr_models::Series::default()]); + let mut app = App::test_default_fully_populated(); app.push_navigation_stack($base.into()); app.push_navigation_stack($active_block.into()); diff --git a/src/handlers/handlers_tests.rs b/src/handlers/handlers_tests.rs index 650511c..819921b 100644 --- a/src/handlers/handlers_tests.rs +++ b/src/handlers/handlers_tests.rs @@ -21,6 +21,7 @@ mod tests { 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_models::KeybindingItem; @@ -60,11 +61,16 @@ mod tests { } #[rstest] - #[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)] - #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)] - fn test_handle_change_tabs(#[case] index: usize, #[case] left_block: T, #[case] right_block: T) - where + #[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)] + #[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)] + #[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)] + fn test_handle_change_tabs( + #[case] index: usize, + #[case] left_block: T, + #[case] right_block: U, + ) where T: Into + Copy, + U: Into + Copy, { let mut app = App::test_default(); app.error = "Test".into(); diff --git a/src/handlers/keybinding_handler.rs b/src/handlers/keybinding_handler.rs index b9560a0..6965b1e 100644 --- a/src/handlers/keybinding_handler.rs +++ b/src/handlers/keybinding_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; use crate::models::servarr_data::ActiveKeybindingBlock; #[cfg(test)] @@ -75,7 +76,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandle self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/lidarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/lidarr_handlers/blocklist/blocklist_handler_tests.rs new file mode 100644 index 0000000..eda8e80 --- /dev/null +++ b/src/handlers/lidarr_handlers/blocklist/blocklist_handler_tests.rs @@ -0,0 +1,615 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::blocklist::{BlocklistHandler, blocklist_sorting_options}; + use crate::models::lidarr_models::{Artist, BlocklistItem}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS}; + use crate::models::servarr_models::{Quality, QualityWrapper}; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_blocklist_item_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteBlocklistItemPrompt.into()); + } + + #[test] + fn test_delete_blocklist_item_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(2); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Downloads.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into()); + } + + #[rstest] + fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(2); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::History.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::History.into()); + } + + #[rstest] + fn test_blocklist_left_right_prompt_toggle( + #[values( + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + BlocklistHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + BlocklistHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_blocklist_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::BlocklistItemDetails.into()); + } + + #[test] + fn test_blocklist_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into()); + } + + #[rstest] + #[case( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + LidarrEvent::DeleteBlocklistItem(3) + )] + #[case( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt, + LidarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + + #[rstest] + fn test_blocklist_prompt_decline_submit( + #[values( + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt + )] + prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::Blocklist.into()); + } + } + + mod test_handle_esc { + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::DeleteBlocklistItemPrompt + )] + #[case( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt + )] + fn test_blocklist_prompt_blocks_esc( + #[case] base_block: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.lidarr_data.prompt_confirm = true; + + BlocklistHandler::new(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert_navigation_popped!(app, base_block.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_esc_blocklist_item_details() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveLidarrBlock::BlocklistItemDetails.into()); + + BlocklistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::BlocklistItemDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + BlocklistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Blocklist, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Blocklist.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::{assert_navigation_popped, assert_navigation_pushed}; + + #[test] + fn test_refresh_blocklist_key() { + let mut app = App::test_default(); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::Blocklist.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_blocklist_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_clear_blocklist_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.clear.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::BlocklistClearAllItemsPrompt.into()); + } + + #[test] + fn test_clear_blocklist_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.clear.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Blocklist.into()); + } + + #[rstest] + #[case( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + LidarrEvent::DeleteBlocklistItem(3) + )] + #[case( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt, + LidarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + } + + #[test] + fn test_blocklist_sorting_options_artist_name() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.artist + .artist_name + .text + .to_lowercase() + .cmp(&b.artist.artist_name.text.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[0].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Artist Name"); + } + + #[test] + fn test_blocklist_sorting_options_source_title() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[1].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_blocklist_sorting_options_quality() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[2].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_blocklist_sorting_options_date() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[3].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_blocklist_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_lidarr_block) { + assert!(BlocklistHandler::accepts(active_lidarr_block)); + } else { + assert!(!BlocklistHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_blocklist_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_blocklist_item_id() { + let mut app = App::test_default(); + app.data.lidarr_data.blocklist.set_items(blocklist_vec()); + + let blocklist_item_id = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ) + .extract_blocklist_item_id(); + + assert_eq!(blocklist_item_id, 3); + } + + #[test] + fn test_blocklist_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.is_loading = true; + + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_blocklist_handler_not_ready_when_blocklist_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.is_loading = false; + + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + app.is_loading = false; + app + .data + .lidarr_data + .blocklist + .set_items(vec![BlocklistItem::default()]); + + let handler = BlocklistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Blocklist, + None, + ); + + assert!(handler.is_ready()); + } + + fn blocklist_vec() -> Vec { + vec![ + BlocklistItem { + id: 3, + source_title: "test 1".to_owned(), + quality: QualityWrapper { + quality: Quality { + name: "Lossless".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + artist: Artist { + artist_name: "test 3".into(), + ..artist() + }, + ..BlocklistItem::default() + }, + BlocklistItem { + id: 2, + source_title: "test 2".to_owned(), + quality: QualityWrapper { + quality: Quality { + name: "Lossy".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + artist: Artist { + artist_name: "test 2".into(), + ..artist() + }, + ..BlocklistItem::default() + }, + BlocklistItem { + id: 1, + source_title: "test 3".to_owned(), + quality: QualityWrapper { + quality: Quality { + name: "Lossless".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + artist: Artist { + artist_name: "".into(), + ..artist() + }, + ..BlocklistItem::default() + }, + ] + } +} diff --git a/src/handlers/lidarr_handlers/blocklist/mod.rs b/src/handlers/lidarr_handlers/blocklist/mod.rs new file mode 100644 index 0000000..c2f2276 --- /dev/null +++ b/src/handlers/lidarr_handlers/blocklist/mod.rs @@ -0,0 +1,222 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::Route; +use crate::models::lidarr_models::BlocklistItem; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS}; +use crate::models::stateful_table::SortOption; +use crate::network::lidarr_network::LidarrEvent; + +#[cfg(test)] +#[path = "blocklist_handler_tests.rs"] +mod blocklist_handler_tests; + +pub(super) struct BlocklistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl BlocklistHandler<'_, '_> { + fn extract_blocklist_item_id(&self) -> i64 { + self.app.data.lidarr_data.blocklist.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for BlocklistHandler<'a, 'b> { + fn handle(&mut self) { + let blocklist_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::Blocklist.into()) + .sorting_block(ActiveLidarrBlock::BlocklistSortPrompt.into()) + .sort_options(blocklist_sorting_options()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.blocklist, + blocklist_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + BLOCKLIST_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> Self { + BlocklistHandler { + key, + app, + active_lidarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.blocklist.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Blocklist { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteBlocklistItemPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteBlocklistItemPrompt + | ActiveLidarrBlock::BlocklistClearAllItemsPrompt => handle_prompt_toggle(self.app, self.key), + _ => {} + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteBlocklistItemPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::DeleteBlocklistItem( + self.extract_blocklist_item_id(), + )); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::BlocklistClearAllItemsPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::ClearBlocklist); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::Blocklist => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::BlocklistItemDetails.into()); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteBlocklistItemPrompt + | ActiveLidarrBlock::BlocklistClearAllItemsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::BlocklistItemDetails | ActiveLidarrBlock::BlocklistSortPrompt => { + self.app.pop_navigation_stack(); + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::Blocklist => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(clear, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::BlocklistClearAllItemsPrompt.into()); + } + _ => (), + }, + ActiveLidarrBlock::DeleteBlocklistItemPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::DeleteBlocklistItem( + self.extract_blocklist_item_id(), + )); + + self.app.pop_navigation_stack(); + } + } + ActiveLidarrBlock::BlocklistClearAllItemsPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::ClearBlocklist); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +fn blocklist_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Artist Name", + cmp_fn: Some(|a, b| { + a.artist + .artist_name + .text + .to_lowercase() + .cmp(&b.artist.artist_name.text.to_lowercase()) + }), + }, + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs new file mode 100644 index 0000000..9c65988 --- /dev/null +++ b/src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs @@ -0,0 +1,481 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::downloads::DownloadsHandler; + use crate::models::lidarr_models::DownloadRecord; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_download_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteDownloadPrompt.into()); + } + + #[test] + fn test_delete_download_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(1); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Artists.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(1); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Blocklist.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_downloads_left_right_prompt_toggle( + #[values( + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use rstest::rstest; + + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + LidarrEvent::DeleteDownload(1) + )] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::UpdateDownloadsPrompt, + LidarrEvent::UpdateDownloads + )] + fn test_downloads_prompt_confirm_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + + #[rstest] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)] + fn test_downloads_prompt_decline_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, base_route.into()); + } + } + + mod test_handle_esc { + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)] + fn test_downloads_prompt_blocks_esc( + #[case] base_block: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.lidarr_data.prompt_confirm = true; + + DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert_navigation_popped!(app, base_block.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + DownloadsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Downloads.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_update_downloads_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateDownloadsPrompt.into()); + } + + #[test] + fn test_update_downloads_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into()); + } + + #[test] + fn test_refresh_downloads_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_downloads_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into()); + assert!(!app.should_refresh); + } + + #[rstest] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + LidarrEvent::DeleteDownload(1) + )] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::UpdateDownloadsPrompt, + LidarrEvent::UpdateDownloads + )] + fn test_downloads_prompt_confirm_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + } + + #[test] + fn test_downloads_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) { + assert!(DownloadsHandler::accepts(active_lidarr_block)); + } else { + assert!(!DownloadsHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_downloads_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_download_id() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + + let download_id = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .extract_download_id(); + + assert_eq!(download_id, 1); + } + + #[test] + fn test_downloads_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = true; + + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_downloads_handler_not_ready_when_downloads_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = false; + + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = false; + + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/downloads/mod.rs b/src/handlers/lidarr_handlers/downloads/mod.rs new file mode 100644 index 0000000..eaf8f95 --- /dev/null +++ b/src/handlers/lidarr_handlers/downloads/mod.rs @@ -0,0 +1,171 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; +use crate::network::lidarr_network::LidarrEvent; + +#[cfg(test)] +#[path = "downloads_handler_tests.rs"] +mod downloads_handler_tests; + +pub(super) struct DownloadsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl DownloadsHandler<'_, '_> { + fn extract_download_id(&self) -> i64 { + self.app.data.lidarr_data.downloads.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DownloadsHandler<'a, 'b> { + fn handle(&mut self) { + let download_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::Downloads.into()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.downloads, + download_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + DOWNLOADS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> DownloadsHandler<'a, 'b> { + DownloadsHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.downloads.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Downloads { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteDownloadPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => { + handle_prompt_toggle(self.app, self.key) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteDownloadPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteDownload(self.extract_download_id())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::UpdateDownloadsPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::Downloads => match self.key { + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::UpdateDownloadsPrompt.into()); + } + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + }, + ActiveLidarrBlock::DeleteDownloadPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteDownload(self.extract_download_id())); + + self.app.pop_navigation_stack(); + } + } + ActiveLidarrBlock::UpdateDownloadsPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/history/history_handler_tests.rs b/src/handlers/lidarr_handlers/history/history_handler_tests.rs new file mode 100644 index 0000000..75c4e6f --- /dev/null +++ b/src/handlers/lidarr_handlers/history/history_handler_tests.rs @@ -0,0 +1,397 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::history::{HistoryHandler, history_sorting_options}; + use crate::models::lidarr_models::{LidarrHistoryEventType, LidarrHistoryItem}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, HISTORY_BLOCKS}; + use crate::models::servarr_models::{Quality, QualityWrapper}; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_history_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(3); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Blocklist.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_history_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(3); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_history_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + + HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::History, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::HistoryItemDetails.into()); + } + + #[test] + fn test_history_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + + HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_esc_history_item_details() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .history + .set_items(vec![LidarrHistoryItem::default()]); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.push_navigation_stack(ActiveLidarrBlock::HistoryItemDetails.into()); + + HistoryHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::HistoryItemDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::History.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app + .data + .lidarr_data + .history + .set_items(vec![LidarrHistoryItem::default()]); + + HistoryHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + use crate::assert_navigation_pushed; + + #[test] + fn test_refresh_history_key() { + let mut app = App::test_default(); + app.data.lidarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::History, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::History.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_history_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into()); + assert!(!app.should_refresh); + } + } + + #[test] + fn test_history_sorting_options_source_title() { + let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = |a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[0].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_history_sorting_options_event_type() { + let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = |a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[1].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Event Type"); + } + + #[test] + fn test_history_sorting_options_quality() { + let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[2].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_history_sorting_options_date() { + let expected_cmp_fn: fn(&LidarrHistoryItem, &LidarrHistoryItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[3].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_history_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if HISTORY_BLOCKS.contains(&active_lidarr_block) { + assert!(HistoryHandler::accepts(active_lidarr_block)); + } else { + assert!(!HistoryHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_history_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_history_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.is_loading = true; + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_not_ready_when_history_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.is_loading = false; + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + app.is_loading = false; + app + .data + .lidarr_data + .history + .set_items(vec![LidarrHistoryItem::default()]); + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::History, + None, + ); + + assert!(handler.is_ready()); + } + + fn history_vec() -> Vec { + vec![ + LidarrHistoryItem { + id: 3, + source_title: "test 1".into(), + event_type: LidarrHistoryEventType::Grabbed, + quality: QualityWrapper { + quality: Quality { + name: "FLAC".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + ..LidarrHistoryItem::default() + }, + LidarrHistoryItem { + id: 2, + source_title: "test 2".into(), + event_type: LidarrHistoryEventType::DownloadImported, + quality: QualityWrapper { + quality: Quality { + name: "MP3-320".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + ..LidarrHistoryItem::default() + }, + LidarrHistoryItem { + id: 1, + source_title: "test 3".into(), + event_type: LidarrHistoryEventType::TrackFileDeleted, + quality: QualityWrapper { + quality: Quality { + name: "FLAC".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + ..LidarrHistoryItem::default() + }, + ] + } +} diff --git a/src/handlers/lidarr_handlers/history/mod.rs b/src/handlers/lidarr_handlers/history/mod.rs new file mode 100644 index 0000000..11a2ea7 --- /dev/null +++ b/src/handlers/lidarr_handlers/history/mod.rs @@ -0,0 +1,165 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors}; +use crate::matches_key; +use crate::models::Route; +use crate::models::lidarr_models::LidarrHistoryItem; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, HISTORY_BLOCKS}; +use crate::models::stateful_table::SortOption; + +#[cfg(test)] +#[path = "history_handler_tests.rs"] +mod history_handler_tests; + +pub(super) struct HistoryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl HistoryHandler<'_, '_> {} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for HistoryHandler<'a, 'b> { + fn handle(&mut self) { + let history_table_handling_config = TableHandlingConfig::new(ActiveLidarrBlock::History.into()) + .sorting_block(ActiveLidarrBlock::HistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveLidarrBlock::SearchHistory.into()) + .search_error_block(ActiveLidarrBlock::SearchHistoryError.into()) + .search_field_fn(|history| &history.source_title.text) + .filtering_block(ActiveLidarrBlock::FilterHistory.into()) + .filter_error_block(ActiveLidarrBlock::FilterHistoryError.into()) + .filter_field_fn(|history| &history.source_title.text); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.history, + history_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + HISTORY_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> Self { + HistoryHandler { + key, + app, + active_lidarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.history.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::History { + handle_change_tab_left_right_keys(self.app, self.key) + } + } + + fn handle_submit(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::History { + self + .app + .push_navigation_stack(ActiveLidarrBlock::HistoryItemDetails.into()); + } + } + + fn handle_esc(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::HistoryItemDetails { + self.app.pop_navigation_stack(); + } else { + handle_clear_errors(self.app); + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + if self.active_lidarr_block == ActiveLidarrBlock::History { + match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + } + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +pub(in crate::handlers::lidarr_handlers) fn history_sorting_options() +-> Vec> { + vec![ + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }), + }, + SortOption { + name: "Event Type", + cmp_fn: Some(|a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs new file mode 100644 index 0000000..a1aea6e --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler.rs @@ -0,0 +1,533 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::EditIndexerParams; +use crate::network::lidarr_network::LidarrEvent; +use crate::{ + handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, +}; + +#[cfg(test)] +#[path = "edit_indexer_handler_tests.rs"] +mod edit_indexer_handler_tests; + +pub(super) struct EditIndexerHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl EditIndexerHandler<'_, '_> { + fn build_edit_indexer_params(&mut self) -> EditIndexerParams { + let edit_indexer_modal = self + .app + .data + .lidarr_data + .edit_indexer_modal + .take() + .expect("EditIndexerModal is None"); + let indexer_id = self.app.data.lidarr_data.indexers.current_selection().id; + let tags = edit_indexer_modal.tags.text; + let EditIndexerModal { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + priority, + .. + } = edit_indexer_modal; + + EditIndexerParams { + indexer_id, + name: Some(name.text), + enable_rss, + enable_automatic_search, + enable_interactive_search, + url: Some(url.text), + api_key: Some(api_key.text), + seed_ratio: Some(seed_ratio.text), + tags: None, + tag_input_string: Some(tags), + priority: Some(priority), + clear_tags: false, + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditIndexerHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + EDIT_INDEXER_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> EditIndexerHandler<'a, 'b> { + EditIndexerHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.edit_indexer_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + self.app.data.lidarr_data.selected_block.up(); + } + ActiveLidarrBlock::EditIndexerPriorityInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .priority += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + self.app.data.lidarr_data.selected_block.down(); + } + ActiveLidarrBlock::EditIndexerPriorityInput => { + let edit_indexer_modal = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + if edit_indexer_modal.priority > 1 { + edit_indexer_modal.priority -= 1; + } + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerNameInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .scroll_home(); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .scroll_home(); + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerNameInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .reset_offset(); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .reset_offset(); + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + handle_prompt_left_right_keys!( + self, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + lidarr_data + ); + } + ActiveLidarrBlock::EditIndexerNameInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + let selected_block = self.app.data.lidarr_data.selected_block.get_active_block(); + match selected_block { + ActiveLidarrBlock::EditIndexerConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.edit_indexer_modal = None; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::EditIndexerNameInput + | ActiveLidarrBlock::EditIndexerUrlInput + | ActiveLidarrBlock::EditIndexerApiKeyInput + | ActiveLidarrBlock::EditIndexerSeedRatioInput + | ActiveLidarrBlock::EditIndexerTagsInput => { + self.app.push_navigation_stack(selected_block.into()); + self.app.ignore_special_keys_for_textbox_input = true; + } + ActiveLidarrBlock::EditIndexerPriorityInput => self + .app + .push_navigation_stack(ActiveLidarrBlock::EditIndexerPriorityInput.into()), + ActiveLidarrBlock::EditIndexerToggleEnableRss => { + let indexer = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default()); + } + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch => { + let indexer = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_automatic_search = + Some(!indexer.enable_automatic_search.unwrap_or_default()); + } + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch => { + let indexer = self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_interactive_search = + Some(!indexer.enable_interactive_search.unwrap_or_default()); + } + _ => (), + } + } + ActiveLidarrBlock::EditIndexerNameInput + | ActiveLidarrBlock::EditIndexerUrlInput + | ActiveLidarrBlock::EditIndexerApiKeyInput + | ActiveLidarrBlock::EditIndexerSeedRatioInput + | ActiveLidarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + self.app.data.lidarr_data.edit_indexer_modal = None; + } + ActiveLidarrBlock::EditIndexerNameInput + | ActiveLidarrBlock::EditIndexerUrlInput + | ActiveLidarrBlock::EditIndexerApiKeyInput + | ActiveLidarrBlock::EditIndexerSeedRatioInput + | ActiveLidarrBlock::EditIndexerPriorityInput + | ActiveLidarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditIndexerNameInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveLidarrBlock::EditIndexerUrlInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveLidarrBlock::EditIndexerApiKeyInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveLidarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveLidarrBlock::EditIndexerTagsInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + ActiveLidarrBlock::EditIndexerPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::EditIndexerConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditIndexer(self.build_edit_indexer_params())); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs new file mode 100644 index 0000000..6dd6d67 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -0,0 +1,1916 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_modal_present; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS}; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_models::EditIndexerParams; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_scroll_up_and_down { + use crate::app::App; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::servarr_data::modals::EditIndexerModal; + + use super::*; + + #[rstest] + fn test_edit_indexer_priority_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 2 + ); + } else { + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + + EditIndexerHandler::new( + Key::Up, + &mut app, + ActiveLidarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 2 + ); + + EditIndexerHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + } + } + + #[rstest] + fn test_edit_indexer_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerNameInput + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch + ); + } + } + + #[rstest] + fn test_edit_indexer_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerToggleEnableRss + ); + } + } + + mod test_handle_home_end { + use std::sync::atomic::Ordering; + + use crate::app::App; + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_edit_indexer_name_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + #[case( + 0, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerSeedRatioInput + )] + #[case( + 3, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerTagsInput + )] + fn test_left_right_block_toggle_torrents( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + #[case( + 0, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerTagsInput + )] + #[case( + 3, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput + )] + fn test_left_right_block_toggle_nzb( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + fn test_left_right_block_toggle_torren_empty_row_to_prompt_confirm( + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = 4; + app.data.lidarr_data.prompt_confirm = false; + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerPriorityInput + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerConfirmPrompt + ); + + EditIndexerHandler::new(key, &mut app, ActiveLidarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditIndexerConfirmPrompt + ); + assert!(app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_edit_indexer_name_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::app::App; + use crate::assert_navigation_popped; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert_none!(app.data.lidarr_data.edit_indexer_modal); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.lidarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + app.data.lidarr_data.prompt_confirm = true; + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_modal_absent!(app.data.lidarr_data.edit_indexer_modal); + assert!(app.should_refresh); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + ); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.prompt_confirm = true; + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert_modal_present!(app.data.lidarr_data.edit_indexer_modal); + assert!(!app.should_refresh); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[rstest] + #[case(0, 0, ActiveLidarrBlock::EditIndexerNameInput)] + #[case(0, 1, ActiveLidarrBlock::EditIndexerUrlInput)] + #[case(1, 1, ActiveLidarrBlock::EditIndexerApiKeyInput)] + #[case(2, 1, ActiveLidarrBlock::EditIndexerSeedRatioInput)] + #[case(3, 1, ActiveLidarrBlock::EditIndexerTagsInput)] + fn test_edit_indexer_prompt_submit_input_fields( + #[case] starting_y: usize, + #[case] starting_x: usize, + #[case] block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(starting_x, starting_y); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, block.into()); + assert!(app.ignore_special_keys_for_textbox_input); + } + + #[test] + fn test_edit_indexer_priority_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 4); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::EditIndexerPriorityInput.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[test] + fn test_edit_indexer_toggle_enable_rss_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 1); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap() + ); + } + + #[test] + fn test_edit_indexer_toggle_enable_automatic_search_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 2); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap() + ); + } + + #[test] + fn test_edit_indexer_toggle_enable_interactive_search_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, 3); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap() + ); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditIndexerPrompt.into() + ); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap() + ); + } + + #[test] + fn test_edit_indexer_name_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerNameInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_url_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerUrlInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_api_key_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerApiKeyInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerSeedRatioInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + + #[test] + fn test_edit_indexer_tags_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerTagsInput.into()); + + EditIndexerHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + } + } + + mod test_handle_esc { + use super::*; + use crate::app::App; + use crate::assert_navigation_popped; + use crate::event::Key; + use crate::models::servarr_data::modals::EditIndexerModal; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_indexer_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.edit_indexer_modal); + } + + #[rstest] + fn test_edit_indexer_input_fields_esc( + #[values( + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ActiveLidarrBlock::EditIndexerTagsInput, + ActiveLidarrBlock::EditIndexerPriorityInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.ignore_special_keys_for_textbox_input = true; + + EditIndexerHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + assert_some_eq_x!( + &app.data.lidarr_data.edit_indexer_modal, + &EditIndexerModal::default() + ); + } + } + + mod test_handle_key_char { + use super::*; + use crate::app::App; + use crate::assert_navigation_popped; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use pretty_assertions::{assert_eq, assert_str_eq}; + + #[test] + fn test_edit_indexer_name_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_url_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_name_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_url_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "a" + ); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.lidarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + + EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_modal_absent!(app.data.lidarr_data.edit_indexer_modal); + assert!(app.should_refresh); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + ); + } + } + + #[test] + fn test_edit_indexer_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) { + assert!(EditIndexerHandler::accepts(active_lidarr_block)); + } else { + assert!(!EditIndexerHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_edit_indexer_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_edit_indexer_params() { + let mut app = App::test_default(); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + priority: 0, + }; + app.data.lidarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + + let params = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ) + .build_edit_indexer_params(); + + assert_eq!(params, expected_edit_indexer_params); + assert_modal_absent!(app.data.lidarr_data.edit_indexer_modal); + } + + #[test] + fn test_edit_indexer_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_handler_is_not_ready_when_edit_indexer_modal_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_handler_is_ready_when_edit_indexer_modal_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + app.data.lidarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + let handler = EditIndexerHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditIndexerPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs new file mode 100644 index 0000000..0de6cc1 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -0,0 +1,209 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::models::servarr_models::IndexerSettings; +use crate::network::lidarr_network::LidarrEvent; +use crate::{handle_prompt_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "edit_indexer_settings_handler_tests.rs"] +mod edit_indexer_settings_handler_tests; + +pub(super) struct IndexerSettingsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl IndexerSettingsHandler<'_, '_> { + fn build_edit_indexer_settings_params(&mut self) -> IndexerSettings { + self + .app + .data + .lidarr_data + .indexer_settings + .take() + .expect("IndexerSettings is None") + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexerSettingsHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + INDEXER_SETTINGS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> IndexerSettingsHandler<'a, 'b> { + IndexerSettingsHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.indexer_settings.is_some() + } + + fn handle_scroll_up(&mut self) { + let indexer_settings = self.app.data.lidarr_data.indexer_settings.as_mut().unwrap(); + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self.app.data.lidarr_data.selected_block.up(); + } + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput => { + indexer_settings.minimum_age += 1; + } + ActiveLidarrBlock::IndexerSettingsRetentionInput => { + indexer_settings.retention += 1; + } + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput => { + indexer_settings.maximum_size += 1; + } + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { + indexer_settings.rss_sync_interval += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + let indexer_settings = self.app.data.lidarr_data.indexer_settings.as_mut().unwrap(); + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self.app.data.lidarr_data.selected_block.down() + } + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput => { + if indexer_settings.minimum_age > 0 { + indexer_settings.minimum_age -= 1; + } + } + ActiveLidarrBlock::IndexerSettingsRetentionInput => { + if indexer_settings.retention > 0 { + indexer_settings.retention -= 1; + } + } + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput => { + if indexer_settings.maximum_size > 0 { + indexer_settings.maximum_size -= 1; + } + } + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { + if indexer_settings.rss_sync_interval > 0 { + indexer_settings.rss_sync_interval -= 1; + } + } + _ => (), + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AllIndexerSettingsPrompt { + handle_prompt_left_right_keys!( + self, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + lidarr_data + ); + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::IndexerSettingsConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::EditAllIndexerSettings(self.build_edit_indexer_settings_params()), + ); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.indexer_settings = None; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput + | ActiveLidarrBlock::IndexerSettingsRetentionInput + | ActiveLidarrBlock::IndexerSettingsMaximumSizeInput + | ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => { + self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + None, + ) + .into(), + ) + } + + _ => (), + } + } + + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput + | ActiveLidarrBlock::IndexerSettingsRetentionInput + | ActiveLidarrBlock::IndexerSettingsMaximumSizeInput + | ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AllIndexerSettingsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + self.app.data.lidarr_data.indexer_settings = None; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AllIndexerSettingsPrompt + && self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::IndexerSettingsConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::EditAllIndexerSettings( + self.build_edit_indexer_settings_params(), + )); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs new file mode 100644 index 0000000..c7e6385 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -0,0 +1,609 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, + }; + use crate::models::servarr_models::IndexerSettings; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; + use crate::models::servarr_models::IndexerSettings; + + use super::*; + + macro_rules! test_i64_counter_scroll_value { + ($block:expr, $key:expr, $data_ref:ident, $negatives:literal) => { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new($key, &mut app, $block, None).handle(); + + if $key == Key::Up { + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + } else { + if $negatives { + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + -1 + ); + } else { + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + + IndexerSettingsHandler::new(Key::Up, &mut app, $block, None).handle(); + + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + + IndexerSettingsHandler::new($key, &mut app, $block, None).handle(); + assert_eq!( + app + .data + .lidarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + } + } + }; + } + + #[rstest] + fn test_edit_indexer_settings_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput + ); + } + } + + #[rstest] + fn test_edit_indexer_settings_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::IndexerSettingsRetentionInput + ); + } + + #[rstest] + fn test_edit_indexer_settings_minimum_age_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + key, + minimum_age, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_retention_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsRetentionInput, + key, + retention, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_maximum_size_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + key, + maximum_size, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_rss_sync_interval_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, + key, + rss_sync_interval, + false + ); + } + } + + mod test_handle_left_right_action { + use crate::models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; + + use crate::models::BlockSelectionState; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1; + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + IndexerSettingsHandler::new( + key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + servarr_models::IndexerSettings, + }, + network::lidarr_network::LidarrEvent, + }; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert_none!(app.data.lidarr_data.indexer_settings); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.indexer_settings = Some(indexer_settings()); + app.data.lidarr_data.prompt_confirm = true; + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::EditAllIndexerSettings(indexer_settings()) + ); + assert_modal_absent!(app.data.lidarr_data.indexer_settings); + assert!(app.should_refresh); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.lidarr_data.prompt_confirm = true; + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AllIndexerSettingsPrompt.into() + ); + assert!(!app.should_refresh); + } + + #[rstest] + #[case(ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, 0)] + #[case(ActiveLidarrBlock::IndexerSettingsRetentionInput, 1)] + #[case(ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, 2)] + #[case(ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, 3)] + fn test_edit_indexer_settings_prompt_submit_selected_block( + #[case] selected_block: ActiveLidarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, selected_block.into()); + } + + #[rstest] + fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready( + #[values(0, 1, 2, 3, 4)] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AllIndexerSettingsPrompt.into() + ); + } + + #[rstest] + fn test_edit_indexer_settings_selected_block_submit( + #[values( + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + IndexerSettingsHandler::new(SUBMIT_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + } + } + + mod test_handle_esc { + use rstest::rstest; + + use crate::models::servarr_models::IndexerSettings; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_indexer_settings_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.indexer_settings); + } + + #[rstest] + fn test_edit_indexer_settings_selected_blocks_esc( + #[values( + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.lidarr_data.indexer_settings, + &IndexerSettings::default() + ); + } + } + + mod test_handle_key_char { + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + }, + network::lidarr_network::LidarrEvent, + }; + + use super::*; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.indexer_settings = Some(indexer_settings()); + + IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::EditAllIndexerSettings(indexer_settings()) + ); + assert_modal_absent!(app.data.lidarr_data.indexer_settings); + assert!(app.should_refresh); + } + } + + #[test] + fn test_indexer_settings_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) { + assert!(IndexerSettingsHandler::accepts(active_lidarr_block)); + } else { + assert!(!IndexerSettingsHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_indexer_settings_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_edit_indexer_settings_params() { + let mut app = App::test_default(); + app.data.lidarr_data.indexer_settings = Some(indexer_settings()); + + let actual_indexer_settings = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ) + .build_edit_indexer_settings_params(); + + assert_eq!(actual_indexer_settings, indexer_settings()); + assert_modal_absent!(app.data.lidarr_data.indexer_settings); + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_indexer_settings_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_settings_handler_ready_when_not_loading_and_indexer_settings_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + app.data.lidarr_data.indexer_settings = Some(IndexerSettings::default()); + + let handler = IndexerSettingsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs new file mode 100644 index 0000000..0be65eb --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/indexers_handler_tests.rs @@ -0,0 +1,717 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::IndexersHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::test_handler_delegation; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_indexer_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteIndexerPrompt.into()); + } + + #[test] + fn test_delete_indexer_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(5); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::RootFolders.into()); + } + + #[rstest] + fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(5); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::System.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::System.into()); + } + + #[rstest] + fn test_left_right_delete_indexer_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new(key, &mut app, ActiveLidarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + IndexersHandler::new(key, &mut app, ActiveLidarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use super::*; + use crate::assert_navigation_popped; + use crate::models::servarr_data::lidarr::lidarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, LidarrData, + }; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_models::{Indexer, IndexerField}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use bimap::BiMap; + use pretty_assertions::assert_eq; + use serde_json::{Number, Value}; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + let protocol = if torrent_protocol { + "torrent".to_owned() + } else { + "usenet".to_owned() + }; + let mut expected_edit_indexer_modal = EditIndexerModal { + name: "Test".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "https://test.com".into(), + api_key: "1234".into(), + tags: "usenet, test".into(), + ..EditIndexerModal::default() + }; + let mut lidarr_data = LidarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..LidarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if torrent_protocol { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + expected_edit_indexer_modal.seed_ratio = "1.2".into(); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + protocol, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + lidarr_data.indexers.set_items(vec![indexer]); + app.data.lidarr_data = lidarr_data; + + IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::EditIndexerPrompt.into()); + assert_some_eq_x!( + &app.data.lidarr_data.edit_indexer_modal, + &EditIndexerModal::from(&app.data.lidarr_data) + ); + assert_some_eq_x!( + &app.data.lidarr_data.edit_indexer_modal, + &expected_edit_indexer_modal + ); + if torrent_protocol { + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + EDIT_INDEXER_NZB_SELECTION_BLOCKS + ); + } + } + + #[test] + fn test_edit_indexer_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.edit_indexer_modal); + } + + #[test] + fn test_delete_indexer_prompt_confirm_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteIndexer(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + } + } + + mod test_handle_esc { + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_indexer_prompt_block_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + IndexersHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.lidarr_data.indexer_test_errors = Some("test result".to_owned()); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + + IndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::TestIndexer, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.indexer_test_errors); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Indexers, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::{ + assert_navigation_popped, + models::servarr_data::lidarr::lidarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + network::lidarr_network::LidarrEvent, + }; + + #[test] + fn test_refresh_indexers_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_indexers_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_indexer_settings_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + INDEXER_SETTINGS_SELECTION_BLOCKS + ); + } + + #[test] + fn test_indexer_settings_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_test_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::TestIndexer.into()); + } + + #[test] + fn test_test_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_test_all_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::TestAllIndexers.into()); + } + + #[test] + fn test_test_all_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Indexers.into()); + } + + #[test] + fn test_delete_indexer_prompt_confirm() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteIndexer(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + } + } + + #[rstest] + fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler( + #[values( + ActiveLidarrBlock::EditIndexerPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerUrlInput, + ActiveLidarrBlock::EditIndexerTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveLidarrBlock::Indexers, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler( + #[values( + ActiveLidarrBlock::AllIndexerSettingsPrompt, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveLidarrBlock::Indexers, + active_lidarr_block + ); + } + + #[test] + fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() { + test_handler_delegation!( + IndexersHandler, + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::TestAllIndexers + ); + } + + #[test] + fn test_indexers_handler_accepts() { + let mut indexers_blocks = Vec::new(); + indexers_blocks.extend(INDEXERS_BLOCKS); + indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveLidarrBlock::TestAllIndexers); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if indexers_blocks.contains(&active_lidarr_block) { + assert!(IndexersHandler::accepts(active_lidarr_block)); + } else { + assert!(!IndexersHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_indexers_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_indexer_id() { + let mut app = App::test_default(); + app.data.lidarr_data.indexers.set_items(vec![indexer()]); + + let indexer_id = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ) + .extract_indexer_id(); + + assert_eq!(indexer_id, 1); + } + + #[test] + fn test_indexers_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_indexers_handler_not_ready_when_indexers_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_indexers_handler_ready_when_not_loading_and_indexers_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.is_loading = false; + app + .data + .lidarr_data + .indexers + .set_items(vec![Indexer::default()]); + + let handler = IndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Indexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/indexers/mod.rs b/src/handlers/lidarr_handlers/indexers/mod.rs new file mode 100644 index 0000000..653fcdb --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/mod.rs @@ -0,0 +1,217 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::lidarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; +use crate::handlers::lidarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; +use crate::handlers::lidarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, +}; +use crate::models::{BlockSelectionState, Route}; +use crate::network::lidarr_network::LidarrEvent; + +mod edit_indexer_handler; +mod edit_indexer_settings_handler; +mod test_all_indexers_handler; + +#[cfg(test)] +#[path = "indexers_handler_tests.rs"] +mod indexers_handler_tests; + +pub(super) struct IndexersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl IndexersHandler<'_, '_> { + fn extract_indexer_id(&self) -> i64 { + self.app.data.lidarr_data.indexers.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for IndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexers_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::Indexers.into()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.indexers, + indexers_table_handling_config, + ) { + match self.active_lidarr_block { + _ if EditIndexerHandler::accepts(self.active_lidarr_block) => { + EditIndexerHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ if IndexerSettingsHandler::accepts(self.active_lidarr_block) => { + IndexerSettingsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ if TestAllIndexersHandler::accepts(self.active_lidarr_block) => { + TestAllIndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + EditIndexerHandler::accepts(active_block) + || IndexerSettingsHandler::accepts(active_block) + || TestAllIndexersHandler::accepts(active_block) + || INDEXERS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> IndexersHandler<'a, 'b> { + IndexersHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.indexers.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Indexers { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteIndexerPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Indexers => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteIndexerPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteIndexerPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteIndexer(self.extract_indexer_id())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::Indexers => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + self.app.data.lidarr_data.edit_indexer_modal = Some((&self.app.data.lidarr_data).into()); + let protocol = &self + .app + .data + .lidarr_data + .indexers + .current_selection() + .protocol; + if protocol == "torrent" { + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + } else { + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + } + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::TestIndexer => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.indexer_test_errors = None; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::Indexers => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(test, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + } + _ if matches_key!(test_all, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + } + _ if matches_key!(settings, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AllIndexerSettingsPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + } + _ => (), + }, + ActiveLidarrBlock::DeleteIndexerPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteIndexer(self.extract_indexer_id())); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs new file mode 100644 index 0000000..c1fdb67 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler.rs @@ -0,0 +1,108 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::KeyEventHandler; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + +#[cfg(test)] +#[path = "test_all_indexers_handler_tests.rs"] +mod test_all_indexers_handler_tests; + +pub(super) struct TestAllIndexersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl TestAllIndexersHandler<'_, '_> {} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TestAllIndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexer_test_all_results_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::TestAllIndexers.into()); + + if !handle_table( + self, + |app| { + app + .data + .lidarr_data + .indexer_test_all_results + .as_mut() + .unwrap() + }, + indexer_test_all_results_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + active_block == ActiveLidarrBlock::TestAllIndexers + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> TestAllIndexersHandler<'a, 'b> { + TestAllIndexersHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + let table_is_ready = if let Some(table) = &self.app.data.lidarr_data.indexer_test_all_results { + !table.is_empty() + } else { + false + }; + + !self.app.is_loading && table_is_ready + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::TestAllIndexers { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.indexer_test_all_results = None; + } + } + + fn handle_char_key_event(&mut self) {} + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs new file mode 100644 index 0000000..6379541 --- /dev/null +++ b/src/handlers/lidarr_handlers/indexers/test_all_indexers_handler_tests.rs @@ -0,0 +1,133 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_popped; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_esc { + use super::*; + + const ESC_KEY: crate::event::Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_test_all_indexers_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.data.lidarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + TestAllIndexersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::TestAllIndexers, None) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Indexers.into()); + assert_none!(app.data.lidarr_data.indexer_test_all_results); + } + } + + #[test] + fn test_test_all_indexers_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if active_lidarr_block == ActiveLidarrBlock::TestAllIndexers { + assert!(TestAllIndexersHandler::accepts(active_lidarr_block)); + } else { + assert!(!TestAllIndexersHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_test_all_indexers_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_test_all_indexers_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = true; + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_not_ready_when_results_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = false; + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_not_ready_when_results_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = false; + app.data.lidarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_ready_when_not_loading_and_results_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + app.is_loading = false; + let mut results = StatefulTable::default(); + results.set_items(vec![IndexerTestResultModalItem::default()]); + app.data.lidarr_data.indexer_test_all_results = Some(results); + + let handler = TestAllIndexersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TestAllIndexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/library/add_artist_handler.rs b/src/handlers/lidarr_handlers/library/add_artist_handler.rs new file mode 100644 index 0000000..7f4e63b --- /dev/null +++ b/src/handlers/lidarr_handlers/library/add_artist_handler.rs @@ -0,0 +1,616 @@ +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions, AddArtistSearchResult}; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock, +}; +use crate::models::servarr_data::lidarr::modals::AddArtistModal; +use crate::models::{BlockSelectionState, Route, Scrollable}; +use crate::network::lidarr_network::LidarrEvent; +use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "add_artist_handler_tests.rs"] +mod add_artist_handler_tests; + +pub struct AddArtistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl AddArtistHandler<'_, '_> { + fn build_add_artist_body(&mut self) -> AddArtistBody { + let add_artist_modal = self + .app + .data + .lidarr_data + .add_artist_modal + .take() + .expect("AddArtistModal is None"); + let tags = add_artist_modal.tags.text; + let AddArtistModal { + root_folder_list, + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + .. + } = add_artist_modal; + let (foreign_artist_id, artist_name) = { + let AddArtistSearchResult { + foreign_artist_id, + artist_name, + .. + } = self + .app + .data + .lidarr_data + .add_searched_artists + .as_ref() + .unwrap() + .current_selection(); + (foreign_artist_id.clone(), artist_name.text.clone()) + }; + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self + .app + .data + .lidarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + let metadata_profile = metadata_profile_list.current_selection(); + let metadata_profile_id = *self + .app + .data + .lidarr_data + .metadata_profile_map + .iter() + .filter(|(_, value)| *value == metadata_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let path = root_folder_list.current_selection().path.clone(); + let monitor = *monitor_list.current_selection(); + let monitor_new_items = *monitor_new_items_list.current_selection(); + + AddArtistBody { + foreign_artist_id, + artist_name, + monitored: true, + root_folder_path: path, + quality_profile_id, + metadata_profile_id, + tags: Vec::new(), + tag_input_string: Some(tags), + add_options: AddArtistOptions { + monitor, + monitor_new_items, + search_for_missing_albums: true, + }, + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddArtistHandler<'a, 'b> { + fn handle(&mut self) { + let add_artist_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::AddArtistSearchResults.into()); + + if !handle_table( + self, + |app| { + app + .data + .lidarr_data + .add_searched_artists + .as_mut() + .expect("add_searched_artists should be initialized") + }, + add_artist_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + ADD_ARTIST_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> AddArtistHandler<'a, 'b> { + AddArtistHandler { + key, + app, + active_lidarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_up(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_up(), + ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_down(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_down(), + ActiveLidarrBlock::AddArtistPrompt => self.app.data.lidarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_to_top(), + ActiveLidarrBlock::AddArtistSearchInput => self + .app + .data + .lidarr_data + .add_artist_search + .as_mut() + .unwrap() + .scroll_home(), + ActiveLidarrBlock::AddArtistTagsInput => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSelectRootFolder => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddArtistSearchInput => self + .app + .data + .lidarr_data + .add_artist_search + .as_mut() + .unwrap() + .reset_offset(), + ActiveLidarrBlock::AddArtistTagsInput => self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistPrompt => handle_prompt_toggle(self.app, self.key), + ActiveLidarrBlock::AddArtistSearchInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_artist_search + .as_mut() + .unwrap() + ) + } + ActiveLidarrBlock::AddArtistTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSearchInput + if !self + .app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .text + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::AddArtistSearchResults + if self.app.data.lidarr_data.add_searched_artists.is_some() => + { + let foreign_artist_id = self + .app + .data + .lidarr_data + .add_searched_artists + .as_ref() + .unwrap() + .current_selection() + .foreign_artist_id + .clone(); + + if self + .app + .data + .lidarr_data + .artists + .items + .iter() + .any(|artist| artist.foreign_artist_id == foreign_artist_id) + { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + } else { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + self.app.data.lidarr_data.add_artist_modal = Some((&self.app.data.lidarr_data).into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + } + } + ActiveLidarrBlock::AddArtistPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::AddArtistConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::AddArtist(self.build_add_artist_body())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.push_navigation_stack( + self + .app + .data + .lidarr_data + .selected_block + .get_active_block() + .into(), + ), + ActiveLidarrBlock::AddArtistTagsInput => { + self.app.push_navigation_stack( + self + .app + .data + .lidarr_data + .selected_block + .get_active_block() + .into(), + ); + self.app.ignore_special_keys_for_textbox_input = true; + } + _ => (), + } + } + ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(), + ActiveLidarrBlock::AddArtistTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSearchInput => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.add_artist_search = None; + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::AddArtistSearchResults + | ActiveLidarrBlock::AddArtistEmptySearchResults => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.add_searched_artists = None; + self.app.ignore_special_keys_for_textbox_input = true; + } + ActiveLidarrBlock::AddArtistPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.add_artist_modal = None; + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistAlreadyInLibrary + | ActiveLidarrBlock::AddArtistSelectRootFolder => self.app.pop_navigation_stack(), + ActiveLidarrBlock::AddArtistTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::AddArtistSearchInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_artist_search + .as_mut() + .unwrap() + ) + } + ActiveLidarrBlock::AddArtistTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveLidarrBlock::AddArtistPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::AddArtistConfirmPrompt + && matches_key!(confirm, key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::AddArtist(self.build_add_artist_body())); + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs new file mode 100644 index 0000000..203089b --- /dev/null +++ b/src/handlers/lidarr_handlers/library/add_artist_handler_tests.rs @@ -0,0 +1,1687 @@ +#[cfg(test)] +mod tests { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use std::sync::atomic::Ordering; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_modal_present; + use crate::assert_navigation_popped; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::add_artist_handler::AddArtistHandler; + use crate::models::lidarr_models::{ + AddArtistBody, AddArtistOptions, MonitorType, NewItemMonitorType, + }; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ActiveLidarrBlock, + }; + use crate::models::servarr_data::lidarr::modals::AddArtistModal; + use crate::models::servarr_models::RootFolder; + use crate::models::stateful_table::StatefulTable; + use crate::models::{BlockSelectionState, HorizontallyScrollableText}; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::add_artist_search_result; + use crate::simple_stateful_iterable_vec; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ARTIST_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_add_artist_select_monitor_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_vec.len()).rev() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[i] + ); + } + } else { + for i in 0..monitor_vec.len() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[(i + 1) % monitor_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_artist_select_monitor_new_items_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_new_items_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_new_items_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_new_items_vec.len()).rev() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[i] + ); + } + } else { + for i in 0..monitor_new_items_vec.len() { + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[(i + 1) % monitor_new_items_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_artist_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 2" + ); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_artist_select_metadata_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 2" + ); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_artist_select_root_folder_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(simple_stateful_iterable_vec!(RootFolder, String, path)); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 2" + ); + + AddArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 1" + ); + } + + #[rstest] + fn test_add_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectRootFolder + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectMonitorNewItems + ); + } + } + + #[rstest] + fn test_add_artist_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_loading = true; + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectMonitor + ); + } + } + + mod test_handle_home_end { + use pretty_assertions::assert_eq; + + use crate::extended_stateful_iterable_vec; + + use super::*; + + #[test] + fn test_add_artist_select_monitor_home_end() { + let monitor_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_vec.clone()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[monitor_vec.len() - 1] + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[0] + ); + } + + #[test] + fn test_add_artist_select_monitor_new_items_home_end() { + let monitor_new_items_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_new_items_vec.clone()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[monitor_new_items_vec.len() - 1] + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_new_items_vec[0] + ); + } + + #[test] + fn test_add_artist_select_quality_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 3" + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_artist_select_metadata_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 3" + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_artist_select_root_folder_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(extended_stateful_iterable_vec!(RootFolder, String, path)); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 3" + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 1" + ); + } + + #[test] + fn test_add_artist_search_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + app.data.lidarr_data.add_artist_search = Some("Test".into()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_artist_tags_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "Test".into(), + ..AddArtistModal::default() + }); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + AddArtistHandler::new(key, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_add_artist_search_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + app.data.lidarr_data.add_artist_search = Some("Test".into()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_artist_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "Test".into(), + ..AddArtistModal::default() + }); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use super::*; + use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::artist; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_artist_search_input_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.add_artist_search = Some("test".into()); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_navigation_pushed!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + } + + #[test] + fn test_add_artist_search_input_submit_noop_on_empty_search() { + let mut app = App::test_default(); + app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + app.ignore_special_keys_for_textbox_input = true; + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert!(app.ignore_special_keys_for_textbox_input); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistSearchInput.into() + ); + } + + #[test] + fn test_add_artist_search_results_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistPrompt.into() + ); + assert_modal_present!(app.data.lidarr_data.add_artist_modal); + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddArtistSelectRootFolder + ); + } + + #[test] + fn test_add_artist_search_results_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistSearchResults.into() + ); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } + + #[test] + fn test_add_artist_search_results_submit_does_nothing_on_empty_table() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistSearchResults.into() + ); + } + + #[test] + fn test_add_artist_search_results_submit_artist_already_in_library() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut add_searched_artists = StatefulTable::default(); + let search_result = add_artist_search_result(); + add_searched_artists.set_items(vec![search_result.clone()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.artists.set_items(vec![artist()]); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + } + + #[test] + fn test_add_artist_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_add_artist_confirm_prompt_prompt_confirm_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.prompt_confirm = true; + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test".to_owned()]); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Test".to_owned()]); + app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(vec![RootFolder { + path: "/music".to_owned(), + ..RootFolder::default() + }]); + let expected_add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_string(), + artist_name: "Test Artist".to_string(), + monitored: true, + root_folder_path: "/music".to_string(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: true, + }, + }; + let expected_lidarr_event = LidarrEvent::AddArtist(expected_add_artist_body); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_lidarr_event + ); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } + + #[rstest] + #[case(ActiveLidarrBlock::AddArtistSelectRootFolder, 0)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitor, 1)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitorNewItems, 2)] + #[case(ActiveLidarrBlock::AddArtistSelectQualityProfile, 3)] + #[case(ActiveLidarrBlock::AddArtistSelectMetadataProfile, 4)] + #[case(ActiveLidarrBlock::AddArtistTagsInput, 5)] + fn test_add_artist_prompt_selected_block_submit( + #[case] block: ActiveLidarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + AddArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_pushed!(app, block.into()); + } + + #[rstest] + fn test_add_artist_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveLidarrBlock::AddArtistSelectRootFolder, + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + AddArtistHandler::new(SUBMIT_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistPrompt.into()); + + if active_lidarr_block == ActiveLidarrBlock::AddArtistTagsInput { + assert!(!app.ignore_special_keys_for_textbox_input); + } + } + } + + mod test_handle_esc { + use rstest::rstest; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_add_artist_search_input_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.lidarr_data.add_artist_search = Some("test".into()); + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + + AddArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_modal_absent!(app.data.lidarr_data.add_artist_search); + } + + #[rstest] + fn test_add_artist_search_results_esc( + #[values( + ActiveLidarrBlock::AddArtistSearchResults, + ActiveLidarrBlock::AddArtistEmptySearchResults + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + app.push_navigation_stack(active_lidarr_block.into()); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + + AddArtistHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchInput.into()); + assert_modal_absent!(app.data.lidarr_data.add_searched_artists); + assert!(app.ignore_special_keys_for_textbox_input); + } + + #[test] + fn test_add_artist_already_in_library_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + + AddArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddArtistAlreadyInLibrary, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + } + + #[test] + fn test_add_artist_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + app.data.lidarr_data.prompt_confirm = true; + + AddArtistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::AddArtistPrompt, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_add_artist_tags_input_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistTagsInput.into()); + app.ignore_special_keys_for_textbox_input = true; + + AddArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistPrompt.into()); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[rstest] + fn test_add_artist_selecting_preferences_blocks_esc( + #[values( + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistSelectRootFolder + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + + AddArtistHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistPrompt.into()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::models::lidarr_models::{AddArtistBody, AddArtistOptions}; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_str_eq; + + #[test] + fn test_add_artist_search_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_search = Some("Test".into()); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .text, + "Tes" + ); + } + + #[test] + fn test_add_artist_search_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default()); + + AddArtistHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_search + .as_ref() + .unwrap() + .text, + "a" + ); + } + + #[test] + fn test_add_artist_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal { + tags: "Test".into(), + ..AddArtistModal::default() + }); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_add_artist_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.add_artist_modal = Some(AddArtistModal::default()); + + AddArtistHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddArtistTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .unwrap() + .tags + .text, + "a" + ); + } + + #[test] + fn test_add_artist_confirm_prompt_confirm_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ARTIST_SELECTION_BLOCKS.len() - 1); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Test".to_owned())]); + let mut add_artist_modal = AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }; + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .quality_profile_list + .set_items(vec!["Test".to_owned()]); + add_artist_modal + .metadata_profile_list + .set_items(vec!["Test".to_owned()]); + add_artist_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_artist_modal.root_folder_list.state.select(Some(1)); + app.data.lidarr_data.add_artist_modal = Some(add_artist_modal); + let expected_add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_string(), + artist_name: "Test Artist".to_string(), + monitored: true, + root_folder_path: "/nfs2".to_string(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: Default::default(), + monitor_new_items: Default::default(), + search_for_missing_albums: true, + }, + }; + let expected_lidarr_event = LidarrEvent::AddArtist(expected_add_artist_body); + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddArtistSearchResults.into()); + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_lidarr_event + ); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } + } + + #[test] + fn test_add_artist_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(AddArtistHandler::accepts(active_lidarr_block)); + } else { + assert!(!AddArtistHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_add_artist_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = AddArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_add_artist_search_no_panic_on_none_search_result() { + let mut app = App::test_default(); + app.data.lidarr_data.add_searched_artists = None; + + AddArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchResults, + None, + ) + .handle(); + } + + #[test] + fn test_add_artist_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = AddArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_add_artist_handler_is_ready_when_not_loading() { + let mut app = App::test_default(); + app.is_loading = false; + + let handler = AddArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddArtistSearchInput, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_build_add_artist_body() { + let mut app = App::test_default(); + let mut add_artist_modal = AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }; + add_artist_modal.root_folder_list.set_items(vec![ + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }, + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + add_artist_modal.root_folder_list.state.select(Some(1)); + add_artist_modal + .quality_profile_list + .set_items(vec!["Lossless".to_owned()]); + add_artist_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.add_artist_modal = Some(add_artist_modal); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([(1, "Lossless".to_owned())]); + app.data.lidarr_data.metadata_profile_map = BiMap::from_iter([(1, "Standard".to_owned())]); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + let expected_add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_string(), + artist_name: "Test Artist".into(), + monitored: true, + root_folder_path: "/nfs2".to_string(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: Default::default(), + monitor_new_items: Default::default(), + search_for_missing_albums: true, + }, + }; + + let add_artist_body = AddArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddArtistPrompt, + None, + ) + .build_add_artist_body(); + + assert_eq!(add_artist_body, expected_add_artist_body); + assert_modal_absent!(app.data.lidarr_data.add_artist_modal); + } +} diff --git a/src/handlers/lidarr_handlers/library/album_details_handler.rs b/src/handlers/lidarr_handlers/library/album_details_handler.rs new file mode 100644 index 0000000..4e69596 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/album_details_handler.rs @@ -0,0 +1,468 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::history::history_sorting_options; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::Route; +use crate::models::lidarr_models::{ + LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody, Track, +}; +use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock}; +use crate::models::stateful_table::SortOption; +use crate::network::lidarr_network::LidarrEvent; +use serde_json::Number; + +#[cfg(test)] +#[path = "album_details_handler_tests.rs"] +mod album_details_handler_tests; + +pub(in crate::handlers::lidarr_handlers) struct AlbumDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl AlbumDetailsHandler<'_, '_> { + fn extract_track_file_id(&self) -> i64 { + self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("Album details have not been loaded") + .tracks + .current_selection() + .track_file_id + } + + fn extract_album_id(&self) -> i64 { + self.app.data.lidarr_data.albums.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AlbumDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let tracks_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::AlbumDetails.into()) + .searching_block(ActiveLidarrBlock::SearchTracks.into()) + .search_error_block(ActiveLidarrBlock::SearchTracksError.into()) + .search_field_fn(|track: &Track| &track.title); + let album_history_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::AlbumHistory.into()) + .sorting_block(ActiveLidarrBlock::AlbumHistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveLidarrBlock::SearchAlbumHistory.into()) + .search_error_block(ActiveLidarrBlock::SearchAlbumHistoryError.into()) + .search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text) + .filtering_block(ActiveLidarrBlock::FilterAlbumHistory.into()) + .filter_error_block(ActiveLidarrBlock::FilterAlbumHistoryError.into()) + .filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text); + let album_releases_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::ManualAlbumSearch.into()) + .sorting_block(ActiveLidarrBlock::ManualAlbumSearchSortPrompt.into()) + .sort_options(releases_sorting_options()); + + if !handle_table( + self, + |app| { + &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("Album details modal is undefined") + .tracks + }, + tracks_table_handling_config, + ) && !handle_table( + self, + |app| { + &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("Album details modal is undefined") + .album_history + }, + album_history_table_handling_config, + ) && !handle_table( + self, + |app| { + &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("Album details modal is undefined") + .album_releases + }, + album_releases_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + ALBUM_DETAILS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> AlbumDetailsHandler<'a, 'b> { + AlbumDetailsHandler { + key, + app, + active_lidarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + if self.app.is_loading { + return false; + } + + let Some(album_details_modal) = &self.app.data.lidarr_data.album_details_modal else { + return false; + }; + + match self.active_lidarr_block { + ActiveLidarrBlock::AlbumDetails => !album_details_modal.tracks.is_empty(), + ActiveLidarrBlock::AlbumHistory => !album_details_modal.album_history.is_empty(), + ActiveLidarrBlock::ManualAlbumSearch => !album_details_modal.album_releases.is_empty(), + _ => true, + } + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AlbumDetails { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteTrackFilePrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AlbumDetails + | ActiveLidarrBlock::AlbumHistory + | ActiveLidarrBlock::ManualAlbumSearch => match self.key { + _ if matches_key!(left, self.key) => { + self + .app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_details_tabs + .previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_details_tabs + .get_active_route(), + ); + } + _ if matches_key!(right, self.key) => { + self + .app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_details_tabs + .next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_details_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt + | ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt + | ActiveLidarrBlock::DeleteTrackFilePrompt => { + handle_prompt_toggle(self.app, self.key); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AlbumDetails + if self.app.data.lidarr_data.album_details_modal.is_some() + && !self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .tracks + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveLidarrBlock::TrackDetails.into()) + } + ActiveLidarrBlock::AlbumHistory => self + .app + .push_navigation_stack(ActiveLidarrBlock::AlbumHistoryDetails.into()), + ActiveLidarrBlock::DeleteTrackFilePrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteTrackFile(self.extract_track_file_id())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::TriggerAutomaticAlbumSearch(self.extract_album_id()), + ); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::ManualAlbumSearch => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt.into()); + } + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + let LidarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_releases + .current_selection(); + let params = LidarrReleaseDownloadBody { + guid: guid.clone(), + indexer_id: *indexer_id, + }; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DownloadRelease(params)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AlbumDetails | ActiveLidarrBlock::ManualAlbumSearch => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.album_details_modal = None; + } + ActiveLidarrBlock::AlbumHistoryDetails => { + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::AlbumHistory => { + if self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .filtered_items + .is_some() + { + self + .app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_history + .filtered_items = None; + } else { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.album_details_modal = None; + } + } + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt + | ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt + | ActiveLidarrBlock::DeleteTrackFilePrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::AlbumDetails + | ActiveLidarrBlock::AlbumHistory + | ActiveLidarrBlock::ManualAlbumSearch => match self.key { + _ if matches_key!(refresh, self.key) => { + self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()); + } + _ if matches_key!(auto_search, self.key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchAlbumPrompt.into()); + } + _ => (), + }, + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt if matches_key!(confirm, key) => { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::TriggerAutomaticAlbumSearch(self.extract_album_id()), + ); + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::DeleteTrackFilePrompt if matches_key!(confirm, key) => { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteTrackFile(self.extract_track_file_id())); + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt if matches_key!(confirm, key) => { + self.app.data.lidarr_data.prompt_confirm = true; + let LidarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_releases + .current_selection(); + let params = LidarrReleaseDownloadBody { + guid: guid.clone(), + indexer_id: *indexer_id, + }; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DownloadRelease(params)); + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +pub(in crate::handlers::lidarr_handlers::library) fn releases_sorting_options() +-> Vec> { + vec![ + SortOption { + name: "Source", + cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)), + }, + SortOption { + name: "Age", + cmp_fn: Some(|a, b| a.age.cmp(&b.age)), + }, + SortOption { + name: "Rejected", + cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)), + }, + SortOption { + name: "Title", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Indexer", + cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())), + }, + SortOption { + name: "Size", + cmp_fn: Some(|a, b| a.size.cmp(&b.size)), + }, + SortOption { + name: "Peers", + cmp_fn: Some(|a, b| { + let default_number = Number::from(i64::MAX); + let seeder_a = a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)), + }, + ] +} diff --git a/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs new file mode 100644 index 0000000..843b522 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/album_details_handler_tests.rs @@ -0,0 +1,1028 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_navigation_pushed; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::album_details_handler::{ + AlbumDetailsHandler, releases_sorting_options, + }; + use crate::models::HorizontallyScrollableText; + use crate::models::lidarr_models::{LidarrRelease, LidarrReleaseDownloadBody}; + use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock}; + use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal; + use crate::models::servarr_models::{Quality, QualityWrapper}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::Number; + use std::cmp::Ordering; + use strum::IntoEnumIterator; + + mod test_handle_delete { + use super::*; + use crate::event::Key; + use pretty_assertions::assert_eq; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_track_prompt() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + AlbumDetailsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteTrackFilePrompt.into()); + } + + #[test] + fn test_delete_track_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + app.is_loading = true; + + AlbumDetailsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AlbumDetails.into() + ); + } + } + + mod test_handle_left_right_actions { + use super::*; + use crate::event::Key; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, + ActiveLidarrBlock::DeleteTrackFilePrompt + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + AlbumDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + AlbumDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + #[case(ActiveLidarrBlock::AlbumDetails, ActiveLidarrBlock::AlbumHistory)] + #[case(ActiveLidarrBlock::AlbumHistory, ActiveLidarrBlock::ManualAlbumSearch)] + #[case(ActiveLidarrBlock::ManualAlbumSearch, ActiveLidarrBlock::AlbumDetails)] + fn test_album_details_tabs_left_right_action( + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.is_loading = is_ready; + app.push_navigation_stack(right_block.into()); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_details_tabs + .index = app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_details_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + AlbumDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_details_tabs + .get_active_route() + ); + assert_navigation_pushed!(app, left_block.into()); + + AlbumDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_details_tabs + .get_active_route() + ); + assert_navigation_pushed!(app, right_block.into()); + } + } + + mod test_handle_submit { + use super::*; + use crate::assert_navigation_popped; + use crate::event::Key; + use crate::models::stateful_table::StatefulTable; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_eq; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_album_details_submit() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::TrackDetails.into()); + } + + #[test] + fn test_album_details_submit_no_op_on_empty_tracks_table() { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .tracks = StatefulTable::default(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AlbumDetails.into() + ); + } + + #[test] + fn test_album_details_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AlbumDetails.into() + ); + } + + #[test] + fn test_album_history_submit() { + let mut app = App::test_default_fully_populated(); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumHistory, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AlbumHistoryDetails.into()); + } + + #[test] + fn test_album_history_submit_no_op_when_album_history_is_empty() { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_history = StatefulTable::default(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AlbumHistory.into() + ); + } + + #[test] + fn test_album_history_submit_no_op_when_not_ready() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::AlbumHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AlbumHistory.into() + ); + } + + #[rstest] + #[case( + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + LidarrEvent::TriggerAutomaticAlbumSearch(1) + )] + #[case( + ActiveLidarrBlock::DeleteTrackFilePrompt, + LidarrEvent::DeleteTrackFile(1) + )] + fn test_album_details_prompt_confirm_submit( + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + #[values(ActiveLidarrBlock::AlbumDetails, ActiveLidarrBlock::AlbumHistory)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(active_lidarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, active_lidarr_block.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + } + + #[test] + fn test_album_details_manual_search_confirm_prompt_confirm_submit() { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearch.into()); + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt.into()); + + AlbumDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ManualAlbumSearch.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 2, + }) + ); + } + + #[rstest] + fn test_album_details_prompt_decline_submit( + #[values( + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + ActiveLidarrBlock::DeleteTrackFilePrompt, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt + )] + prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + AlbumDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::AlbumDetails.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_manual_album_search_submit() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearch.into()); + + AlbumDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualAlbumSearch, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt.into() + ); + } + + #[test] + fn test_manual_album_search_submit_no_op_when_not_ready() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearch.into()); + + AlbumDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualAlbumSearch, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ManualAlbumSearch.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use crate::event::Key; + use crate::models::lidarr_models::LidarrHistoryItem; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_album_history_details_block_esc() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into()); + app.push_navigation_stack(ActiveLidarrBlock::AlbumHistoryDetails.into()); + + AlbumDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AlbumHistoryDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AlbumHistory.into()); + } + + #[rstest] + fn test_album_details_prompts_esc( + #[values( + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + ActiveLidarrBlock::DeleteTrackFilePrompt, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt + )] + prompt_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = is_ready; + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + AlbumDetailsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::AlbumDetails.into()); + } + + #[test] + fn test_album_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() { + let mut app = App::test_default_fully_populated(); + let mut album_history = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(vec![LidarrHistoryItem::default()]), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + album_history.set_items(vec![LidarrHistoryItem::default()]); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_history = album_history; + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into()); + + AlbumDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::AlbumHistory, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AlbumHistory.into() + ); + assert_none!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .filter + ); + assert_none!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .filtered_items + ); + assert_none!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .filtered_state + ); + } + + #[rstest] + fn test_album_details_tabs_esc( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::ManualAlbumSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + AlbumDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert_modal_absent!(app.data.lidarr_data.album_details_modal); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_eq; + + #[rstest] + fn test_auto_search_key( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::ManualAlbumSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt.into() + ); + } + + #[rstest] + fn test_auto_search_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::ManualAlbumSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + } + + #[rstest] + fn test_refresh_key( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::ManualAlbumSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.is_routing = false; + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, active_lidarr_block.into()); + assert!(app.is_routing); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::ManualAlbumSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + app.is_routing = false; + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + assert!(!app.is_routing); + } + + #[rstest] + #[case( + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + LidarrEvent::TriggerAutomaticAlbumSearch(1) + )] + #[case( + ActiveLidarrBlock::DeleteTrackFilePrompt, + LidarrEvent::DeleteTrackFile(1) + )] + fn test_album_details_prompt_confirm_confirm_key( + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + #[values(ActiveLidarrBlock::AlbumDetails, ActiveLidarrBlock::AlbumHistory)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(active_lidarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, active_lidarr_block.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + } + + #[test] + fn test_album_details_manual_search_confirm_prompt_confirm_confirm_key() { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearch.into()); + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt.into()); + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ManualAlbumSearch.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody { + guid: "1234".to_owned(), + indexer_id: 2, + }) + ); + } + } + + #[test] + fn test_album_details_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(AlbumDetailsHandler::accepts(active_lidarr_block)); + } else { + assert!(!AlbumDetailsHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_album_details_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_track_file_id() { + let mut app = App::test_default_fully_populated(); + + let track_file_id = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumDetails, + None, + ) + .extract_track_file_id(); + + assert_eq!(track_file_id, 1); + } + + #[test] + #[should_panic(expected = "Album details have not been loaded")] + fn test_extract_track_file_id_empty_album_details_modal_panics() { + let mut app = App::test_default(); + + AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumDetails, + None, + ) + .extract_track_file_id(); + } + + #[test] + fn test_extract_album_id() { + let mut app = App::test_default_fully_populated(); + + let track_file_id = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumDetails, + None, + ) + .extract_album_id(); + + assert_eq!(track_file_id, 1); + } + + #[test] + fn test_album_details_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + app.is_loading = true; + + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_album_details_handler_is_not_ready_when_not_loading_and_album_details_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_album_details_handler_is_not_ready_when_not_loading_and_tracks_table_is_empty() { + let mut app = App::test_default(); + app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_album_details_handler_is_not_ready_when_not_loading_and_history_table_is_empty() { + let mut app = App::test_default(); + app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::AlbumHistory.into()); + + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AlbumHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_album_details_handler_is_not_ready_when_not_loading_and_releases_table_is_empty() { + let mut app = App::test_default(); + app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::ManualAlbumSearch.into()); + + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ManualAlbumSearch, + None, + ); + + assert!(!handler.is_ready()); + } + + #[rstest] + fn test_album_details_handler_is_ready_when_not_loading_and_album_details_modal_is_populated( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::ManualAlbumSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + let handler = AlbumDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_lidarr_block, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_releases_sorting_options_source() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.protocol.cmp(&b.protocol); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[0].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Source"); + } + + #[test] + fn test_releases_sorting_options_age() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[1].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Age"); + } + + #[test] + fn test_releases_sorting_options_rejected() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.rejected.cmp(&b.rejected); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[2].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Rejected"); + } + + #[test] + fn test_releases_sorting_options_title() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[3].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Title"); + } + + #[test] + fn test_releases_sorting_options_indexer() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase()); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[4].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Indexer"); + } + + #[test] + fn test_releases_sorting_options_size() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.size.cmp(&b.size); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[5].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Size"); + } + + #[test] + fn test_releases_sorting_options_peers() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| { + let default_number = Number::from(i64::MAX); + let seeder_a = a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[6].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Peers"); + } + + #[test] + fn test_releases_sorting_options_quality() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.quality.cmp(&b.quality); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[7].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + fn release_vec() -> Vec { + let release_a = LidarrRelease { + protocol: "Protocol A".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Title A"), + indexer: "Indexer A".to_owned(), + size: 1, + rejected: true, + seeders: Some(Number::from(1)), + quality: QualityWrapper { + quality: Quality { + name: "Quality A".to_owned(), + }, + }, + ..LidarrRelease::default() + }; + let release_b = LidarrRelease { + protocol: "Protocol B".to_owned(), + age: 2, + title: HorizontallyScrollableText::from("title B"), + indexer: "indexer B".to_owned(), + size: 2, + rejected: false, + seeders: Some(Number::from(2)), + quality: QualityWrapper { + quality: Quality { + name: "Quality B".to_owned(), + }, + }, + ..LidarrRelease::default() + }; + let release_c = LidarrRelease { + protocol: "Protocol C".to_owned(), + age: 3, + title: HorizontallyScrollableText::from("Title C"), + indexer: "Indexer C".to_owned(), + size: 3, + rejected: false, + seeders: None, + quality: QualityWrapper { + quality: Quality { + name: "Quality C".to_owned(), + }, + }, + ..LidarrRelease::default() + }; + + vec![release_a, release_b, release_c] + } +} diff --git a/src/handlers/lidarr_handlers/library/artist_details_handler.rs b/src/handlers/lidarr_handlers/library/artist_details_handler.rs new file mode 100644 index 0000000..6911ff8 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/artist_details_handler.rs @@ -0,0 +1,444 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::history::history_sorting_options; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::lidarr_models::{ + Album, LidarrHistoryItem, LidarrRelease, LidarrReleaseDownloadBody, +}; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS, + EDIT_ARTIST_SELECTION_BLOCKS, +}; +use crate::models::stateful_table::SortOption; +use crate::models::{BlockSelectionState, Route}; +use crate::network::lidarr_network::LidarrEvent; +use serde_json::Number; + +#[cfg(test)] +#[path = "artist_details_handler_tests.rs"] +mod artist_details_handler_tests; + +pub struct ArtistDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl ArtistDetailsHandler<'_, '_> { + fn extract_artist_id(&self) -> i64 { + self.app.data.lidarr_data.artists.current_selection().id + } + + fn extract_album_id(&self) -> i64 { + self.app.data.lidarr_data.albums.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for ArtistDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let albums_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::ArtistDetails.into()) + .searching_block(ActiveLidarrBlock::SearchAlbums.into()) + .search_error_block(ActiveLidarrBlock::SearchAlbumsError.into()) + .search_field_fn(|album: &Album| &album.title.text); + + let artist_history_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::ArtistHistory.into()) + .sorting_block(ActiveLidarrBlock::ArtistHistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveLidarrBlock::SearchArtistHistory.into()) + .search_error_block(ActiveLidarrBlock::SearchArtistHistoryError.into()) + .search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text) + .filtering_block(ActiveLidarrBlock::FilterArtistHistory.into()) + .filter_error_block(ActiveLidarrBlock::FilterArtistHistoryError.into()) + .filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text); + + let artist_releases_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::ManualArtistSearch.into()) + .sorting_block(ActiveLidarrBlock::ManualArtistSearchSortPrompt.into()) + .sort_options(releases_sorting_options()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.albums, + albums_table_handling_config, + ) && !handle_table( + self, + |app| &mut app.data.lidarr_data.artist_history, + artist_history_table_handling_config, + ) && !handle_table( + self, + |app| &mut app.data.lidarr_data.discography_releases, + artist_releases_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + ARTIST_DETAILS_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> ArtistDetailsHandler<'a, 'b> { + ArtistDetailsHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + if self.app.is_loading { + return false; + } + + match self.active_lidarr_block { + ActiveLidarrBlock::ArtistHistory => !self.app.data.lidarr_data.artist_history.is_empty(), + ActiveLidarrBlock::ManualArtistSearch => { + !self.app.data.lidarr_data.discography_releases.is_empty() + } + _ => true, + } + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::ArtistDetails { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::ArtistDetails + | ActiveLidarrBlock::ArtistHistory + | ActiveLidarrBlock::ManualArtistSearch => match self.key { + _ if matches_key!(left, self.key) => { + self.app.data.lidarr_data.artist_info_tabs.previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .lidarr_data + .artist_info_tabs + .get_active_route(), + ); + } + _ if matches_key!(right, self.key) => { + self.app.data.lidarr_data.artist_info_tabs.next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .lidarr_data + .artist_info_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveLidarrBlock::UpdateAndScanArtistPrompt + | ActiveLidarrBlock::AutomaticallySearchArtistPrompt + | ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => { + handle_prompt_toggle(self.app, self.key); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::ArtistDetails if !self.app.data.lidarr_data.albums.is_empty() => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + } + ActiveLidarrBlock::ArtistHistory if !self.app.data.lidarr_data.artist_history.is_empty() => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into()); + } + ActiveLidarrBlock::ManualArtistSearch => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into()); + } + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + let LidarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .lidarr_data + .discography_releases + .current_selection() + .clone(); + let params = LidarrReleaseDownloadBody { guid, indexer_id }; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DownloadRelease(params)); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::AutomaticallySearchArtistPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()), + ); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::UpdateAndScanArtistPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id())); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::UpdateAndScanArtistPrompt + | ActiveLidarrBlock::AutomaticallySearchArtistPrompt + | ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::ArtistHistoryDetails => { + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::ArtistHistory => { + if self + .app + .data + .lidarr_data + .artist_history + .filtered_items + .is_some() + { + self.app.data.lidarr_data.artist_history.reset_filter(); + } else { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.reset_artist_info_tabs(); + } + } + ActiveLidarrBlock::ArtistDetails | ActiveLidarrBlock::ManualArtistSearch => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.reset_artist_info_tabs(); + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::ArtistDetails => match self.key { + _ if matches_key!(refresh, key) => self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()), + _ if matches_key!(auto_search, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into()); + } + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into()); + } + _ if matches_key!(edit, key) => { + self.app.push_navigation_stack( + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(self.active_lidarr_block), + ) + .into(), + ); + self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + } + _ if matches_key!(toggle_monitoring, key) => { + if !self.app.data.lidarr_data.albums.is_empty() { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::ToggleAlbumMonitoring(self.extract_album_id())); + + self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()); + } + } + _ => (), + }, + ActiveLidarrBlock::ArtistHistory | ActiveLidarrBlock::ManualArtistSearch => match self.key { + _ if matches_key!(refresh, key) => self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()), + _ if matches_key!(auto_search, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into()); + } + _ if matches_key!(edit, key) => { + self.app.push_navigation_stack( + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(self.active_lidarr_block), + ) + .into(), + ); + self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + } + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into()); + } + _ => (), + }, + ActiveLidarrBlock::AutomaticallySearchArtistPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::TriggerAutomaticArtistSearch(self.extract_artist_id()), + ); + + self.app.pop_navigation_stack(); + } + } + ActiveLidarrBlock::UpdateAndScanArtistPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::UpdateAndScanArtist(self.extract_artist_id())); + + self.app.pop_navigation_stack(); + } + } + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + let LidarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .lidarr_data + .discography_releases + .current_selection() + .clone(); + let params = LidarrReleaseDownloadBody { guid, indexer_id }; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DownloadRelease(params)); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +fn releases_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Source", + cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)), + }, + SortOption { + name: "Age", + cmp_fn: Some(|a, b| a.age.cmp(&b.age)), + }, + SortOption { + name: "Rejected", + cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)), + }, + SortOption { + name: "Title", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Indexer", + cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())), + }, + SortOption { + name: "Size", + cmp_fn: Some(|a, b| a.size.cmp(&b.size)), + }, + SortOption { + name: "Peers", + cmp_fn: Some(|a, b| { + let default_number = Number::from(i64::MAX); + let seeder_a = a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)), + }, + ] +} diff --git a/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs new file mode 100644 index 0000000..2e6e15e --- /dev/null +++ b/src/handlers/lidarr_handlers/library/artist_details_handler_tests.rs @@ -0,0 +1,1162 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::Number; + use std::cmp::Ordering; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::{ + ArtistDetailsHandler, releases_sorting_options, + }; + use crate::models::HorizontallyScrollableText; + use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, + }; + use crate::models::servarr_models::{Quality, QualityWrapper}; + + mod test_handle_delete { + use super::*; + use crate::assert_delete_prompt; + use crate::event::Key; + use crate::models::lidarr_models::Album; + use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS; + use pretty_assertions::assert_eq; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_album_delete() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .albums + .set_items(vec![Album::default()]); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + + assert_delete_prompt!( + ArtistDetailsHandler, + app, + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::DeleteAlbumPrompt + ); + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + DELETE_ALBUM_SELECTION_BLOCKS + ); + } + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveLidarrBlock::UpdateAndScanArtistPrompt, + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + ArtistDetailsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + #[case(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)] + #[case( + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + #[case( + ActiveLidarrBlock::ManualArtistSearch, + ActiveLidarrBlock::ArtistDetails + )] + fn test_artist_details_tabs_left_right_action( + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + #[values(true, false)] is_loading: bool, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = is_loading; + app.push_navigation_stack(right_block.into()); + app.data.lidarr_data.artist_info_tabs.index = app + .data + .lidarr_data + .artist_info_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle(); + + assert_eq!( + app.get_current_route(), + app.data.lidarr_data.artist_info_tabs.get_active_route() + ); + assert_navigation_pushed!(app, left_block.into()); + + ArtistDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle(); + + assert_eq!( + app.get_current_route(), + app.data.lidarr_data.artist_info_tabs.get_active_route() + ); + assert_navigation_pushed!(app, right_block.into()); + } + } + + mod test_handle_submit { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; + use crate::models::lidarr_models::{LidarrHistoryItem, LidarrReleaseDownloadBody}; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + artist, torrent_release, + }; + use crate::{assert_navigation_popped, assert_navigation_pushed}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + #[case( + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + LidarrEvent::TriggerAutomaticArtistSearch(1) + )] + #[case( + ActiveLidarrBlock::UpdateAndScanArtistPrompt, + LidarrEvent::UpdateAndScanArtist(1) + )] + fn test_artist_details_prompt_confirm_submit( + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.artists.set_items(vec![artist()]); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + } + + #[rstest] + fn test_artist_details_prompt_decline_submit( + #[values( + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + ActiveLidarrBlock::UpdateAndScanArtistPrompt + )] + prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_artist_history_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artist_history + .set_items(vec![LidarrHistoryItem::default()]); + + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::ArtistHistoryDetails.into()); + } + + #[test] + fn test_artist_history_submit_no_op_when_artist_history_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into()); + + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistHistory.into() + ); + } + + #[test] + fn test_artist_history_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + ArtistDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + } + + #[test] + fn test_manual_artist_search_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .discography_releases + .set_items(vec![torrent_release()]); + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into()); + + ArtistDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualArtistSearch, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into() + ); + } + + #[test] + fn test_manual_artist_search_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into()); + + ArtistDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualArtistSearch, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ManualArtistSearch.into() + ); + } + + #[test] + fn test_manual_artist_search_confirm_prompt_confirm_submit() { + let mut app = App::test_default(); + let release = torrent_release(); + app + .data + .lidarr_data + .discography_releases + .set_items(vec![release.clone()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into()); + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into()); + + ArtistDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody { + guid: release.guid, + indexer_id: release.indexer_id, + })) + ); + } + + #[test] + fn test_manual_artist_search_confirm_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .discography_releases + .set_items(vec![torrent_release()]); + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into()); + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into()); + + ArtistDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + } + + mod test_handle_esc { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_popped; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; + use crate::models::lidarr_models::LidarrHistoryItem; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_artist_history_details_block_esc() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into()); + app.push_navigation_stack(ActiveLidarrBlock::ArtistHistoryDetails.into()); + + ArtistDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::ArtistHistoryDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistHistory.into()); + } + + #[test] + fn test_artist_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() { + let mut app = App::test_default(); + app.data.lidarr_data.artist_history = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(vec![LidarrHistoryItem::default()]), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::ArtistHistory.into()); + + ArtistDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::ArtistHistory, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistHistory.into() + ); + assert_none!(app.data.lidarr_data.artist_history.filter); + assert_none!(app.data.lidarr_data.artist_history.filtered_items); + assert_none!(app.data.lidarr_data.artist_history.filtered_state); + } + + #[rstest] + fn test_artist_details_esc( + #[values( + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + ActiveLidarrBlock::UpdateAndScanArtistPrompt, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt + )] + prompt_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + ArtistDetailsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + } + + #[rstest] + fn test_artist_details_blocks_esc( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.artist_history.filter = None; + app.data.lidarr_data.artist_history.filtered_items = None; + app.data.lidarr_data.artist_history.filtered_state = None; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_is_empty!(app.data.lidarr_data.albums); + assert_is_empty!(app.data.lidarr_data.discography_releases); + assert_is_empty!(app.data.lidarr_data.artist_history); + assert_eq!(app.data.lidarr_data.artist_info_tabs.index, 0); + } + } + + mod test_handle_char_key_event { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::artist_details_handler::ArtistDetailsHandler; + use crate::models::lidarr_models::{Artist, LidarrReleaseDownloadBody}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_ARTIST_SELECTION_BLOCKS, + }; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::torrent_release; + use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_artist_details_edit_key( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(active_lidarr_block) + ) + .into() + ); + assert_modal_present!(app.data.lidarr_data.edit_artist_modal); + assert_some!(app.data.lidarr_data.edit_artist_modal); + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + EDIT_ARTIST_SELECTION_BLOCKS + ); + } + + #[rstest] + fn test_artist_details_edit_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); + } + + #[test] + fn test_artist_details_toggle_monitoring_key() { + let mut app = App::test_default_fully_populated(); + app.is_routing = false; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistDetails.into() + ); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(app.is_routing); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::ToggleAlbumMonitoring(1)) + ); + } + + #[test] + fn test_artist_details_toggle_monitoring_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.prompt_confirm = false; + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistDetails.into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_artist_details_toggle_monitoring_key_no_op_when_albums_empty() { + let mut app = App::test_default(); + app.data.lidarr_data.artists.set_items(vec![Artist { + id: 1, + ..Artist::default() + }]); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(app.data.lidarr_data.prompt_confirm_action.is_none()); + } + + #[rstest] + fn test_artist_details_auto_search_key( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!( + app, + ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into() + ); + } + + #[rstest] + fn test_artist_details_auto_search_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + } + + #[rstest] + fn test_artist_details_update_key( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAndScanArtistPrompt.into()); + } + + #[rstest] + fn test_artist_details_update_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + } + + #[rstest] + fn test_artist_details_refresh_key( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.is_routing = false; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, active_lidarr_block.into()); + assert!(app.is_routing); + } + + #[rstest] + fn test_artist_details_refresh_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ManualArtistSearch + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + app.is_routing = false; + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + assert!(!app.is_routing); + } + + #[rstest] + #[case( + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + LidarrEvent::TriggerAutomaticArtistSearch(1) + )] + #[case( + ActiveLidarrBlock::UpdateAndScanArtistPrompt, + LidarrEvent::UpdateAndScanArtist(1) + )] + fn test_artist_details_prompt_confirm_key( + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + #[values(ActiveLidarrBlock::ArtistDetails, ActiveLidarrBlock::ArtistHistory)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, active_lidarr_block.into()); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + } + + #[test] + fn test_manual_artist_search_confirm_prompt_confirm_key() { + let mut app = App::test_default(); + let release = torrent_release(); + app + .data + .lidarr_data + .discography_releases + .set_items(vec![release.clone()]); + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearch.into()); + app.push_navigation_stack(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt.into()); + + ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_navigation_popped!(app, ActiveLidarrBlock::ManualArtistSearch.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody { + guid: release.guid, + indexer_id: release.indexer_id, + })) + ); + } + } + + #[test] + fn test_artist_details_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(ArtistDetailsHandler::accepts(active_lidarr_block)); + } else { + assert!(!ArtistDetailsHandler::accepts(active_lidarr_block)); + } + }); + } + + #[test] + fn test_extract_artist_id() { + let mut app = App::test_default_fully_populated(); + + let artist_id = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .extract_artist_id(); + + assert_eq!(artist_id, 1); + } + + #[test] + fn test_extract_album_id() { + let mut app = App::test_default_fully_populated(); + + let album_id = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ) + .extract_album_id(); + + assert_eq!(album_id, 1); + } + + #[rstest] + fn test_artist_details_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_artist_details_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.is_loading = true; + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_artist_details_handler_is_ready_when_not_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.is_loading = false; + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistDetails, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_artist_details_handler_is_not_ready_when_not_loading_and_artist_history_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_artist_details_handler_ready_when_not_loading_and_artist_history_is_non_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app + .data + .lidarr_data + .artist_history + .set_items(vec![LidarrHistoryItem::default()]); + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ArtistHistory, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_artist_details_handler_is_not_ready_when_not_loading_and_discography_releases_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ManualArtistSearch, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_artist_details_handler_ready_when_not_loading_and_discography_releases_is_non_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app + .data + .lidarr_data + .discography_releases + .set_items(vec![LidarrRelease::default()]); + + let handler = ArtistDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::ManualArtistSearch, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_releases_sorting_options_source() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.protocol.cmp(&b.protocol); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[0].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Source"); + } + + #[test] + fn test_releases_sorting_options_age() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[1].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Age"); + } + + #[test] + fn test_releases_sorting_options_rejected() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.rejected.cmp(&b.rejected); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[2].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Rejected"); + } + + #[test] + fn test_releases_sorting_options_title() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[3].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Title"); + } + + #[test] + fn test_releases_sorting_options_indexer() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase()); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[4].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Indexer"); + } + + #[test] + fn test_releases_sorting_options_size() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.size.cmp(&b.size); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[5].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Size"); + } + + #[test] + fn test_releases_sorting_options_peers() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = |a, b| { + let default_number = Number::from(i64::MAX); + let seeder_a = a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[6].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Peers"); + } + + #[test] + fn test_releases_sorting_options_quality() { + let expected_cmp_fn: fn(&LidarrRelease, &LidarrRelease) -> Ordering = + |a, b| a.quality.cmp(&b.quality); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[7].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + fn release_vec() -> Vec { + let release_a = LidarrRelease { + protocol: "Protocol A".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Title A"), + indexer: "Indexer A".to_owned(), + size: 1, + rejected: true, + seeders: Some(Number::from(1)), + quality: QualityWrapper { + quality: Quality { + name: "Quality A".to_owned(), + }, + }, + ..LidarrRelease::default() + }; + let release_b = LidarrRelease { + protocol: "Protocol B".to_owned(), + age: 2, + title: HorizontallyScrollableText::from("title B"), + indexer: "indexer B".to_owned(), + size: 2, + rejected: false, + seeders: Some(Number::from(2)), + quality: QualityWrapper { + quality: Quality { + name: "Quality B".to_owned(), + }, + }, + ..LidarrRelease::default() + }; + let release_c = LidarrRelease { + protocol: "Protocol C".to_owned(), + age: 3, + title: HorizontallyScrollableText::from("Title C"), + indexer: "Indexer C".to_owned(), + size: 3, + rejected: false, + seeders: None, + quality: QualityWrapper { + quality: Quality { + name: "Quality C".to_owned(), + }, + }, + ..LidarrRelease::default() + }; + + vec![release_a, release_b, release_c] + } +} diff --git a/src/handlers/lidarr_handlers/library/delete_album_handler.rs b/src/handlers/lidarr_handlers/library/delete_album_handler.rs new file mode 100644 index 0000000..ab8e82d --- /dev/null +++ b/src/handlers/lidarr_handlers/library/delete_album_handler.rs @@ -0,0 +1,150 @@ +use crate::models::Route; +use crate::models::lidarr_models::DeleteParams; +use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_BLOCKS; +use crate::network::lidarr_network::LidarrEvent; +use crate::{ + app::App, + event::Key, + handlers::{KeyEventHandler, handle_prompt_toggle}, + matches_key, + models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, +}; + +#[cfg(test)] +#[path = "delete_album_handler_tests.rs"] +mod delete_album_handler_tests; + +pub(in crate::handlers::lidarr_handlers) struct DeleteAlbumHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl DeleteAlbumHandler<'_, '_> { + fn build_delete_album_params(&mut self) -> DeleteParams { + let id = self.app.data.lidarr_data.albums.current_selection().id; + let delete_files = self.app.data.lidarr_data.delete_files; + let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion; + self.app.data.lidarr_data.reset_delete_preferences(); + + DeleteParams { + id, + delete_files, + add_import_list_exclusion, + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteAlbumHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + DELETE_ALBUM_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> Self { + DeleteAlbumHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt { + self.app.data.lidarr_data.selected_block.up(); + } + } + + fn handle_scroll_down(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt { + self.app.data.lidarr_data.selected_block.down(); + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt { + handle_prompt_toggle(self.app, self.key); + } + } + + fn handle_submit(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::DeleteAlbumConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params())); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.reset_delete_preferences(); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::DeleteAlbumToggleDeleteFile => { + self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files; + } + ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion => { + self.app.data.lidarr_data.add_import_list_exclusion = + !self.app.data.lidarr_data.add_import_list_exclusion; + } + _ => (), + } + } + } + + fn handle_esc(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.reset_delete_preferences(); + self.app.data.lidarr_data.prompt_confirm = false; + } + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteAlbumPrompt + && self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::DeleteAlbumConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteAlbum(self.build_delete_album_params())); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/delete_album_handler_tests.rs b/src/handlers/lidarr_handlers/library/delete_album_handler_tests.rs new file mode 100644 index 0000000..65ddd49 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/delete_album_handler_tests.rs @@ -0,0 +1,404 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler; + use crate::models::lidarr_models::{Album, DeleteParams}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ALBUM_BLOCKS}; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_delete_album_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteAlbumToggleDeleteFile + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteAlbumConfirmPrompt + ); + } + } + + #[rstest] + fn test_delete_album_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion + ); + } + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + + DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + DeleteAlbumHandler::new(key, &mut app, ActiveLidarrBlock::DeleteAlbumPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS; + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::assert_navigation_popped; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_delete_album_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteAlbumHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_album_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + app + .data + .lidarr_data + .albums + .set_items(vec![Album::default()]); + let expected_delete_album_params = DeleteParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1); + + DeleteAlbumHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DeleteAlbum(expected_delete_album_params)) + ); + assert!(app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_album_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteAlbumHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::DeleteAlbumPrompt.into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(app.data.lidarr_data.delete_files); + assert!(app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_album_toggle_delete_files_submit() { + let current_route = ActiveLidarrBlock::DeleteAlbumPrompt.into(); + let mut app = App::test_default(); + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + + DeleteAlbumHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.lidarr_data.delete_files, true); + + DeleteAlbumHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.lidarr_data.delete_files, false); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_album_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteAlbumHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + } + + mod test_handle_key_char { + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ALBUM_SELECTION_BLOCKS, + }, + network::lidarr_network::LidarrEvent, + }; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_delete_album_confirm_prompt_prompt_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + app + .data + .lidarr_data + .albums + .set_items(vec![Album::default()]); + let expected_delete_album_params = DeleteParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ALBUM_SELECTION_BLOCKS.len() - 1); + + DeleteAlbumHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::ArtistDetails.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DeleteAlbum(expected_delete_album_params)) + ); + assert!(app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + } + + #[test] + fn test_delete_album_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) { + assert!(DeleteAlbumHandler::accepts(active_lidarr_block)); + } else { + assert!(!DeleteAlbumHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_delete_album_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = DeleteAlbumHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_delete_album_params() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .albums + .set_items(vec![Album::default()]); + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + let expected_delete_album_params = DeleteParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + + let delete_album_params = DeleteAlbumHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .build_delete_album_params(); + + assert_eq!(delete_album_params, expected_delete_album_params); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_album_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = DeleteAlbumHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_delete_album_handler_ready_when_not_loading() { + let mut app = App::test_default(); + app.is_loading = false; + + let handler = DeleteAlbumHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs new file mode 100644 index 0000000..e94e0a7 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler.rs @@ -0,0 +1,149 @@ +use crate::models::Route; +use crate::models::lidarr_models::DeleteParams; +use crate::network::lidarr_network::LidarrEvent; +use crate::{ + app::App, + event::Key, + handlers::{KeyEventHandler, handle_prompt_toggle}, + matches_key, + models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}, +}; + +#[cfg(test)] +#[path = "delete_artist_handler_tests.rs"] +mod delete_artist_handler_tests; + +pub(in crate::handlers::lidarr_handlers) struct DeleteArtistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl DeleteArtistHandler<'_, '_> { + fn build_delete_artist_params(&mut self) -> DeleteParams { + let id = self.app.data.lidarr_data.artists.current_selection().id; + let delete_files = self.app.data.lidarr_data.delete_files; + let add_import_list_exclusion = self.app.data.lidarr_data.add_import_list_exclusion; + self.app.data.lidarr_data.reset_delete_preferences(); + + DeleteParams { + id, + delete_files, + add_import_list_exclusion, + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DeleteArtistHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + DELETE_ARTIST_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> Self { + DeleteArtistHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + self.app.data.lidarr_data.selected_block.up(); + } + } + + fn handle_scroll_down(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + self.app.data.lidarr_data.selected_block.down(); + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + handle_prompt_toggle(self.app, self.key); + } + } + + fn handle_submit(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::DeleteArtistConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params())); + self.app.should_refresh = true; + } else { + self.app.data.lidarr_data.reset_delete_preferences(); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::DeleteArtistToggleDeleteFile => { + self.app.data.lidarr_data.delete_files = !self.app.data.lidarr_data.delete_files; + } + ActiveLidarrBlock::DeleteArtistToggleAddListExclusion => { + self.app.data.lidarr_data.add_import_list_exclusion = + !self.app.data.lidarr_data.add_import_list_exclusion; + } + _ => (), + } + } + } + + fn handle_esc(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.reset_delete_preferences(); + self.app.data.lidarr_data.prompt_confirm = false; + } + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteArtistPrompt + && self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::DeleteArtistConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteArtist(self.build_delete_artist_params())); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs new file mode 100644 index 0000000..6c06840 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/delete_artist_handler_tests.rs @@ -0,0 +1,410 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::delete_artist_handler::DeleteArtistHandler; + use crate::models::lidarr_models::{Artist, DeleteParams}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_delete_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteArtistToggleDeleteFile + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteArtistConfirmPrompt + ); + } + } + + #[rstest] + fn test_delete_artist_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::DeleteArtistToggleAddListExclusion + ); + } + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + DeleteArtistHandler::new(key, &mut app, ActiveLidarrBlock::DeleteArtistPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS; + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::assert_navigation_popped; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_delete_artist_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1); + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + let expected_delete_artist_params = DeleteParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1); + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DeleteArtist(expected_delete_artist_params)) + ); + assert!(app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::DeleteArtistPrompt.into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(app.data.lidarr_data.delete_files); + assert!(app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_toggle_delete_files_submit() { + let current_route = ActiveLidarrBlock::DeleteArtistPrompt.into(); + let mut app = App::test_default(); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.lidarr_data.delete_files, true); + + DeleteArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.lidarr_data.delete_files, false); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_artist_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + + DeleteArtistHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + } + + mod test_handle_key_char { + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, servarr_data::lidarr::lidarr_data::DELETE_ARTIST_SELECTION_BLOCKS, + }, + network::lidarr_network::LidarrEvent, + }; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_delete_artist_confirm_prompt_prompt_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + let expected_delete_artist_params = DeleteParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, DELETE_ARTIST_SELECTION_BLOCKS.len() - 1); + + DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::DeleteArtist(expected_delete_artist_params)) + ); + assert!(app.should_refresh); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + } + + #[test] + fn test_delete_artist_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(DeleteArtistHandler::accepts(active_lidarr_block)); + } else { + assert!(!DeleteArtistHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_delete_artist_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_delete_artist_params() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.data.lidarr_data.delete_files = true; + app.data.lidarr_data.add_import_list_exclusion = true; + let expected_delete_artist_params = DeleteParams { + id: 0, + delete_files: true, + add_import_list_exclusion: true, + }; + + let delete_artist_params = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .build_delete_artist_params(); + + assert_eq!(delete_artist_params, expected_delete_artist_params); + assert!(!app.data.lidarr_data.delete_files); + assert!(!app.data.lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_delete_artist_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_delete_artist_handler_ready_when_not_loading() { + let mut app = App::test_default(); + app.is_loading = false; + + let handler = DeleteArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/library/edit_artist_handler.rs b/src/handlers/lidarr_handlers/library/edit_artist_handler.rs new file mode 100644 index 0000000..194a5e0 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/edit_artist_handler.rs @@ -0,0 +1,455 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::lidarr_models::EditArtistParams; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS}; +use crate::models::servarr_data::lidarr::modals::EditArtistModal; +use crate::models::{Route, Scrollable}; +use crate::network::lidarr_network::LidarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "edit_artist_handler_tests.rs"] +mod edit_artist_handler_tests; + +pub(in crate::handlers::lidarr_handlers) struct EditArtistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl EditArtistHandler<'_, '_> { + fn build_edit_artist_params(&mut self) -> EditArtistParams { + let edit_artist_modal = self + .app + .data + .lidarr_data + .edit_artist_modal + .take() + .expect("EditArtistModal is None"); + let artist_id = self.app.data.lidarr_data.artists.current_selection().id; + let tags = edit_artist_modal.tags.text; + + let EditArtistModal { + monitored, + path, + monitor_list, + quality_profile_list, + metadata_profile_list, + .. + } = edit_artist_modal; + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self + .app + .data + .lidarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + let metadata_profile = metadata_profile_list.current_selection(); + let metadata_profile_id = *self + .app + .data + .lidarr_data + .metadata_profile_map + .iter() + .filter(|(_, value)| *value == metadata_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + EditArtistParams { + artist_id, + monitored, + monitor_new_items: Some(*monitor_list.current_selection()), + quality_profile_id: Some(quality_profile_id), + metadata_profile_id: Some(metadata_profile_id), + root_folder_path: Some(path.text), + tag_input_string: Some(tags), + ..EditArtistParams::default() + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for EditArtistHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + EDIT_ARTIST_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> EditArtistHandler<'a, 'b> { + EditArtistHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.edit_artist_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_up(), + ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_down(), + ActiveLidarrBlock::EditArtistPrompt => self.app.data.lidarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_top(), + ActiveLidarrBlock::EditArtistPathInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + .scroll_home(), + ActiveLidarrBlock::EditArtistTagsInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveLidarrBlock::EditArtistSelectQualityProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::EditArtistSelectMetadataProfile => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::EditArtistPathInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + .reset_offset(), + ActiveLidarrBlock::EditArtistTagsInput => self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistPrompt => handle_prompt_toggle(self.app, self.key), + ActiveLidarrBlock::EditArtistPathInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveLidarrBlock::EditArtistTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::EditArtistConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditArtist(self.build_edit_artist_params())); + self.app.should_refresh = true; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + | ActiveLidarrBlock::EditArtistSelectQualityProfile + | ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ), + ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => { + self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ); + self.app.ignore_special_keys_for_textbox_input = true; + } + ActiveLidarrBlock::EditArtistToggleMonitored => { + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitored = Some( + !self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitored + .unwrap_or_default(), + ) + } + _ => (), + } + } + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + | ActiveLidarrBlock::EditArtistSelectQualityProfile + | ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(), + ActiveLidarrBlock::EditArtistPathInput | ActiveLidarrBlock::EditArtistTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistTagsInput | ActiveLidarrBlock::EditArtistPathInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::EditArtistPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.edit_artist_modal = None; + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + | ActiveLidarrBlock::EditArtistSelectQualityProfile + | ActiveLidarrBlock::EditArtistSelectMetadataProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::EditArtistPathInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveLidarrBlock::EditArtistTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveLidarrBlock::EditArtistPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::EditArtistConfirmPrompt + && matches_key!(confirm, key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::EditArtist(self.build_edit_artist_params())); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs b/src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs new file mode 100644 index 0000000..fee625c --- /dev/null +++ b/src/handlers/lidarr_handlers/library/edit_artist_handler_tests.rs @@ -0,0 +1,1423 @@ +#[cfg(test)] +mod tests { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::edit_artist_handler::EditArtistHandler; + use crate::models::lidarr_models::{Artist, EditArtistParams, NewItemMonitorType}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_ARTIST_BLOCKS}; + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use crate::network::lidarr_network::LidarrEvent; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_ARTIST_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_edit_artist_select_monitor_new_items_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_type_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_type_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_type_vec.len()).rev() { + EditArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[i] + ); + } + } else { + for i in 0..monitor_type_vec.len() { + EditArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[(i + 1) % monitor_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_edit_artist_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + EditArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 2" + ); + + EditArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_edit_artist_select_metadata_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + EditArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 2" + ); + + EditArtistHandler::new( + key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_edit_artist_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditArtistToggleMonitored + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditArtistSelectQualityProfile + ); + } + } + + #[rstest] + fn test_edit_artist_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_loading = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::EditArtistSelectMonitorNewItems + ); + } + } + + mod test_handle_home_end { + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + + use strum::IntoEnumIterator; + + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + + use super::*; + + #[test] + fn test_edit_artist_select_monitor_new_items_home_end() { + let monitor_type_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_type_vec.clone()); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[monitor_type_vec.len() - 1] + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[0] + ); + } + + #[test] + fn test_edit_artist_select_quality_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 3" + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_edit_artist_select_metadata_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 3" + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_edit_artist_path_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + path: "Test".into(), + ..EditArtistModal::default() + }); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_artist_tags_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + tags: "Test".into(), + ..EditArtistModal::default() + }); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + EditArtistHandler::new(key, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_edit_artist_path_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + path: "Test".into(), + ..EditArtistModal::default() + }); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_artist_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + tags: "Test".into(), + ..EditArtistModal::default() + }); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use crate::assert_navigation_popped; + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_ARTIST_SELECTION_BLOCKS; + use crate::models::{BlockSelectionState, Route}; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_artist_path_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + path: "Test Path".into(), + ..EditArtistModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPathInput.into()); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditArtistPrompt.into()); + } + + #[test] + fn test_edit_artist_tags_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + tags: "Test Tags".into(), + ..EditArtistModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistTagsInput.into()); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .unwrap() + .tags + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::EditArtistPrompt.into()); + } + + #[test] + fn test_edit_artist_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_ARTIST_SELECTION_BLOCKS.len() - 1); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_edit_artist_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + let mut edit_artist = EditArtistModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditArtistModal::default() + }; + edit_artist + .quality_profile_list + .set_items(vec!["Lossless".to_owned(), "HD - 1080p".to_owned()]); + edit_artist + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Full".to_owned()]); + edit_artist + .monitor_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.edit_artist_modal = Some(edit_artist); + app.data.lidarr_data.artists.set_items(vec![Artist { + monitored: false, + ..Artist::default() + }]); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([ + (1111, "Lossless".to_owned()), + (2222, "HD - 1080p".to_owned()), + ]); + app.data.lidarr_data.metadata_profile_map = + BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]); + let expected_edit_artist_params = EditArtistParams { + artist_id: 0, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1111), + metadata_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditArtistParams::default() + }; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_ARTIST_SELECTION_BLOCKS.len() - 1); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::EditArtist(expected_edit_artist_params)) + ); + assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); + assert!(app.should_refresh); + } + + #[test] + fn test_edit_artist_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::EditArtistPrompt.into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + } + + #[test] + fn test_edit_artist_toggle_monitored_submit() { + let current_route = Route::from(( + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + )); + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(current_route); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_some_eq_x!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .monitored, + true + ); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_some_eq_x!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .monitored, + false + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::EditArtistSelectQualityProfile, 2)] + #[case(ActiveLidarrBlock::EditArtistSelectMetadataProfile, 3)] + #[case(ActiveLidarrBlock::EditArtistPathInput, 4)] + #[case(ActiveLidarrBlock::EditArtistTagsInput, 5)] + fn test_edit_artist_prompt_selected_block_submit( + #[case] selected_block: ActiveLidarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack( + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .into(), + ); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .handle(); + + assert_navigation_pushed!( + app, + (selected_block, Some(ActiveLidarrBlock::Artists)).into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + + if selected_block == ActiveLidarrBlock::EditArtistPathInput + || selected_block == ActiveLidarrBlock::EditArtistTagsInput + { + assert!(app.ignore_special_keys_for_textbox_input); + } + } + + #[rstest] + fn test_edit_artist_prompt_selected_block_submit_no_op_when_not_ready( + #[values(1, 2, 3, 4)] y_index: usize, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack( + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .into(), + ); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(ActiveLidarrBlock::Artists), + ) + .into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[rstest] + fn test_edit_artist_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + ActiveLidarrBlock::EditArtistPathInput, + ActiveLidarrBlock::EditArtistTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + EditArtistHandler::new( + SUBMIT_KEY, + &mut app, + active_lidarr_block, + Some(ActiveLidarrBlock::Artists), + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::EditArtistPrompt.into()); + + if active_lidarr_block == ActiveLidarrBlock::EditArtistPathInput + || active_lidarr_block == ActiveLidarrBlock::EditArtistTagsInput + { + assert!(!app.ignore_special_keys_for_textbox_input); + } + } + } + + mod test_handle_esc { + use crate::assert_navigation_popped; + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use rstest::rstest; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_artist_input_esc( + #[values( + ActiveLidarrBlock::EditArtistTagsInput, + ActiveLidarrBlock::EditArtistPathInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + EditArtistHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_navigation_popped!(app, ActiveLidarrBlock::EditArtistPrompt.into()); + } + + #[test] + fn test_edit_artist_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + EditArtistHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::EditArtistPrompt, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + + assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_edit_artist_esc( + #[values( + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + ActiveLidarrBlock::EditArtistSelectMetadataProfile + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + EditArtistHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, + servarr_data::lidarr::{ + lidarr_data::EDIT_ARTIST_SELECTION_BLOCKS, modals::EditArtistModal, + }, + }, + }; + use pretty_assertions::assert_eq; + + #[test] + fn test_edit_artist_path_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + path: "Test".into(), + ..EditArtistModal::default() + }); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .text, + "Tes" + ); + } + + #[test] + fn test_edit_artist_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + tags: "Test".into(), + ..EditArtistModal::default() + }); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_edit_artist_path_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + EditArtistHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditArtistPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .path + .text, + "a" + ); + } + + #[test] + fn test_edit_artist_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + EditArtistHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::EditArtistTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .unwrap() + .tags + .text, + "a" + ); + } + + #[test] + fn test_edit_artist_confirm_prompt_prompt_confirm() { + let mut app = App::test_default(); + let mut edit_artist = EditArtistModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditArtistModal::default() + }; + edit_artist + .quality_profile_list + .set_items(vec!["Lossless".to_owned(), "HD - 1080p".to_owned()]); + edit_artist + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Full".to_owned()]); + edit_artist + .monitor_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.edit_artist_modal = Some(edit_artist); + app.data.lidarr_data.artists.set_items(vec![Artist { + monitored: false, + ..Artist::default() + }]); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([ + (1111, "Lossless".to_owned()), + (2222, "HD - 1080p".to_owned()), + ]); + app.data.lidarr_data.metadata_profile_map = + BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]); + let expected_edit_artist_params = EditArtistParams { + artist_id: 0, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1111), + metadata_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditArtistParams::default() + }; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, EDIT_ARTIST_SELECTION_BLOCKS.len() - 1); + + EditArtistHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::EditArtist(expected_edit_artist_params)) + ); + assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); + assert!(app.should_refresh); + } + } + + #[test] + fn test_edit_artist_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(EditArtistHandler::accepts(active_lidarr_block)); + } else { + assert!(!EditArtistHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_edit_artist_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = EditArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_edit_artist_params() { + let mut app = App::test_default(); + let mut edit_artist = EditArtistModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditArtistModal::default() + }; + edit_artist + .quality_profile_list + .set_items(vec!["Lossless".to_owned(), "HD - 1080p".to_owned()]); + edit_artist + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Full".to_owned()]); + edit_artist + .monitor_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.edit_artist_modal = Some(edit_artist); + app.data.lidarr_data.artists.set_items(vec![Artist { + monitored: false, + ..Artist::default() + }]); + app.data.lidarr_data.quality_profile_map = BiMap::from_iter([ + (1111, "Lossless".to_owned()), + (2222, "HD - 1080p".to_owned()), + ]); + app.data.lidarr_data.metadata_profile_map = + BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]); + let expected_edit_artist_params = EditArtistParams { + artist_id: 0, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(1111), + metadata_profile_id: Some(1111), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditArtistParams::default() + }; + + let edit_artist_params = EditArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ) + .build_edit_artist_params(); + + assert_eq!(edit_artist_params, expected_edit_artist_params); + assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); + } + + #[test] + fn test_edit_artist_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_loading = true; + + let handler = EditArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_artist_handler_is_not_ready_when_edit_artist_modal_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_loading = false; + + let handler = EditArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_artist_handler_is_ready_when_edit_artist_modal_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_loading = false; + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + + let handler = EditArtistHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::EditArtistPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/library/library_handler_tests.rs b/src/handlers/lidarr_handlers/library/library_handler_tests.rs new file mode 100644 index 0000000..b28cb15 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/library_handler_tests.rs @@ -0,0 +1,772 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::Number; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options}; + use crate::models::lidarr_models::{Album, Artist, ArtistStatistics, ArtistStatus}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, + DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, + LIBRARY_BLOCKS, TRACK_DETAILS_BLOCKS, + }; + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use crate::network::lidarr_network::LidarrEvent; + use crate::{ + assert_modal_absent, assert_modal_present, assert_navigation_popped, assert_navigation_pushed, + test_handler_delegation, + }; + + #[test] + fn test_library_handler_accepts() { + let mut library_handler_blocks = Vec::new(); + library_handler_blocks.extend(LIBRARY_BLOCKS); + library_handler_blocks.extend(ARTIST_DETAILS_BLOCKS); + library_handler_blocks.extend(DELETE_ARTIST_BLOCKS); + library_handler_blocks.extend(DELETE_ALBUM_BLOCKS); + library_handler_blocks.extend(EDIT_ARTIST_BLOCKS); + library_handler_blocks.extend(ADD_ARTIST_BLOCKS); + library_handler_blocks.extend(ALBUM_DETAILS_BLOCKS); + library_handler_blocks.extend(TRACK_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|lidarr_block| { + if library_handler_blocks.contains(&lidarr_block) { + assert!( + LibraryHandler::accepts(lidarr_block), + "{lidarr_block} is not accepted by the LibraryHandler" + ); + } else { + assert!(!LibraryHandler::accepts(lidarr_block)); + } + }); + } + + #[test] + fn test_artists_sorting_options_name() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + a.artist_name + .text + .to_lowercase() + .cmp(&b.artist_name.text.to_lowercase()) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[0].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Name"); + } + + #[test] + fn test_artists_sorting_options_type() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + a.artist_type + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.artist_type + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[1].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Type"); + } + + #[test] + fn test_artists_sorting_options_status() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + a.status + .to_string() + .to_lowercase() + .cmp(&b.status.to_string().to_lowercase()) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[2].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Status"); + } + + #[test] + fn test_artists_sorting_options_quality_profile() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = + |a, b| a.quality_profile_id.cmp(&b.quality_profile_id); + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[3].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Quality Profile"); + } + + #[test] + fn test_artists_sorting_options_metadata_profile() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = + |a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id); + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[4].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Metadata Profile"); + } + + #[test] + fn test_artists_sorting_options_albums() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + a.statistics + .as_ref() + .map_or(0, |stats| stats.album_count) + .cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count)) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[5].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Albums"); + } + + #[test] + fn test_artists_sorting_options_tracks() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + a.statistics + .as_ref() + .map_or(0, |stats| stats.track_count) + .cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count)) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[6].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Tracks"); + } + + #[test] + fn test_artists_sorting_options_size() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + a.statistics + .as_ref() + .map_or(0, |stats| stats.size_on_disk) + .cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk)) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[7].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Size"); + } + + #[test] + fn test_artists_sorting_options_monitored() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| a.monitored.cmp(&b.monitored); + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[8].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Monitored"); + } + + #[test] + fn test_artists_sorting_options_tags() { + let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| { + let a_str = a + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + let b_str = b + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + a_str.cmp(&b_str) + }; + let mut expected_artists_vec = artists_vec(); + expected_artists_vec.sort_by(expected_cmp_fn); + + let sort_option = artists_sorting_options()[9].clone(); + let mut sorted_artists_vec = artists_vec(); + sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_artists_vec, expected_artists_vec); + assert_str_eq!(sort_option.name, "Tags"); + } + + #[test] + fn test_toggle_monitoring_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_routing = false; + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + assert!(app.data.lidarr_data.prompt_confirm); + assert!(app.is_routing); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::ToggleArtistMonitoring(0) + ); + } + + #[test] + fn test_toggle_monitoring_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.is_routing = false; + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_modal_absent!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.is_routing); + } + + #[test] + fn test_update_all_artists_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + } + + #[test] + fn test_update_all_artists_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + } + + #[test] + fn test_update_all_artists_prompt_confirm_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.submit.key, + &mut app, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::UpdateAllArtists + ); + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + } + + #[test] + fn test_update_all_artists_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.submit.key, + &mut app, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + } + + #[test] + fn test_update_all_artists_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_update_all_artists_prompt_left_right() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_update_all_artists_prompt_confirm_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::UpdateAllArtists + ); + assert_navigation_popped!(app, ActiveLidarrBlock::Artists.into()); + } + + fn artists_vec() -> Vec { + vec![ + Artist { + id: 3, + artist_name: "Test Artist 1".into(), + artist_type: Some("Group".to_owned()), + status: ArtistStatus::Ended, + quality_profile_id: 1, + metadata_profile_id: 1, + monitored: false, + tags: vec![Number::from(1), Number::from(2)], + statistics: Some(ArtistStatistics { + album_count: 5, + track_count: 50, + size_on_disk: 789, + ..ArtistStatistics::default() + }), + ..Artist::default() + }, + Artist { + id: 2, + artist_name: "Test Artist 2".into(), + artist_type: Some("Solo".to_owned()), + status: ArtistStatus::Continuing, + quality_profile_id: 2, + metadata_profile_id: 2, + monitored: false, + tags: vec![Number::from(1), Number::from(3)], + statistics: Some(ArtistStatistics { + album_count: 10, + track_count: 100, + size_on_disk: 456, + ..ArtistStatistics::default() + }), + ..Artist::default() + }, + Artist { + id: 1, + artist_name: "Test Artist 3".into(), + artist_type: None, + status: ArtistStatus::Deleted, + quality_profile_id: 3, + metadata_profile_id: 3, + monitored: true, + tags: vec![Number::from(2), Number::from(3)], + statistics: Some(ArtistStatistics { + album_count: 3, + track_count: 30, + size_on_disk: 123, + ..ArtistStatistics::default() + }), + ..Artist::default() + }, + ] + } + + #[rstest] + fn test_delegates_add_artist_blocks_to_add_artist_handler( + #[values( + ActiveLidarrBlock::AddArtistSearchInput, + ActiveLidarrBlock::AddArtistEmptySearchResults, + ActiveLidarrBlock::AddArtistSearchResults + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + } + + #[test] + fn test_delegates_delete_album_blocks_to_delete_album_handler() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .albums + .set_items(vec![Album::default()]); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteAlbumPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::ArtistDetails.into() + ); + } + + #[test] + fn test_delegates_delete_artist_blocks_to_delete_artist_handler() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteArtistPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_delegates_edit_artist_blocks_to_edit_artist_handler( + #[values( + ActiveLidarrBlock::EditArtistPrompt, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + ActiveLidarrBlock::EditArtistTagsInput, + ActiveLidarrBlock::EditArtistPathInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_delegates_artist_details_blocks_to_artist_details_handler( + #[values( + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + ActiveLidarrBlock::SearchAlbums, + ActiveLidarrBlock::SearchAlbumsError, + ActiveLidarrBlock::UpdateAndScanArtistPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_delegates_album_details_blocks_to_album_details_handler( + #[values( + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::SearchTracks, + ActiveLidarrBlock::SearchTracksError, + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + ActiveLidarrBlock::SearchAlbumHistory, + ActiveLidarrBlock::SearchAlbumHistoryError, + ActiveLidarrBlock::FilterAlbumHistory, + ActiveLidarrBlock::FilterAlbumHistoryError, + ActiveLidarrBlock::AlbumHistorySortPrompt, + ActiveLidarrBlock::AlbumHistoryDetails, + ActiveLidarrBlock::ManualAlbumSearch, + ActiveLidarrBlock::ManualAlbumSearchSortPrompt, + ActiveLidarrBlock::DeleteTrackFilePrompt + )] + active_sonarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveLidarrBlock::Artists, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_track_details_blocks_to_track_details_handler( + #[values( + ActiveLidarrBlock::TrackDetails, + ActiveLidarrBlock::TrackHistory, + ActiveLidarrBlock::TrackHistoryDetails, + ActiveLidarrBlock::SearchTrackHistory, + ActiveLidarrBlock::SearchTrackHistoryError, + ActiveLidarrBlock::FilterTrackHistory, + ActiveLidarrBlock::FilterTrackHistoryError, + ActiveLidarrBlock::TrackHistorySortPrompt + )] + active_sonarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveLidarrBlock::AlbumDetails, + active_sonarr_block + ); + } + + #[test] + fn test_edit_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.data.lidarr_data.quality_profile_map = + bimap::BiMap::from_iter([(0i64, "Default Quality".to_owned())]); + app.data.lidarr_data.metadata_profile_map = + bimap::BiMap::from_iter([(0i64, "Default Metadata".to_owned())]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::EditArtistPrompt.into()); + assert_modal_present!(app.data.lidarr_data.edit_artist_modal); + assert_eq!( + app.data.lidarr_data.selected_block.blocks, + EDIT_ARTIST_SELECTION_BLOCKS + ); + } + + #[test] + fn test_edit_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + assert_modal_absent!(app.data.lidarr_data.edit_artist_modal); + } + + #[test] + fn test_refresh_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .artists + .set_items(vec![Artist::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + LibraryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Artists, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + assert!(app.should_refresh); + } +} diff --git a/src/handlers/lidarr_handlers/library/mod.rs b/src/handlers/lidarr_handlers/library/mod.rs new file mode 100644 index 0000000..82db4b2 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/mod.rs @@ -0,0 +1,353 @@ +use crate::{ + app::App, + event::Key, + handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}, + matches_key, + models::{ + BlockSelectionState, HorizontallyScrollableText, + lidarr_models::Artist, + servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, + LIBRARY_BLOCKS, + }, + stateful_table::SortOption, + }, + network::lidarr_network::LidarrEvent, +}; + +use super::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; + +mod add_artist_handler; +mod album_details_handler; +mod artist_details_handler; +mod delete_album_handler; +mod delete_artist_handler; +mod edit_artist_handler; +mod track_details_handler; + +use crate::handlers::lidarr_handlers::library::album_details_handler::AlbumDetailsHandler; +use crate::handlers::lidarr_handlers::library::delete_album_handler::DeleteAlbumHandler; +use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler; +use crate::models::Route; +pub(in crate::handlers::lidarr_handlers) use add_artist_handler::AddArtistHandler; +pub(in crate::handlers::lidarr_handlers) use artist_details_handler::ArtistDetailsHandler; +pub(in crate::handlers::lidarr_handlers) use delete_artist_handler::DeleteArtistHandler; +pub(in crate::handlers::lidarr_handlers) use edit_artist_handler::EditArtistHandler; + +#[cfg(test)] +#[path = "library_handler_tests.rs"] +mod library_handler_tests; + +pub(super) struct LibraryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl LibraryHandler<'_, '_> { + fn extract_artist_id(&self) -> i64 { + self.app.data.lidarr_data.artists.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, 'b> { + fn handle(&mut self) { + let artists_table_handling_config = TableHandlingConfig::new(ActiveLidarrBlock::Artists.into()) + .sorting_block(ActiveLidarrBlock::ArtistsSortPrompt.into()) + .sort_options(artists_sorting_options()) + .searching_block(ActiveLidarrBlock::SearchArtists.into()) + .search_error_block(ActiveLidarrBlock::SearchArtistsError.into()) + .search_field_fn(|artist| &artist.artist_name.text) + .filtering_block(ActiveLidarrBlock::FilterArtists.into()) + .filter_error_block(ActiveLidarrBlock::FilterArtistsError.into()) + .filter_field_fn(|artist| &artist.artist_name.text); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.artists, + artists_table_handling_config, + ) { + match self.active_lidarr_block { + _ if AddArtistHandler::accepts(self.active_lidarr_block) => { + AddArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if DeleteArtistHandler::accepts(self.active_lidarr_block) => { + DeleteArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if EditArtistHandler::accepts(self.active_lidarr_block) => { + EditArtistHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if ArtistDetailsHandler::accepts(self.active_lidarr_block) => { + ArtistDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if DeleteAlbumHandler::accepts(self.active_lidarr_block) => { + DeleteAlbumHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if AlbumDetailsHandler::accepts(self.active_lidarr_block) => { + AlbumDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if TrackDetailsHandler::accepts(self.active_lidarr_block) => { + TrackDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ => self.handle_key_event(), + } + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + AddArtistHandler::accepts(active_block) + || DeleteArtistHandler::accepts(active_block) + || DeleteAlbumHandler::accepts(active_block) + || EditArtistHandler::accepts(active_block) + || ArtistDetailsHandler::accepts(active_block) + || AlbumDetailsHandler::accepts(active_block) + || TrackDetailsHandler::accepts(active_block) + || LIBRARY_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> LibraryHandler<'a, 'b> { + LibraryHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.artists.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Artists { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Artists => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::UpdateAllArtistsPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Artists => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + } + ActiveLidarrBlock::UpdateAllArtistsPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::UpdateAllArtistsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => { + handle_clear_errors(self.app); + } + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::Artists => match key { + _ if matches_key!(add, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddArtistSearchInput.into()); + self.app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default()); + self.app.ignore_special_keys_for_textbox_input = true; + } + _ if matches_key!(toggle_monitoring, key) => { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some( + LidarrEvent::ToggleArtistMonitoring(self.extract_artist_id()), + ); + + self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()); + } + _ if matches_key!(edit, key) => { + self.app.data.lidarr_data.edit_artist_modal = Some((&self.app.data.lidarr_data).into()); + self + .app + .push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + } + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + } + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + }, + ActiveLidarrBlock::UpdateAllArtistsPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateAllArtists); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +fn artists_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Name", + cmp_fn: Some(|a, b| { + a.artist_name + .text + .to_lowercase() + .cmp(&b.artist_name.text.to_lowercase()) + }), + }, + SortOption { + name: "Type", + cmp_fn: Some(|a, b| { + a.artist_type + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.artist_type + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }), + }, + SortOption { + name: "Status", + cmp_fn: Some(|a, b| { + a.status + .to_string() + .to_lowercase() + .cmp(&b.status.to_string().to_lowercase()) + }), + }, + SortOption { + name: "Quality Profile", + cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)), + }, + SortOption { + name: "Metadata Profile", + cmp_fn: Some(|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id)), + }, + SortOption { + name: "Albums", + cmp_fn: Some(|a, b| { + a.statistics + .as_ref() + .map_or(0, |stats| stats.album_count) + .cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count)) + }), + }, + SortOption { + name: "Tracks", + cmp_fn: Some(|a, b| { + a.statistics + .as_ref() + .map_or(0, |stats| stats.track_count) + .cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count)) + }), + }, + SortOption { + name: "Size", + cmp_fn: Some(|a, b| { + a.statistics + .as_ref() + .map_or(0, |stats| stats.size_on_disk) + .cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk)) + }), + }, + SortOption { + name: "Monitored", + cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)), + }, + SortOption { + name: "Tags", + cmp_fn: Some(|a, b| { + let a_str = a + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + let b_str = b + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + + a_str.cmp(&b_str) + }), + }, + ] +} diff --git a/src/handlers/lidarr_handlers/library/track_details_handler.rs b/src/handlers/lidarr_handlers/library/track_details_handler.rs new file mode 100644 index 0000000..0c765d8 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/track_details_handler.rs @@ -0,0 +1,225 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::KeyEventHandler; +use crate::handlers::lidarr_handlers::history::history_sorting_options; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::matches_key; +use crate::models::Route; +use crate::models::lidarr_models::LidarrHistoryItem; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS}; + +#[cfg(test)] +#[path = "track_details_handler_tests.rs"] +mod track_details_handler_tests; + +pub(super) struct TrackDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for TrackDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let track_history_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::TrackHistory.into()) + .sorting_block(ActiveLidarrBlock::TrackHistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveLidarrBlock::SearchTrackHistory.into()) + .search_error_block(ActiveLidarrBlock::SearchTrackHistoryError.into()) + .search_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text) + .filtering_block(ActiveLidarrBlock::FilterTrackHistory.into()) + .filter_error_block(ActiveLidarrBlock::FilterTrackHistoryError.into()) + .filter_field_fn(|history_item: &LidarrHistoryItem| &history_item.source_title.text); + + if !handle_table( + self, + |app| { + &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("Album details modal is undefined") + .track_details_modal + .as_mut() + .expect("Track details modal is undefined") + .track_history + }, + track_history_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + TRACK_DETAILS_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, + ) -> Self { + Self { + key, + app, + active_lidarr_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + if self.app.is_loading { + return false; + } + + let Some(album_details_modal) = self.app.data.lidarr_data.album_details_modal.as_ref() else { + return false; + }; + + let Some(track_details_modal) = &album_details_modal.track_details_modal else { + return false; + }; + + match self.active_lidarr_block { + ActiveLidarrBlock::TrackDetails => !track_details_modal.track_details.is_empty(), + ActiveLidarrBlock::TrackHistory => !track_details_modal.track_history.is_empty(), + _ => true, + } + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key { + _ if matches_key!(left, self.key) => { + self + .app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_details_tabs + .previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route(), + ); + } + _ if matches_key!(right, self.key) => { + self + .app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_details_tabs + .next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route(), + ); + } + _ => (), + }, + _ => (), + } + } + + fn handle_submit(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::TrackHistory { + self + .app + .push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into()); + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => { + self.app.pop_navigation_stack(); + self + .app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal = None; + } + ActiveLidarrBlock::TrackHistoryDetails => { + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::TrackDetails | ActiveLidarrBlock::TrackHistory => match self.key { + _ if matches_key!(refresh, self.key) => { + self + .app + .pop_and_push_navigation_stack(self.active_lidarr_block.into()); + } + _ => (), + }, + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/library/track_details_handler_tests.rs b/src/handlers/lidarr_handlers/library/track_details_handler_tests.rs new file mode 100644 index 0000000..9529db7 --- /dev/null +++ b/src/handlers/lidarr_handlers/library/track_details_handler_tests.rs @@ -0,0 +1,407 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::library::track_details_handler::TrackDetailsHandler; + use crate::models::ScrollableText; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS}; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_left_right_actions { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + #[case(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)] + #[case(ActiveLidarrBlock::TrackHistory, ActiveLidarrBlock::TrackDetails)] + fn test_track_details_tabs_left_right_action( + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + app.is_loading = is_ready; + app.push_navigation_stack(right_block.into()); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_details_tabs + .index = app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route() + ); + assert_navigation_pushed!(app, left_block.into()); + + TrackDetailsHandler::new(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route() + ); + assert_navigation_pushed!(app, right_block.into()); + } + } + + mod test_handle_submit { + use super::*; + use crate::event::Key; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_track_history_submit() { + let mut app = App::test_default_fully_populated(); + + TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::TrackHistoryDetails.into()); + } + + #[test] + fn test_track_history_submit_no_op_when_track_history_is_empty() { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_history = StatefulTable::default(); + app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into()); + + TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::TrackHistory.into() + ); + } + + #[test] + fn test_track_history_submit_no_op_when_not_ready() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into()); + + TrackDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::TrackHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::TrackHistory.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use crate::event::Key; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_track_history_details_block_esc() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into()); + app.push_navigation_stack(ActiveLidarrBlock::TrackHistoryDetails.into()); + + TrackDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::TrackHistoryDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::TrackHistory.into()); + } + + #[rstest] + fn test_track_details_tabs_esc( + #[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AlbumDetails.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + TrackDetailsHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AlbumDetails.into()); + assert_none!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + ); + } + } + + mod test_handle_key_char { + use super::*; + use pretty_assertions::assert_eq; + + #[rstest] + fn test_refresh_key( + #[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.is_routing = false; + + TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, active_lidarr_block.into()); + assert!(app.is_routing); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values(ActiveLidarrBlock::TrackDetails, ActiveLidarrBlock::TrackHistory)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + app.is_routing = false; + + TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + assert!(!app.is_routing); + } + } + + #[test] + fn test_track_details_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(TrackDetailsHandler::accepts(active_lidarr_block)); + } else { + assert!(!TrackDetailsHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_track_details_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_track_details_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into()); + app.is_loading = true; + + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TrackDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_track_details_handler_is_not_ready_when_album_details_modal_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into()); + app.is_loading = false; + + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TrackDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_track_details_handler_is_not_ready_when_track_details_modal_is_empty() { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal = None; + app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into()); + app.is_loading = false; + + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TrackDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_track_details_handler_is_not_ready_when_track_details_is_empty() { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_details = ScrollableText::default(); + app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into()); + app.is_loading = false; + + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TrackDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_track_details_handler_is_not_ready_when_track_history_table_is_empty() { + let mut app = App::test_default_fully_populated(); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_history = StatefulTable::default(); + app.push_navigation_stack(ActiveLidarrBlock::TrackHistory.into()); + app.is_loading = false; + + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::TrackHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[rstest] + fn test_track_details_handler_is_ready( + #[values( + ActiveLidarrBlock::TrackDetails, + ActiveLidarrBlock::TrackHistory, + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.is_loading = false; + + let handler = TrackDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_lidarr_block, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs new file mode 100644 index 0000000..1a50709 --- /dev/null +++ b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs @@ -0,0 +1,296 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::{LidarrHandler, handle_change_tab_left_right_keys}; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::{assert_navigation_pushed, test_handler_delegation}; + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + #[rstest] + fn test_lidarr_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = LidarrHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_lidarr_handler_is_ready() { + let mut app = App::test_default(); + app.is_loading = true; + + let handler = LidarrHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_lidarr_handler_accepts() { + for lidarr_block in ActiveLidarrBlock::iter() { + assert!(LidarrHandler::accepts(lidarr_block)); + } + } + + #[rstest] + #[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)] + #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Blocklist)] + #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::History)] + #[case(3, ActiveLidarrBlock::Blocklist, ActiveLidarrBlock::RootFolders)] + #[case(4, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)] + #[case(5, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)] + #[case(6, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)] + fn test_lidarr_handler_change_tab_left_right_keys( + #[case] index: usize, + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.key); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + left_block.into() + ); + assert_navigation_pushed!(app, left_block.into()); + + app.data.lidarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.key); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + right_block.into() + ); + assert_navigation_pushed!(app, right_block.into()); + } + + #[rstest] + #[case(0, ActiveLidarrBlock::System, ActiveLidarrBlock::Downloads)] + #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Blocklist)] + #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::History)] + #[case(3, ActiveLidarrBlock::Blocklist, ActiveLidarrBlock::RootFolders)] + #[case(4, ActiveLidarrBlock::History, ActiveLidarrBlock::Indexers)] + #[case(5, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::System)] + #[case(6, ActiveLidarrBlock::Indexers, ActiveLidarrBlock::Artists)] + fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation( + #[case] index: usize, + #[case] left_block: ActiveLidarrBlock, + #[case] right_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap()); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + left_block.into() + ); + assert_navigation_pushed!(app, left_block.into()); + + app.data.lidarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap()); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + right_block.into() + ); + assert_navigation_pushed!(app, right_block.into()); + } + + #[rstest] + #[case(0, ActiveLidarrBlock::Artists)] + #[case(1, ActiveLidarrBlock::Downloads)] + #[case(2, ActiveLidarrBlock::Blocklist)] + #[case(3, ActiveLidarrBlock::History)] + #[case(4, ActiveLidarrBlock::RootFolders)] + #[case(5, ActiveLidarrBlock::Indexers)] + #[case(6, ActiveLidarrBlock::System)] + fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( + #[case] index: usize, + #[case] block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(block.into()); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap()); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + block.into() + ); + assert_eq!(app.get_current_route(), block.into()); + + app.data.lidarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap()); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + block.into() + ); + assert_eq!(app.get_current_route(), block.into()); + } + + #[rstest] + fn test_delegates_library_blocks_to_library_handler( + #[values( + ActiveLidarrBlock::Artists, + ActiveLidarrBlock::ArtistsSortPrompt, + ActiveLidarrBlock::FilterArtists, + ActiveLidarrBlock::FilterArtistsError, + ActiveLidarrBlock::SearchArtists, + ActiveLidarrBlock::SearchArtistsError, + ActiveLidarrBlock::UpdateAllArtistsPrompt, + ActiveLidarrBlock::DeleteArtistPrompt, + ActiveLidarrBlock::EditArtistPrompt, + ActiveLidarrBlock::EditArtistPathInput, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + ActiveLidarrBlock::EditArtistTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Artists, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_downloads_blocks_to_downloads_handler( + #[values( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Downloads, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_blocklist_blocks_to_blocklist_handler( + #[values( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::BlocklistItemDetails, + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt, + ActiveLidarrBlock::BlocklistSortPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Blocklist, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_history_blocks_to_history_handler( + #[values( + ActiveLidarrBlock::History, + ActiveLidarrBlock::HistoryItemDetails, + ActiveLidarrBlock::HistorySortPrompt, + ActiveLidarrBlock::FilterHistory, + ActiveLidarrBlock::FilterHistoryError, + ActiveLidarrBlock::SearchHistory, + ActiveLidarrBlock::SearchHistoryError + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::History, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_root_folders_blocks_to_root_folders_handler( + #[values( + ActiveLidarrBlock::RootFolders, + ActiveLidarrBlock::AddRootFolderPrompt, + ActiveLidarrBlock::DeleteRootFolderPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::RootFolders, + active_lidarr_block + ); + } + + #[rstest] + fn test_delegates_indexers_blocks_to_indexers_handler( + #[values( + ActiveLidarrBlock::DeleteIndexerPrompt, + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::AllIndexerSettingsPrompt, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Indexers, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_system_blocks_to_system_handler( + #[values( + ActiveLidarrBlock::System, + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::System, + active_sonarr_block + ); + } +} diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs new file mode 100644 index 0000000..7a102c2 --- /dev/null +++ b/src/handlers/lidarr_handlers/mod.rs @@ -0,0 +1,133 @@ +use history::HistoryHandler; +use indexers::IndexersHandler; +use library::LibraryHandler; + +use super::KeyEventHandler; +use crate::handlers::lidarr_handlers::blocklist::BlocklistHandler; +use crate::handlers::lidarr_handlers::downloads::DownloadsHandler; +use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler; +use crate::handlers::lidarr_handlers::system::SystemHandler; +use crate::models::Route; +use crate::{ + app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, +}; + +mod blocklist; +mod downloads; +mod history; +mod indexers; +mod library; +mod root_folders; +mod system; + +#[cfg(test)] +#[path = "lidarr_handler_tests.rs"] +mod lidarr_handler_tests; + +pub(super) struct LidarrHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b> { + fn handle(&mut self) { + match self.active_lidarr_block { + _ if LibraryHandler::accepts(self.active_lidarr_block) => { + LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ if DownloadsHandler::accepts(self.active_lidarr_block) => { + DownloadsHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ if BlocklistHandler::accepts(self.active_lidarr_block) => { + BlocklistHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ if HistoryHandler::accepts(self.active_lidarr_block) => { + HistoryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ if RootFoldersHandler::accepts(self.active_lidarr_block) => { + RootFoldersHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + _ if IndexersHandler::accepts(self.active_lidarr_block) => { + IndexersHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ if SystemHandler::accepts(self.active_lidarr_block) => { + SystemHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } + _ => self.handle_key_event(), + } + } + + fn accepts(_active_block: ActiveLidarrBlock) -> bool { + true + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> LidarrHandler<'a, 'b> { + LidarrHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + true + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) {} + + fn handle_char_key_event(&mut self) {} + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) { + let key_ref = key; + match key_ref { + _ if matches_key!(left, key, app.ignore_special_keys_for_textbox_input) => { + app.data.lidarr_data.main_tabs.previous(); + app.pop_and_push_navigation_stack(app.data.lidarr_data.main_tabs.get_active_route()); + } + _ if matches_key!(right, key, app.ignore_special_keys_for_textbox_input) => { + app.data.lidarr_data.main_tabs.next(); + app.pop_and_push_navigation_stack(app.data.lidarr_data.main_tabs.get_active_route()); + } + _ => (), + } +} diff --git a/src/handlers/lidarr_handlers/root_folders/add_root_folder_handler.rs b/src/handlers/lidarr_handlers/root_folders/add_root_folder_handler.rs new file mode 100644 index 0000000..b6d1bf7 --- /dev/null +++ b/src/handlers/lidarr_handlers/root_folders/add_root_folder_handler.rs @@ -0,0 +1,533 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::lidarr_models::AddLidarrRootFolderBody; +use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock}; +use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; +use crate::models::{Route, Scrollable}; +use crate::network::lidarr_network::LidarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "add_root_folder_handler_tests.rs"] +mod add_root_folder_handler_tests; + +pub(super) struct AddRootFolderHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl AddRootFolderHandler<'_, '_> { + fn build_add_root_folder_body(&mut self) -> AddLidarrRootFolderBody { + let add_root_folder_modal = self + .app + .data + .lidarr_data + .add_root_folder_modal + .take() + .expect("AddRootFolderModal is None"); + + let tags = add_root_folder_modal.tags.text.clone(); + + let AddRootFolderModal { + name, + path, + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + .. + } = add_root_folder_modal; + + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self + .app + .data + .lidarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let metadata_profile = metadata_profile_list.current_selection(); + let metadata_profile_id = *self + .app + .data + .lidarr_data + .metadata_profile_map + .iter() + .filter(|(_, value)| *value == metadata_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + AddLidarrRootFolderBody { + name: name.text, + path: path.text, + default_quality_profile_id: quality_profile_id, + default_metadata_profile_id: metadata_profile_id, + default_monitor_option: *monitor_list.current_selection(), + default_new_item_monitor_option: *monitor_new_items_list.current_selection(), + default_tags: Vec::new(), + tag_input_string: Some(tags), + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for AddRootFolderHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + ADD_ROOT_FOLDER_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> AddRootFolderHandler<'a, 'b> { + AddRootFolderHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.lidarr_data.add_root_folder_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_up(), + ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_up(), + ActiveLidarrBlock::AddRootFolderPrompt => self.app.data.lidarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_down(), + ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_down(), + ActiveLidarrBlock::AddRootFolderPrompt => self.app.data.lidarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_top(), + ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_top(), + ActiveLidarrBlock::AddRootFolderNameInput => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + .scroll_home(), + ActiveLidarrBlock::AddRootFolderPathInput => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + .scroll_home(), + ActiveLidarrBlock::AddRootFolderTagsInput => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_bottom(), + ActiveLidarrBlock::AddRootFolderNameInput => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + .reset_offset(), + ActiveLidarrBlock::AddRootFolderPathInput => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + .reset_offset(), + ActiveLidarrBlock::AddRootFolderTagsInput => self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderPrompt => handle_prompt_toggle(self.app, self.key), + ActiveLidarrBlock::AddRootFolderNameInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + ) + } + ActiveLidarrBlock::AddRootFolderPathInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveLidarrBlock::AddRootFolderTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderPrompt => { + match self.app.data.lidarr_data.selected_block.get_active_block() { + ActiveLidarrBlock::AddRootFolderConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); + self.app.should_refresh = true; + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::AddRootFolderSelectMonitor + | ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems + | ActiveLidarrBlock::AddRootFolderSelectQualityProfile + | ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => { + self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ) + } + ActiveLidarrBlock::AddRootFolderNameInput + | ActiveLidarrBlock::AddRootFolderPathInput + | ActiveLidarrBlock::AddRootFolderTagsInput => { + self.app.push_navigation_stack( + ( + self.app.data.lidarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ); + self.app.ignore_special_keys_for_textbox_input = true; + } + _ => (), + } + } + ActiveLidarrBlock::AddRootFolderSelectMonitor + | ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems + | ActiveLidarrBlock::AddRootFolderSelectQualityProfile + | ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(), + ActiveLidarrBlock::AddRootFolderNameInput + | ActiveLidarrBlock::AddRootFolderPathInput + | ActiveLidarrBlock::AddRootFolderTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderNameInput + | ActiveLidarrBlock::AddRootFolderPathInput + | ActiveLidarrBlock::AddRootFolderTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::AddRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.add_root_folder_modal = None; + self.app.data.lidarr_data.prompt_confirm = false; + } + ActiveLidarrBlock::AddRootFolderSelectMonitor + | ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems + | ActiveLidarrBlock::AddRootFolderSelectQualityProfile + | ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderNameInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + ) + } + ActiveLidarrBlock::AddRootFolderPathInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveLidarrBlock::AddRootFolderTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveLidarrBlock::AddRootFolderPrompt => { + if self.app.data.lidarr_data.selected_block.get_active_block() + == ActiveLidarrBlock::AddRootFolderConfirmPrompt + && matches_key!(confirm, key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/root_folders/add_root_folder_handler_tests.rs b/src/handlers/lidarr_handlers/root_folders/add_root_folder_handler_tests.rs new file mode 100644 index 0000000..004706d --- /dev/null +++ b/src/handlers/lidarr_handlers/root_folders/add_root_folder_handler_tests.rs @@ -0,0 +1,1707 @@ +#[cfg(test)] +mod tests { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::root_folders::add_root_folder_handler::AddRootFolderHandler; + use crate::models::lidarr_models::{AddLidarrRootFolderBody, MonitorType, NewItemMonitorType}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, + }; + use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; + use crate::network::lidarr_network::LidarrEvent; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_add_root_folder_select_monitor_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_type_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_type_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_type_vec.len()).rev() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[i] + ); + } + } else { + for i in 0..monitor_type_vec.len() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[(i + 1) % monitor_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_root_folder_select_monitor_new_items_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_type_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_type_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_type_vec.len()).rev() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[i] + ); + } + } else { + for i in 0..monitor_type_vec.len() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[(i + 1) % monitor_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_root_folder_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 2" + ); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_root_folder_select_metadata_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 2" + ); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_root_folder_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + AddRootFolderHandler::new(key, &mut app, ActiveLidarrBlock::AddRootFolderPrompt, None) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddRootFolderNameInput + ); + } else { + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddRootFolderSelectMonitor + ); + } + } + + #[rstest] + fn test_add_root_folder_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = true; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.down(); + + AddRootFolderHandler::new(key, &mut app, ActiveLidarrBlock::AddRootFolderPrompt, None) + .handle(); + + assert_eq!( + app.data.lidarr_data.selected_block.get_active_block(), + ActiveLidarrBlock::AddRootFolderPathInput + ); + } + } + + mod test_handle_home_end { + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + + use strum::IntoEnumIterator; + + use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; + + use super::*; + + #[test] + fn test_add_root_folder_select_monitor_home_end() { + let monitor_type_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_type_vec.clone()); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[monitor_type_vec.len() - 1] + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[0] + ); + } + + #[test] + fn test_add_root_folder_select_monitor_new_items_home_end() { + let monitor_type_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_type_vec.clone()); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[monitor_type_vec.len() - 1] + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[0] + ); + } + + #[test] + fn test_add_root_folder_select_quality_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 3" + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_root_folder_select_metadata_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 3" + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_root_folder_name_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + name: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_path_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + path: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_tags_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + tags: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + + use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + + AddRootFolderHandler::new(key, &mut app, ActiveLidarrBlock::AddRootFolderPrompt, None) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + AddRootFolderHandler::new(key, &mut app, ActiveLidarrBlock::AddRootFolderPrompt, None) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_name_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + name: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_path_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + path: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + tags: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use crate::assert_navigation_popped; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_root_folder_name_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + name: "Test Name".into(), + ..AddRootFolderModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderNameInput.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_path_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + path: "Test Path".into(), + ..AddRootFolderModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPathInput.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_tags_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + tags: "Test Tags".into(), + ..AddRootFolderModal::default() + }); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderTagsInput.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert!( + !app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + .text + .is_empty() + ); + assert_navigation_popped!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ROOT_FOLDER_SELECTION_BLOCKS.len() - 1); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + } + + #[test] + fn test_add_root_folder_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + let mut add_root_folder = AddRootFolderModal { + name: "Test Name".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + tags: "usenet, testing".to_owned().into(), + ..AddRootFolderModal::default() + }; + add_root_folder + .quality_profile_list + .set_items(vec!["Lossless".to_owned(), "FLAC".to_owned()]); + add_root_folder + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Full".to_owned()]); + add_root_folder + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.add_root_folder_modal = Some(add_root_folder); + app.data.lidarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Lossless".to_owned()), (2222, "FLAC".to_owned())]); + app.data.lidarr_data.metadata_profile_map = + BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]); + let expected_add_root_folder_body = AddLidarrRootFolderBody { + name: "Test Name".to_owned(), + path: "/nfs/Test Path".to_owned(), + default_quality_profile_id: 1111, + default_metadata_profile_id: 1111, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: Vec::new(), + tag_input_string: Some("usenet, testing".to_owned()), + }; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ROOT_FOLDER_SELECTION_BLOCKS.len() - 1); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::AddRootFolder(expected_add_root_folder_body)) + ); + assert_modal_absent!(app.data.lidarr_data.add_root_folder_modal); + assert!(app.should_refresh); + } + + #[test] + fn test_add_root_folder_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddRootFolderPrompt.into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + } + + #[rstest] + #[case(ActiveLidarrBlock::AddRootFolderNameInput, 0)] + #[case(ActiveLidarrBlock::AddRootFolderPathInput, 1)] + #[case(ActiveLidarrBlock::AddRootFolderSelectMonitor, 2)] + #[case(ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, 3)] + #[case(ActiveLidarrBlock::AddRootFolderSelectQualityProfile, 4)] + #[case(ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, 5)] + #[case(ActiveLidarrBlock::AddRootFolderTagsInput, 6)] + fn test_add_root_folder_prompt_selected_block_submit( + #[case] selected_block: ActiveLidarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack( + ( + ActiveLidarrBlock::AddRootFolderPrompt, + Some(ActiveLidarrBlock::RootFolders), + ) + .into(), + ); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + Some(ActiveLidarrBlock::RootFolders), + ) + .handle(); + + assert_navigation_pushed!( + app, + (selected_block, Some(ActiveLidarrBlock::RootFolders)).into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + + if selected_block == ActiveLidarrBlock::AddRootFolderNameInput + || selected_block == ActiveLidarrBlock::AddRootFolderPathInput + || selected_block == ActiveLidarrBlock::AddRootFolderTagsInput + { + assert!(app.ignore_special_keys_for_textbox_input); + } + } + + #[rstest] + fn test_add_root_folder_prompt_selected_block_submit_no_op_when_not_ready( + #[values(0, 1, 2, 3, 4, 5, 6)] y_index: usize, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack( + ( + ActiveLidarrBlock::AddRootFolderPrompt, + Some(ActiveLidarrBlock::RootFolders), + ) + .into(), + ); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.lidarr_data.selected_block.set_index(0, y_index); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + Some(ActiveLidarrBlock::RootFolders), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ( + ActiveLidarrBlock::AddRootFolderPrompt, + Some(ActiveLidarrBlock::RootFolders), + ) + .into() + ); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[rstest] + fn test_add_root_folder_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveLidarrBlock::AddRootFolderSelectMonitor, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, + ActiveLidarrBlock::AddRootFolderNameInput, + ActiveLidarrBlock::AddRootFolderPathInput, + ActiveLidarrBlock::AddRootFolderTagsInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + active_lidarr_block, + Some(ActiveLidarrBlock::RootFolders), + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + + if active_lidarr_block == ActiveLidarrBlock::AddRootFolderNameInput + || active_lidarr_block == ActiveLidarrBlock::AddRootFolderPathInput + || active_lidarr_block == ActiveLidarrBlock::AddRootFolderTagsInput + { + assert!(!app.ignore_special_keys_for_textbox_input); + } + } + } + + mod test_handle_esc { + use crate::assert_navigation_popped; + use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; + use rstest::rstest; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_add_root_folder_input_esc( + #[values( + ActiveLidarrBlock::AddRootFolderTagsInput, + ActiveLidarrBlock::AddRootFolderPathInput, + ActiveLidarrBlock::AddRootFolderNameInput + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + AddRootFolderHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_navigation_popped!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + + AddRootFolderHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + + assert_modal_absent!(app.data.lidarr_data.add_root_folder_modal); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_add_root_folder_esc( + #[values( + ActiveLidarrBlock::AddRootFolderSelectMonitor, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + AddRootFolderHandler::new(ESC_KEY, &mut app, active_lidarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::{ + assert_navigation_popped, + models::{ + BlockSelectionState, + servarr_data::lidarr::{ + lidarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS, modals::AddRootFolderModal, + }, + }, + }; + use pretty_assertions::assert_eq; + + #[test] + fn test_add_root_folder_name_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + name: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text, + "Tes" + ); + } + + #[test] + fn test_add_root_folder_path_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + path: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text, + "Tes" + ); + } + + #[test] + fn test_add_root_folder_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal { + tags: "Test".into(), + ..AddRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_add_root_folder_name_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + + AddRootFolderHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text, + "a" + ); + } + + #[test] + fn test_add_root_folder_path_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + + AddRootFolderHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text, + "a" + ); + } + + #[test] + fn test_add_root_folder_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + + AddRootFolderHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text, + "a" + ); + } + + #[test] + fn test_add_root_folder_confirm_prompt_prompt_confirm() { + let mut app = App::test_default(); + let mut add_root_folder = AddRootFolderModal { + name: "Test Name".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + tags: "usenet, testing".to_owned().into(), + ..AddRootFolderModal::default() + }; + add_root_folder + .quality_profile_list + .set_items(vec!["Lossless".to_owned(), "FLAC".to_owned()]); + add_root_folder + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Full".to_owned()]); + add_root_folder + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.add_root_folder_modal = Some(add_root_folder); + app.data.lidarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Lossless".to_owned()), (2222, "FLAC".to_owned())]); + app.data.lidarr_data.metadata_profile_map = + BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]); + let expected_add_root_folder_body = AddLidarrRootFolderBody { + name: "Test Name".to_owned(), + path: "/nfs/Test Path".to_owned(), + default_quality_profile_id: 1111, + default_metadata_profile_id: 1111, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: Vec::new(), + tag_input_string: Some("usenet, testing".to_owned()), + }; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app + .data + .lidarr_data + .selected_block + .set_index(0, ADD_ROOT_FOLDER_SELECTION_BLOCKS.len() - 1); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::AddRootFolder(expected_add_root_folder_body)) + ); + assert_modal_absent!(app.data.lidarr_data.add_root_folder_modal); + assert!(app.should_refresh); + } + } + + #[test] + fn test_add_root_folder_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) { + assert!(AddRootFolderHandler::accepts(active_lidarr_block)); + } else { + assert!(!AddRootFolderHandler::accepts(active_lidarr_block)); + } + }); + } + + #[rstest] + fn test_add_root_folder_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_add_root_folder_body() { + let mut app = App::test_default(); + let mut add_root_folder = AddRootFolderModal { + name: "Test Name".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + tags: "usenet, testing".to_owned().into(), + ..AddRootFolderModal::default() + }; + add_root_folder + .quality_profile_list + .set_items(vec!["Lossless".to_owned(), "FLAC".to_owned()]); + add_root_folder + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Full".to_owned()]); + add_root_folder + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + app.data.lidarr_data.add_root_folder_modal = Some(add_root_folder); + app.data.lidarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Lossless".to_owned()), (2222, "FLAC".to_owned())]); + app.data.lidarr_data.metadata_profile_map = + BiMap::from_iter([(1111, "Standard".to_owned()), (2222, "Full".to_owned())]); + let expected_add_root_folder_body = AddLidarrRootFolderBody { + name: "Test Name".to_owned(), + path: "/nfs/Test Path".to_owned(), + default_quality_profile_id: 1111, + default_metadata_profile_id: 1111, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: Vec::new(), + tag_input_string: Some("usenet, testing".to_owned()), + }; + + let add_root_folder_body = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .build_add_root_folder_body(); + + assert_eq!(add_root_folder_body, expected_add_root_folder_body); + assert_modal_absent!(app.data.lidarr_data.add_root_folder_modal); + } + + #[test] + fn test_add_root_folder_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = true; + + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_add_root_folder_handler_is_not_ready_when_add_root_folder_modal_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = false; + + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_add_root_folder_handler_is_ready_when_add_root_folder_modal_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = false; + app.data.lidarr_data.add_root_folder_modal = Some(AddRootFolderModal::default()); + + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/root_folders/mod.rs b/src/handlers/lidarr_handlers/root_folders/mod.rs new file mode 100644 index 0000000..858717b --- /dev/null +++ b/src/handlers/lidarr_handlers/root_folders/mod.rs @@ -0,0 +1,173 @@ +use add_root_folder_handler::AddRootFolderHandler; + +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ADD_ROOT_FOLDER_SELECTION_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS, +}; +use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; +use crate::models::{BlockSelectionState, Route}; +use crate::network::lidarr_network::LidarrEvent; + +mod add_root_folder_handler; + +#[cfg(test)] +#[path = "root_folders_handler_tests.rs"] +mod root_folders_handler_tests; + +pub(super) struct RootFoldersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl RootFoldersHandler<'_, '_> { + fn extract_root_folder_id(&self) -> i64 { + self + .app + .data + .lidarr_data + .root_folders + .current_selection() + .id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for RootFoldersHandler<'a, 'b> { + fn handle(&mut self) { + let root_folders_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::RootFolders.into()); + + if AddRootFolderHandler::accepts(self.active_lidarr_block) { + return AddRootFolderHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.root_folders, + root_folders_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + ROOT_FOLDERS_BLOCKS.contains(&active_block) || ADD_ROOT_FOLDER_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> RootFoldersHandler<'a, 'b> { + RootFoldersHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.root_folders.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::RootFolders { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::RootFolders => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteRootFolderPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::DeleteRootFolderPrompt { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteRootFolder(self.extract_root_folder_id())); + } + + self.app.pop_navigation_stack(); + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::RootFolders => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(add, key) => { + self.app.data.lidarr_data.add_root_folder_modal = + Some(AddRootFolderModal::from(&self.app.data.lidarr_data)); + self.app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + } + _ => (), + }, + ActiveLidarrBlock::DeleteRootFolderPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteRootFolder(self.extract_root_folder_id())); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs new file mode 100644 index 0000000..18afae6 --- /dev/null +++ b/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs @@ -0,0 +1,480 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_present; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS, + }; + use crate::models::servarr_models::RootFolder; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_root_folder_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + } + + #[test] + fn test_delete_root_folder_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(4); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::History.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::History.into()); + } + + #[rstest] + fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(4); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Indexers.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into()); + } + + #[rstest] + fn test_left_right_delete_root_folder_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new( + key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + RootFoldersHandler::new( + key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_delete_root_folder_prompt_confirm_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![root_folder()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteRootFolder(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + + #[test] + fn test_delete_root_folder_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_delete_root_folder_prompt_block_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + RootFoldersHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + use pretty_assertions::assert_eq; + + #[test] + fn test_root_folder_add() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + assert_modal_present!(app.data.lidarr_data.add_root_folder_modal); + } + + #[test] + fn test_root_folder_add_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert_none!(app.data.lidarr_data.add_root_folder_modal); + } + + #[test] + fn test_refresh_root_folders_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::RootFolders.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_root_folders_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert!(!app.should_refresh); + } + + #[test] + fn test_delete_root_folder_prompt_confirm() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![root_folder()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteRootFolder(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + } + + #[test] + fn test_root_folders_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block) + || ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) + { + assert!(RootFoldersHandler::accepts(active_lidarr_block)); + } else { + assert!(!RootFoldersHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_root_folders_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_root_folder_id() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![root_folder()]); + + let root_folder_id = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .extract_root_folder_id(); + + assert_eq!(root_folder_id, 1); + } + + #[test] + fn test_root_folders_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = true; + + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_not_ready_when_root_folders_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = false; + + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_ready_when_not_loading_and_root_folders_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = false; + + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/system/mod.rs b/src/handlers/lidarr_handlers/system/mod.rs new file mode 100644 index 0000000..0faead2 --- /dev/null +++ b/src/handlers/lidarr_handlers/system/mod.rs @@ -0,0 +1,135 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::lidarr_handlers::system::system_details_handler::SystemDetailsHandler; +use crate::handlers::{KeyEventHandler, handle_clear_errors}; +use crate::matches_key; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::{Route, Scrollable}; + +mod system_details_handler; + +#[cfg(test)] +#[path = "system_handler_tests.rs"] +mod system_handler_tests; + +pub(super) struct SystemHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for SystemHandler<'a, 'b> { + fn handle(&mut self) { + match self.active_lidarr_block { + _ if SystemDetailsHandler::accepts(self.active_lidarr_block) => { + SystemDetailsHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + SystemDetailsHandler::accepts(active_block) || active_block == ActiveLidarrBlock::System + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> SystemHandler<'a, 'b> { + SystemHandler { + key, + app, + active_lidarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + && !self.app.data.lidarr_data.logs.is_empty() + && !self.app.data.lidarr_data.queued_events.is_empty() + && !self.app.data.lidarr_data.tasks.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::System { + handle_change_tab_left_right_keys(self.app, self.key); + } + } + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + handle_clear_errors(self.app) + } + + fn handle_char_key_event(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::System { + let key = self.key; + match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(events, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemQueuedEvents.into()); + } + _ if matches_key!(logs, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemLogs.into()); + self + .app + .data + .lidarr_data + .log_details + .set_items(self.app.data.lidarr_data.logs.items.to_vec()); + self.app.data.lidarr_data.log_details.scroll_to_bottom(); + } + _ if matches_key!(tasks, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + } + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into()); + } + _ => (), + } + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/system/system_details_handler.rs b/src/handlers/lidarr_handlers/system/system_details_handler.rs new file mode 100644 index 0000000..01a7b0e --- /dev/null +++ b/src/handlers/lidarr_handlers/system/system_details_handler.rs @@ -0,0 +1,207 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::lidarr_models::LidarrTaskName; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::models::stateful_list::StatefulList; +use crate::models::{Route, Scrollable}; +use crate::network::lidarr_network::LidarrEvent; + +#[cfg(test)] +#[path = "system_details_handler_tests.rs"] +mod system_details_handler_tests; + +pub(super) struct SystemDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl SystemDetailsHandler<'_, '_> { + fn extract_task_name(&self) -> LidarrTaskName { + self + .app + .data + .lidarr_data + .tasks + .current_selection() + .task_name + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for SystemDetailsHandler<'a, 'b> { + fn accepts(active_block: ActiveLidarrBlock) -> bool { + SYSTEM_DETAILS_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + context: Option, + ) -> SystemDetailsHandler<'a, 'b> { + SystemDetailsHandler { + key, + app, + active_lidarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + && (!self.app.data.lidarr_data.log_details.is_empty() + || !self.app.data.lidarr_data.tasks.is_empty() + || !self.app.data.lidarr_data.updates.is_empty()) + } + + fn handle_scroll_up(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_up(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_up(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_up(), + ActiveLidarrBlock::SystemQueuedEvents => self.app.data.lidarr_data.queued_events.scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_down(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_down(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_down(), + ActiveLidarrBlock::SystemQueuedEvents => { + self.app.data.lidarr_data.queued_events.scroll_down() + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_to_top(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_to_top(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_to_top(), + ActiveLidarrBlock::SystemQueuedEvents => { + self.app.data.lidarr_data.queued_events.scroll_to_top() + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => self.app.data.lidarr_data.log_details.scroll_to_bottom(), + ActiveLidarrBlock::SystemTasks => self.app.data.lidarr_data.tasks.scroll_to_bottom(), + ActiveLidarrBlock::SystemUpdates => self.app.data.lidarr_data.updates.scroll_to_bottom(), + ActiveLidarrBlock::SystemQueuedEvents => { + self.app.data.lidarr_data.queued_events.scroll_to_bottom() + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + let key = self.key; + + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => match self.key { + _ if matches_key!(left, key) => { + self + .app + .data + .lidarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_right()); + } + _ if matches_key!(right, key) => { + self + .app + .data + .lidarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_left()); + } + _ => (), + }, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemTasks => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + } + ActiveLidarrBlock::SystemTaskStartConfirmPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::StartTask(self.extract_task_name())); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::SystemLogs => { + self.app.data.lidarr_data.log_details = StatefulList::default(); + self.app.pop_navigation_stack() + } + ActiveLidarrBlock::SystemQueuedEvents + | ActiveLidarrBlock::SystemTasks + | ActiveLidarrBlock::SystemUpdates => self.app.pop_navigation_stack(), + ActiveLidarrBlock::SystemTaskStartConfirmPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + if SYSTEM_DETAILS_BLOCKS.contains(&self.active_lidarr_block) && matches_key!(refresh, self.key) + { + self.app.should_refresh = true; + } + + if self.active_lidarr_block == ActiveLidarrBlock::SystemTaskStartConfirmPrompt + && matches_key!(confirm, self.key) + { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::StartTask(self.extract_task_name())); + self.app.pop_navigation_stack(); + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/lidarr_handlers/system/system_details_handler_tests.rs b/src/handlers/lidarr_handlers/system/system_details_handler_tests.rs new file mode 100644 index 0000000..0487ef5 --- /dev/null +++ b/src/handlers/lidarr_handlers/system/system_details_handler_tests.rs @@ -0,0 +1,1080 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::system::system_details_handler::SystemDetailsHandler; + use crate::models::lidarr_models::{LidarrTask, LidarrTaskName}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::QueueEvent; + use crate::models::{HorizontallyScrollableText, ScrollableText}; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_log_details_scroll, + SystemDetailsHandler, + lidarr_data, + log_details, + simple_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveLidarrBlock::SystemLogs, + None, + text + ); + + #[rstest] + fn test_log_details_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app + .data + .lidarr_data + .log_details + .set_items(simple_stateful_iterable_vec!( + HorizontallyScrollableText, + String, + text + )); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemLogs, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemLogs, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + } + + #[rstest] + fn test_tasks_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(simple_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 2" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_tasks_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(simple_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_queued_events_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 2" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_queued_events_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new(key, &mut app, ActiveLidarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_system_updates_scroll() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 1); + } + + #[test] + fn test_system_updates_scroll_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + } + } + + mod test_handle_home_end { + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + use super::*; + use pretty_assertions::assert_eq; + + test_iterable_home_and_end!( + test_log_details_home_end, + SystemDetailsHandler, + lidarr_data, + log_details, + extended_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveLidarrBlock::SystemLogs, + None, + text + ); + + #[test] + fn test_log_details_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app + .data + .lidarr_data + .log_details + .set_items(extended_stateful_iterable_vec!( + HorizontallyScrollableText, + String, + text + )); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemLogs, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemLogs, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "Test 1" + ); + } + + #[test] + fn test_tasks_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(extended_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 3" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_tasks_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(extended_stateful_iterable_vec!(LidarrTask, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_queued_events_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 3" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_queued_events_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .lidarr_data + .queued_events + .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_system_updates_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 1); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + } + + #[test] + fn test_system_updates_home_end_no_op_when_not_ready() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.updates.offset, 0); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[test] + fn test_handle_log_details_left_right() { + let active_lidarr_block = ActiveLidarrBlock::SystemLogs; + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app + .data + .lidarr_data + .log_details + .set_items(vec!["t1".into(), "t22".into()]); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "t22"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.lidarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.lidarr_data.log_details.items[1].to_string(), "t22"); + } + + #[rstest] + fn test_left_right_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemDetailsHandler::new( + key, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + SystemDetailsHandler::new( + key, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use pretty_assertions::assert_eq; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_system_tasks_submit() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + SystemDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::SystemTasks, None) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + } + + #[test] + fn test_system_tasks_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + SystemDetailsHandler::new(SUBMIT_KEY, &mut app, ActiveLidarrBlock::SystemTasks, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::SystemTasks.into() + ); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.data.lidarr_data.prompt_confirm = true; + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::StartTask(LidarrTaskName::default()) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + } + + #[test] + fn test_system_tasks_start_task_prompt_decline_submit() { + let mut app = App::test_default(); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + } + } + + mod test_handle_esc { + use crate::models::HorizontallyScrollableText; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_esc_system_logs(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app + .data + .lidarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::from("test")]); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemLogs.into()); + app + .data + .lidarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + SystemDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::SystemLogs, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + assert_is_empty!(app.data.lidarr_data.log_details.items); + } + + #[rstest] + fn test_esc_system_tasks(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::SystemTasks, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + } + + #[rstest] + fn test_esc_system_queued_events(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemQueuedEvents.into()); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + SystemDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + } + + #[rstest] + fn test_esc_system_updates(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into()); + + SystemDetailsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::SystemUpdates, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + } + + #[test] + fn test_system_tasks_start_task_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + SystemDetailsHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::{assert_navigation_popped, assert_navigation_pushed}; + + #[rstest] + fn test_refresh_key( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(active_lidarr_block.into()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_navigation_pushed!(app, active_lidarr_block.into()); + assert!(app.should_refresh); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(active_lidarr_block.into()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_lidarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_lidarr_block.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveLidarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::StartTask(LidarrTaskName::default())) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::SystemTasks.into()); + } + } + + #[test] + fn test_system_details_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if SYSTEM_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(SystemDetailsHandler::accepts(active_lidarr_block)); + } else { + assert!(!SystemDetailsHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_system_details_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_task_name() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + let task_name = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ) + .extract_task_name(); + + assert_eq!(task_name, LidarrTaskName::default()); + } + + #[test] + fn test_system_details_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_system_details_handler_not_ready_when_log_details_and_updates_and_tasks_are_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_log_details_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app + .data + .lidarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_tasks_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemTasks, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_updates_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + let handler = SystemDetailsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/system/system_handler_tests.rs b/src/handlers/lidarr_handlers/system/system_handler_tests.rs new file mode 100644 index 0000000..60e79a1 --- /dev/null +++ b/src/handlers/lidarr_handlers/system/system_handler_tests.rs @@ -0,0 +1,580 @@ +#[cfg(test)] +mod tests { + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::system::SystemHandler; + use crate::models::lidarr_models::LidarrTask; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::QueueEvent; + use crate::test_handler_delegation; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_system_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(6); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Indexers.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Indexers.into()); + } + + #[rstest] + fn test_system_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(6); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Artists.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + } + } + + mod test_handle_esc { + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_default_esc(#[values(true, false)] is_loading: bool) { + let mut app = App::test_default(); + app.is_loading = is_loading; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::System, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::System.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::models::HorizontallyScrollableText; + + use super::*; + + #[test] + fn test_update_system_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemUpdates.into()); + } + + #[test] + fn test_update_system_key_no_op_if_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + } + + #[test] + fn test_queued_events_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.events.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemQueuedEvents.into()); + } + + #[test] + fn test_queued_events_key_no_op_if_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.events.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + } + + #[test] + fn test_refresh_system_key() { + let mut app = App::test_default(); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::System.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_system_key_no_op_if_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_logs_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.logs.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemLogs.into()); + assert_eq!( + app.data.lidarr_data.log_details.items, + app.data.lidarr_data.logs.items + ); + assert_str_eq!( + app.data.lidarr_data.log_details.current_selection().text, + "test 2" + ); + } + + #[test] + fn test_logs_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.logs.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + assert_is_empty!(app.data.lidarr_data.log_details); + } + + #[test] + fn test_tasks_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::SystemTasks.into()); + } + + #[test] + fn test_tasks_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + SystemHandler::new( + DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + ActiveLidarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::System.into()); + } + } + + #[rstest] + fn test_delegates_system_details_blocks_to_system_details_handler( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + SystemHandler, + ActiveLidarrBlock::System, + active_lidarr_block + ); + } + + #[test] + fn test_system_handler_accepts() { + let mut system_blocks = vec![ActiveLidarrBlock::System]; + system_blocks.extend(SYSTEM_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if system_blocks.contains(&active_lidarr_block) { + assert!(SystemHandler::accepts(active_lidarr_block)); + } else { + assert!(!SystemHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_system_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_system_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_logs_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_tasks_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.logs.set_items(vec!["test".into()]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_queued_events_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.logs.set_items(vec!["test".into()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_ready_when_all_required_tables_are_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = false; + app.data.lidarr_data.logs.set_items(vec!["test".into()]); + app + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask::default()]); + app + .data + .lidarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::System, + None, + ); + + assert!(system_handler.is_ready()); + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index c64ef20..316ba13 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,3 +1,4 @@ +use lidarr_handlers::LidarrHandler; use radarr_handlers::RadarrHandler; use sonarr_handlers::SonarrHandler; @@ -15,6 +16,7 @@ use crate::models::stateful_table::StatefulTable; use crate::models::{HorizontallyScrollableText, Route}; mod keybinding_handler; +mod lidarr_handlers; mod radarr_handlers; mod sonarr_handlers; @@ -125,6 +127,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) { Route::Sonarr(active_sonarr_block, context) => { SonarrHandler::new(key, app, active_sonarr_block, context).handle() } + Route::Lidarr(active_lidarr_block, context) => { + LidarrHandler::new(key, app, active_lidarr_block, context).handle() + } _ => (), } } @@ -187,6 +192,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) { Route::Sonarr(_, _) => { app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm } + Route::Lidarr(_, _) => { + app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm + } _ => (), }, _ => (), diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs index af27925..bc53e17 100644 --- a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -91,9 +91,9 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::History.into() ); - assert_navigation_pushed!(app, ActiveRadarrBlock::RootFolders.into()); + assert_navigation_pushed!(app, ActiveRadarrBlock::History.into()); } #[rstest] diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs index 9cf43a0..3199c38 100644 --- a/src/handlers/radarr_handlers/blocklist/mod.rs +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::radarr_models::BlocklistItem; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::stateful_table::SortOption; @@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/collections/collection_details_handler.rs b/src/handlers/radarr_handlers/collections/collection_details_handler.rs index 2cb628b..92ca215 100644 --- a/src/handlers/radarr_handlers/collections/collection_details_handler.rs +++ b/src/handlers/radarr_handlers/collections/collection_details_handler.rs @@ -3,12 +3,12 @@ use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::matches_key; -use crate::models::BlockSelectionState; use crate::models::servarr_data::radarr::radarr_data::{ ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, }; use crate::models::stateful_table::StatefulTable; +use crate::models::{BlockSelectionState, Route}; #[cfg(test)] #[path = "collection_details_handler_tests.rs"] @@ -148,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs index dff7e63..fe34986 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs @@ -1,10 +1,10 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; -use crate::models::Scrollable; use crate::models::radarr_models::EditCollectionParams; use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS}; +use crate::models::{Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -376,7 +376,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/collections/mod.rs b/src/handlers/radarr_handlers/collections/mod.rs index 69e22d1..4e2e32a 100644 --- a/src/handlers/radarr_handlers/collections/mod.rs +++ b/src/handlers/radarr_handlers/collections/mod.rs @@ -6,12 +6,12 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; use crate::models::radarr_models::Collection; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, }; use crate::models::stateful_table::SortOption; +use crate::models::{BlockSelectionState, Route}; use crate::network::radarr_network::RadarrEvent; mod collection_details_handler; @@ -179,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/downloads/mod.rs b/src/handlers/radarr_handlers/downloads/mod.rs index 3a449d1..53899a5 100644 --- a/src/handlers/radarr_handlers/downloads/mod.rs +++ b/src/handlers/radarr_handlers/downloads/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::network::radarr_network::RadarrEvent; @@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/history/history_handler_tests.rs b/src/handlers/radarr_handlers/history/history_handler_tests.rs new file mode 100644 index 0000000..4bd6e51 --- /dev/null +++ b/src/handlers/radarr_handlers/history/history_handler_tests.rs @@ -0,0 +1,438 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::radarr_handlers::history::{HistoryHandler, history_sorting_options}; + use crate::models::radarr_models::{RadarrHistoryEventType, RadarrHistoryItem}; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; + use crate::models::servarr_models::{Language, Quality, QualityWrapper}; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_history_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = is_ready; + app.data.radarr_data.main_tabs.set_index(4); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + ActiveRadarrBlock::Blocklist.into() + ); + assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_history_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = is_ready; + app.data.radarr_data.main_tabs.set_index(4); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + ActiveRadarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_history_submit() { + let mut app = App::test_default(); + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::History, None).handle(); + + assert_navigation_pushed!(app, ActiveRadarrBlock::HistoryItemDetails.into()); + } + + #[test] + fn test_history_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_esc_history_item_details() { + let mut app = App::test_default(); + app + .data + .radarr_data + .history + .set_items(vec![RadarrHistoryItem::default()]); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.push_navigation_stack(ActiveRadarrBlock::HistoryItemDetails.into()); + + HistoryHandler::new( + ESC_KEY, + &mut app, + ActiveRadarrBlock::HistoryItemDetails, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveRadarrBlock::History.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.data.radarr_data = create_test_radarr_data(); + app + .data + .radarr_data + .history + .set_items(vec![RadarrHistoryItem::default()]); + + HistoryHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + use crate::assert_navigation_pushed; + + #[test] + fn test_refresh_history_key() { + let mut app = App::test_default(); + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveRadarrBlock::History.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_history_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.radarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + HistoryHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveRadarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::History.into()); + assert!(!app.should_refresh); + } + } + + #[test] + fn test_history_sorting_options_source_title() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[0].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_history_sorting_options_event_type() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[1].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Event Type"); + } + + #[test] + fn test_history_sorting_options_language() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + let default_language = Language { + id: 1, + name: "_".to_owned(), + }; + let language_a = a.languages.first().unwrap_or(&default_language); + let language_b = b.languages.first().unwrap_or(&default_language); + + language_a.cmp(language_b) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[2].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_history_sorting_options_quality() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[3].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_history_sorting_options_date() { + let expected_cmp_fn: fn(&RadarrHistoryItem, &RadarrHistoryItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[4].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_history_handler_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if HISTORY_BLOCKS.contains(&active_radarr_block) { + assert!(HistoryHandler::accepts(active_radarr_block)); + } else { + assert!(!HistoryHandler::accepts(active_radarr_block)); + } + }) + } + + #[rstest] + fn test_history_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_history_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = true; + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_not_ready_when_history_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = false; + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + app.is_loading = false; + app + .data + .radarr_data + .history + .set_items(vec![RadarrHistoryItem::default()]); + + let handler = HistoryHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::History, + None, + ); + + assert!(handler.is_ready()); + } + + fn history_vec() -> Vec { + vec![ + RadarrHistoryItem { + id: 3, + source_title: "test 1".into(), + movie_id: 1, + event_type: RadarrHistoryEventType::Grabbed, + languages: vec![Language { + id: 1, + name: "telgu".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + ..RadarrHistoryItem::default() + }, + RadarrHistoryItem { + id: 2, + source_title: "test 2".into(), + movie_id: 2, + event_type: RadarrHistoryEventType::DownloadFolderImported, + languages: vec![Language { + id: 3, + name: "chinese".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "SD - 720p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + ..RadarrHistoryItem::default() + }, + RadarrHistoryItem { + id: 1, + source_title: "test 3".into(), + movie_id: 3, + event_type: RadarrHistoryEventType::MovieFileDeleted, + languages: vec![Language { + id: 1, + name: "english".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + ..RadarrHistoryItem::default() + }, + ] + } +} diff --git a/src/handlers/radarr_handlers/history/mod.rs b/src/handlers/radarr_handlers/history/mod.rs new file mode 100644 index 0000000..a424b2e --- /dev/null +++ b/src/handlers/radarr_handlers/history/mod.rs @@ -0,0 +1,179 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors}; +use crate::matches_key; +use crate::models::Route; +use crate::models::radarr_models::RadarrHistoryItem; +use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; +use crate::models::servarr_models::Language; +use crate::models::stateful_table::SortOption; + +#[cfg(test)] +#[path = "history_handler_tests.rs"] +mod history_handler_tests; + +pub(super) struct HistoryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl HistoryHandler<'_, '_> {} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for HistoryHandler<'a, 'b> { + fn handle(&mut self) { + let history_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::History.into()) + .sorting_block(ActiveRadarrBlock::HistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .searching_block(ActiveRadarrBlock::SearchHistory.into()) + .search_error_block(ActiveRadarrBlock::SearchHistoryError.into()) + .search_field_fn(|history| &history.source_title.text) + .filtering_block(ActiveRadarrBlock::FilterHistory.into()) + .filter_error_block(ActiveRadarrBlock::FilterHistoryError.into()) + .filter_field_fn(|history| &history.source_title.text); + + if !handle_table( + self, + |app| &mut app.data.radarr_data.history, + history_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + HISTORY_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveRadarrBlock, + context: Option, + ) -> Self { + HistoryHandler { + key, + app, + active_radarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.radarr_data.history.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_radarr_block == ActiveRadarrBlock::History { + handle_change_tab_left_right_keys(self.app, self.key) + } + } + + fn handle_submit(&mut self) { + if self.active_radarr_block == ActiveRadarrBlock::History { + self + .app + .push_navigation_stack(ActiveRadarrBlock::HistoryItemDetails.into()); + } + } + + fn handle_esc(&mut self) { + if self.active_radarr_block == ActiveRadarrBlock::HistoryItemDetails { + self.app.pop_navigation_stack(); + } else { + handle_clear_errors(self.app); + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + if self.active_radarr_block == ActiveRadarrBlock::History { + match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + } + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} + +pub(in crate::handlers::radarr_handlers) fn history_sorting_options() +-> Vec> { + vec![ + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }), + }, + SortOption { + name: "Event Type", + cmp_fn: Some(|a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + let default_language = Language { + id: 1, + name: "_".to_owned(), + }; + let language_a = a.languages.first().unwrap_or(&default_language); + let language_b = b.languages.first().unwrap_or(&default_language); + + language_a.cmp(language_b) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index 33a2383..d83ab1b 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -1,6 +1,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_models::EditIndexerParams; @@ -124,7 +125,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' .edit_indexer_modal .as_mut() .unwrap(); - if edit_indexer_modal.priority > 0 { + if edit_indexer_modal.priority > 1 { edit_indexer_modal.priority -= 1; } } @@ -527,7 +528,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs index 179e7fe..adc8454 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -50,7 +50,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); } else { assert_eq!( @@ -61,7 +61,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); EditIndexerHandler::new( @@ -80,7 +80,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); EditIndexerHandler::new( @@ -98,7 +98,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); } } diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index f933187..9145529 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -1,6 +1,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; use crate::models::radarr_models::IndexerSettings; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, @@ -293,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index e25c24c..cc1885d 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -65,7 +65,7 @@ mod tests { fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(5); + app.data.radarr_data.main_tabs.set_index(6); IndexersHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -86,7 +86,7 @@ mod tests { fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(5); + app.data.radarr_data.main_tabs.set_index(6); IndexersHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 85f5adc..72b99e8 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -7,11 +7,11 @@ use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestA use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, }; +use crate::models::{BlockSelectionState, Route}; use crate::network::radarr_network::RadarrEvent; mod edit_indexer_handler; @@ -212,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs index c8e26d4..a19cbf2 100644 --- a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs +++ b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; #[cfg(test)] @@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index e668304..28609a7 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -7,7 +7,7 @@ use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, ActiveRadarrBlock, }; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::{BlockSelectionState, Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -83,8 +83,8 @@ impl AddMovieHandler<'_, '_> { .unwrap(); let path = root_folder_list.current_selection().path.clone(); - let monitor = monitor_list.current_selection().to_string(); - let minimum_availability = minimum_availability_list.current_selection().to_string(); + let monitor = *monitor_list.current_selection(); + let minimum_availability = *minimum_availability_list.current_selection(); AddMovieBody { tmdb_id, @@ -558,7 +558,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler.rs b/src/handlers/radarr_handlers/library/delete_movie_handler.rs index 561489f..c3817c2 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::radarr_models::DeleteMovieParams; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; use crate::network::radarr_network::RadarrEvent; @@ -141,7 +142,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler.rs b/src/handlers/radarr_handlers/library/edit_movie_handler.rs index f463d0f..593f08d 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler.rs @@ -1,10 +1,10 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; -use crate::models::Scrollable; use crate::models::radarr_models::EditMovieParams; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS}; +use crate::models::{Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -397,7 +397,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index 9877b79..bf321a1 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -11,7 +11,7 @@ use crate::models::servarr_data::radarr::radarr_data::{ }; use crate::models::servarr_models::Language; use crate::models::stateful_table::SortOption; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::{BlockSelectionState, Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] @@ -379,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index d8f8075..0188e29 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -2,16 +2,19 @@ use crate::handlers::KeyEventHandler; use crate::handlers::radarr_handlers::blocklist::BlocklistHandler; use crate::handlers::radarr_handlers::collections::CollectionsHandler; use crate::handlers::radarr_handlers::downloads::DownloadsHandler; +use crate::handlers::radarr_handlers::history::HistoryHandler; use crate::handlers::radarr_handlers::indexers::IndexersHandler; use crate::handlers::radarr_handlers::library::LibraryHandler; use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::radarr_handlers::system::SystemHandler; +use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::{App, Key, matches_key}; mod blocklist; mod collections; mod downloads; +mod history; mod indexers; mod library; mod root_folders; @@ -50,6 +53,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b _ if DownloadsHandler::accepts(self.active_radarr_block) => { DownloadsHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle() } + _ if HistoryHandler::accepts(self.active_radarr_block) => { + HistoryHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle() + } _ if RootFoldersHandler::accepts(self.active_radarr_block) => { RootFoldersHandler::new(self.key, self.app, self.active_radarr_block, self.context).handle() } @@ -112,7 +118,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index b97b31e..cc54e07 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -5,7 +5,7 @@ pub(in crate::handlers::radarr_handlers) mod utils { use crate::models::radarr_models::{ AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie, DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection, - MovieFile, RadarrRelease, Rating, RatingsList, + MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList, }; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, @@ -470,13 +470,13 @@ pub(in crate::handlers::radarr_handlers) mod utils { tmdb_id: 1234, title: "Test".to_owned(), root_folder_path: "/nfs2".to_owned(), - minimum_availability: "announced".to_owned(), + minimum_availability: MinimumAvailability::Announced, monitored: true, quality_profile_id: 2222, tags: Vec::new(), tag_input_string: Some("usenet, testing".into()), add_options: AddMovieOptions { - monitor: "movieOnly".to_owned(), + monitor: MovieMonitor::MovieOnly, search_for_movie: true, }, } diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 0a3bc46..5333f69 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -16,10 +16,11 @@ mod tests { #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)] #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)] #[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)] - #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] - #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)] - #[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] - #[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] + #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::History)] + #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::RootFolders)] + #[case(5, ActiveRadarrBlock::History, ActiveRadarrBlock::Indexers)] + #[case(6, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] + #[case(7, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] fn test_radarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveRadarrBlock, @@ -51,10 +52,11 @@ mod tests { #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)] #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)] #[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)] - #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] - #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)] - #[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] - #[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] + #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::History)] + #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::RootFolders)] + #[case(5, ActiveRadarrBlock::History, ActiveRadarrBlock::Indexers)] + #[case(6, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] + #[case(7, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] fn test_radarr_handler_change_tab_left_right_keys_alt_navigation( #[case] index: usize, #[case] left_block: ActiveRadarrBlock, @@ -88,9 +90,10 @@ mod tests { #[case(1, ActiveRadarrBlock::Collections)] #[case(2, ActiveRadarrBlock::Downloads)] #[case(3, ActiveRadarrBlock::Blocklist)] - #[case(4, ActiveRadarrBlock::RootFolders)] - #[case(5, ActiveRadarrBlock::Indexers)] - #[case(6, ActiveRadarrBlock::System)] + #[case(4, ActiveRadarrBlock::History)] + #[case(5, ActiveRadarrBlock::RootFolders)] + #[case(6, ActiveRadarrBlock::Indexers)] + #[case(7, ActiveRadarrBlock::System)] fn test_radarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( #[case] index: usize, #[case] block: ActiveRadarrBlock, @@ -281,6 +284,26 @@ mod tests { ); } + #[rstest] + fn test_delegates_history_blocks_to_history_handler( + #[values( + ActiveRadarrBlock::History, + ActiveRadarrBlock::HistoryItemDetails, + ActiveRadarrBlock::HistorySortPrompt, + ActiveRadarrBlock::FilterHistory, + ActiveRadarrBlock::FilterHistoryError, + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::SearchHistoryError + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!( + RadarrHandler, + ActiveRadarrBlock::History, + active_radarr_block + ); + } + #[test] fn test_radarr_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index aa2a64f..eadd21d 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -3,9 +3,9 @@ use crate::event::Key; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; -use crate::models::HorizontallyScrollableText; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; use crate::models::servarr_models::AddRootFolderBody; +use crate::models::{HorizontallyScrollableText, Route}; use crate::network::radarr_network::RadarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -231,7 +231,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index a032253..9c42cb2 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -130,7 +130,7 @@ mod tests { fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(4); + app.data.radarr_data.main_tabs.set_index(5); RootFoldersHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -142,16 +142,16 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - ActiveRadarrBlock::Blocklist.into() + ActiveRadarrBlock::History.into() ); - assert_navigation_pushed!(app, ActiveRadarrBlock::Blocklist.into()); + assert_navigation_pushed!(app, ActiveRadarrBlock::History.into()); } #[rstest] fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(4); + app.data.radarr_data.main_tabs.set_index(5); RootFoldersHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/radarr_handlers/system/mod.rs b/src/handlers/radarr_handlers/system/mod.rs index 50201b0..8bc1778 100644 --- a/src/handlers/radarr_handlers/system/mod.rs +++ b/src/handlers/radarr_handlers/system/mod.rs @@ -4,8 +4,8 @@ use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler; use crate::handlers::{KeyEventHandler, handle_clear_errors}; use crate::matches_key; -use crate::models::Scrollable; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; +use crate::models::{Route, Scrollable}; mod system_details_handler; @@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index 7b13d4e..77b553c 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -2,10 +2,10 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; -use crate::models::Scrollable; use crate::models::radarr_models::RadarrTaskName; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::stateful_list::StatefulList; +use crate::models::{Route, Scrollable}; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] @@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/radarr_handlers/system/system_handler_tests.rs b/src/handlers/radarr_handlers/system/system_handler_tests.rs index cd4ab61..0c95818 100644 --- a/src/handlers/radarr_handlers/system/system_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_handler_tests.rs @@ -26,7 +26,7 @@ mod tests { fn test_system_tab_left(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(6); + app.data.radarr_data.main_tabs.set_index(7); SystemHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -47,7 +47,7 @@ mod tests { fn test_system_tab_right(#[values(true, false)] is_ready: bool) { let mut app = App::test_default(); app.is_loading = is_ready; - app.data.radarr_data.main_tabs.set_index(6); + app.data.radarr_data.main_tabs.set_index(7); SystemHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/sonarr_handlers/blocklist/mod.rs b/src/handlers/sonarr_handlers/blocklist/mod.rs index 2989042..c09f829 100644 --- a/src/handlers/sonarr_handlers/blocklist/mod.rs +++ b/src/handlers/sonarr_handlers/blocklist/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; use crate::models::sonarr_models::BlocklistItem; use crate::models::stateful_table::SortOption; @@ -178,7 +179,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/downloads/mod.rs b/src/handlers/sonarr_handlers/downloads/mod.rs index 7a9819a..f585388 100644 --- a/src/handlers/sonarr_handlers/downloads/mod.rs +++ b/src/handlers/sonarr_handlers/downloads/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; use crate::network::sonarr_network::SonarrEvent; @@ -164,7 +165,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/history/mod.rs b/src/handlers/sonarr_handlers/history/mod.rs index 049de25..827270f 100644 --- a/src/handlers/sonarr_handlers/history/mod.rs +++ b/src/handlers/sonarr_handlers/history/mod.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; use crate::models::servarr_models::Language; use crate::models::sonarr_models::SonarrHistoryItem; @@ -121,7 +122,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, ' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs index c253008..8d1394f 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs @@ -1,6 +1,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; use crate::models::servarr_data::modals::EditIndexerModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_models::EditIndexerParams; @@ -123,7 +124,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<' .edit_indexer_modal .as_mut() .unwrap(); - if edit_indexer_modal.priority > 0 { + if edit_indexer_modal.priority > 1 { edit_indexer_modal.priority -= 1; } } @@ -526,7 +527,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs index 824ecae..6ebe7fb 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -50,7 +50,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); } else { assert_eq!( @@ -61,7 +61,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); EditIndexerHandler::new( @@ -80,7 +80,7 @@ mod tests { .as_ref() .unwrap() .priority, - 1 + 2 ); EditIndexerHandler::new( @@ -98,7 +98,7 @@ mod tests { .as_ref() .unwrap() .priority, - 0 + 1 ); } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs index 8da2449..a6e3bb0 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -1,10 +1,11 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, }; -use crate::models::sonarr_models::IndexerSettings; +use crate::models::servarr_models::IndexerSettings; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_prompt_left_right_keys, matches_key}; @@ -202,7 +203,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index c8464f7..8e7a0b8 100644 --- a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -15,7 +15,7 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, }; - use crate::models::sonarr_models::IndexerSettings; + use crate::models::servarr_models::IndexerSettings; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; @@ -23,7 +23,7 @@ mod tests { use crate::models::BlockSelectionState; use crate::models::servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; - use crate::models::sonarr_models::IndexerSettings; + use crate::models::servarr_models::IndexerSettings; use super::*; @@ -242,7 +242,7 @@ mod tests { assert_navigation_popped, models::{ BlockSelectionState, servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, - sonarr_models::IndexerSettings, + servarr_models::IndexerSettings, }, network::sonarr_network::SonarrEvent, }; @@ -415,7 +415,7 @@ mod tests { mod test_handle_esc { use rstest::rstest; - use crate::models::sonarr_models::IndexerSettings; + use crate::models::servarr_models::IndexerSettings; use super::*; use crate::assert_navigation_popped; diff --git a/src/handlers/sonarr_handlers/indexers/mod.rs b/src/handlers/sonarr_handlers/indexers/mod.rs index cc33890..cc8e852 100644 --- a/src/handlers/sonarr_handlers/indexers/mod.rs +++ b/src/handlers/sonarr_handlers/indexers/mod.rs @@ -7,11 +7,11 @@ use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestA use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, }; +use crate::models::{BlockSelectionState, Route}; use crate::network::sonarr_network::SonarrEvent; mod edit_indexer_handler; @@ -211,7 +211,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs index d0e3d57..8e0c70a 100644 --- a/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs +++ b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::event::Key; use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; #[cfg(test)] @@ -101,7 +102,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/add_series_handler.rs b/src/handlers/sonarr_handlers/library/add_series_handler.rs index e6f8162..12af178 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler.rs @@ -5,7 +5,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, ActiveSonarrBlock, }; use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult}; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::{BlockSelectionState, Route, Scrollable}; use crate::network::sonarr_network::SonarrEvent; use crate::{App, Key, handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -74,8 +74,8 @@ impl AddSeriesHandler<'_, '_> { .unwrap(); let path = root_folder_list.current_selection().path.clone(); - let monitor = monitor_list.current_selection().to_string(); - let series_type = series_type_list.current_selection().to_string(); + let monitor = *monitor_list.current_selection(); + let series_type = *series_type_list.current_selection(); AddSeriesBody { tvdb_id, @@ -426,8 +426,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, fn handle_submit(&mut self) { match self.active_sonarr_block { - _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchInput - && !self + ActiveSonarrBlock::AddSeriesSearchInput + if !self .app .data .sonarr_data @@ -442,8 +442,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, .push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); self.app.ignore_special_keys_for_textbox_input = false; } - _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults - && self.app.data.sonarr_data.add_searched_series.is_some() => + ActiveSonarrBlock::AddSeriesSearchResults + if self.app.data.sonarr_data.add_searched_series.is_some() => { let tvdb_id = self .app @@ -625,7 +625,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs index e7a783a..ee28d97 100644 --- a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs @@ -1030,8 +1030,6 @@ mod tests { app.is_loading = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); - let mut add_searched_series = StatefulTable::default(); - add_searched_series.set_items(vec![AddSeriesSearchResult::default()]); AddSeriesHandler::new( SUBMIT_KEY, @@ -1053,6 +1051,7 @@ mod tests { let mut app = App::test_default(); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + AddSeriesHandler::new( SUBMIT_KEY, &mut app, @@ -1092,7 +1091,7 @@ mod tests { } #[test] - fn test_add_series_prompt_prompt_decline_submit() { + fn test_add_series_confirm_prompt_prompt_decline_submit() { let mut app = App::test_default(); app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); @@ -1169,12 +1168,12 @@ mod tests { root_folder_path: "/nfs2".to_owned(), quality_profile_id: 2222, language_profile_id: 2222, - series_type: "standard".to_owned(), + series_type: SeriesType::Standard, season_folder: true, tags: Vec::default(), tag_input_string: Some("usenet, testing".to_owned()), add_options: AddSeriesOptions { - monitor: "all".to_owned(), + monitor: SeriesMonitor::All, search_for_cutoff_unmet_episodes: true, search_for_missing_episodes: true, }, @@ -1195,9 +1194,9 @@ mod tests { .handle(); assert_navigation_popped!(app, ActiveSonarrBlock::Series.into()); - assert_eq!( - app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::AddSeries(expected_add_series_body)) + assert_some_eq_x!( + &app.data.sonarr_data.prompt_confirm_action, + &SonarrEvent::AddSeries(expected_add_series_body.clone()) ); assert_modal_absent!(app.data.sonarr_data.add_series_modal); } @@ -1647,12 +1646,12 @@ mod tests { root_folder_path: "/nfs2".to_owned(), quality_profile_id: 2222, language_profile_id: 2222, - series_type: "standard".to_owned(), + series_type: SeriesType::Standard, season_folder: true, tags: Vec::default(), tag_input_string: Some("usenet, testing".to_owned()), add_options: AddSeriesOptions { - monitor: "all".to_owned(), + monitor: SeriesMonitor::All, search_for_cutoff_unmet_episodes: true, search_for_missing_episodes: true, }, @@ -1777,12 +1776,12 @@ mod tests { root_folder_path: "/nfs2".to_owned(), quality_profile_id: 2222, language_profile_id: 2222, - series_type: "standard".to_owned(), + series_type: SeriesType::Standard, season_folder: true, tags: Vec::default(), tag_input_string: Some("usenet, testing".to_owned()), add_options: AddSeriesOptions { - monitor: "all".to_owned(), + monitor: SeriesMonitor::All, search_for_cutoff_unmet_episodes: true, search_for_missing_episodes: true, }, diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler.rs b/src/handlers/sonarr_handlers/library/delete_series_handler.rs index 97fde80..479545f 100644 --- a/src/handlers/sonarr_handlers/library/delete_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/delete_series_handler.rs @@ -1,3 +1,4 @@ +use crate::models::Route; use crate::models::sonarr_models::DeleteSeriesParams; use crate::network::sonarr_network::SonarrEvent; use crate::{ @@ -143,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler< self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/edit_series_handler.rs b/src/handlers/sonarr_handlers/library/edit_series_handler.rs index a57224e..4bd4d79 100644 --- a/src/handlers/sonarr_handlers/library/edit_series_handler.rs +++ b/src/handlers/sonarr_handlers/library/edit_series_handler.rs @@ -1,10 +1,10 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; -use crate::models::Scrollable; use crate::models::servarr_data::sonarr::modals::EditSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; use crate::models::sonarr_models::EditSeriesParams; +use crate::models::{Route, Scrollable}; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -471,7 +471,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/episode_details_handler.rs b/src/handlers/sonarr_handlers/library/episode_details_handler.rs index c007778..107cc59 100644 --- a/src/handlers/sonarr_handlers/library/episode_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/episode_details_handler.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::library::season_details_handler::releases_ use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS}; use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody}; use crate::network::sonarr_network::SonarrEvent; @@ -370,7 +371,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandle self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/mod.rs b/src/handlers/sonarr_handlers/library/mod.rs index dc60579..01934a4 100644 --- a/src/handlers/sonarr_handlers/library/mod.rs +++ b/src/handlers/sonarr_handlers/library/mod.rs @@ -25,6 +25,7 @@ use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeD use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler; use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::models::Route; mod add_series_handler; mod delete_series_handler; @@ -245,7 +246,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index 193c3ff..6eb3659 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -4,6 +4,7 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS}; use crate::models::servarr_models::Language; use crate::models::sonarr_models::{ @@ -278,8 +279,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler } ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => { if self.app.data.sonarr_data.prompt_confirm { + let (series_id, season_number) = self.extract_series_id_season_number_tuple(); self.app.data.sonarr_data.prompt_confirm_action = Some( - SonarrEvent::TriggerAutomaticSeasonSearch(self.extract_series_id_season_number_tuple()), + SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number), ); } @@ -403,8 +405,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler }, ActiveSonarrBlock::AutomaticallySearchSeasonPrompt if matches_key!(confirm, key) => { self.app.data.sonarr_data.prompt_confirm = true; + let (series_id, season_number) = self.extract_series_id_season_number_tuple(); self.app.data.sonarr_data.prompt_confirm_action = Some( - SonarrEvent::TriggerAutomaticSeasonSearch(self.extract_series_id_season_number_tuple()), + SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number), ); self.app.pop_navigation_stack(); @@ -458,7 +461,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index 35b4c70..da8d159 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -268,7 +268,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, - SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)) + SonarrEvent::TriggerAutomaticSeasonSearch(0, 0) )] #[case( ActiveSonarrBlock::DeleteEpisodeFilePrompt, @@ -694,7 +694,7 @@ mod tests { #[rstest] #[case( ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, - SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)) + SonarrEvent::TriggerAutomaticSeasonSearch(0, 0) )] #[case( ActiveSonarrBlock::DeleteEpisodeFilePrompt, diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs index ee60e3e..f5132ba 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -4,11 +4,11 @@ use crate::handlers::sonarr_handlers::history::history_sorting_options; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; -use crate::models::BlockSelectionState; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS, }; use crate::models::sonarr_models::{Season, SonarrHistoryItem}; +use crate::models::{BlockSelectionState, Route}; use crate::network::sonarr_network::SonarrEvent; #[cfg(test)] @@ -278,8 +278,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler } _ if matches_key!(toggle_monitoring, key) => { self.app.data.sonarr_data.prompt_confirm = true; + let (series_id, season_number) = self.extract_series_id_season_number_tuple(); self.app.data.sonarr_data.prompt_confirm_action = Some( - SonarrEvent::ToggleSeasonMonitoring(self.extract_series_id_season_number_tuple()), + SonarrEvent::ToggleSeasonMonitoring(series_id, season_number), ); self @@ -327,10 +328,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler } } ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { - if self.app.data.sonarr_data.prompt_confirm { - self.app.data.sonarr_data.prompt_confirm_action = - Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id())); - } + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id())); self.app.pop_navigation_stack(); } @@ -342,7 +342,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs index 048f512..152966a 100644 --- a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -378,7 +378,7 @@ mod tests { assert!(app.is_routing); assert_some_eq_x!( &app.data.sonarr_data.prompt_confirm_action, - &SonarrEvent::ToggleSeasonMonitoring((0, 0)) + &SonarrEvent::ToggleSeasonMonitoring(0, 0) ); } @@ -402,7 +402,7 @@ mod tests { ActiveSonarrBlock::SeriesDetails.into() ); assert!(!app.data.sonarr_data.prompt_confirm); - assert_modal_absent!(app.data.sonarr_data.prompt_confirm_action); + assert_none!(app.data.sonarr_data.prompt_confirm_action); assert!(!app.is_routing); } @@ -555,7 +555,6 @@ mod tests { active_sonarr_block: ActiveSonarrBlock, ) { let mut app = App::test_default(); - app.data.sonarr_data.prompt_confirm = true; app.data.sonarr_data.series.set_items(vec![series()]); app.push_navigation_stack(active_sonarr_block.into()); app.push_navigation_stack(prompt_block.into()); diff --git a/src/handlers/sonarr_handlers/mod.rs b/src/handlers/sonarr_handlers/mod.rs index 033392e..ebd139d 100644 --- a/src/handlers/sonarr_handlers/mod.rs +++ b/src/handlers/sonarr_handlers/mod.rs @@ -6,12 +6,12 @@ use library::LibraryHandler; use root_folders::RootFoldersHandler; use system::SystemHandler; +use super::KeyEventHandler; +use crate::models::Route; use crate::{ app::App, event::Key, matches_key, models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, }; -use super::KeyEventHandler; - mod blocklist; mod downloads; mod history; @@ -115,7 +115,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs index 04da09c..964060c 100644 --- a/src/handlers/sonarr_handlers/root_folders/mod.rs +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -3,9 +3,9 @@ use crate::event::Key; use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; -use crate::models::HorizontallyScrollableText; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; use crate::models::servarr_models::AddRootFolderBody; +use crate::models::{HorizontallyScrollableText, Route}; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; @@ -229,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 08cefc2..39e5435 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -2,13 +2,14 @@ #[macro_use] pub(in crate::handlers::sonarr_handlers) mod utils { use crate::models::HorizontallyScrollableText; + use crate::models::servarr_models::IndexerSettings; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, }; use crate::models::sonarr_models::{ AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadRecord, DownloadStatus, - Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, Season, SeasonStatistics, Series, - SeriesStatistics, SeriesStatus, SeriesType, + Episode, EpisodeFile, MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics, + SeriesStatus, SeriesType, }; use chrono::DateTime; use serde_json::{Number, json}; diff --git a/src/handlers/sonarr_handlers/system/mod.rs b/src/handlers/sonarr_handlers/system/mod.rs index 9c70ac4..0734350 100644 --- a/src/handlers/sonarr_handlers/system/mod.rs +++ b/src/handlers/sonarr_handlers/system/mod.rs @@ -4,8 +4,8 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler; use crate::handlers::{KeyEventHandler, handle_clear_errors}; use crate::matches_key; -use crate::models::Scrollable; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; +use crate::models::{Route, Scrollable}; mod system_details_handler; @@ -129,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/sonarr_handlers/system/system_details_handler.rs b/src/handlers/sonarr_handlers/system/system_details_handler.rs index 57879e2..63495fd 100644 --- a/src/handlers/sonarr_handlers/system/system_details_handler.rs +++ b/src/handlers/sonarr_handlers/system/system_details_handler.rs @@ -2,10 +2,10 @@ use crate::app::App; use crate::event::Key; use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; use crate::matches_key; -use crate::models::Scrollable; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::sonarr_models::SonarrTaskName; use crate::models::stateful_list::StatefulList; +use crate::models::{Route, Scrollable}; use crate::network::sonarr_network::SonarrEvent; #[cfg(test)] @@ -201,7 +201,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/handlers/table_handler_tests.rs b/src/handlers/table_handler_tests.rs index 4e5b9ab..0e3ff10 100644 --- a/src/handlers/table_handler_tests.rs +++ b/src/handlers/table_handler_tests.rs @@ -9,6 +9,7 @@ mod tests { use crate::handlers::KeyEventHandler; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::handle_table; + use crate::models::Route; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::Language; @@ -98,7 +99,7 @@ mod tests { self.app } - fn current_route(&self) -> crate::models::Route { + fn current_route(&self) -> Route { self.app.get_current_route() } } diff --git a/src/main.rs b/src/main.rs index b611d22..4baa095 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,12 +3,15 @@ extern crate assertables; use anyhow::Result; -use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version}; +use clap::{ + Args, CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version, +}; use clap_complete::generate; use crossterm::execute; use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; +use indoc::indoc; use log::{debug, error, warn}; use network::NetworkTrait; use ratatui::Terminal; @@ -64,6 +67,13 @@ mod utils; struct Cli { #[command(subcommand)] command: Option, + #[command(flatten)] + global: GlobalOpts, +} + +#[derive(Args, Debug)] +#[command(next_help_heading = "Global Options")] +struct GlobalOpts { #[arg( long, global = true, @@ -98,9 +108,12 @@ struct Cli { #[arg( long, global = true, - help = "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." + help = indoc!{" + 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. + "} )] servarr_name: Option, } @@ -114,13 +127,13 @@ 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.config_file { + let mut config = if let Some(ref config_file) = args.global.config_file { load_config(config_file.to_str().expect("Invalid config file specified"))? } else { confy::load("managarr", "config")? }; let theme_name = config.theme.clone(); - let spinner_disabled = args.disable_spinner; + let spinner_disabled = args.global.disable_spinner; debug!("Managarr loaded using config: {config:?}"); config.validate(); config.post_process_initialization(); @@ -145,7 +158,7 @@ async fn main() -> Result<()> { match args.command { Some(command) => match command { - Command::Radarr(_) | Command::Sonarr(_) => { + Command::Radarr(_) | Command::Sonarr(_) | Command::Lidarr(_) => { if spinner_disabled { start_cli_no_spinner(config, reqwest_client, cancellation_token, app, command).await; } else { @@ -165,8 +178,8 @@ async fn main() -> Result<()> { }); start_ui( &app, - &args.themes_file, - args.theme.unwrap_or(theme_name.unwrap_or_default()), + &args.global.themes_file, + args.global.theme.unwrap_or(theme_name.unwrap_or_default()), ) .await?; } diff --git a/src/models/lidarr_models.rs b/src/models/lidarr_models.rs new file mode 100644 index 0000000..72cdd3c --- /dev/null +++ b/src/models/lidarr_models.rs @@ -0,0 +1,625 @@ +use super::{ + HorizontallyScrollableText, Serdeable, + servarr_models::{ + DiskSpace, HostConfig, Indexer, IndexerTestResult, QualityProfile, QualityWrapper, RootFolder, + SecurityConfig, Tag, + }, +}; +use crate::models::servarr_models::{IndexerSettings, LogResponse, QueueEvent, Update}; +use crate::serde_enum_from; +use chrono::{DateTime, Utc}; +use clap::ValueEnum; +use derivative::Derivative; +use enum_display_style_derive::EnumDisplayStyle; +use serde::{Deserialize, Serialize}; +use serde_json::{Number, Value}; +use std::fmt::{Display, Formatter}; +use strum::{Display, EnumIter}; + +#[cfg(test)] +#[path = "lidarr_models_tests.rs"] +mod lidarr_models_tests; + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Artist { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub artist_name: HorizontallyScrollableText, + pub foreign_artist_id: String, + pub status: ArtistStatus, + pub overview: Option, + pub artist_type: Option, + pub disambiguation: Option, + pub members: Option>, + pub path: String, + #[serde(deserialize_with = "super::from_i64")] + pub quality_profile_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub metadata_profile_id: i64, + pub monitored: bool, + pub monitor_new_items: NewItemMonitorType, + pub genres: Vec, + pub tags: Vec, + pub added: DateTime, + pub ratings: Option, + pub statistics: Option, +} + +#[derive( + Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, Display, EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum ArtistStatus { + #[default] + Continuing, + Ended, + Deleted, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Ratings { + #[serde(deserialize_with = "super::from_i64")] + pub votes: i64, + #[serde(deserialize_with = "super::from_f64")] + pub value: f64, +} + +impl Eq for Ratings {} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Member { + pub name: Option, + pub instrument: Option, +} + +impl Eq for Member {} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ArtistStatistics { + #[serde(deserialize_with = "super::from_i64")] + pub album_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub track_file_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub track_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub total_track_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size_on_disk: i64, + #[serde(deserialize_with = "super::from_f64")] + pub percent_of_tracks: f64, +} + +impl Eq for ArtistStatistics {} + +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct MetadataProfile { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub name: String, +} + +impl From<(&i64, &String)> for MetadataProfile { + fn from(value: (&i64, &String)) -> Self { + MetadataProfile { + id: *value.0, + name: value.1.clone(), + } + } +} + +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + clap::ValueEnum, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum NewItemMonitorType { + #[default] + #[display_style(name = "All Albums")] + All, + #[display_style(name = "No New Albums")] + None, + #[display_style(name = "New Albums")] + New, +} + +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + clap::ValueEnum, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum MonitorType { + #[default] + #[display_style(name = "All Albums")] + All, + #[display_style(name = "Future Albums")] + Future, + #[display_style(name = "Missing Albums")] + Missing, + #[display_style(name = "Existing Albums")] + Existing, + #[display_style(name = "First Album")] + First, + #[display_style(name = "Latest Album")] + Latest, + None, + Unknown, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct DownloadRecord { + pub title: String, + pub status: DownloadStatus, + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub album_id: Option, + pub artist_id: Option, + #[serde(deserialize_with = "super::from_f64")] + pub size: f64, + #[serde(deserialize_with = "super::from_f64")] + pub sizeleft: f64, + pub output_path: Option, + #[serde(default)] + pub indexer: String, + pub download_client: Option, +} + +impl Eq for DownloadRecord {} + +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum DownloadStatus { + #[default] + Unknown, + Queued, + Paused, + Downloading, + Completed, + Failed, + Warning, + Delay, + #[display_style(name = "Download Client Unavailable")] + DownloadClientUnavailable, + Fallback, +} + +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DownloadsResponse { + pub records: Vec, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SystemStatus { + pub version: String, + pub start_time: DateTime, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddArtistSearchResult { + pub foreign_artist_id: String, + pub artist_name: HorizontallyScrollableText, + pub status: ArtistStatus, + pub overview: Option, + pub artist_type: Option, + pub disambiguation: Option, + pub genres: Vec, + pub ratings: Option, +} + +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct LidarrCommandBody { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub artist_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub album_ids: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub struct DeleteParams { + pub id: i64, + pub delete_files: bool, + pub add_import_list_exclusion: bool, +} + +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddArtistBody { + pub foreign_artist_id: String, + pub artist_name: String, + pub monitored: bool, + pub root_folder_path: String, + pub quality_profile_id: i64, + pub metadata_profile_id: i64, + pub tags: Vec, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, + pub add_options: AddArtistOptions, +} + +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddArtistOptions { + pub monitor: MonitorType, + pub monitor_new_items: NewItemMonitorType, + pub search_for_missing_albums: bool, +} + +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EditArtistParams { + pub artist_id: i64, + pub monitored: Option, + pub monitor_new_items: Option, + pub quality_profile_id: Option, + pub metadata_profile_id: Option, + pub root_folder_path: Option, + pub tags: Option>, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, + pub clear_tags: bool, +} + +#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddLidarrRootFolderBody { + pub name: String, + pub path: String, + pub default_quality_profile_id: i64, + pub default_metadata_profile_id: i64, + pub default_monitor_option: MonitorType, + pub default_new_item_monitor_option: NewItemMonitorType, + pub default_tags: Vec, + #[serde(skip_serializing, skip_deserializing)] + pub tag_input_string: Option, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Album { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub title: HorizontallyScrollableText, + pub foreign_album_id: String, + pub monitored: bool, + #[serde(default)] + pub any_release_ok: bool, + #[serde(deserialize_with = "super::from_i64")] + pub profile_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub duration: i64, + pub album_type: Option, + pub genres: Vec, + pub ratings: Option, + pub release_date: Option>, + pub statistics: Option, +} + +#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AlbumStatistics { + #[serde(deserialize_with = "super::from_i64")] + pub track_file_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub track_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub total_track_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size_on_disk: i64, + #[serde(deserialize_with = "super::from_f64")] + pub percent_of_tracks: f64, +} + +impl Eq for AlbumStatistics {} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LidarrHistoryWrapper { + pub records: Vec, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LidarrHistoryData { + pub indexer: Option, + pub release_group: Option, + pub nzb_info_url: Option, + pub download_client_name: Option, + pub download_client: Option, + pub age: Option, + pub published_date: Option>, + pub message: Option, + pub reason: Option, + pub dropped_path: Option, + pub imported_path: Option, + pub source_path: Option, + pub path: Option, + pub status_messages: Option, +} + +#[derive( + Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Display, EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum LidarrHistoryEventType { + #[default] + Unknown, + Grabbed, + #[display_style(name = "Artist Folder Imported")] + ArtistFolderImported, + #[display_style(name = "Album Import Incomplete")] + AlbumImportIncomplete, + #[display_style(name = "Download Ignored")] + DownloadIgnored, + #[display_style(name = "Download Imported")] + DownloadImported, + #[display_style(name = "Download Failed")] + DownloadFailed, + #[display_style(name = "Track File Deleted")] + TrackFileDeleted, + #[display_style(name = "Track File Imported")] + TrackFileImported, + #[display_style(name = "Track File Renamed")] + TrackFileRenamed, + #[display_style(name = "Track File Retagged")] + TrackFileRetagged, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LidarrHistoryItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub source_title: HorizontallyScrollableText, + #[serde(deserialize_with = "super::from_i64")] + pub album_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub artist_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub track_id: i64, + #[serde(default)] + pub quality: QualityWrapper, + pub date: DateTime, + pub event_type: LidarrHistoryEventType, + #[serde(default)] + pub data: LidarrHistoryData, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LidarrTask { + pub name: String, + pub task_name: LidarrTaskName, + #[serde(deserialize_with = "super::from_i64")] + pub interval: i64, + pub last_execution: DateTime, + pub next_execution: DateTime, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, ValueEnum)] +#[serde(rename_all = "PascalCase")] +pub enum LidarrTaskName { + #[default] + ApplicationUpdateCheck, + Backup, + CheckHealth, + Housekeeping, + ImportListSync, + MessagingCleanup, + RefreshArtist, + RefreshMonitoredDownloads, + RescanFolders, + RssSync, +} + +impl Display for LidarrTaskName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let task_name = serde_json::to_string(&self) + .expect("Unable to serialize task name") + .replace('"', ""); + write!(f, "{task_name}") + } +} + +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct LidarrRelease { + pub guid: String, + pub protocol: String, + #[serde(deserialize_with = "super::from_i64")] + pub age: i64, + pub title: HorizontallyScrollableText, + pub discography: bool, + pub artist_name: Option, + pub album_title: Option, + pub indexer: String, + #[serde(deserialize_with = "super::from_i64")] + pub indexer_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub size: i64, + pub rejected: bool, + pub rejections: Option>, + pub seeders: Option, + pub leechers: Option, + pub quality: QualityWrapper, +} + +#[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LidarrReleaseDownloadBody { + pub guid: String, + pub indexer_id: i64, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlocklistItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub artist_id: i64, + pub album_ids: Option>, + pub source_title: String, + pub quality: QualityWrapper, + pub date: DateTime, + pub protocol: String, + pub indexer: String, + pub message: String, + pub artist: Artist, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct BlocklistResponse { + pub records: Vec, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TrackFile { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub path: String, + #[serde(deserialize_with = "super::from_i64")] + pub size: i64, + pub quality: QualityWrapper, + pub date_added: DateTime, + pub media_info: Option, + pub audio_tags: Option, +} + +#[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct MediaInfo { + pub audio_bit_rate: Option, + #[serde(deserialize_with = "super::from_i64")] + pub audio_channels: i64, + pub audio_codec: Option, + pub audio_bits: Option, + pub audio_sample_rate: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AudioTags { + pub title: String, + pub artist_title: String, + pub album_title: String, + #[serde(deserialize_with = "super::from_i64")] + pub disc_number: i64, + #[serde(deserialize_with = "super::from_i64")] + pub disc_count: i64, + #[serde(deserialize_with = "super::from_i64")] + pub year: i64, + pub duration: String, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Track { + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub artist_id: i64, + pub foreign_track_id: String, + #[serde(deserialize_with = "super::from_i64")] + pub track_file_id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub album_id: i64, + pub explicit: bool, + pub track_number: String, + pub title: String, + #[serde(deserialize_with = "super::from_i64")] + pub duration: i64, + pub has_file: bool, + pub ratings: Ratings, + pub track_file: Option, +} + +impl From for Serdeable { + fn from(value: LidarrSerdeable) -> Serdeable { + Serdeable::Lidarr(value) + } +} + +serde_enum_from!( + LidarrSerdeable { + AddArtistSearchResults(Vec), + Albums(Vec), + Album(Album), + Artist(Artist), + Artists(Vec), + BlocklistResponse(BlocklistResponse), + DiskSpaces(Vec), + DownloadsResponse(DownloadsResponse), + LidarrHistoryWrapper(LidarrHistoryWrapper), + LidarrHistoryItems(Vec), + HostConfig(HostConfig), + IndexerSettings(IndexerSettings), + Indexers(Vec), + IndexerTestResults(Vec), + LogResponse(LogResponse), + MetadataProfiles(Vec), + QualityProfiles(Vec), + QueueEvents(Vec), + Releases(Vec), + RootFolders(Vec), + SecurityConfig(SecurityConfig), + SystemStatus(SystemStatus), + Tag(Tag), + Tags(Vec), + Tasks(Vec), + Track(Track), + Tracks(Vec), + TrackFiles(Vec), + Updates(Vec), + Value(Value), + } +); diff --git a/src/models/lidarr_models_tests.rs b/src/models/lidarr_models_tests.rs new file mode 100644 index 0000000..f59b464 --- /dev/null +++ b/src/models/lidarr_models_tests.rs @@ -0,0 +1,826 @@ +#[cfg(test)] +mod tests { + use chrono::Utc; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::json; + + use crate::models::lidarr_models::{ + AddArtistSearchResult, Album, AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord, + DownloadStatus, DownloadsResponse, LidarrHistoryEventType, LidarrHistoryItem, + LidarrHistoryWrapper, LidarrRelease, LidarrTask, MediaInfo, Member, MetadataProfile, + MonitorType, NewItemMonitorType, SystemStatus, Track, TrackFile, + }; + use crate::models::servarr_models::{ + DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Log, LogResponse, + QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, + }; + use crate::models::{ + Serdeable, + lidarr_models::{Artist, ArtistStatistics, ArtistStatus, LidarrSerdeable, Ratings}, + }; + + #[test] + fn test_artist_status_default() { + assert_eq!(ArtistStatus::default(), ArtistStatus::Continuing); + } + + #[test] + fn test_new_item_monitor_type_display() { + assert_str_eq!(NewItemMonitorType::All.to_string(), "all"); + assert_str_eq!(NewItemMonitorType::None.to_string(), "none"); + assert_str_eq!(NewItemMonitorType::New.to_string(), "new"); + } + + #[test] + fn test_new_item_monitor_type_to_display_str() { + assert_str_eq!(NewItemMonitorType::All.to_display_str(), "All Albums"); + assert_str_eq!(NewItemMonitorType::None.to_display_str(), "No New Albums"); + assert_str_eq!(NewItemMonitorType::New.to_display_str(), "New Albums"); + } + + #[test] + fn test_monitor_type_display() { + assert_str_eq!(MonitorType::All.to_string(), "all"); + assert_str_eq!(MonitorType::Future.to_string(), "future"); + assert_str_eq!(MonitorType::Missing.to_string(), "missing"); + assert_str_eq!(MonitorType::Existing.to_string(), "existing"); + assert_str_eq!(MonitorType::First.to_string(), "first"); + assert_str_eq!(MonitorType::Latest.to_string(), "latest"); + assert_str_eq!(MonitorType::None.to_string(), "none"); + assert_str_eq!(MonitorType::Unknown.to_string(), "unknown"); + } + + #[test] + fn test_monitor_type_to_display_str() { + assert_str_eq!(MonitorType::All.to_display_str(), "All Albums"); + assert_str_eq!(MonitorType::Future.to_display_str(), "Future Albums"); + assert_str_eq!(MonitorType::Missing.to_display_str(), "Missing Albums"); + assert_str_eq!(MonitorType::Existing.to_display_str(), "Existing Albums"); + assert_str_eq!(MonitorType::First.to_display_str(), "First Album"); + assert_str_eq!(MonitorType::Latest.to_display_str(), "Latest Album"); + assert_str_eq!(MonitorType::None.to_display_str(), "None"); + assert_str_eq!(MonitorType::Unknown.to_display_str(), "Unknown"); + } + + #[test] + fn test_lidarr_serdeable_from() { + let lidarr_serdeable = LidarrSerdeable::Value(json!({})); + + let serdeable: Serdeable = Serdeable::from(lidarr_serdeable.clone()); + + assert_eq!(serdeable, Serdeable::Lidarr(lidarr_serdeable)); + } + + #[test] + fn test_lidarr_serdeable_from_unit() { + let lidarr_serdeable = LidarrSerdeable::from(()); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Value(json!({}))); + } + + #[test] + fn test_lidarr_serdeable_from_value() { + let value = json!({"test": "test"}); + + let lidarr_serdeable: LidarrSerdeable = value.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Value(value)); + } + + #[test] + fn test_lidarr_serdeable_from_artists() { + let artists = vec![Artist { + id: 1, + ..Artist::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = artists.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Artists(artists)); + } + + #[test] + fn test_artist_deserialization() { + let artist_json = json!({ + "id": 1, + "artistName": "Test Artist", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "overview": "Test overview", + "artistType": "Group", + "disambiguation": "UK Band", + "path": "/music/test-artist", + "members": [ + { "name": "alex", "instrument": "piano" }, + { "name": "madi", "instrument": "vocals" } + ], + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["Rock", "Alternative"], + "tags": [1, 2], + "added": "2023-01-01T00:00:00Z", + "ratings": { + "votes": 100, + "value": 4.5 + }, + "statistics": { + "albumCount": 5, + "trackFileCount": 50, + "trackCount": 60, + "totalTrackCount": 70, + "sizeOnDisk": 1000000000, + "percentOfTracks": 83.33 + } + }); + let expected_members_vec = vec![ + Member { + name: Some("alex".to_string()), + instrument: Some("piano".to_string()), + }, + Member { + name: Some("madi".to_string()), + instrument: Some("vocals".to_string()), + }, + ]; + + let artist: Artist = serde_json::from_value(artist_json).unwrap(); + + assert_eq!(artist.id, 1); + assert_str_eq!(artist.artist_name.text, "Test Artist"); + assert_str_eq!(artist.foreign_artist_id, "test-foreign-id"); + assert_eq!(artist.status, ArtistStatus::Continuing); + assert_some_eq_x!(&artist.overview, "Test overview"); + assert_some_eq_x!(&artist.artist_type, "Group"); + assert_some_eq_x!(&artist.disambiguation, "UK Band"); + assert_str_eq!(artist.path, "/music/test-artist"); + assert_some_eq_x!(&artist.members, &expected_members_vec); + assert_eq!(artist.quality_profile_id, 1); + assert_eq!(artist.metadata_profile_id, 1); + assert!(artist.monitored); + assert_eq!(artist.monitor_new_items, NewItemMonitorType::All); + assert_eq!(artist.genres, vec!["Rock", "Alternative"]); + assert_eq!(artist.tags.len(), 2); + assert_some!(&artist.ratings); + assert_some!(&artist.statistics); + + let ratings = artist.ratings.unwrap(); + assert_eq!(ratings.votes, 100); + assert_eq!(ratings.value, 4.5); + + let stats = artist.statistics.unwrap(); + assert_eq!(stats.album_count, 5); + assert_eq!(stats.track_file_count, 50); + assert_eq!(stats.track_count, 60); + assert_eq!(stats.total_track_count, 70); + assert_eq!(stats.size_on_disk, 1000000000); + assert_eq!(stats.percent_of_tracks, 83.33); + } + + #[test] + fn test_artist_status_deserialization() { + assert_eq!( + serde_json::from_str::("\"continuing\"").unwrap(), + ArtistStatus::Continuing + ); + assert_eq!( + serde_json::from_str::("\"ended\"").unwrap(), + ArtistStatus::Ended + ); + assert_eq!( + serde_json::from_str::("\"deleted\"").unwrap(), + ArtistStatus::Deleted + ); + } + + #[test] + fn test_ratings_equality() { + let ratings1 = Ratings { + votes: 100, + value: 4.5, + }; + let ratings2 = Ratings { + votes: 100, + value: 4.5, + }; + let ratings3 = Ratings { + votes: 50, + value: 3.0, + }; + + assert_eq!(ratings1, ratings2); + assert_ne!(ratings1, ratings3); + } + + #[test] + fn test_artist_statistics_equality() { + let stats1 = ArtistStatistics { + album_count: 5, + track_file_count: 50, + track_count: 60, + total_track_count: 70, + size_on_disk: 1000000000, + percent_of_tracks: 83.33, + }; + let stats2 = ArtistStatistics { + album_count: 5, + track_file_count: 50, + track_count: 60, + total_track_count: 70, + size_on_disk: 1000000000, + percent_of_tracks: 83.33, + }; + let stats3 = ArtistStatistics::default(); + + assert_eq!(stats1, stats2); + assert_ne!(stats1, stats3); + } + + #[test] + fn test_artist_with_optional_fields_none() { + let artist_json = json!({ + "id": 1, + "artistName": "Test Artist", + "foreignArtistId": "", + "status": "continuing", + "path": "", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": false, + "monitorNewItems": "all", + "genres": [], + "tags": [], + "added": "2023-01-01T00:00:00Z" + }); + + let artist: Artist = serde_json::from_value(artist_json).unwrap(); + + assert_none!(&artist.overview); + assert_none!(&artist.artist_type); + assert_none!(&artist.disambiguation); + assert_eq!(artist.monitor_new_items, NewItemMonitorType::All); + assert_none!(&artist.ratings); + assert_none!(&artist.statistics); + } + + #[test] + fn test_lidarr_serdeable_from_artist() { + let artist = Artist { + id: 1, + ..Artist::default() + }; + + let lidarr_serdeable: LidarrSerdeable = artist.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Artist(artist)); + } + + #[test] + fn test_lidarr_serdeable_from_blocklist_response() { + let blocklist_response = BlocklistResponse { + records: vec![BlocklistItem { + id: 1, + ..BlocklistItem::default() + }], + }; + + let lidarr_serdeable: LidarrSerdeable = blocklist_response.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::BlocklistResponse(blocklist_response) + ); + } + + #[test] + fn test_lidarr_serdeable_from_disk_spaces() { + let disk_spaces = vec![DiskSpace { + free_space: 1, + total_space: 1, + }]; + + let lidarr_serdeable: LidarrSerdeable = disk_spaces.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::DiskSpaces(disk_spaces)); + } + + #[test] + fn test_lidarr_serdeable_from_downloads_response() { + let downloads_response = DownloadsResponse { + records: vec![DownloadRecord { + id: 1, + ..DownloadRecord::default() + }], + }; + + let lidarr_serdeable: LidarrSerdeable = downloads_response.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::DownloadsResponse(downloads_response) + ); + } + + #[test] + fn test_lidarr_serdeable_from_lidarr_history_wrapper() { + let history_wrapper = LidarrHistoryWrapper { + records: vec![LidarrHistoryItem { + id: 1, + ..LidarrHistoryItem::default() + }], + }; + + let lidarr_serdeable: LidarrSerdeable = history_wrapper.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::LidarrHistoryWrapper(history_wrapper) + ); + } + + #[test] + fn test_lidarr_serdeable_from_lidarr_history_items() { + let history_items = vec![LidarrHistoryItem { + id: 1, + ..LidarrHistoryItem::default() + }]; + let lidarr_serdeable: LidarrSerdeable = history_items.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::LidarrHistoryItems(history_items) + ); + } + + #[test] + fn test_lidarr_serdeable_from_indexers() { + let indexers = vec![Indexer { + id: 1, + ..Indexer::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = indexers.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Indexers(indexers)); + } + + #[test] + fn test_lidarr_serdeable_from_indexer_settings() { + let indexer_settings = IndexerSettings { + id: 1, + ..IndexerSettings::default() + }; + + let lidarr_serdeable: LidarrSerdeable = indexer_settings.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::IndexerSettings(indexer_settings) + ); + } + + #[test] + fn test_lidarr_serdeable_from_indexer_test_results() { + let indexer_test_results = vec![IndexerTestResult { + id: 1, + ..IndexerTestResult::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = indexer_test_results.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::IndexerTestResults(indexer_test_results) + ); + } + + #[test] + fn test_lidarr_serdeable_from_log_response() { + let log_response = LogResponse { + records: vec![Log { + level: "info".to_owned(), + ..Log::default() + }], + }; + + let lidarr_serdeable: LidarrSerdeable = log_response.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::LogResponse(log_response)); + } + + #[test] + fn test_lidarr_serdeable_from_metadata_profiles() { + let metadata_profiles = vec![MetadataProfile { + id: 1, + name: "Standard".to_owned(), + }]; + + let lidarr_serdeable: LidarrSerdeable = metadata_profiles.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::MetadataProfiles(metadata_profiles) + ); + } + + #[test] + fn test_lidarr_serdeable_from_host_config() { + let host_config = HostConfig { + port: 8686, + ..HostConfig::default() + }; + + let lidarr_serdeable: LidarrSerdeable = host_config.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::HostConfig(host_config)); + } + + #[test] + fn test_lidarr_serdeable_from_quality_profiles() { + let quality_profiles = vec![QualityProfile { + id: 1, + name: "Any".to_owned(), + }]; + + let lidarr_serdeable: LidarrSerdeable = quality_profiles.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::QualityProfiles(quality_profiles) + ); + } + + #[test] + fn test_lidarr_serdeable_from_queue_events() { + let queue_events = vec![QueueEvent { + trigger: "test".to_owned(), + ..QueueEvent::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = queue_events.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::QueueEvents(queue_events)); + } + + #[test] + fn test_lidarr_serdeable_from_root_folders() { + let root_folders = vec![RootFolder { + id: 1, + path: "/music".to_owned(), + accessible: true, + free_space: 1000000, + unmapped_folders: None, + }]; + + let lidarr_serdeable: LidarrSerdeable = root_folders.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::RootFolders(root_folders)); + } + + #[test] + fn test_lidarr_serdeable_from_releases() { + let releases = vec![LidarrRelease { + guid: "test".to_owned(), + ..LidarrRelease::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = releases.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Releases(releases)); + } + + #[test] + fn test_lidarr_serdeable_from_security_config() { + let security_config = SecurityConfig { + api_key: "test-key".to_owned(), + ..SecurityConfig::default() + }; + + let lidarr_serdeable: LidarrSerdeable = security_config.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::SecurityConfig(security_config) + ); + } + + #[test] + fn test_lidarr_serdeable_from_system_status() { + let system_status = SystemStatus { + version: "1.0.0".to_owned(), + start_time: Utc::now(), + }; + + let lidarr_serdeable: LidarrSerdeable = system_status.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::SystemStatus(system_status) + ); + } + + #[test] + fn test_lidarr_serdeable_from_tags() { + let tags = vec![Tag { + id: 1, + label: "rock".to_owned(), + }]; + + let lidarr_serdeable: LidarrSerdeable = tags.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Tags(tags)); + } + + #[test] + fn test_lidarr_serdeable_from_add_artist_search_results() { + let search_results = vec![AddArtistSearchResult { + foreign_artist_id: "test-id".to_owned(), + ..AddArtistSearchResult::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = search_results.clone().into(); + + assert_eq!( + lidarr_serdeable, + LidarrSerdeable::AddArtistSearchResults(search_results) + ); + } + + #[test] + fn test_lidarr_serdeable_from_albums() { + let albums = vec![Album { + id: 1, + ..Album::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = albums.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Albums(albums)); + } + + #[test] + fn test_lidarr_serdeable_from_album() { + let album = Album { + id: 1, + ..Album::default() + }; + + let lidarr_serdeable: LidarrSerdeable = album.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Album(album)); + } + + #[test] + fn test_lidarr_serdeable_from_tasks() { + let tasks = vec![LidarrTask { + name: "test".to_owned(), + ..LidarrTask::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = tasks.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Tasks(tasks)); + } + + #[test] + fn test_lidarr_serdeable_from_updates() { + let updates = vec![Update { + version: "test".to_owned(), + ..Update::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = updates.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Updates(updates)); + } + + #[test] + fn test_lidarr_serdeable_from_track_file() { + let track_files = vec![TrackFile { + id: 1, + media_info: Some(MediaInfo { + audio_channels: 2, + ..MediaInfo::default() + }), + audio_tags: Some(AudioTags { + disc_number: 1, + ..AudioTags::default() + }), + ..TrackFile::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = track_files.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::TrackFiles(track_files)); + } + + #[test] + fn test_lidarr_serdeable_from_track() { + let track = Track { + id: 1, + ..Track::default() + }; + + let lidarr_serdeable: LidarrSerdeable = track.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Track(track)); + } + + #[test] + fn test_lidarr_serdeable_from_tracks() { + let tracks = vec![Track { + id: 1, + ..Track::default() + }]; + + let lidarr_serdeable: LidarrSerdeable = tracks.clone().into(); + + assert_eq!(lidarr_serdeable, LidarrSerdeable::Tracks(tracks)); + } + + #[test] + fn test_artist_status_display() { + assert_str_eq!(ArtistStatus::Continuing.to_string(), "continuing"); + assert_str_eq!(ArtistStatus::Ended.to_string(), "ended"); + assert_str_eq!(ArtistStatus::Deleted.to_string(), "deleted"); + } + + #[test] + fn test_artist_status_to_display_str() { + assert_str_eq!(ArtistStatus::Continuing.to_display_str(), "Continuing"); + assert_str_eq!(ArtistStatus::Ended.to_display_str(), "Ended"); + assert_str_eq!(ArtistStatus::Deleted.to_display_str(), "Deleted"); + } + + #[test] + fn test_download_status_display() { + assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown"); + assert_str_eq!(DownloadStatus::Queued.to_string(), "queued"); + assert_str_eq!(DownloadStatus::Paused.to_string(), "paused"); + assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading"); + assert_str_eq!(DownloadStatus::Completed.to_string(), "completed"); + assert_str_eq!(DownloadStatus::Failed.to_string(), "failed"); + assert_str_eq!(DownloadStatus::Warning.to_string(), "warning"); + assert_str_eq!(DownloadStatus::Delay.to_string(), "delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_string(), + "downloadClientUnavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback"); + } + + #[test] + fn test_download_status_to_display_str() { + assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued"); + assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused"); + assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading"); + assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed"); + assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed"); + assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning"); + assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_display_str(), + "Download Client Unavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback"); + } + + #[test] + fn test_lidarr_history_event_type_display() { + assert_str_eq!(LidarrHistoryEventType::Unknown.to_string(), "unknown"); + assert_str_eq!(LidarrHistoryEventType::Grabbed.to_string(), "grabbed"); + assert_str_eq!( + LidarrHistoryEventType::ArtistFolderImported.to_string(), + "artistFolderImported" + ); + assert_str_eq!( + LidarrHistoryEventType::AlbumImportIncomplete.to_string(), + "albumImportIncomplete" + ); + assert_str_eq!( + LidarrHistoryEventType::DownloadIgnored.to_string(), + "downloadIgnored" + ); + assert_str_eq!( + LidarrHistoryEventType::DownloadImported.to_string(), + "downloadImported" + ); + assert_str_eq!( + LidarrHistoryEventType::DownloadFailed.to_string(), + "downloadFailed" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileDeleted.to_string(), + "trackFileDeleted" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileImported.to_string(), + "trackFileImported" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileRenamed.to_string(), + "trackFileRenamed" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileRetagged.to_string(), + "trackFileRetagged" + ); + } + + #[test] + fn test_lidarr_history_event_type_to_display_str() { + assert_str_eq!(LidarrHistoryEventType::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(LidarrHistoryEventType::Grabbed.to_display_str(), "Grabbed"); + assert_str_eq!( + LidarrHistoryEventType::ArtistFolderImported.to_display_str(), + "Artist Folder Imported" + ); + assert_str_eq!( + LidarrHistoryEventType::AlbumImportIncomplete.to_display_str(), + "Album Import Incomplete" + ); + assert_str_eq!( + LidarrHistoryEventType::DownloadIgnored.to_display_str(), + "Download Ignored" + ); + assert_str_eq!( + LidarrHistoryEventType::DownloadImported.to_display_str(), + "Download Imported" + ); + assert_str_eq!( + LidarrHistoryEventType::DownloadFailed.to_display_str(), + "Download Failed" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileDeleted.to_display_str(), + "Track File Deleted" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileImported.to_display_str(), + "Track File Imported" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileRenamed.to_display_str(), + "Track File Renamed" + ); + assert_str_eq!( + LidarrHistoryEventType::TrackFileRetagged.to_display_str(), + "Track File Retagged" + ); + } + + #[test] + fn test_add_artist_search_result_deserialization() { + let search_result_json = json!({ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "status": "continuing", + "overview": "Test overview", + "artistType": "Group", + "disambiguation": "UK Band", + "genres": ["Rock", "Alternative"], + "ratings": { + "votes": 100, + "value": 4.5 + } + }); + + let search_result: AddArtistSearchResult = serde_json::from_value(search_result_json).unwrap(); + + assert_str_eq!(search_result.foreign_artist_id, "test-foreign-id"); + assert_str_eq!(search_result.artist_name.text, "Test Artist"); + assert_eq!(search_result.status, ArtistStatus::Continuing); + assert_some_eq_x!(&search_result.overview, "Test overview"); + assert_some_eq_x!(&search_result.artist_type, "Group"); + assert_some_eq_x!(&search_result.disambiguation, "UK Band"); + assert_eq!(search_result.genres, vec!["Rock", "Alternative"]); + assert_some!(&search_result.ratings); + + let ratings = search_result.ratings.unwrap(); + assert_eq!(ratings.votes, 100); + assert_eq!(ratings.value, 4.5); + } + + #[test] + fn test_add_artist_search_result_with_optional_fields_none() { + let search_result_json = json!({ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "status": "ended", + "genres": [] + }); + + let search_result: AddArtistSearchResult = serde_json::from_value(search_result_json).unwrap(); + + assert_str_eq!(search_result.foreign_artist_id, "test-foreign-id"); + assert_str_eq!(search_result.artist_name.text, "Test Artist"); + assert_eq!(search_result.status, ArtistStatus::Ended); + assert_none!(&search_result.overview); + assert_none!(&search_result.artist_type); + assert_none!(&search_result.disambiguation); + assert!(search_result.genres.is_empty()); + assert_none!(&search_result.ratings); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index fc8d5b6..3eea7c3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,7 +3,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use crate::app::ServarrConfig; use crate::app::context_clues::ContextClue; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; +use lidarr_models::LidarrSerdeable; use radarr_models::RadarrSerdeable; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; @@ -11,6 +13,7 @@ use serde_json::Number; use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use sonarr_models::SonarrSerdeable; +pub mod lidarr_models; pub mod radarr_models; pub mod servarr_data; pub mod servarr_models; @@ -30,7 +33,7 @@ pub enum Route { Radarr(ActiveRadarrBlock, Option), Sonarr(ActiveSonarrBlock, Option), Readarr, - Lidarr, + Lidarr(ActiveLidarrBlock, Option), Whisparr, Bazarr, Prowlarr, @@ -43,6 +46,7 @@ pub enum Route { pub enum Serdeable { Radarr(RadarrSerdeable), Sonarr(SonarrSerdeable), + Lidarr(LidarrSerdeable), } pub trait Scrollable { @@ -289,8 +293,7 @@ impl TabState { TabState { tabs, index: 0 } } - // Allowing this code for now since we'll eventually be implementing additional Servarr support, and we'll need it then - #[allow(dead_code)] + #[cfg(test)] pub fn set_index(&mut self, index: usize) -> &TabRoute { self.index = index; &self.tabs[self.index] diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 70b9bdd..baf1c62 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -27,7 +27,7 @@ pub struct AddMovieBody { pub title: String, pub root_folder_path: String, pub quality_profile_id: i64, - pub minimum_availability: String, + pub minimum_availability: MinimumAvailability, pub monitored: bool, pub tags: Vec, #[serde(skip_serializing, skip_deserializing)] @@ -55,7 +55,7 @@ pub struct AddMovieSearchResult { #[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AddMovieOptions { - pub monitor: String, + pub monitor: MovieMonitor, pub search_for_movie: bool, } @@ -268,8 +268,20 @@ pub enum MinimumAvailability { } #[derive( - Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum, Display, EnumDisplayStyle, + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + ValueEnum, + Display, + EnumDisplayStyle, )] +#[serde(rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum MovieMonitor { #[default] @@ -396,6 +408,69 @@ pub struct SystemStatus { pub start_time: DateTime, } +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RadarrHistoryWrapper { + pub records: Vec, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RadarrHistoryData { + pub indexer: Option, + pub release_group: Option, + pub nzb_info_url: Option, + pub download_client: Option, + pub download_client_name: Option, + pub age: Option, + pub published_date: Option>, + pub message: Option, + pub reason: Option, + pub dropped_path: Option, + pub imported_path: Option, + pub source_path: Option, + pub path: Option, +} + +#[derive( + Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Display, EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum RadarrHistoryEventType { + #[default] + Unknown, + Grabbed, + #[display_style(name = "Download Folder Imported")] + DownloadFolderImported, + #[display_style(name = "Download Failed")] + DownloadFailed, + #[display_style(name = "Movie File Deleted")] + MovieFileDeleted, + #[display_style(name = "Movie Folder Imported")] + MovieFolderImported, + #[display_style(name = "Movie File Renamed")] + MovieFileRenamed, + #[display_style(name = "Download Ignored")] + DownloadIgnored, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RadarrHistoryItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + pub source_title: HorizontallyScrollableText, + #[serde(deserialize_with = "super::from_i64")] + pub movie_id: i64, + pub quality: QualityWrapper, + pub languages: Vec, + pub date: DateTime, + pub event_type: RadarrHistoryEventType, + #[serde(default)] + pub data: RadarrHistoryData, +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct RadarrTask { @@ -449,6 +524,7 @@ serde_enum_from!( Credits(Vec), DiskSpaces(Vec), DownloadsResponse(DownloadsResponse), + HistoryWrapper(RadarrHistoryWrapper), HostConfig(HostConfig), Indexers(Vec), IndexerSettings(IndexerSettings), diff --git a/src/models/radarr_models_tests.rs b/src/models/radarr_models_tests.rs index 0820f39..2c84364 100644 --- a/src/models/radarr_models_tests.rs +++ b/src/models/radarr_models_tests.rs @@ -3,6 +3,9 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::json; + use crate::models::radarr_models::{ + RadarrHistoryEventType, RadarrHistoryItem, RadarrHistoryWrapper, + }; use crate::models::{ Serdeable, radarr_models::{ @@ -61,6 +64,66 @@ mod tests { assert_str_eq!(MovieMonitor::None.to_display_str(), "None"); } + #[test] + fn test_radarr_history_event_type_display() { + assert_str_eq!(RadarrHistoryEventType::Unknown.to_string(), "unknown"); + assert_str_eq!(RadarrHistoryEventType::Grabbed.to_string(), "grabbed"); + assert_str_eq!( + RadarrHistoryEventType::DownloadFolderImported.to_string(), + "downloadFolderImported" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadFailed.to_string(), + "downloadFailed" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileDeleted.to_string(), + "movieFileDeleted" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFolderImported.to_string(), + "movieFolderImported" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileRenamed.to_string(), + "movieFileRenamed" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadIgnored.to_string(), + "downloadIgnored" + ); + } + + #[test] + fn test_radarr_history_event_type_to_display_str() { + assert_str_eq!(RadarrHistoryEventType::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(RadarrHistoryEventType::Grabbed.to_display_str(), "Grabbed"); + assert_str_eq!( + RadarrHistoryEventType::DownloadFolderImported.to_display_str(), + "Download Folder Imported" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadFailed.to_display_str(), + "Download Failed" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileDeleted.to_display_str(), + "Movie File Deleted" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFolderImported.to_display_str(), + "Movie Folder Imported" + ); + assert_str_eq!( + RadarrHistoryEventType::MovieFileRenamed.to_display_str(), + "Movie File Renamed" + ); + assert_str_eq!( + RadarrHistoryEventType::DownloadIgnored.to_display_str(), + "Download Ignored" + ); + } + #[test] fn test_download_record_default_indexer_value() { let json = r#"{ @@ -235,6 +298,23 @@ mod tests { ); } + #[test] + fn test_radarr_serdeable_from_history_wrapper() { + let history_wrapper = RadarrHistoryWrapper { + records: vec![RadarrHistoryItem { + id: 1, + ..RadarrHistoryItem::default() + }], + }; + + let radarr_serdeable: RadarrSerdeable = history_wrapper.clone().into(); + + assert_eq!( + radarr_serdeable, + RadarrSerdeable::HistoryWrapper(history_wrapper) + ); + } + #[test] fn test_radarr_serdeable_from_log_response() { let log_response = LogResponse { diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs new file mode 100644 index 0000000..497c828 --- /dev/null +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -0,0 +1,840 @@ +use serde_json::Number; + +use super::modals::{AddArtistModal, AddRootFolderModal, AlbumDetailsModal, EditArtistModal}; +use crate::app::context_clues::{ + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, +}; +use crate::app::lidarr::lidarr_context_clues::{ + ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, + MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, +}; +use crate::models::lidarr_models::{BlocklistItem, LidarrRelease, LidarrTask}; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::{IndexerSettings, QueueEvent}; +use crate::models::stateful_list::StatefulList; +use crate::models::{ + BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState, + lidarr_models::{AddArtistSearchResult, Album, Artist, DownloadRecord, LidarrHistoryItem}, + servarr_data::modals::IndexerTestResultModalItem, + servarr_models::{DiskSpace, Indexer, RootFolder}, + stateful_table::StatefulTable, +}; +use crate::network::lidarr_network::LidarrEvent; +use bimap::BiMap; +use chrono::{DateTime, Utc}; +use itertools::Itertools; +use strum::EnumIter; +#[cfg(test)] +use { + super::modals::TrackDetailsModal, + crate::models::lidarr_models::{MonitorType, NewItemMonitorType}, + crate::models::stateful_table::SortOption, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::blocklist_item, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer_settings, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::quality_profile_map, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + add_artist_search_result, album, artist, download_record, indexer, lidarr_history_item, + metadata_profile, metadata_profile_map, quality_profile, root_folder, tags_map, + }, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{log_line, task}, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + torrent_release, usenet_release, + }, + crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{track, track_file}, + crate::network::servarr_test_utils::diskspace, + crate::network::servarr_test_utils::indexer_test_result, + crate::network::servarr_test_utils::queued_event, + crate::network::sonarr_network::sonarr_network_test_utils::test_utils::updates, + crate::sort_option, + strum::{Display, EnumString, IntoEnumIterator}, +}; + +#[cfg(test)] +#[path = "lidarr_data_tests.rs"] +mod lidarr_data_tests; + +pub struct LidarrData<'a> { + pub add_artist_modal: Option, + pub add_artist_search: Option, + pub add_import_list_exclusion: bool, + pub add_root_folder_modal: Option, + pub add_searched_artists: Option>, + pub albums: StatefulTable, + pub album_details_modal: Option, + pub artist_history: StatefulTable, + pub artist_info_tabs: TabState, + pub artists: StatefulTable, + pub blocklist: StatefulTable, + pub delete_files: bool, + pub discography_releases: StatefulTable, + pub disk_space_vec: Vec, + pub downloads: StatefulTable, + pub edit_artist_modal: Option, + pub edit_indexer_modal: Option, + pub history: StatefulTable, + pub indexers: StatefulTable, + pub indexer_settings: Option, + pub indexer_test_all_results: Option>, + pub indexer_test_errors: Option, + pub logs: StatefulList, + pub log_details: StatefulList, + pub main_tabs: TabState, + pub metadata_profile_map: BiMap, + pub prompt_confirm: bool, + pub prompt_confirm_action: Option, + pub quality_profile_map: BiMap, + pub queued_events: StatefulTable, + pub root_folders: StatefulTable, + pub selected_block: BlockSelectionState<'a, ActiveLidarrBlock>, + pub start_time: DateTime, + pub tags_map: BiMap, + pub tasks: StatefulTable, + pub updates: ScrollableText, + pub version: String, +} + +impl LidarrData<'_> { + pub fn reset_delete_preferences(&mut self) { + self.delete_files = false; + self.add_import_list_exclusion = false; + } + + pub fn reset_artist_info_tabs(&mut self) { + self.albums = StatefulTable::default(); + self.discography_releases = StatefulTable::default(); + self.artist_history = StatefulTable::default(); + self.artist_info_tabs.index = 0; + } + + pub fn tag_ids_to_display(&self, tag_ids: &[Number]) -> String { + tag_ids + .iter() + .filter_map(|id| { + let id = id.as_i64()?; + self.tags_map.get_by_left(&id).cloned() + }) + .collect::>() + .join(", ") + } + + pub fn sorted_quality_profile_names(&self) -> Vec { + self + .quality_profile_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() + } + + pub fn sorted_metadata_profile_names(&self) -> Vec { + self + .metadata_profile_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() + } +} + +impl<'a> Default for LidarrData<'a> { + fn default() -> LidarrData<'a> { + LidarrData { + add_artist_modal: None, + add_artist_search: None, + add_import_list_exclusion: false, + add_root_folder_modal: None, + add_searched_artists: None, + albums: StatefulTable::default(), + album_details_modal: None, + artist_history: StatefulTable::default(), + artists: StatefulTable::default(), + blocklist: StatefulTable::default(), + delete_files: false, + discography_releases: StatefulTable::default(), + disk_space_vec: Vec::new(), + downloads: StatefulTable::default(), + edit_artist_modal: None, + edit_indexer_modal: None, + history: StatefulTable::default(), + indexers: StatefulTable::default(), + indexer_settings: None, + indexer_test_all_results: None, + indexer_test_errors: None, + logs: StatefulList::default(), + log_details: StatefulList::default(), + metadata_profile_map: BiMap::new(), + prompt_confirm: false, + prompt_confirm_action: None, + quality_profile_map: BiMap::new(), + queued_events: StatefulTable::default(), + root_folders: StatefulTable::default(), + selected_block: BlockSelectionState::default(), + start_time: DateTime::default(), + tags_map: BiMap::new(), + tasks: StatefulTable::default(), + updates: ScrollableText::default(), + version: String::new(), + main_tabs: TabState::new(vec![ + TabRoute { + title: "Library".to_string(), + route: ActiveLidarrBlock::Artists.into(), + contextual_help: Some(&ARTISTS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Downloads".to_string(), + route: ActiveLidarrBlock::Downloads.into(), + contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Blocklist".to_string(), + route: ActiveLidarrBlock::Blocklist.into(), + contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveLidarrBlock::History.into(), + contextual_help: Some(&HISTORY_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Root Folders".to_string(), + route: ActiveLidarrBlock::RootFolders.into(), + contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Indexers".to_string(), + route: ActiveLidarrBlock::Indexers.into(), + contextual_help: Some(&INDEXERS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "System".to_string(), + route: ActiveLidarrBlock::System.into(), + contextual_help: Some(&SYSTEM_CONTEXT_CLUES), + config: None, + }, + ]), + artist_info_tabs: TabState::new(vec![ + TabRoute { + title: "Albums".to_string(), + route: ActiveLidarrBlock::ArtistDetails.into(), + contextual_help: Some(&ARTIST_DETAILS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveLidarrBlock::ArtistHistory.into(), + contextual_help: Some(&ARTIST_HISTORY_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Manual Search".to_string(), + route: ActiveLidarrBlock::ManualArtistSearch.into(), + contextual_help: Some(&MANUAL_ARTIST_SEARCH_CONTEXT_CLUES), + config: None, + }, + ]), + } + } +} + +#[cfg(test)] +impl LidarrData<'_> { + pub fn test_default_fully_populated() -> Self { + let mut add_artist_modal = AddArtistModal { + tags: "usenet, testing".into(), + ..AddArtistModal::default() + }; + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .metadata_profile_list + .set_items(vec![metadata_profile().name]); + add_artist_modal + .quality_profile_list + .set_items(vec![quality_profile().name]); + add_artist_modal + .root_folder_list + .set_items(vec![root_folder()]); + let mut edit_artist_modal = EditArtistModal { + monitored: Some(true), + path: "/nfs/music".into(), + tags: "alex".into(), + ..EditArtistModal::default() + }; + edit_artist_modal + .monitor_list + .set_items(NewItemMonitorType::iter().collect()); + edit_artist_modal + .quality_profile_list + .set_items(vec![quality_profile().name]); + edit_artist_modal + .metadata_profile_list + .set_items(vec![metadata_profile().name]); + + let mut add_root_folder_modal = AddRootFolderModal { + name: "Test Root Folder".into(), + path: "/nfs/music".into(), + tags: "test".into(), + ..AddRootFolderModal::default() + }; + add_root_folder_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_root_folder_modal + .quality_profile_list + .set_items(vec![quality_profile().name]); + add_root_folder_modal + .metadata_profile_list + .set_items(vec![metadata_profile().name]); + + let mut track_details_modal = TrackDetailsModal { + track_details: ScrollableText::with_string("Some details".to_owned()), + ..TrackDetailsModal::default() + }; + track_details_modal + .track_history + .set_items(vec![lidarr_history_item()]); + track_details_modal.track_history.search = Some("track history search".into()); + track_details_modal.track_history.filter = Some("track history filter".into()); + track_details_modal + .track_history + .sorting(vec![sort_option!(id)]); + + let mut album_details_modal = AlbumDetailsModal { + track_details_modal: Some(track_details_modal), + ..AlbumDetailsModal::default() + }; + album_details_modal.tracks.set_items(vec![track()]); + album_details_modal.tracks.search = Some("album search".into()); + album_details_modal + .track_files + .set_items(vec![track_file()]); + album_details_modal + .album_history + .set_items(vec![lidarr_history_item()]); + album_details_modal.album_history.search = Some("album history search".into()); + album_details_modal.album_history.filter = Some("album history filter".into()); + album_details_modal + .album_history + .sorting(vec![sort_option!(id)]); + album_details_modal + .album_releases + .set_items(vec![torrent_release(), usenet_release()]); + album_details_modal + .album_releases + .sorting(vec![sort_option!(indexer_id)]); + + let edit_indexer_modal = EditIndexerModal { + name: "DrunkenSlug".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "http://127.0.0.1:9696/1/".into(), + api_key: "someApiKey".into(), + seed_ratio: "ratio".into(), + tags: "25".into(), + priority: 1, + }; + + let mut indexer_test_all_results = StatefulTable::default(); + indexer_test_all_results.set_items(vec![indexer_test_result()]); + + let mut lidarr_data = LidarrData { + album_details_modal: Some(album_details_modal), + delete_files: true, + disk_space_vec: vec![diskspace()], + quality_profile_map: quality_profile_map(), + metadata_profile_map: metadata_profile_map(), + edit_artist_modal: Some(edit_artist_modal), + edit_indexer_modal: Some(edit_indexer_modal), + add_root_folder_modal: Some(add_root_folder_modal), + add_artist_modal: Some(add_artist_modal), + indexer_settings: Some(indexer_settings()), + indexer_test_all_results: Some(indexer_test_all_results), + indexer_test_errors: Some("error".to_string()), + start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()), + tags_map: tags_map(), + updates: updates(), + version: "1.2.3.4".to_owned(), + ..LidarrData::default() + }; + lidarr_data + .artist_history + .set_items(vec![lidarr_history_item()]); + lidarr_data.artist_history.sorting(vec![sort_option!(id)]); + lidarr_data.artist_history.search = Some("artist history search".into()); + lidarr_data.artist_history.filter = Some("artist history filter".into()); + lidarr_data.albums.set_items(vec![album()]); + lidarr_data.albums.search = Some("album search".into()); + lidarr_data.artists.set_items(vec![artist()]); + lidarr_data.artists.sorting(vec![SortOption { + name: "Name", + cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)), + }]); + lidarr_data.artists.search = Some("artist search".into()); + lidarr_data.artists.filter = Some("artist filter".into()); + lidarr_data.blocklist.set_items(vec![blocklist_item()]); + lidarr_data.blocklist.sorting(vec![sort_option!(id)]); + lidarr_data.downloads.set_items(vec![download_record()]); + lidarr_data.history.set_items(vec![lidarr_history_item()]); + lidarr_data.history.sorting(vec![SortOption { + name: "Date", + cmp_fn: Some(|a: &LidarrHistoryItem, b: &LidarrHistoryItem| a.date.cmp(&b.date)), + }]); + lidarr_data.history.search = Some("test search".into()); + lidarr_data.history.filter = Some("test filter".into()); + lidarr_data + .discography_releases + .set_items(vec![torrent_release(), usenet_release()]); + lidarr_data + .discography_releases + .sorting(vec![sort_option!(indexer_id)]); + lidarr_data.root_folders.set_items(vec![root_folder()]); + lidarr_data.indexers.set_items(vec![indexer()]); + lidarr_data.queued_events.set_items(vec![queued_event()]); + lidarr_data.add_artist_search = Some("Test Artist".into()); + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(vec![add_artist_search_result()]); + lidarr_data.add_searched_artists = Some(add_searched_artists); + lidarr_data.logs.set_items(vec![log_line().into()]); + lidarr_data.log_details.set_items(vec![log_line().into()]); + lidarr_data.tasks.set_items(vec![task()]); + + lidarr_data + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)] +#[cfg_attr(test, derive(Display, EnumString))] +pub enum ActiveLidarrBlock { + #[default] + Artists, + ArtistDetails, + ArtistHistory, + ArtistHistoryDetails, + ArtistHistorySortPrompt, + ArtistsSortPrompt, + AddArtistAlreadyInLibrary, + AddArtistConfirmPrompt, + AddArtistEmptySearchResults, + AddArtistPrompt, + AddArtistSearchInput, + AddArtistSearchResults, + AddArtistSelectMetadataProfile, + AddArtistSelectMonitor, + AddArtistSelectMonitorNewItems, + AddArtistSelectQualityProfile, + AddArtistSelectRootFolder, + AddArtistTagsInput, + AddRootFolderPrompt, + AddRootFolderConfirmPrompt, + AddRootFolderNameInput, + AddRootFolderPathInput, + AddRootFolderSelectMonitor, + AddRootFolderSelectMonitorNewItems, + AddRootFolderSelectQualityProfile, + AddRootFolderSelectMetadataProfile, + AddRootFolderTagsInput, + AlbumDetails, + AlbumHistory, + AlbumHistoryDetails, + AlbumHistorySortPrompt, + AllIndexerSettingsPrompt, + AutomaticallySearchAlbumPrompt, + AutomaticallySearchArtistPrompt, + Blocklist, + BlocklistItemDetails, + DeleteBlocklistItemPrompt, + BlocklistClearAllItemsPrompt, + BlocklistSortPrompt, + DeleteAlbumPrompt, + DeleteAlbumConfirmPrompt, + DeleteAlbumToggleDeleteFile, + DeleteAlbumToggleAddListExclusion, + DeleteArtistPrompt, + DeleteArtistConfirmPrompt, + DeleteArtistToggleDeleteFile, + DeleteArtistToggleAddListExclusion, + DeleteTrackFilePrompt, + DeleteDownloadPrompt, + DeleteRootFolderPrompt, + Downloads, + EditArtistPrompt, + EditArtistConfirmPrompt, + EditArtistPathInput, + EditArtistSelectMetadataProfile, + EditArtistSelectMonitorNewItems, + EditArtistSelectQualityProfile, + EditArtistTagsInput, + EditArtistToggleMonitored, + EditIndexerPrompt, + EditIndexerConfirmPrompt, + EditIndexerApiKeyInput, + EditIndexerNameInput, + EditIndexerSeedRatioInput, + EditIndexerToggleEnableRss, + EditIndexerToggleEnableAutomaticSearch, + EditIndexerToggleEnableInteractiveSearch, + EditIndexerUrlInput, + EditIndexerPriorityInput, + EditIndexerTagsInput, + DeleteIndexerPrompt, + FilterAlbumHistory, + FilterAlbumHistoryError, + FilterArtists, + FilterArtistsError, + FilterHistory, + FilterHistoryError, + FilterArtistHistory, + FilterArtistHistoryError, + FilterTrackHistory, + FilterTrackHistoryError, + History, + HistoryItemDetails, + HistorySortPrompt, + Indexers, + IndexerSettingsConfirmPrompt, + IndexerSettingsMaximumSizeInput, + IndexerSettingsMinimumAgeInput, + IndexerSettingsRetentionInput, + IndexerSettingsRssSyncIntervalInput, + ManualAlbumSearch, + ManualAlbumSearchConfirmPrompt, + ManualAlbumSearchSortPrompt, + ManualArtistSearch, + ManualArtistSearchConfirmPrompt, + ManualArtistSearchSortPrompt, + TestAllIndexers, + TestIndexer, + RootFolders, + SearchAlbumHistory, + SearchAlbumHistoryError, + SearchAlbums, + SearchAlbumsError, + SearchArtists, + SearchArtistsError, + SearchHistory, + SearchHistoryError, + SearchArtistHistory, + SearchArtistHistoryError, + SearchTracks, + SearchTracksError, + SearchTrackHistory, + SearchTrackHistoryError, + System, + SystemLogs, + SystemQueuedEvents, + SystemTasks, + SystemTaskStartConfirmPrompt, + SystemUpdates, + TrackDetails, + TrackHistory, + TrackHistoryDetails, + TrackHistorySortPrompt, + UpdateAllArtistsPrompt, + UpdateAndScanArtistPrompt, + UpdateDownloadsPrompt, +} + +pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [ + ActiveLidarrBlock::Artists, + ActiveLidarrBlock::ArtistsSortPrompt, + ActiveLidarrBlock::FilterArtists, + ActiveLidarrBlock::FilterArtistsError, + ActiveLidarrBlock::SearchArtists, + ActiveLidarrBlock::SearchArtistsError, + ActiveLidarrBlock::UpdateAllArtistsPrompt, +]; + +pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 15] = [ + ActiveLidarrBlock::ArtistDetails, + ActiveLidarrBlock::ArtistHistory, + ActiveLidarrBlock::ArtistHistoryDetails, + ActiveLidarrBlock::ArtistHistorySortPrompt, + ActiveLidarrBlock::AutomaticallySearchArtistPrompt, + ActiveLidarrBlock::FilterArtistHistory, + ActiveLidarrBlock::FilterArtistHistoryError, + ActiveLidarrBlock::ManualArtistSearch, + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, + ActiveLidarrBlock::ManualArtistSearchSortPrompt, + ActiveLidarrBlock::SearchAlbums, + ActiveLidarrBlock::SearchAlbumsError, + ActiveLidarrBlock::SearchArtistHistory, + ActiveLidarrBlock::SearchArtistHistoryError, + ActiveLidarrBlock::UpdateAndScanArtistPrompt, +]; + +pub static ALBUM_DETAILS_BLOCKS: [ActiveLidarrBlock; 15] = [ + ActiveLidarrBlock::AlbumDetails, + ActiveLidarrBlock::AlbumHistory, + ActiveLidarrBlock::SearchTracks, + ActiveLidarrBlock::SearchTracksError, + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, + ActiveLidarrBlock::SearchAlbumHistory, + ActiveLidarrBlock::SearchAlbumHistoryError, + ActiveLidarrBlock::FilterAlbumHistory, + ActiveLidarrBlock::FilterAlbumHistoryError, + ActiveLidarrBlock::AlbumHistorySortPrompt, + ActiveLidarrBlock::AlbumHistoryDetails, + ActiveLidarrBlock::ManualAlbumSearch, + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, + ActiveLidarrBlock::ManualAlbumSearchSortPrompt, + ActiveLidarrBlock::DeleteTrackFilePrompt, +]; + +pub static BLOCKLIST_BLOCKS: [ActiveLidarrBlock; 5] = [ + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::BlocklistItemDetails, + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt, + ActiveLidarrBlock::BlocklistSortPrompt, +]; + +pub static DOWNLOADS_BLOCKS: [ActiveLidarrBlock; 3] = [ + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt, +]; + +pub static HISTORY_BLOCKS: [ActiveLidarrBlock; 7] = [ + ActiveLidarrBlock::History, + ActiveLidarrBlock::HistoryItemDetails, + ActiveLidarrBlock::HistorySortPrompt, + ActiveLidarrBlock::SearchHistory, + ActiveLidarrBlock::SearchHistoryError, + ActiveLidarrBlock::FilterHistory, + ActiveLidarrBlock::FilterHistoryError, +]; + +pub static ADD_ARTIST_BLOCKS: [ActiveLidarrBlock; 12] = [ + ActiveLidarrBlock::AddArtistAlreadyInLibrary, + ActiveLidarrBlock::AddArtistConfirmPrompt, + ActiveLidarrBlock::AddArtistEmptySearchResults, + ActiveLidarrBlock::AddArtistPrompt, + ActiveLidarrBlock::AddArtistSearchInput, + ActiveLidarrBlock::AddArtistSearchResults, + ActiveLidarrBlock::AddArtistSelectMetadataProfile, + ActiveLidarrBlock::AddArtistSelectMonitor, + ActiveLidarrBlock::AddArtistSelectMonitorNewItems, + ActiveLidarrBlock::AddArtistSelectQualityProfile, + ActiveLidarrBlock::AddArtistSelectRootFolder, + ActiveLidarrBlock::AddArtistTagsInput, +]; + +pub const ADD_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::AddArtistSelectRootFolder], + &[ActiveLidarrBlock::AddArtistSelectMonitor], + &[ActiveLidarrBlock::AddArtistSelectMonitorNewItems], + &[ActiveLidarrBlock::AddArtistSelectQualityProfile], + &[ActiveLidarrBlock::AddArtistSelectMetadataProfile], + &[ActiveLidarrBlock::AddArtistTagsInput], + &[ActiveLidarrBlock::AddArtistConfirmPrompt], +]; + +pub static DELETE_ARTIST_BLOCKS: [ActiveLidarrBlock; 4] = [ + ActiveLidarrBlock::DeleteArtistPrompt, + ActiveLidarrBlock::DeleteArtistConfirmPrompt, + ActiveLidarrBlock::DeleteArtistToggleDeleteFile, + ActiveLidarrBlock::DeleteArtistToggleAddListExclusion, +]; + +pub const DELETE_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::DeleteArtistToggleDeleteFile], + &[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion], + &[ActiveLidarrBlock::DeleteArtistConfirmPrompt], +]; + +pub static DELETE_ALBUM_BLOCKS: [ActiveLidarrBlock; 4] = [ + ActiveLidarrBlock::DeleteAlbumPrompt, + ActiveLidarrBlock::DeleteAlbumConfirmPrompt, + ActiveLidarrBlock::DeleteAlbumToggleDeleteFile, + ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion, +]; + +pub const DELETE_ALBUM_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::DeleteAlbumToggleDeleteFile], + &[ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion], + &[ActiveLidarrBlock::DeleteAlbumConfirmPrompt], +]; + +pub static EDIT_ARTIST_BLOCKS: [ActiveLidarrBlock; 8] = [ + ActiveLidarrBlock::EditArtistPrompt, + ActiveLidarrBlock::EditArtistConfirmPrompt, + ActiveLidarrBlock::EditArtistPathInput, + ActiveLidarrBlock::EditArtistSelectMetadataProfile, + ActiveLidarrBlock::EditArtistSelectMonitorNewItems, + ActiveLidarrBlock::EditArtistSelectQualityProfile, + ActiveLidarrBlock::EditArtistTagsInput, + ActiveLidarrBlock::EditArtistToggleMonitored, +]; + +pub const EDIT_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::EditArtistToggleMonitored], + &[ActiveLidarrBlock::EditArtistSelectMonitorNewItems], + &[ActiveLidarrBlock::EditArtistSelectQualityProfile], + &[ActiveLidarrBlock::EditArtistSelectMetadataProfile], + &[ActiveLidarrBlock::EditArtistPathInput], + &[ActiveLidarrBlock::EditArtistTagsInput], + &[ActiveLidarrBlock::EditArtistConfirmPrompt], +]; + +pub const ROOT_FOLDERS_BLOCKS: [ActiveLidarrBlock; 2] = [ + ActiveLidarrBlock::RootFolders, + ActiveLidarrBlock::DeleteRootFolderPrompt, +]; + +pub static ADD_ROOT_FOLDER_BLOCKS: [ActiveLidarrBlock; 9] = [ + ActiveLidarrBlock::AddRootFolderPrompt, + ActiveLidarrBlock::AddRootFolderConfirmPrompt, + ActiveLidarrBlock::AddRootFolderNameInput, + ActiveLidarrBlock::AddRootFolderPathInput, + ActiveLidarrBlock::AddRootFolderSelectMonitor, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile, + ActiveLidarrBlock::AddRootFolderTagsInput, +]; + +pub const ADD_ROOT_FOLDER_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::AddRootFolderNameInput], + &[ActiveLidarrBlock::AddRootFolderPathInput], + &[ActiveLidarrBlock::AddRootFolderSelectMonitor], + &[ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems], + &[ActiveLidarrBlock::AddRootFolderSelectQualityProfile], + &[ActiveLidarrBlock::AddRootFolderSelectMetadataProfile], + &[ActiveLidarrBlock::AddRootFolderTagsInput], + &[ActiveLidarrBlock::AddRootFolderConfirmPrompt], +]; + +pub static EDIT_INDEXER_BLOCKS: [ActiveLidarrBlock; 11] = [ + ActiveLidarrBlock::EditIndexerPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ActiveLidarrBlock::EditIndexerTagsInput, +]; + +pub const EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveLidarrBlock::EditIndexerPriorityInput, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ], + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub const EDIT_INDEXER_NZB_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput, + ], + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub static INDEXER_SETTINGS_BLOCKS: [ActiveLidarrBlock; 6] = [ + ActiveLidarrBlock::AllIndexerSettingsPrompt, + ActiveLidarrBlock::IndexerSettingsConfirmPrompt, + ActiveLidarrBlock::IndexerSettingsMaximumSizeInput, + ActiveLidarrBlock::IndexerSettingsMinimumAgeInput, + ActiveLidarrBlock::IndexerSettingsRetentionInput, + ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput, +]; + +pub const INDEXER_SETTINGS_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ + &[ActiveLidarrBlock::IndexerSettingsMinimumAgeInput], + &[ActiveLidarrBlock::IndexerSettingsRetentionInput], + &[ActiveLidarrBlock::IndexerSettingsMaximumSizeInput], + &[ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput], + &[ActiveLidarrBlock::IndexerSettingsConfirmPrompt], +]; + +pub static INDEXERS_BLOCKS: [ActiveLidarrBlock; 3] = [ + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::DeleteIndexerPrompt, + ActiveLidarrBlock::TestIndexer, +]; + +pub static SYSTEM_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [ + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates, +]; + +pub static TRACK_DETAILS_BLOCKS: [ActiveLidarrBlock; 8] = [ + ActiveLidarrBlock::TrackDetails, + ActiveLidarrBlock::TrackHistory, + ActiveLidarrBlock::TrackHistoryDetails, + ActiveLidarrBlock::SearchTrackHistory, + ActiveLidarrBlock::SearchTrackHistoryError, + ActiveLidarrBlock::FilterTrackHistory, + ActiveLidarrBlock::FilterTrackHistoryError, + ActiveLidarrBlock::TrackHistorySortPrompt, +]; + +impl From for Route { + fn from(active_lidarr_block: ActiveLidarrBlock) -> Route { + Route::Lidarr(active_lidarr_block, None) + } +} + +impl From<(ActiveLidarrBlock, Option)> for Route { + fn from(value: (ActiveLidarrBlock, Option)) -> Route { + Route::Lidarr(value.0, value.1) + } +} diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs new file mode 100644 index 0000000..308024b --- /dev/null +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -0,0 +1,734 @@ +#[cfg(test)] +mod tests { + use crate::app::context_clues::{ + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }; + use crate::app::lidarr::lidarr_context_clues::{ + ARTIST_DETAILS_CONTEXT_CLUES, ARTIST_HISTORY_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, + MANUAL_ARTIST_SEARCH_CONTEXT_CLUES, + }; + use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ADD_ROOT_FOLDER_BLOCKS, ALBUM_DETAILS_BLOCKS, + ARTIST_DETAILS_BLOCKS, BLOCKLIST_BLOCKS, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, + DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, + EDIT_ARTIST_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS, + INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, + TRACK_DETAILS_BLOCKS, + }; + use crate::models::{ + BlockSelectionState, Route, + servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS, LidarrData}, + }; + use bimap::BiMap; + use chrono::{DateTime, Utc}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; + + #[test] + fn test_from_active_lidarr_block_to_route() { + assert_eq!( + Route::from(ActiveLidarrBlock::Artists), + Route::Lidarr(ActiveLidarrBlock::Artists, None) + ); + } + + #[test] + fn test_from_tuple_to_route_with_context() { + assert_eq!( + Route::from((ActiveLidarrBlock::Artists, Some(ActiveLidarrBlock::Artists))), + Route::Lidarr(ActiveLidarrBlock::Artists, Some(ActiveLidarrBlock::Artists),) + ); + } + + #[test] + fn test_reset_delete_preferences() { + let mut lidarr_data = LidarrData { + delete_files: true, + add_import_list_exclusion: true, + ..LidarrData::default() + }; + + lidarr_data.reset_delete_preferences(); + + assert!(!lidarr_data.delete_files); + assert!(!lidarr_data.add_import_list_exclusion); + } + + #[test] + fn test_reset_artist_info_tabs() { + let mut lidarr_data = LidarrData::default(); + lidarr_data.albums.set_items(vec![Album::default()]); + lidarr_data + .discography_releases + .set_items(vec![LidarrRelease::default()]); + lidarr_data + .artist_history + .set_items(vec![LidarrHistoryItem::default()]); + lidarr_data.artist_info_tabs.index = 1; + + lidarr_data.reset_artist_info_tabs(); + + assert_is_empty!(lidarr_data.albums); + assert_is_empty!(lidarr_data.discography_releases); + assert_is_empty!(lidarr_data.artist_history); + assert_eq!(lidarr_data.artist_info_tabs.index, 0); + } + + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let lidarr_data = LidarrData { + tags_map, + ..LidarrData::default() + }; + + assert_str_eq!( + lidarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), + "test 1, test 2" + ); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 1".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 3".to_owned()); + let lidarr_data = LidarrData { + quality_profile_map, + ..LidarrData::default() + }; + let expected_quality_profile_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + lidarr_data.sorted_quality_profile_names(), + expected_quality_profile_vec + ); + } + + #[test] + fn test_sorted_metadata_profile_names() { + let mut metadata_profile_map = BiMap::new(); + metadata_profile_map.insert(3, "test 1".to_owned()); + metadata_profile_map.insert(2, "test 2".to_owned()); + metadata_profile_map.insert(1, "test 3".to_owned()); + let lidarr_data = LidarrData { + metadata_profile_map, + ..LidarrData::default() + }; + let expected_metadata_profile_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + lidarr_data.sorted_metadata_profile_names(), + expected_metadata_profile_vec + ); + } + + #[test] + fn test_lidarr_data_default() { + let lidarr_data = LidarrData::default(); + + assert_none!(lidarr_data.add_artist_search); + assert!(!lidarr_data.add_import_list_exclusion); + assert_none!(lidarr_data.add_searched_artists); + assert_is_empty!(lidarr_data.albums); + assert_none!(lidarr_data.album_details_modal); + assert_is_empty!(lidarr_data.artists); + assert_is_empty!(lidarr_data.artist_history); + assert_is_empty!(lidarr_data.blocklist); + assert!(!lidarr_data.delete_files); + assert_is_empty!(lidarr_data.disk_space_vec); + assert_is_empty!(lidarr_data.downloads); + assert_none!(lidarr_data.edit_artist_modal); + assert_none!(lidarr_data.add_root_folder_modal); + assert_is_empty!(lidarr_data.discography_releases); + assert_is_empty!(lidarr_data.history); + assert_is_empty!(lidarr_data.logs); + assert_is_empty!(lidarr_data.log_details); + assert_is_empty!(lidarr_data.metadata_profile_map); + assert!(!lidarr_data.prompt_confirm); + assert_none!(lidarr_data.prompt_confirm_action); + assert_is_empty!(lidarr_data.quality_profile_map); + assert_is_empty!(lidarr_data.queued_events); + assert_is_empty!(lidarr_data.root_folders); + assert_eq!(lidarr_data.selected_block, BlockSelectionState::default()); + assert_eq!(lidarr_data.start_time, >::default()); + assert_is_empty!(lidarr_data.tags_map); + assert_is_empty!(lidarr_data.tasks); + assert_is_empty!(lidarr_data.updates); + assert_is_empty!(lidarr_data.version); + + assert_eq!(lidarr_data.main_tabs.tabs.len(), 7); + + assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library"); + assert_eq!( + lidarr_data.main_tabs.tabs[0].route, + ActiveLidarrBlock::Artists.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[0].contextual_help, + &ARTISTS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[0].config); + + assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "Downloads"); + assert_eq!( + lidarr_data.main_tabs.tabs[1].route, + ActiveLidarrBlock::Downloads.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[1].contextual_help, + &DOWNLOADS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[1].config); + + assert_str_eq!(lidarr_data.main_tabs.tabs[2].title, "Blocklist"); + assert_eq!( + lidarr_data.main_tabs.tabs[2].route, + ActiveLidarrBlock::Blocklist.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[2].contextual_help, + &BLOCKLIST_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[2].config); + + assert_str_eq!(lidarr_data.main_tabs.tabs[3].title, "History"); + assert_eq!( + lidarr_data.main_tabs.tabs[3].route, + ActiveLidarrBlock::History.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[3].contextual_help, + &HISTORY_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[3].config); + + assert_str_eq!(lidarr_data.main_tabs.tabs[4].title, "Root Folders"); + assert_eq!( + lidarr_data.main_tabs.tabs[4].route, + ActiveLidarrBlock::RootFolders.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[4].contextual_help, + &ROOT_FOLDERS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[4].config); + + assert_str_eq!(lidarr_data.main_tabs.tabs[5].title, "Indexers"); + assert_eq!( + lidarr_data.main_tabs.tabs[5].route, + ActiveLidarrBlock::Indexers.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[5].contextual_help, + &INDEXERS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[5].config); + + assert_str_eq!(lidarr_data.main_tabs.tabs[6].title, "System"); + assert_eq!( + lidarr_data.main_tabs.tabs[6].route, + ActiveLidarrBlock::System.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[6].contextual_help, + &SYSTEM_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[6].config); + + assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 3); + assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums"); + assert_eq!( + lidarr_data.artist_info_tabs.tabs[0].route, + ActiveLidarrBlock::ArtistDetails.into() + ); + assert_some_eq_x!( + &lidarr_data.artist_info_tabs.tabs[0].contextual_help, + &ARTIST_DETAILS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.artist_info_tabs.tabs[0].config); + + assert_str_eq!(lidarr_data.artist_info_tabs.tabs[1].title, "History"); + assert_eq!( + lidarr_data.artist_info_tabs.tabs[1].route, + ActiveLidarrBlock::ArtistHistory.into() + ); + assert_some_eq_x!( + &lidarr_data.artist_info_tabs.tabs[1].contextual_help, + &ARTIST_HISTORY_CONTEXT_CLUES + ); + assert_none!(lidarr_data.artist_info_tabs.tabs[1].config); + + assert_str_eq!(lidarr_data.artist_info_tabs.tabs[2].title, "Manual Search"); + assert_eq!( + lidarr_data.artist_info_tabs.tabs[2].route, + ActiveLidarrBlock::ManualArtistSearch.into() + ); + assert_some_eq_x!( + &lidarr_data.artist_info_tabs.tabs[2].contextual_help, + &MANUAL_ARTIST_SEARCH_CONTEXT_CLUES + ); + assert_none!(lidarr_data.artist_info_tabs.tabs[2].config); + } + + #[test] + fn test_library_blocks_contains_expected_blocks() { + assert_eq!(LIBRARY_BLOCKS.len(), 7); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::Artists)); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::ArtistsSortPrompt)); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::SearchArtists)); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistsError)); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::FilterArtists)); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistsError)); + assert!(LIBRARY_BLOCKS.contains(&ActiveLidarrBlock::UpdateAllArtistsPrompt)); + } + + #[test] + fn test_artist_details_blocks_contains_expected_blocks() { + assert_eq!(ARTIST_DETAILS_BLOCKS.len(), 15); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistDetails)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistory)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistoryDetails)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ArtistHistorySortPrompt)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchArtistPrompt)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistory)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterArtistHistoryError)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualArtistSearch)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualArtistSearchConfirmPrompt)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualArtistSearchSortPrompt)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbums)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumsError)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistory)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchArtistHistoryError)); + assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt)); + } + + #[test] + fn test_album_details_blocks_contents() { + assert_eq!(ALBUM_DETAILS_BLOCKS.len(), 15); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumDetails)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumHistory)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTracks)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTracksError)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AutomaticallySearchAlbumPrompt)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumHistory)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchAlbumHistoryError)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterAlbumHistory)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterAlbumHistoryError)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumHistorySortPrompt)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::AlbumHistoryDetails)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualAlbumSearch)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::ManualAlbumSearchSortPrompt)); + assert!(ALBUM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::DeleteTrackFilePrompt)); + } + + #[test] + fn test_blocklist_blocks_contents() { + assert_eq!(BLOCKLIST_BLOCKS.len(), 5); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::Blocklist)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::BlocklistItemDetails)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteBlocklistItemPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::BlocklistClearAllItemsPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveLidarrBlock::BlocklistSortPrompt)); + } + + #[test] + fn test_downloads_blocks_contains_expected_blocks() { + assert_eq!(DOWNLOADS_BLOCKS.len(), 3); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::Downloads)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::DeleteDownloadPrompt)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::UpdateDownloadsPrompt)); + } + + #[test] + fn test_history_blocks_contains_expected_blocks() { + assert_eq!(HISTORY_BLOCKS.len(), 7); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::History)); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::HistoryItemDetails)); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::HistorySortPrompt)); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::SearchHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::SearchHistoryError)); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::FilterHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveLidarrBlock::FilterHistoryError)); + } + + #[test] + fn test_add_artist_blocks_contents() { + assert_eq!(ADD_ARTIST_BLOCKS.len(), 12); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistAlreadyInLibrary)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistConfirmPrompt)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistEmptySearchResults)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistPrompt)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchInput)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSearchResults)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMetadataProfile)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitor)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectMonitorNewItems)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectQualityProfile)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistSelectRootFolder)); + assert!(ADD_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::AddArtistTagsInput)); + } + + #[test] + fn test_add_artist_selection_blocks_ordering() { + let mut add_artist_block_iter = ADD_ARTIST_SELECTION_BLOCKS.iter(); + + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectRootFolder] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectMonitor] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectMonitorNewItems] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectQualityProfile] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistSelectMetadataProfile] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistTagsInput] + ); + assert_eq!( + add_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::AddArtistConfirmPrompt] + ); + assert_none!(add_artist_block_iter.next()); + } + + #[test] + fn test_delete_artist_blocks_contents() { + assert_eq!(DELETE_ARTIST_BLOCKS.len(), 4); + assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistPrompt)); + assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistConfirmPrompt)); + assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistToggleDeleteFile)); + assert!(DELETE_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::DeleteArtistToggleAddListExclusion)); + } + + #[test] + fn test_delete_artist_selection_blocks_ordering() { + let mut delete_artist_block_iter = DELETE_ARTIST_SELECTION_BLOCKS.iter(); + + assert_eq!( + delete_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::DeleteArtistToggleDeleteFile] + ); + assert_eq!( + delete_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::DeleteArtistToggleAddListExclusion] + ); + assert_eq!( + delete_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::DeleteArtistConfirmPrompt] + ); + assert_none!(delete_artist_block_iter.next()); + } + + #[test] + fn test_delete_album_blocks_contents() { + assert_eq!(DELETE_ALBUM_BLOCKS.len(), 4); + assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumPrompt)); + assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumConfirmPrompt)); + assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumToggleDeleteFile)); + assert!(DELETE_ALBUM_BLOCKS.contains(&ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion)); + } + + #[test] + fn test_delete_album_selection_blocks_ordering() { + let mut delete_album_block_iter = DELETE_ALBUM_SELECTION_BLOCKS.iter(); + + assert_eq!( + delete_album_block_iter.next().unwrap(), + &[ActiveLidarrBlock::DeleteAlbumToggleDeleteFile] + ); + assert_eq!( + delete_album_block_iter.next().unwrap(), + &[ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion] + ); + assert_eq!( + delete_album_block_iter.next().unwrap(), + &[ActiveLidarrBlock::DeleteAlbumConfirmPrompt] + ); + assert_none!(delete_album_block_iter.next()); + } + + #[test] + fn test_edit_artist_blocks() { + assert_eq!(EDIT_ARTIST_BLOCKS.len(), 8); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPrompt)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistConfirmPrompt)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistPathInput)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMetadataProfile)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectMonitorNewItems)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistSelectQualityProfile)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistTagsInput)); + assert!(EDIT_ARTIST_BLOCKS.contains(&ActiveLidarrBlock::EditArtistToggleMonitored)); + } + + #[test] + fn test_edit_artist_selection_blocks_ordering() { + let mut edit_artist_block_iter = EDIT_ARTIST_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistToggleMonitored] + ); + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistSelectMonitorNewItems] + ); + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistSelectQualityProfile] + ); + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistSelectMetadataProfile] + ); + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistPathInput] + ); + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistTagsInput] + ); + assert_eq!( + edit_artist_block_iter.next().unwrap(), + &[ActiveLidarrBlock::EditArtistConfirmPrompt] + ); + assert_none!(edit_artist_block_iter.next()); + } + + #[test] + fn test_root_folders_blocks_contents() { + assert_eq!(ROOT_FOLDERS_BLOCKS.len(), 2); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::RootFolders)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteRootFolderPrompt)); + } + + #[test] + fn test_edit_indexer_blocks_contents() { + assert_eq!(EDIT_INDEXER_BLOCKS.len(), 11); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerConfirmPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerApiKeyInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerNameInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerSeedRatioInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableRss)); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch) + ); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch) + ); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerUrlInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerTagsInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveLidarrBlock::EditIndexerPriorityInput)); + } + + #[test] + fn test_edit_indexer_nzb_selection_blocks_ordering() { + let mut edit_indexer_nzb_selection_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerPriorityInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None); + } + + #[test] + fn test_edit_indexer_torrent_selection_blocks_ordering() { + let mut edit_indexer_torrent_selection_block_iter = + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerNameInput, + ActiveLidarrBlock::EditIndexerUrlInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableRss, + ActiveLidarrBlock::EditIndexerApiKeyInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveLidarrBlock::EditIndexerSeedRatioInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveLidarrBlock::EditIndexerTagsInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerPriorityInput, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ActiveLidarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None); + } + + #[test] + fn test_indexer_settings_blocks_contents() { + assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 6); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::AllIndexerSettingsPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsConfirmPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsMaximumSizeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsMinimumAgeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsRetentionInput)); + assert!( + INDEXER_SETTINGS_BLOCKS.contains(&ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput) + ); + } + + #[test] + fn test_indexer_settings_selection_blocks_ordering() { + let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter(); + + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsMinimumAgeInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsRetentionInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsMaximumSizeInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveLidarrBlock::IndexerSettingsConfirmPrompt,] + ); + assert_eq!(indexer_settings_block_iter.next(), None); + } + + #[test] + fn test_indexers_blocks_contents() { + assert_eq!(INDEXERS_BLOCKS.len(), 3); + assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::Indexers)); + assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteIndexerPrompt)); + assert!(INDEXERS_BLOCKS.contains(&ActiveLidarrBlock::TestIndexer)); + } + + #[test] + fn test_add_root_folder_blocks_contents() { + assert_eq!(ADD_ROOT_FOLDER_BLOCKS.len(), 9); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPrompt)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderConfirmPrompt)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderNameInput)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPathInput)); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectMonitor)); + assert!( + ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems) + ); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectQualityProfile)); + assert!( + ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderSelectMetadataProfile) + ); + assert!(ADD_ROOT_FOLDER_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderTagsInput)); + } + + #[test] + fn test_system_details_blocks_contents() { + assert_eq!(SYSTEM_DETAILS_BLOCKS.len(), 5); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemLogs)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemQueuedEvents)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTasks)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemTaskStartConfirmPrompt)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SystemUpdates)); + } + + #[test] + fn test_track_details_blocks_contents() { + assert_eq!(TRACK_DETAILS_BLOCKS.len(), 8); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackDetails)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistory)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistoryDetails)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistory)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::SearchTrackHistoryError)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistory)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::FilterTrackHistoryError)); + assert!(TRACK_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::TrackHistorySortPrompt)); + } +} diff --git a/src/models/servarr_data/lidarr/mod.rs b/src/models/servarr_data/lidarr/mod.rs new file mode 100644 index 0000000..2db4fdf --- /dev/null +++ b/src/models/servarr_data/lidarr/mod.rs @@ -0,0 +1,2 @@ +pub mod lidarr_data; +pub mod modals; diff --git a/src/models/servarr_data/lidarr/modals.rs b/src/models/servarr_data/lidarr/modals.rs new file mode 100644 index 0000000..c969c47 --- /dev/null +++ b/src/models/servarr_data/lidarr/modals.rs @@ -0,0 +1,296 @@ +use super::lidarr_data::{ActiveLidarrBlock, LidarrData}; +use crate::app::lidarr::lidarr_context_clues::{ + ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES, + TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES, +}; +use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track, TrackFile}; +use crate::models::servarr_data::modals::EditIndexerModal; +use crate::models::servarr_models::Indexer; +use crate::models::stateful_table::StatefulTable; +use crate::models::{ + HorizontallyScrollableText, ScrollableText, TabRoute, TabState, + lidarr_models::{MonitorType, NewItemMonitorType}, + servarr_models::RootFolder, + stateful_list::StatefulList, +}; +use strum::IntoEnumIterator; + +#[cfg(test)] +#[path = "modals_tests.rs"] +mod modals_tests; + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct AddArtistModal { + pub root_folder_list: StatefulList, + pub monitor_list: StatefulList, + pub monitor_new_items_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub tags: HorizontallyScrollableText, +} + +impl From<&LidarrData<'_>> for AddArtistModal { + fn from(lidarr_data: &LidarrData<'_>) -> AddArtistModal { + let mut add_artist_modal = AddArtistModal::default(); + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .quality_profile_list + .set_items(lidarr_data.sorted_quality_profile_names()); + add_artist_modal + .metadata_profile_list + .set_items(lidarr_data.sorted_metadata_profile_names()); + add_artist_modal + .root_folder_list + .set_items(lidarr_data.root_folders.items.to_vec()); + + add_artist_modal + } +} + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct EditArtistModal { + pub monitor_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub monitored: Option, + pub path: HorizontallyScrollableText, + pub tags: HorizontallyScrollableText, +} + +impl From<&LidarrData<'_>> for EditArtistModal { + fn from(lidarr_data: &LidarrData<'_>) -> EditArtistModal { + let mut edit_artist_modal = EditArtistModal::default(); + let artist = lidarr_data.artists.current_selection(); + + edit_artist_modal + .monitor_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + edit_artist_modal.path = artist.path.clone().into(); + edit_artist_modal.tags = lidarr_data.tag_ids_to_display(&artist.tags).into(); + edit_artist_modal.monitored = Some(artist.monitored); + + let monitor_index = edit_artist_modal + .monitor_list + .items + .iter() + .position(|m| *m == artist.monitor_new_items); + edit_artist_modal.monitor_list.state.select(monitor_index); + + edit_artist_modal + .quality_profile_list + .set_items(lidarr_data.sorted_quality_profile_names()); + let quality_profile_name = lidarr_data + .quality_profile_map + .get_by_left(&artist.quality_profile_id) + .unwrap(); + let quality_profile_index = edit_artist_modal + .quality_profile_list + .items + .iter() + .position(|profile| profile == quality_profile_name); + edit_artist_modal + .quality_profile_list + .state + .select(quality_profile_index); + + edit_artist_modal + .metadata_profile_list + .set_items(lidarr_data.sorted_metadata_profile_names()); + let metadata_profile_name = lidarr_data + .metadata_profile_map + .get_by_left(&artist.metadata_profile_id) + .unwrap(); + let metadata_profile_index = edit_artist_modal + .metadata_profile_list + .items + .iter() + .position(|profile| profile == metadata_profile_name); + edit_artist_modal + .metadata_profile_list + .state + .select(metadata_profile_index); + + edit_artist_modal + } +} + +impl From<&LidarrData<'_>> for EditIndexerModal { + fn from(lidarr_data: &LidarrData<'_>) -> EditIndexerModal { + let mut edit_indexer_modal = EditIndexerModal::default(); + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + tags, + fields, + priority, + .. + } = lidarr_data.indexers.current_selection(); + let seed_ratio_field_option = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| { + field.name.as_ref().expect("indexer field name must exist") == "seedCriteria.seedRatio" + }); + let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field.value.clone() + } else { + None + }; + + edit_indexer_modal.name = name.clone().expect("indexer name must exist").into(); + edit_indexer_modal.enable_rss = Some(*enable_rss); + edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search); + edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search); + edit_indexer_modal.priority = *priority; + edit_indexer_modal.url = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| field.name.as_ref().expect("indexer field name must exist") == "baseUrl") + .expect("baseUrl field must exist") + .value + .clone() + .expect("baseUrl field value must exist") + .as_str() + .expect("baseUrl field value must be a string") + .into(); + edit_indexer_modal.api_key = fields + .as_ref() + .expect("indexer fields must exist") + .iter() + .find(|field| field.name.as_ref().expect("indexer field name must exist") == "apiKey") + .expect("apiKey field must exist") + .value + .clone() + .expect("apiKey field value must exist") + .as_str() + .expect("apiKey field value must be a string") + .into(); + + if let Some(seed_ratio_value) = seed_ratio_value_option { + edit_indexer_modal.seed_ratio = seed_ratio_value + .as_f64() + .expect("Seed ratio value must be a valid f64") + .to_string() + .into(); + } + + edit_indexer_modal.tags = lidarr_data.tag_ids_to_display(tags).into(); + + edit_indexer_modal + } +} + +#[derive(Default)] +#[cfg_attr(test, derive(Debug))] +pub struct AddRootFolderModal { + pub name: HorizontallyScrollableText, + pub path: HorizontallyScrollableText, + pub monitor_list: StatefulList, + pub monitor_new_items_list: StatefulList, + pub quality_profile_list: StatefulList, + pub metadata_profile_list: StatefulList, + pub tags: HorizontallyScrollableText, +} + +impl From<&LidarrData<'_>> for AddRootFolderModal { + fn from(lidarr_data: &LidarrData<'_>) -> AddRootFolderModal { + let mut add_root_folder_modal = AddRootFolderModal::default(); + add_root_folder_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_root_folder_modal + .quality_profile_list + .set_items(lidarr_data.sorted_quality_profile_names()); + add_root_folder_modal + .metadata_profile_list + .set_items(lidarr_data.sorted_metadata_profile_names()); + + add_root_folder_modal + } +} + +#[cfg_attr(test, derive(Debug))] +pub struct AlbumDetailsModal { + pub tracks: StatefulTable, + pub track_files: StatefulTable, + pub track_details_modal: Option, + pub album_history: StatefulTable, + pub album_releases: StatefulTable, + pub album_details_tabs: TabState, +} + +impl Default for AlbumDetailsModal { + fn default() -> AlbumDetailsModal { + AlbumDetailsModal { + tracks: StatefulTable::default(), + track_details_modal: None, + track_files: StatefulTable::default(), + album_releases: StatefulTable::default(), + album_history: StatefulTable::default(), + album_details_tabs: TabState::new(vec![ + TabRoute { + title: "Tracks".to_string(), + route: ActiveLidarrBlock::AlbumDetails.into(), + contextual_help: Some(&ALBUM_DETAILS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveLidarrBlock::AlbumHistory.into(), + contextual_help: Some(&ALBUM_HISTORY_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "Manual Search".to_string(), + route: ActiveLidarrBlock::ManualAlbumSearch.into(), + contextual_help: Some(&MANUAL_ALBUM_SEARCH_CONTEXT_CLUES), + config: None, + }, + ]), + } + } +} + +#[cfg_attr(test, derive(Debug))] +pub struct TrackDetailsModal { + pub track_details: ScrollableText, + pub track_history: StatefulTable, + pub track_details_tabs: TabState, +} + +impl Default for TrackDetailsModal { + fn default() -> Self { + TrackDetailsModal { + track_details: ScrollableText::default(), + track_history: StatefulTable::default(), + track_details_tabs: TabState::new(vec![ + TabRoute { + title: "Track Details".to_string(), + route: ActiveLidarrBlock::TrackDetails.into(), + contextual_help: Some(&TRACK_DETAILS_CONTEXT_CLUES), + config: None, + }, + TabRoute { + title: "History".to_string(), + route: ActiveLidarrBlock::TrackHistory.into(), + contextual_help: Some(&TRACK_HISTORY_CONTEXT_CLUES), + config: None, + }, + ]), + } + } +} diff --git a/src/models/servarr_data/lidarr/modals_tests.rs b/src/models/servarr_data/lidarr/modals_tests.rs new file mode 100644 index 0000000..4419326 --- /dev/null +++ b/src/models/servarr_data/lidarr/modals_tests.rs @@ -0,0 +1,310 @@ +#[cfg(test)] +mod tests { + use crate::app::lidarr::lidarr_context_clues::{ + ALBUM_DETAILS_CONTEXT_CLUES, ALBUM_HISTORY_CONTEXT_CLUES, MANUAL_ALBUM_SEARCH_CONTEXT_CLUES, + TRACK_DETAILS_CONTEXT_CLUES, TRACK_HISTORY_CONTEXT_CLUES, + }; + use crate::models::lidarr_models::{Artist, MonitorType, NewItemMonitorType}; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData}; + use crate::models::servarr_data::lidarr::modals::{ + AddArtistModal, AlbumDetailsModal, EditArtistModal, TrackDetailsModal, + }; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_models::{Indexer, IndexerField, RootFolder}; + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::{Number, Value}; + + #[test] + fn test_add_artist_modal_from_lidarr_data() { + let mut lidarr_data = LidarrData { + quality_profile_map: BiMap::from_iter([ + (2i64, "Lossless".to_owned()), + (1i64, "Standard".to_owned()), + ]), + metadata_profile_map: BiMap::from_iter([ + (2i64, "None".to_owned()), + (1i64, "Standard".to_owned()), + ]), + ..LidarrData::default() + }; + let root_folder_1 = RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + }; + lidarr_data.root_folders.set_items(vec![ + root_folder_1.clone(), + RootFolder { + id: 2, + path: "/nfs2".to_owned(), + accessible: true, + free_space: 21990232555520, + unmapped_folders: None, + }, + ]); + + let add_artist_modal = AddArtistModal::from(&lidarr_data); + + assert_eq!( + *add_artist_modal.monitor_list.current_selection(), + MonitorType::default() + ); + assert_eq!( + *add_artist_modal.monitor_new_items_list.current_selection(), + NewItemMonitorType::default() + ); + assert_str_eq!( + add_artist_modal.quality_profile_list.current_selection(), + "Standard" + ); + assert_str_eq!( + add_artist_modal.metadata_profile_list.current_selection(), + "Standard" + ); + assert_eq!( + add_artist_modal.root_folder_list.current_selection(), + &root_folder_1 + ); + assert_is_empty!(add_artist_modal.tags.text); + } + + #[test] + fn test_edit_artist_modal_from_lidarr_data() { + let mut lidarr_data = LidarrData { + quality_profile_map: BiMap::from_iter([ + (1i64, "HD - 1080p".to_owned()), + (2i64, "Any".to_owned()), + ]), + metadata_profile_map: BiMap::from_iter([ + (1i64, "Standard".to_owned()), + (2i64, "None".to_owned()), + ]), + tags_map: BiMap::from_iter([(1i64, "usenet".to_owned())]), + ..LidarrData::default() + }; + let artist = Artist { + id: 1, + monitored: true, + monitor_new_items: NewItemMonitorType::All, + quality_profile_id: 1, + metadata_profile_id: 1, + path: "/nfs/music/test_artist".to_owned(), + tags: vec![Number::from(1)], + ..Artist::default() + }; + lidarr_data.artists.set_items(vec![artist]); + + let edit_artist_modal = EditArtistModal::from(&lidarr_data); + + assert_some_eq_x!(&edit_artist_modal.monitored, &true); + assert_eq!( + *edit_artist_modal.monitor_list.current_selection(), + NewItemMonitorType::All + ); + assert_str_eq!( + edit_artist_modal.quality_profile_list.current_selection(), + "HD - 1080p" + ); + assert_str_eq!( + edit_artist_modal.metadata_profile_list.current_selection(), + "Standard" + ); + assert_str_eq!(edit_artist_modal.path.text, "/nfs/music/test_artist"); + assert_str_eq!(edit_artist_modal.tags.text, "usenet"); + } + + #[rstest] + fn test_edit_indexer_modal_from_lidarr_data(#[values(true, false)] seed_ratio_present: bool) { + let mut lidarr_data = LidarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..LidarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if seed_ratio_present { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + priority: 1, + ..Indexer::default() + }; + lidarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&lidarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_eq!(edit_indexer_modal.enable_rss, Some(true)); + assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); + assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + + if seed_ratio_present { + assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2"); + } else { + assert!(edit_indexer_modal.seed_ratio.text.is_empty()); + } + } + + #[test] + fn test_edit_indexer_modal_from_lidarr_data_seed_ratio_value_is_none() { + let mut lidarr_data = LidarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..LidarrData::default() + }; + let fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: None, + }, + ]; + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + priority: 1, + ..Indexer::default() + }; + lidarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&lidarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_some_eq_x!(&edit_indexer_modal.enable_rss, &true); + assert_some_eq_x!(&edit_indexer_modal.enable_automatic_search, &true); + assert_some_eq_x!(&edit_indexer_modal.enable_interactive_search, &true); + assert_eq!(edit_indexer_modal.priority, 1); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + assert_is_empty!(edit_indexer_modal.seed_ratio.text); + } + + #[test] + fn test_album_details_modal_default() { + let album_details_modal = AlbumDetailsModal::default(); + + assert_is_empty!(album_details_modal.tracks); + assert_none!(album_details_modal.track_details_modal); + assert_is_empty!(album_details_modal.track_files); + assert_is_empty!(album_details_modal.album_releases); + assert_is_empty!(album_details_modal.album_history); + + assert_eq!(album_details_modal.album_details_tabs.tabs.len(), 3); + + assert_str_eq!( + album_details_modal.album_details_tabs.tabs[0].title, + "Tracks" + ); + assert_eq!( + album_details_modal.album_details_tabs.tabs[0].route, + ActiveLidarrBlock::AlbumDetails.into() + ); + assert_some_eq_x!( + &album_details_modal.album_details_tabs.tabs[0].contextual_help, + &ALBUM_DETAILS_CONTEXT_CLUES + ); + assert_eq!(album_details_modal.album_details_tabs.tabs[0].config, None); + + assert_str_eq!( + album_details_modal.album_details_tabs.tabs[1].title, + "History" + ); + assert_eq!( + album_details_modal.album_details_tabs.tabs[1].route, + ActiveLidarrBlock::AlbumHistory.into() + ); + assert_some_eq_x!( + &album_details_modal.album_details_tabs.tabs[1].contextual_help, + &ALBUM_HISTORY_CONTEXT_CLUES + ); + assert_eq!(album_details_modal.album_details_tabs.tabs[1].config, None); + + assert_str_eq!( + album_details_modal.album_details_tabs.tabs[2].title, + "Manual Search" + ); + assert_eq!( + album_details_modal.album_details_tabs.tabs[2].route, + ActiveLidarrBlock::ManualAlbumSearch.into() + ); + assert_some_eq_x!( + &album_details_modal.album_details_tabs.tabs[2].contextual_help, + &MANUAL_ALBUM_SEARCH_CONTEXT_CLUES + ); + assert_eq!(album_details_modal.album_details_tabs.tabs[2].config, None); + } + + #[test] + fn test_track_details_modal_default() { + let track_details_modal = TrackDetailsModal::default(); + + assert_is_empty!(track_details_modal.track_details); + assert_is_empty!(track_details_modal.track_history); + + assert_eq!(track_details_modal.track_details_tabs.tabs.len(), 2); + + assert_str_eq!( + track_details_modal.track_details_tabs.tabs[0].title, + "Track Details" + ); + assert_eq!( + track_details_modal.track_details_tabs.tabs[0].route, + ActiveLidarrBlock::TrackDetails.into() + ); + assert_some_eq_x!( + &track_details_modal.track_details_tabs.tabs[0].contextual_help, + &TRACK_DETAILS_CONTEXT_CLUES + ); + assert_eq!(track_details_modal.track_details_tabs.tabs[0].config, None); + + assert_str_eq!( + track_details_modal.track_details_tabs.tabs[1].title, + "History" + ); + assert_eq!( + track_details_modal.track_details_tabs.tabs[1].route, + ActiveLidarrBlock::TrackHistory.into() + ); + assert_some_eq_x!( + &track_details_modal.track_details_tabs.tabs[1].contextual_help, + &TRACK_HISTORY_CONTEXT_CLUES + ); + assert_eq!(track_details_modal.track_details_tabs.tabs[1].config, None); + } +} diff --git a/src/models/servarr_data/mod.rs b/src/models/servarr_data/mod.rs index 1545315..256f0bc 100644 --- a/src/models/servarr_data/mod.rs +++ b/src/models/servarr_data/mod.rs @@ -1,5 +1,6 @@ use crate::models::Route; +pub mod lidarr; pub mod modals; pub mod radarr; pub mod sonarr; diff --git a/src/models/servarr_data/modals.rs b/src/models/servarr_data/modals.rs index 46d329b..2755e80 100644 --- a/src/models/servarr_data/modals.rs +++ b/src/models/servarr_data/modals.rs @@ -1,6 +1,10 @@ use crate::models::HorizontallyScrollableText; -#[derive(Default, Debug, PartialEq, Eq)] +#[cfg(test)] +#[path = "modals_tests.rs"] +mod modals_tests; + +#[derive(Debug, PartialEq, Eq)] pub struct EditIndexerModal { pub name: HorizontallyScrollableText, pub enable_rss: Option, @@ -13,6 +17,22 @@ pub struct EditIndexerModal { pub priority: i64, } +impl Default for EditIndexerModal { + fn default() -> Self { + Self { + name: Default::default(), + enable_rss: None, + enable_automatic_search: None, + enable_interactive_search: None, + url: Default::default(), + api_key: Default::default(), + seed_ratio: Default::default(), + tags: Default::default(), + priority: 1, + } + } +} + #[derive(Default, Clone, Eq, PartialEq, Debug)] pub struct IndexerTestResultModalItem { pub name: String, diff --git a/src/models/servarr_data/modals_tests.rs b/src/models/servarr_data/modals_tests.rs new file mode 100644 index 0000000..26ce17a --- /dev/null +++ b/src/models/servarr_data/modals_tests.rs @@ -0,0 +1,20 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + + #[test] + fn test_edit_indexer_modal_default() { + let edit_indexer_modal = EditIndexerModal::default(); + + assert_is_empty!(edit_indexer_modal.name.text); + assert_none!(&edit_indexer_modal.enable_rss); + assert_none!(&edit_indexer_modal.enable_automatic_search); + assert_none!(&edit_indexer_modal.enable_interactive_search); + assert_is_empty!(edit_indexer_modal.url.text); + assert_is_empty!(edit_indexer_modal.api_key.text); + assert_is_empty!(edit_indexer_modal.seed_ratio.text); + assert_is_empty!(edit_indexer_modal.tags.text); + assert_eq!(edit_indexer_modal.priority, 1); + } +} diff --git a/src/models/servarr_data/radarr/modals.rs b/src/models/servarr_data/radarr/modals.rs index 922757d..9dd2c59 100644 --- a/src/models/servarr_data/radarr/modals.rs +++ b/src/models/servarr_data/radarr/modals.rs @@ -15,6 +15,7 @@ use crate::models::{HorizontallyScrollableText, ScrollableText}; mod modals_tests; #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct MovieDetailsModal { pub movie_details: ScrollableText, pub file_details: String, @@ -97,6 +98,7 @@ impl From<&RadarrData<'_>> for EditIndexerModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct EditMovieModal { pub minimum_availability_list: StatefulList, pub quality_profile_list: StatefulList, @@ -157,6 +159,7 @@ impl From<&RadarrData<'_>> for EditMovieModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct AddMovieModal { pub root_folder_list: StatefulList, pub monitor_list: StatefulList, @@ -186,6 +189,7 @@ impl From<&RadarrData<'_>> for AddMovieModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct EditCollectionModal { pub monitored: Option, pub minimum_availability_list: StatefulList, diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index 1d2d351..22fc4a1 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -1,5 +1,5 @@ use crate::app::context_clues::{ - BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::radarr::radarr_context_clues::{ @@ -8,7 +8,7 @@ use crate::app::radarr::radarr_context_clues::{ }; use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord, - IndexerSettings, Movie, RadarrTask, + IndexerSettings, Movie, RadarrHistoryItem, RadarrTask, }; use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}; use crate::models::servarr_data::radarr::modals::{ @@ -23,6 +23,7 @@ use crate::models::{ use crate::network::radarr_network::RadarrEvent; use bimap::BiMap; use chrono::{DateTime, Utc}; +use itertools::Itertools; use serde_json::Number; use strum::EnumIter; #[cfg(test)] @@ -33,7 +34,8 @@ use { crate::network::radarr_network::radarr_network_test_utils::test_utils::{ add_movie_search_result, blocklist_item, cast_credit, collection, collection_movie, crew_credit, download_record, indexer, log_line, movie, movie_history_item, - quality_profile_map, tags_map, task, torrent_release, updates, usenet_release, + quality_profile_map, radarr_history_item, tags_map, task, torrent_release, updates, + usenet_release, }, crate::network::servarr_test_utils::diskspace, crate::network::servarr_test_utils::indexer_test_result, @@ -61,6 +63,7 @@ pub struct RadarrData<'a> { pub downloads: StatefulTable, pub indexers: StatefulTable, pub blocklist: StatefulTable, + pub history: StatefulTable, pub quality_profile_map: BiMap, pub tags_map: BiMap, pub collections: StatefulTable, @@ -112,9 +115,13 @@ impl RadarrData<'_> { } pub fn sorted_quality_profile_names(&self) -> Vec { - let mut names: Vec = self.quality_profile_map.right_values().cloned().collect(); - names.sort(); - names + self + .quality_profile_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() } } @@ -130,6 +137,7 @@ impl<'a> Default for RadarrData<'a> { downloads: StatefulTable::default(), indexers: StatefulTable::default(), blocklist: StatefulTable::default(), + history: StatefulTable::default(), quality_profile_map: BiMap::default(), tags_map: BiMap::default(), collections: StatefulTable::default(), @@ -179,6 +187,12 @@ impl<'a> Default for RadarrData<'a> { contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES), config: None, }, + TabRoute { + title: "History".to_string(), + route: ActiveRadarrBlock::History.into(), + contextual_help: Some(&HISTORY_CONTEXT_CLUES), + config: None, + }, TabRoute { title: "Root Folders".to_string(), route: ActiveRadarrBlock::RootFolders.into(), @@ -382,6 +396,10 @@ impl RadarrData<'_> { radarr_data.downloads.set_items(vec![download_record()]); radarr_data.blocklist.set_items(vec![blocklist_item()]); radarr_data.blocklist.sorting(vec![sort_option!(id)]); + radarr_data.history.set_items(vec![radarr_history_item()]); + radarr_data.history.sorting(vec![sort_option!(id)]); + radarr_data.history.search = Some("Something".into()); + radarr_data.history.filter = Some("Something".into()); radarr_data.indexers.set_items(vec![indexer()]); radarr_data.indexers.sorting(vec![sort_option!(id)]); radarr_data.indexers.search = Some("Something".into()); @@ -415,6 +433,13 @@ pub enum ActiveRadarrBlock { BlocklistClearAllItemsPrompt, BlocklistItemDetails, BlocklistSortPrompt, + History, + HistoryItemDetails, + HistorySortPrompt, + FilterHistory, + FilterHistoryError, + SearchHistory, + SearchHistoryError, Collections, CollectionsSortPrompt, CollectionDetails, @@ -533,6 +558,15 @@ pub static BLOCKLIST_BLOCKS: [ActiveRadarrBlock; 5] = [ ActiveRadarrBlock::BlocklistClearAllItemsPrompt, ActiveRadarrBlock::BlocklistSortPrompt, ]; +pub static HISTORY_BLOCKS: [ActiveRadarrBlock; 7] = [ + ActiveRadarrBlock::History, + ActiveRadarrBlock::HistoryItemDetails, + ActiveRadarrBlock::HistorySortPrompt, + ActiveRadarrBlock::FilterHistory, + ActiveRadarrBlock::FilterHistoryError, + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::SearchHistoryError, +]; pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::AddMovieSearchInput, ActiveRadarrBlock::AddMovieSearchResults, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index b56d232..15b25f0 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -1,17 +1,18 @@ #[cfg(test)] mod tests { mod radarr_data_tests { - use chrono::{DateTime, Utc}; - use pretty_assertions::{assert_eq, assert_str_eq}; - use crate::app::context_clues::{ - BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::radarr::radarr_context_clues::{ COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, }; + use bimap::BiMap; + use chrono::{DateTime, Utc}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils; @@ -61,45 +62,85 @@ mod tests { assert_movie_info_tabs_reset!(radarr_data); } + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let radarr_data = RadarrData { + tags_map, + ..RadarrData::default() + }; + + assert_str_eq!( + radarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), + "test 1, test 2" + ); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 1".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 3".to_owned()); + let radarr_data = RadarrData { + quality_profile_map, + ..RadarrData::default() + }; + let expected_quality_profile_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + radarr_data.sorted_quality_profile_names(), + expected_quality_profile_vec + ); + } + #[test] fn test_radarr_data_defaults() { let radarr_data = RadarrData::default(); - assert!(radarr_data.root_folders.items.is_empty()); + assert_is_empty!(radarr_data.root_folders.items); assert_eq!(radarr_data.disk_space_vec, Vec::new()); - assert!(radarr_data.version.is_empty()); + assert_is_empty!(radarr_data.version); assert_eq!(radarr_data.start_time, >::default()); - assert!(radarr_data.movies.is_empty()); + assert_is_empty!(radarr_data.movies); assert_eq!(radarr_data.selected_block, BlockSelectionState::default()); - assert!(radarr_data.downloads.items.is_empty()); - assert!(radarr_data.indexers.items.is_empty()); - assert!(radarr_data.blocklist.items.is_empty()); - assert!(radarr_data.quality_profile_map.is_empty()); - assert!(radarr_data.tags_map.is_empty()); - assert!(radarr_data.collections.items.is_empty()); - assert!(radarr_data.collection_movies.items.is_empty()); - assert!(radarr_data.logs.items.is_empty()); - assert!(radarr_data.log_details.items.is_empty()); - assert!(radarr_data.tasks.items.is_empty()); - assert!(radarr_data.queued_events.items.is_empty()); - assert!(radarr_data.updates.get_text().is_empty()); - assert!(radarr_data.add_movie_search.is_none()); - assert!(radarr_data.add_movie_modal.is_none()); - assert!(radarr_data.add_searched_movies.is_none()); - assert!(radarr_data.edit_movie_modal.is_none()); - assert!(radarr_data.edit_collection_modal.is_none()); - assert!(radarr_data.edit_root_folder.is_none()); - assert!(radarr_data.edit_indexer_modal.is_none()); - assert!(radarr_data.indexer_settings.is_none()); - assert!(radarr_data.indexer_test_errors.is_none()); - assert!(radarr_data.indexer_test_all_results.is_none()); - assert!(radarr_data.movie_details_modal.is_none()); - assert!(radarr_data.prompt_confirm_action.is_none()); + assert_is_empty!(radarr_data.downloads.items); + assert_is_empty!(radarr_data.indexers.items); + assert_is_empty!(radarr_data.blocklist.items); + assert_is_empty!(radarr_data.history.items); + assert_is_empty!(radarr_data.quality_profile_map); + assert_is_empty!(radarr_data.tags_map); + assert_is_empty!(radarr_data.collections.items); + assert_is_empty!(radarr_data.collection_movies.items); + assert_is_empty!(radarr_data.logs.items); + assert_is_empty!(radarr_data.log_details.items); + assert_is_empty!(radarr_data.tasks.items); + assert_is_empty!(radarr_data.queued_events.items); + assert_is_empty!(radarr_data.updates.get_text()); + assert_none!(&radarr_data.add_movie_search); + assert_none!(&radarr_data.add_movie_modal); + assert_none!(&radarr_data.add_searched_movies); + assert_none!(&radarr_data.edit_movie_modal); + assert_none!(&radarr_data.edit_collection_modal); + assert_none!(&radarr_data.edit_root_folder); + assert_none!(&radarr_data.edit_indexer_modal); + assert_none!(&radarr_data.indexer_settings); + assert_none!(&radarr_data.indexer_test_errors); + assert_none!(&radarr_data.indexer_test_all_results); + assert_none!(&radarr_data.movie_details_modal); + assert_none!(&radarr_data.prompt_confirm_action); assert!(!radarr_data.prompt_confirm); assert!(!radarr_data.delete_movie_files); assert!(!radarr_data.add_list_exclusion); - assert_eq!(radarr_data.main_tabs.tabs.len(), 7); + assert_eq!(radarr_data.main_tabs.tabs.len(), 8); assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -149,42 +190,54 @@ mod tests { ); assert_eq!(radarr_data.main_tabs.tabs[3].config, None); - assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Root Folders"); + assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "History"); assert_eq!( radarr_data.main_tabs.tabs[4].route, - ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::History.into() ); assert!(radarr_data.main_tabs.tabs[4].contextual_help.is_some()); assert_eq!( radarr_data.main_tabs.tabs[4].contextual_help.unwrap(), - &ROOT_FOLDERS_CONTEXT_CLUES + &HISTORY_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[4].config, None); - assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Indexers"); + assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Root Folders"); assert_eq!( radarr_data.main_tabs.tabs[5].route, - ActiveRadarrBlock::Indexers.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(radarr_data.main_tabs.tabs[5].contextual_help.is_some()); assert_eq!( radarr_data.main_tabs.tabs[5].contextual_help.unwrap(), - &INDEXERS_CONTEXT_CLUES + &ROOT_FOLDERS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[5].config, None); - assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "System"); + assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "Indexers"); assert_eq!( radarr_data.main_tabs.tabs[6].route, - ActiveRadarrBlock::System.into() + ActiveRadarrBlock::Indexers.into() ); assert!(radarr_data.main_tabs.tabs[6].contextual_help.is_some()); assert_eq!( radarr_data.main_tabs.tabs[6].contextual_help.unwrap(), - &SYSTEM_CONTEXT_CLUES + &INDEXERS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[6].config, None); + assert_str_eq!(radarr_data.main_tabs.tabs[7].title, "System"); + assert_eq!( + radarr_data.main_tabs.tabs[7].route, + ActiveRadarrBlock::System.into() + ); + assert!(radarr_data.main_tabs.tabs[7].contextual_help.is_some()); + assert_eq!( + radarr_data.main_tabs.tabs[7].contextual_help.unwrap(), + &SYSTEM_CONTEXT_CLUES + ); + assert_eq!(radarr_data.main_tabs.tabs[7].config, None); + assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); assert_str_eq!(radarr_data.movie_info_tabs.tabs[0].title, "Details"); @@ -294,8 +347,8 @@ mod tests { DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, - INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, LIBRARY_BLOCKS, - MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, + HISTORY_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, INDEXERS_BLOCKS, + LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, }; #[test] @@ -348,6 +401,18 @@ mod tests { assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistSortPrompt)); } + #[test] + fn test_history_blocks_contents() { + assert_eq!(HISTORY_BLOCKS.len(), 7); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::History)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::HistoryItemDetails)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::HistorySortPrompt)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::FilterHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::FilterHistoryError)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::SearchHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveRadarrBlock::SearchHistoryError)); + } + #[test] fn test_add_movie_blocks_contents() { assert_eq!(ADD_MOVIE_BLOCKS.len(), 10); diff --git a/src/models/servarr_data/sonarr/modals.rs b/src/models/servarr_data/sonarr/modals.rs index 026c974..d1d12d8 100644 --- a/src/models/servarr_data/sonarr/modals.rs +++ b/src/models/servarr_data/sonarr/modals.rs @@ -23,6 +23,7 @@ use crate::{ mod modals_tests; #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct AddSeriesModal { pub root_folder_list: StatefulList, pub monitor_list: StatefulList, @@ -130,6 +131,7 @@ impl From<&SonarrData<'_>> for EditIndexerModal { } #[derive(Default)] +#[cfg_attr(test, derive(Debug))] pub struct EditSeriesModal { pub series_type_list: StatefulList, pub quality_profile_list: StatefulList, @@ -260,6 +262,7 @@ impl Default for EpisodeDetailsModal { } } +#[cfg_attr(test, derive(Debug))] pub struct SeasonDetailsModal { pub episodes: StatefulTable, pub episode_files: StatefulTable, diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 8713468..c470280 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -2,21 +2,20 @@ use super::modals::{AddSeriesModal, EditSeriesModal, SeasonDetailsModal}; use crate::{ app::{ context_clues::{ - BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }, sonarr::sonarr_context_clues::{ - HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, - SERIES_HISTORY_CONTEXT_CLUES, + SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, }, }, models::{ BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState, servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}, - servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder}, + servarr_models::{DiskSpace, Indexer, IndexerSettings, QueueEvent, RootFolder}, sonarr_models::{ - AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series, - SonarrHistoryItem, SonarrTask, + AddSeriesSearchResult, BlocklistItem, DownloadRecord, Season, Series, SonarrHistoryItem, + SonarrTask, }, stateful_list::StatefulList, stateful_table::StatefulTable, @@ -25,6 +24,7 @@ use crate::{ }; use bimap::BiMap; use chrono::{DateTime, Utc}; +use itertools::Itertools; use serde_json::Number; use strum::EnumIter; #[cfg(test)] @@ -33,11 +33,12 @@ use { crate::models::sonarr_models::{SeriesMonitor, SeriesType}, crate::models::stateful_table::SortOption, crate::network::servarr_test_utils::diskspace, + crate::network::servarr_test_utils::indexer_settings, crate::network::servarr_test_utils::indexer_test_result, crate::network::servarr_test_utils::queued_event, crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - add_series_search_result, blocklist_item, download_record, history_item, indexer, - indexer_settings, log_line, root_folder, + add_series_search_result, blocklist_item, download_record, indexer, log_line, root_folder, + sonarr_history_item, }, crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ episode, episode_file, language_profiles_map, quality_profile_map, season, series, tags_map, @@ -119,15 +120,23 @@ impl SonarrData<'_> { } pub fn sorted_quality_profile_names(&self) -> Vec { - let mut names: Vec = self.quality_profile_map.right_values().cloned().collect(); - names.sort(); - names + self + .quality_profile_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() } pub fn sorted_language_profile_names(&self) -> Vec { - let mut names: Vec = self.language_profiles_map.right_values().cloned().collect(); - names.sort(); - names + self + .language_profiles_map + .iter() + .sorted_by_key(|(id, _)| *id) + .map(|(_, name)| name) + .cloned() + .collect() } } @@ -300,7 +309,7 @@ impl SonarrData<'_> { }; episode_details_modal .episode_history - .set_items(vec![history_item()]); + .set_items(vec![sonarr_history_item()]); episode_details_modal .episode_releases .set_items(vec![torrent_release(), usenet_release()]); @@ -319,7 +328,7 @@ impl SonarrData<'_> { .set_items(vec![episode_file()]); season_details_modal .season_history - .set_items(vec![history_item()]); + .set_items(vec![sonarr_history_item()]); season_details_modal.season_history.search = Some("season history search".into()); season_details_modal.season_history.filter = Some("season history filter".into()); season_details_modal @@ -333,7 +342,7 @@ impl SonarrData<'_> { .sorting(vec![sort_option!(indexer_id)]); let mut series_history = StatefulTable::default(); - series_history.set_items(vec![history_item()]); + series_history.set_items(vec![sonarr_history_item()]); series_history.sorting(vec![sort_option!(id)]); series_history.search = Some("series history search".into()); series_history.filter = Some("series history filter".into()); @@ -365,7 +374,7 @@ impl SonarrData<'_> { sonarr_data.blocklist.set_items(vec![blocklist_item()]); sonarr_data.blocklist.sorting(vec![sort_option!(id)]); sonarr_data.downloads.set_items(vec![download_record()]); - sonarr_data.history.set_items(vec![history_item()]); + sonarr_data.history.set_items(vec![sonarr_history_item()]); sonarr_data.history.sorting(vec![sort_option!(id)]); sonarr_data.history.search = Some("test search".into()); sonarr_data.history.filter = Some("test filter".into()); diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 92b9275..25c5756 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -1,9 +1,7 @@ #[cfg(test)] mod tests { mod sonarr_data_tests { - use chrono::{DateTime, Utc}; - use pretty_assertions::{assert_eq, assert_str_eq}; - + use crate::app::context_clues::HISTORY_CONTEXT_CLUES; use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES; use crate::models::sonarr_models::{Season, SonarrHistoryItem}; use crate::models::stateful_table::StatefulTable; @@ -13,15 +11,17 @@ mod tests { BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }, - sonarr::sonarr_context_clues::{ - HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, - }, + sonarr::sonarr_context_clues::{SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES}, }, models::{ BlockSelectionState, Route, servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}, }, }; + use bimap::BiMap; + use chrono::{DateTime, Utc}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; #[test] fn test_from_active_sonarr_block_to_route() { @@ -77,44 +77,105 @@ mod tests { assert_eq!(sonarr_data.series_info_tabs.index, 0); } + #[test] + fn test_tag_ids_to_display() { + let mut tags_map = BiMap::new(); + tags_map.insert(3, "test 3".to_owned()); + tags_map.insert(2, "test 2".to_owned()); + tags_map.insert(1, "test 1".to_owned()); + let sonarr_data = SonarrData { + tags_map, + ..SonarrData::default() + }; + + assert_str_eq!( + sonarr_data.tag_ids_to_display(&[Number::from(1), Number::from(2)]), + "test 1, test 2" + ); + } + + #[test] + fn test_sorted_quality_profile_names() { + let mut quality_profile_map = BiMap::new(); + quality_profile_map.insert(3, "test 1".to_owned()); + quality_profile_map.insert(2, "test 2".to_owned()); + quality_profile_map.insert(1, "test 3".to_owned()); + let sonarr_data = SonarrData { + quality_profile_map, + ..SonarrData::default() + }; + let expected_quality_profile_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + sonarr_data.sorted_quality_profile_names(), + expected_quality_profile_vec + ); + } + + #[test] + fn test_sorted_language_profile_names() { + let mut language_profiles_map = BiMap::new(); + language_profiles_map.insert(3, "test 1".to_owned()); + language_profiles_map.insert(2, "test 2".to_owned()); + language_profiles_map.insert(1, "test 3".to_owned()); + let sonarr_data = SonarrData { + language_profiles_map, + ..SonarrData::default() + }; + let expected_language_profiles_vec = vec![ + "test 3".to_owned(), + "test 2".to_owned(), + "test 1".to_owned(), + ]; + + assert_iter_eq!( + sonarr_data.sorted_language_profile_names(), + expected_language_profiles_vec + ); + } + #[test] fn test_sonarr_data_defaults() { let sonarr_data = SonarrData::default(); assert!(!sonarr_data.add_list_exclusion); - assert!(sonarr_data.add_searched_series.is_none()); - assert!(sonarr_data.add_series_search.is_none()); - assert!(sonarr_data.add_series_modal.is_none()); - assert!(sonarr_data.blocklist.is_empty()); + assert_none!(sonarr_data.add_searched_series); + assert_none!(sonarr_data.add_series_search); + assert_none!(sonarr_data.add_series_modal); + assert_is_empty!(sonarr_data.blocklist); assert!(!sonarr_data.delete_series_files); - assert!(sonarr_data.downloads.is_empty()); - assert!(sonarr_data.disk_space_vec.is_empty()); - assert!(sonarr_data.edit_indexer_modal.is_none()); - assert!(sonarr_data.edit_root_folder.is_none()); - assert!(sonarr_data.edit_series_modal.is_none()); - assert!(sonarr_data.history.is_empty()); - assert!(sonarr_data.indexers.is_empty()); - assert!(sonarr_data.indexer_settings.is_none()); - assert!(sonarr_data.indexer_test_errors.is_none()); - assert!(sonarr_data.indexer_test_all_results.is_none()); - assert!(sonarr_data.language_profiles_map.is_empty()); - assert!(sonarr_data.logs.is_empty()); - assert!(sonarr_data.log_details.is_empty()); + assert_is_empty!(sonarr_data.downloads); + assert_is_empty!(sonarr_data.disk_space_vec); + assert_none!(sonarr_data.edit_indexer_modal); + assert_none!(sonarr_data.edit_root_folder); + assert_none!(sonarr_data.edit_series_modal); + assert_is_empty!(sonarr_data.history); + assert_is_empty!(sonarr_data.indexers); + assert_none!(sonarr_data.indexer_settings); + assert_none!(sonarr_data.indexer_test_errors); + assert_none!(sonarr_data.indexer_test_all_results); + assert_is_empty!(sonarr_data.language_profiles_map); + assert_is_empty!(sonarr_data.logs); + assert_is_empty!(sonarr_data.log_details); assert!(!sonarr_data.prompt_confirm); - assert!(sonarr_data.prompt_confirm_action.is_none()); - assert!(sonarr_data.quality_profile_map.is_empty()); - assert!(sonarr_data.queued_events.is_empty()); - assert!(sonarr_data.root_folders.is_empty()); - assert!(sonarr_data.seasons.is_empty()); - assert!(sonarr_data.season_details_modal.is_none()); + assert_none!(sonarr_data.prompt_confirm_action); + assert_is_empty!(sonarr_data.quality_profile_map); + assert_is_empty!(sonarr_data.queued_events); + assert_is_empty!(sonarr_data.root_folders); + assert_is_empty!(sonarr_data.seasons); + assert_none!(sonarr_data.season_details_modal); assert_eq!(sonarr_data.selected_block, BlockSelectionState::default()); - assert!(sonarr_data.series.is_empty()); - assert!(sonarr_data.series_history.is_none()); + assert_is_empty!(sonarr_data.series); + assert_none!(sonarr_data.series_history); assert_eq!(sonarr_data.start_time, >::default()); - assert!(sonarr_data.tags_map.is_empty()); - assert!(sonarr_data.tasks.is_empty()); - assert!(sonarr_data.updates.is_empty()); - assert!(sonarr_data.version.is_empty()); + assert_is_empty!(sonarr_data.tags_map); + assert_is_empty!(sonarr_data.tasks); + assert_is_empty!(sonarr_data.updates); + assert_is_empty!(sonarr_data.version); assert_eq!(sonarr_data.main_tabs.tabs.len(), 7); @@ -123,84 +184,77 @@ mod tests { sonarr_data.main_tabs.tabs[0].route, ActiveSonarrBlock::Series.into() ); - assert!(sonarr_data.main_tabs.tabs[0].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[0].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[0].contextual_help, &SERIES_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[0].config, None); + assert_none!(sonarr_data.main_tabs.tabs[0].config); assert_str_eq!(sonarr_data.main_tabs.tabs[1].title, "Downloads"); assert_eq!( sonarr_data.main_tabs.tabs[1].route, ActiveSonarrBlock::Downloads.into() ); - assert!(sonarr_data.main_tabs.tabs[1].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[1].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[1].contextual_help, &DOWNLOADS_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[1].config, None); + assert_none!(sonarr_data.main_tabs.tabs[1].config); assert_str_eq!(sonarr_data.main_tabs.tabs[2].title, "Blocklist"); assert_eq!( sonarr_data.main_tabs.tabs[2].route, ActiveSonarrBlock::Blocklist.into() ); - assert!(sonarr_data.main_tabs.tabs[2].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[2].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[2].contextual_help, &BLOCKLIST_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[2].config, None); + assert_none!(sonarr_data.main_tabs.tabs[2].config); assert_str_eq!(sonarr_data.main_tabs.tabs[3].title, "History"); assert_eq!( sonarr_data.main_tabs.tabs[3].route, ActiveSonarrBlock::History.into() ); - assert!(sonarr_data.main_tabs.tabs[3].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[3].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[3].contextual_help, &HISTORY_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[3].config, None); + assert_none!(sonarr_data.main_tabs.tabs[3].config); assert_str_eq!(sonarr_data.main_tabs.tabs[4].title, "Root Folders"); assert_eq!( sonarr_data.main_tabs.tabs[4].route, ActiveSonarrBlock::RootFolders.into() ); - assert!(sonarr_data.main_tabs.tabs[4].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[4].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[4].contextual_help, &ROOT_FOLDERS_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[4].config, None); + assert_none!(sonarr_data.main_tabs.tabs[4].config); assert_str_eq!(sonarr_data.main_tabs.tabs[5].title, "Indexers"); assert_eq!( sonarr_data.main_tabs.tabs[5].route, ActiveSonarrBlock::Indexers.into() ); - assert!(sonarr_data.main_tabs.tabs[5].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[5].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[5].contextual_help, &INDEXERS_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[5].config, None); + assert_none!(sonarr_data.main_tabs.tabs[5].config); assert_str_eq!(sonarr_data.main_tabs.tabs[6].title, "System"); assert_eq!( sonarr_data.main_tabs.tabs[6].route, ActiveSonarrBlock::System.into() ); - assert!(sonarr_data.main_tabs.tabs[6].contextual_help.is_some()); - assert_eq!( - sonarr_data.main_tabs.tabs[6].contextual_help.unwrap(), + assert_some_eq_x!( + &sonarr_data.main_tabs.tabs[6].contextual_help, &SYSTEM_CONTEXT_CLUES ); - assert_eq!(sonarr_data.main_tabs.tabs[6].config, None); + assert_none!(sonarr_data.main_tabs.tabs[6].config); assert_eq!(sonarr_data.series_info_tabs.tabs.len(), 2); @@ -209,36 +263,22 @@ mod tests { sonarr_data.series_info_tabs.tabs[0].route, ActiveSonarrBlock::SeriesDetails.into() ); - assert!( - sonarr_data.series_info_tabs.tabs[0] - .contextual_help - .is_some() - ); - assert_eq!( - sonarr_data.series_info_tabs.tabs[0] - .contextual_help - .unwrap(), + assert_some_eq_x!( + &sonarr_data.series_info_tabs.tabs[0].contextual_help, &SERIES_DETAILS_CONTEXT_CLUES ); - assert_eq!(sonarr_data.series_info_tabs.tabs[0].config, None); + assert_none!(sonarr_data.series_info_tabs.tabs[0].config); assert_str_eq!(sonarr_data.series_info_tabs.tabs[1].title, "History"); assert_eq!( sonarr_data.series_info_tabs.tabs[1].route, ActiveSonarrBlock::SeriesHistory.into() ); - assert!( - sonarr_data.series_info_tabs.tabs[1] - .contextual_help - .is_some() - ); - assert_eq!( - sonarr_data.series_info_tabs.tabs[1] - .contextual_help - .unwrap(), + assert_some_eq_x!( + &sonarr_data.series_info_tabs.tabs[1].contextual_help, &SERIES_HISTORY_CONTEXT_CLUES ); - assert_eq!(sonarr_data.series_info_tabs.tabs[1].config, None); + assert_none!(sonarr_data.series_info_tabs.tabs[1].config); } } diff --git a/src/models/servarr_models.rs b/src/models/servarr_models.rs index 8c039bb..477e04c 100644 --- a/src/models/servarr_models.rs +++ b/src/models/servarr_models.rs @@ -89,6 +89,21 @@ pub struct DiskSpace { pub total_space: i64, } +#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IndexerSettings { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub minimum_age: i64, + #[serde(deserialize_with = "super::from_i64")] + pub retention: i64, + #[serde(deserialize_with = "super::from_i64")] + pub maximum_size: i64, + #[serde(deserialize_with = "super::from_i64")] + pub rss_sync_interval: i64, +} + #[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EditIndexerParams { @@ -130,7 +145,7 @@ pub struct HostConfig { pub ssl_cert_password: Option, } -#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Indexer { #[serde(deserialize_with = "super::from_i64")] @@ -153,6 +168,28 @@ pub struct Indexer { pub tags: Vec, } +impl Default for Indexer { + fn default() -> Self { + Self { + id: 0, + name: None, + implementation: None, + implementation_name: None, + config_contract: None, + supports_rss: false, + supports_search: false, + fields: None, + enable_rss: false, + enable_automatic_search: false, + enable_interactive_search: false, + protocol: "".to_string(), + priority: 1, + download_client_id: 0, + tags: vec![], + } + } +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct IndexerTestResult { diff --git a/src/models/servarr_models_tests.rs b/src/models/servarr_models_tests.rs index dfe4cc9..e57dac7 100644 --- a/src/models/servarr_models_tests.rs +++ b/src/models/servarr_models_tests.rs @@ -3,9 +3,30 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use crate::models::servarr_models::{ - AuthenticationMethod, AuthenticationRequired, CertificateValidation, QualityProfile, + AuthenticationMethod, AuthenticationRequired, CertificateValidation, Indexer, QualityProfile, }; + #[test] + fn test_indexer_default() { + let indexer = Indexer::default(); + + assert_eq!(indexer.id, 0); + assert_none!(indexer.name); + assert_none!(indexer.implementation); + assert_none!(indexer.implementation_name); + assert_none!(indexer.config_contract); + assert!(!indexer.supports_rss); + assert!(!indexer.supports_search); + assert_none!(indexer.fields); + assert!(!indexer.enable_rss); + assert!(!indexer.enable_automatic_search); + assert!(!indexer.enable_interactive_search); + assert_is_empty!(indexer.protocol); + assert_eq!(indexer.priority, 1); + assert_eq!(indexer.download_client_id, 0); + assert_is_empty!(indexer.tags); + } + #[test] fn test_authentication_method_display() { assert_str_eq!(AuthenticationMethod::Basic.to_string(), "basic"); diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 3dda720..3de2d11 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -1,6 +1,9 @@ use std::fmt::{Display, Formatter}; -use crate::{models::servarr_models::IndexerTestResult, serde_enum_from}; +use crate::{ + models::servarr_models::{IndexerSettings, IndexerTestResult}, + serde_enum_from, +}; use chrono::{DateTime, Utc}; use clap::ValueEnum; use derivative::Derivative; @@ -31,7 +34,7 @@ pub struct AddSeriesBody { pub root_folder_path: String, pub quality_profile_id: i64, pub language_profile_id: i64, - pub series_type: String, + pub series_type: SeriesType, pub season_folder: bool, pub tags: Vec, #[serde(skip_serializing, skip_deserializing)] @@ -68,7 +71,7 @@ pub struct AddSeriesSearchResultStatistics { #[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AddSeriesOptions { - pub monitor: String, + pub monitor: SeriesMonitor, pub search_for_cutoff_unmet_episodes: bool, pub search_for_missing_episodes: bool, } @@ -221,21 +224,6 @@ pub struct EpisodeFile { pub media_info: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IndexerSettings { - #[serde(deserialize_with = "super::from_i64")] - pub id: i64, - #[serde(deserialize_with = "super::from_i64")] - pub minimum_age: i64, - #[serde(deserialize_with = "super::from_i64")] - pub retention: i64, - #[serde(deserialize_with = "super::from_i64")] - pub maximum_size: i64, - #[serde(deserialize_with = "super::from_i64")] - pub rss_sync_interval: i64, -} - #[derive(Serialize, Deserialize, Derivative, Debug, Clone, PartialEq, Eq)] #[derivative(Default)] #[serde(rename_all = "camelCase")] @@ -549,6 +537,7 @@ pub struct SonarrRelease { pub quality: QualityWrapper, pub full_season: bool, } + #[derive(Default, Serialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "camelCase")] pub struct SonarrReleaseDownloadBody { diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index 30c96fb..0d4c0cf 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -6,14 +6,14 @@ mod tests { use crate::models::{ Serdeable, servarr_models::{ - DiskSpace, HostConfig, Indexer, IndexerTestResult, Language, Log, LogResponse, - QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, + DiskSpace, HostConfig, Indexer, IndexerSettings, IndexerTestResult, Language, Log, + LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus, - DownloadsResponse, Episode, EpisodeFile, IndexerSettings, Series, SeriesMonitor, - SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, - SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus, + DownloadsResponse, Episode, EpisodeFile, Series, SeriesMonitor, SeriesStatus, SeriesType, + SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask, + SonarrTaskName, SystemStatus, }, }; diff --git a/src/models/stateful_table.rs b/src/models/stateful_table.rs index a029570..3648819 100644 --- a/src/models/stateful_table.rs +++ b/src/models/stateful_table.rs @@ -174,9 +174,25 @@ where } pub fn set_filtered_items(&mut self, filtered_items: Vec) { + let items_len = filtered_items.len(); self.filtered_items = Some(filtered_items); + + let preserved_selection = self + .filtered_state + .as_ref() + .and_then(|state| state.selected()) + .map_or(0, |i| { + if i > 0 && i < items_len { + i + } else if i >= items_len && items_len > 0 { + items_len - 1 + } else { + 0 + } + }); + let mut filtered_state: TableState = Default::default(); - filtered_state.select(Some(0)); + filtered_state.select(Some(preserved_selection)); self.filtered_state = Some(filtered_state); } diff --git a/src/models/stateful_table_tests.rs b/src/models/stateful_table_tests.rs index 4cb823e..5e0311d 100644 --- a/src/models/stateful_table_tests.rs +++ b/src/models/stateful_table_tests.rs @@ -390,6 +390,47 @@ mod tests { assert_some_eq_x!(&filtered_stateful_table.filtered_items, &filtered_items_vec); } + #[test] + fn test_stateful_table_set_filtered_items_preserves_selection() { + let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"]; + let mut filtered_stateful_table: StatefulTable<&str> = StatefulTable::default(); + + filtered_stateful_table.set_filtered_items(filtered_items_vec.clone()); + filtered_stateful_table + .filtered_state + .as_mut() + .unwrap() + .select(Some(1)); + + filtered_stateful_table.set_filtered_items(filtered_items_vec.clone()); + + assert_some_eq_x!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + 1 + ); + + filtered_stateful_table + .filtered_state + .as_mut() + .unwrap() + .select(Some(5)); + + filtered_stateful_table.set_filtered_items(filtered_items_vec); + + assert_some_eq_x!( + filtered_stateful_table + .filtered_state + .as_ref() + .unwrap() + .selected(), + 2 + ); + } + #[test] fn test_stateful_table_current_selection() { let mut stateful_table = create_test_stateful_table(); diff --git a/src/network/lidarr_network/blocklist/lidarr_blocklist_network_tests.rs b/src/network/lidarr_network/blocklist/lidarr_blocklist_network_tests.rs new file mode 100644 index 0000000..08540c6 --- /dev/null +++ b/src/network/lidarr_network/blocklist/lidarr_blocklist_network_tests.rs @@ -0,0 +1,353 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{Artist, BlocklistItem, BlocklistResponse, LidarrSerdeable}; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::stateful_table::SortOption; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + artist, blocklist_item, + }; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::{Number, json}; + + #[tokio::test] + async fn test_handle_clear_lidarr_blocklist_event() { + let blocklist_items = vec![ + BlocklistItem { + id: 1, + ..blocklist_item() + }, + BlocklistItem { + id: 2, + ..blocklist_item() + }, + BlocklistItem { + id: 3, + ..blocklist_item() + }, + ]; + let expected_request_json = json!({ "ids": [1, 2, 3]}); + let (mock, app, _server) = MockServarrApi::delete() + .with_request_body(expected_request_json) + .build_for(LidarrEvent::ClearBlocklist) + .await; + app + .lock() + .await + .data + .lidarr_data + .blocklist + .set_items(blocklist_items); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::ClearBlocklist) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_delete_lidarr_blocklist_item_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteBlocklistItem(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .blocklist + .set_items(vec![blocklist_item()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteBlocklistItem(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[rstest] + #[tokio::test] + async fn test_handle_get_lidarr_blocklist_event(#[values(true, false)] use_custom_sorting: bool) { + let blocklist_json = json!({"records": [{ + "artistId": 1007, + "albumIds": [42020], + "sourceTitle": "z artist", + "quality": { "quality": { "name": "Lossless" }}, + "date": "2023-05-20T21:29:16Z", + "protocol": "usenet", + "indexer": "NZBgeek (Prowlarr)", + "message": "test message", + "id": 123, + "artist": { + "id": 1, + "artistName": "Alex", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "overview": "some interesting description of the artist", + "artistType": "Person", + "disambiguation": "American pianist", + "path": "/nfs/music/test-artist", + "members": [{"name": "alex", "instrument": "piano"}], + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["soundtrack"], + "tags": [1], + "added": "2023-01-01T00:00:00Z", + "ratings": { "votes": 15, "value": 8.4 }, + "statistics": { + "albumCount": 1, + "trackFileCount": 15, + "trackCount": 15, + "totalTrackCount": 15, + "sizeOnDisk": 12345, + "percentOfTracks": 99.9 + } + } + }, + { + "artistId": 2001, + "artistTitle": "Test Artist", + "albumIds": [42018], + "sourceTitle": "A Artist", + "quality": { "quality": { "name": "Lossless" }}, + "date": "2023-05-20T21:29:16Z", + "protocol": "usenet", + "indexer": "NZBgeek (Prowlarr)", + "message": "test message", + "id": 456, + "artist": { + "id": 1, + "artistName": "Alex", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "overview": "some interesting description of the artist", + "artistType": "Person", + "disambiguation": "American pianist", + "path": "/nfs/music/test-artist", + "members": [{"name": "alex", "instrument": "piano"}], + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["soundtrack"], + "tags": [1], + "added": "2023-01-01T00:00:00Z", + "ratings": { "votes": 15, "value": 8.4 }, + "statistics": { + "albumCount": 1, + "trackFileCount": 15, + "trackCount": 15, + "totalTrackCount": 15, + "sizeOnDisk": 12345, + "percentOfTracks": 99.9 + } + } + }]}); + let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap(); + let mut expected_blocklist = vec![ + BlocklistItem { + id: 123, + artist_id: 1007, + source_title: "z artist".into(), + album_ids: Some(vec![Number::from(42020)]), + ..blocklist_item() + }, + BlocklistItem { + id: 456, + artist_id: 2001, + source_title: "A Artist".into(), + album_ids: Some(vec![Number::from(42018)]), + ..blocklist_item() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(blocklist_json) + .build_for(LidarrEvent::GetBlocklist) + .await; + app + .lock() + .await + .data + .lidarr_data + .artists + .set_items(vec![Artist { + id: 1007, + artist_name: "Z Artist".into(), + ..artist() + }]); + app.lock().await.data.lidarr_data.blocklist.sort_asc = true; + if use_custom_sorting { + let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + expected_blocklist.sort_by(cmp_fn); + + let blocklist_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .blocklist + .sorting(vec![blocklist_sort_option]); + } + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::BlocklistResponse(blocklist) = network + .handle_lidarr_event(LidarrEvent::GetBlocklist) + .await + .unwrap() + else { + panic!("Expected BlocklistResponse") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.blocklist.items, + expected_blocklist + ); + assert!(app.lock().await.data.lidarr_data.blocklist.sort_asc); + assert_eq!(blocklist, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_blocklist_event_no_op_when_user_is_selecting_sort_options() { + let blocklist_json = json!({"records": [{ + "artistId": 1007, + "albumIds": [42020], + "sourceTitle": "z artist", + "quality": { "quality": { "name": "Lossless" }}, + "date": "2023-05-20T21:29:16Z", + "protocol": "usenet", + "indexer": "NZBgeek (Prowlarr)", + "message": "test message", + "id": 123, + "artist": { + "id": 1, + "artistName": "Alex", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "overview": "some interesting description of the artist", + "artistType": "Person", + "disambiguation": "American pianist", + "path": "/nfs/music/test-artist", + "members": [{"name": "alex", "instrument": "piano"}], + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["soundtrack"], + "tags": [1], + "added": "2023-01-01T00:00:00Z", + "ratings": { "votes": 15, "value": 8.4 }, + "statistics": { + "albumCount": 1, + "trackFileCount": 15, + "trackCount": 15, + "totalTrackCount": 15, + "sizeOnDisk": 12345, + "percentOfTracks": 99.9 + } + } + }, + { + "artistId": 2001, + "albumIds": [42018], + "sourceTitle": "A Artist", + "quality": { "quality": { "name": "Lossless" }}, + "date": "2023-05-20T21:29:16Z", + "protocol": "usenet", + "indexer": "NZBgeek (Prowlarr)", + "message": "test message", + "id": 456, + "artist": { + "id": 1, + "artistName": "Alex", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "overview": "some interesting description of the artist", + "artistType": "Person", + "disambiguation": "American pianist", + "path": "/nfs/music/test-artist", + "members": [{"name": "alex", "instrument": "piano"}], + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["soundtrack"], + "tags": [1], + "added": "2023-01-01T00:00:00Z", + "ratings": { "votes": 15, "value": 8.4 }, + "statistics": { + "albumCount": 1, + "trackFileCount": 15, + "trackCount": 15, + "totalTrackCount": 15, + "sizeOnDisk": 12345, + "percentOfTracks": 99.9 + } + } + }]}); + let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(blocklist_json) + .build_for(LidarrEvent::GetBlocklist) + .await; + app.lock().await.data.lidarr_data.blocklist.sort_asc = true; + app + .lock() + .await + .push_navigation_stack(ActiveLidarrBlock::BlocklistSortPrompt.into()); + let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + let blocklist_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .blocklist + .sorting(vec![blocklist_sort_option]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::BlocklistResponse(blocklist) = network + .handle_lidarr_event(LidarrEvent::GetBlocklist) + .await + .unwrap() + else { + panic!("Expected BlocklistResponse") + }; + mock.assert_async().await; + assert_is_empty!(app.lock().await.data.lidarr_data.blocklist); + assert!(app.lock().await.data.lidarr_data.blocklist.sort_asc); + assert_eq!(blocklist, response); + } +} diff --git a/src/network/lidarr_network/blocklist/mod.rs b/src/network/lidarr_network/blocklist/mod.rs new file mode 100644 index 0000000..6f542e2 --- /dev/null +++ b/src/network/lidarr_network/blocklist/mod.rs @@ -0,0 +1,92 @@ +use crate::models::Route; +use crate::models::lidarr_models::{BlocklistItem, BlocklistResponse}; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::{Value, json}; + +#[cfg(test)] +#[path = "lidarr_blocklist_network_tests.rs"] +mod lidarr_blocklist_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn clear_lidarr_blocklist(&mut self) -> Result<()> { + info!("Clearing Lidarr blocklist"); + let event = LidarrEvent::ClearBlocklist; + + let ids = self + .app + .lock() + .await + .data + .lidarr_data + .blocklist + .items + .iter() + .map(|item| item.id) + .collect::>(); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + Some(json!({"ids": ids})), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn delete_lidarr_blocklist_item( + &mut self, + blocklist_item_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteBlocklistItem(blocklist_item_id); + info!("Deleting Lidarr blocklist item for item with id: {blocklist_item_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{blocklist_item_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_blocklist( + &mut self, + ) -> Result { + info!("Fetching Lidarr blocklist"); + let event = LidarrEvent::GetBlocklist; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| { + if !matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::BlocklistSortPrompt, _) + ) { + let mut blocklist_vec: Vec = blocklist_resp.records; + blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.lidarr_data.blocklist.set_items(blocklist_vec); + app.data.lidarr_data.blocklist.apply_sorting_toggle(false); + } + }) + .await + } +} diff --git a/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs b/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs new file mode 100644 index 0000000..32aa0ec --- /dev/null +++ b/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs @@ -0,0 +1,92 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable}; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_delete_lidarr_download_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteDownload(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteDownload(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_downloads_event() { + let downloads_json = json!({ + "records": [{ + "title": "Test Album", + "status": "downloading", + "id": 1, + "size": 100.0, + "sizeleft": 50.0, + "indexer": "test-indexer" + }] + }); + let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(downloads_json) + .query("pageSize=500") + .build_for(LidarrEvent::GetDownloads(500)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetDownloads(500)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else { + panic!("Expected DownloadsResponse"); + }; + + assert_eq!(downloads_response, response); + assert!(!app.lock().await.data.lidarr_data.downloads.is_empty()); + } + + #[tokio::test] + async fn test_handle_update_lidarr_downloads_event() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "RefreshMonitoredDownloads" + })) + .returns(json!({})) + .build_for(LidarrEvent::UpdateDownloads) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::UpdateDownloads) + .await + .is_ok() + ); + + mock.assert_async().await; + } +} diff --git a/src/network/lidarr_network/downloads/mod.rs b/src/network/lidarr_network/downloads/mod.rs new file mode 100644 index 0000000..d3d6d1f --- /dev/null +++ b/src/network/lidarr_network/downloads/mod.rs @@ -0,0 +1,81 @@ +use crate::models::lidarr_models::DownloadsResponse; +use crate::models::servarr_models::CommandBody; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; + +#[cfg(test)] +#[path = "lidarr_downloads_network_tests.rs"] +mod lidarr_downloads_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_lidarr_download( + &mut self, + download_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteDownload(download_id); + info!("Deleting Lidarr download for download with id: {download_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{download_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_downloads( + &mut self, + count: u64, + ) -> Result { + info!("Fetching Lidarr downloads"); + let event = LidarrEvent::GetDownloads(count); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("pageSize={count}")), + ) + .await; + + self + .handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| { + app + .data + .lidarr_data + .downloads + .set_items(queue_response.records); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn update_lidarr_downloads( + &mut self, + ) -> Result { + info!("Updating Lidarr downloads"); + let event = LidarrEvent::UpdateDownloads; + let body = CommandBody { + name: "RefreshMonitoredDownloads".to_owned(), + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/lidarr_network/history/lidarr_history_network_tests.rs b/src/network/lidarr_network/history/lidarr_history_network_tests.rs new file mode 100644 index 0000000..49e88b9 --- /dev/null +++ b/src/network/lidarr_network/history/lidarr_history_network_tests.rs @@ -0,0 +1,205 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{LidarrHistoryItem, LidarrHistoryWrapper, LidarrSerdeable}; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::stateful_table::SortOption; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::lidarr_history_item; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use rstest::rstest; + use serde_json::json; + + #[rstest] + #[tokio::test] + async fn test_handle_get_lidarr_history_event(#[values(true, false)] use_custom_sorting: bool) { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]}); + let response: LidarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("pageSize=500&sortDirection=descending&sortKey=date") + .build_for(LidarrEvent::GetHistory(500)) + .await; + let mut expected_history_items = vec![ + LidarrHistoryItem { + id: 123, + album_id: 1007, + artist_id: 1007, + track_id: 1007, + source_title: "z album".into(), + ..lidarr_history_item() + }, + LidarrHistoryItem { + id: 456, + album_id: 2001, + artist_id: 2001, + track_id: 2001, + source_title: "An Album".into(), + ..lidarr_history_item() + }, + ]; + { + let mut app_mut = app.lock().await; + app_mut.server_tabs.set_index(2); + app_mut.data.lidarr_data.history.sort_asc = true; + } + if use_custom_sorting { + let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + expected_history_items.sort_by(cmp_fn); + + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .history + .sorting(vec![history_sort_option]); + } + + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetHistory(500)) + .await; + + mock.assert_async().await; + assert!(result.is_ok()); + let LidarrSerdeable::LidarrHistoryWrapper(history) = result.unwrap() else { + panic!("Expected LidarrHistoryWrapper") + }; + assert_eq!( + app.lock().await.data.lidarr_data.history.items, + expected_history_items + ); + assert!(app.lock().await.data.lidarr_data.history.sort_asc); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_history_event_no_op_when_user_is_selecting_sort_options() { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]}); + let response: LidarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("pageSize=500&sortDirection=descending&sortKey=date") + .build_for(LidarrEvent::GetHistory(500)) + .await; + app.lock().await.data.lidarr_data.history.sort_asc = true; + app + .lock() + .await + .push_navigation_stack(ActiveLidarrBlock::HistorySortPrompt.into()); + let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .history + .sorting(vec![history_sort_option]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryWrapper(history) = network + .handle_lidarr_event(LidarrEvent::GetHistory(500)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryWrapper") + }; + mock.assert_async().await; + assert_is_empty!(app.lock().await.data.lidarr_data.history); + assert!(app.lock().await.data.lidarr_data.history.sort_asc); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_mark_lidarr_history_item_as_failed_event() { + let history_item_id = 1234i64; + let (mock, app, _server) = MockServarrApi::post() + .returns(json!({})) + .path("/1234") + .build_for(LidarrEvent::MarkHistoryItemAsFailed(history_item_id)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::MarkHistoryItemAsFailed(history_item_id)) + .await; + + mock.assert_async().await; + assert_ok!(result); + } +} diff --git a/src/network/lidarr_network/history/mod.rs b/src/network/lidarr_network/history/mod.rs new file mode 100644 index 0000000..de6a069 --- /dev/null +++ b/src/network/lidarr_network/history/mod.rs @@ -0,0 +1,63 @@ +use crate::models::Route; +use crate::models::lidarr_models::LidarrHistoryWrapper; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; + +#[cfg(test)] +#[path = "lidarr_history_network_tests.rs"] +mod lidarr_history_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn get_lidarr_history( + &mut self, + events: u64, + ) -> Result { + info!("Fetching all Lidarr history events"); + let event = LidarrEvent::GetHistory(events); + + let params = format!("pageSize={events}&sortDirection=descending&sortKey=date"); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), LidarrHistoryWrapper>(request_props, |history_response, mut app| { + if !matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::HistorySortPrompt, _) + ) { + let mut history_vec = history_response.records; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.lidarr_data.history.set_items(history_vec); + app.data.lidarr_data.history.apply_sorting_toggle(false); + } + }) + .await + } + + pub(in crate::network::lidarr_network) async fn mark_lidarr_history_item_as_failed( + &mut self, + history_item_id: i64, + ) -> Result { + info!("Marking the Lidarr history item with ID: {history_item_id} as 'failed'"); + let event = LidarrEvent::MarkHistoryItemAsFailed(history_item_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + None, + Some(format!("/{history_item_id}")), + None, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs b/src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs new file mode 100644 index 0000000..3ad41fd --- /dev/null +++ b/src/network/lidarr_network/indexers/lidarr_indexers_network_tests.rs @@ -0,0 +1,901 @@ +#[cfg(test)] +mod tests { + use crate::models::HorizontallyScrollableText; + use crate::models::lidarr_models::LidarrSerdeable; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; + use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult}; + use crate::network::NetworkResource; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + indexer, indexer_settings, + }; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use bimap::BiMap; + use mockito::Matcher; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_delete_lidarr_indexer_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteIndexer(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(vec![indexer()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteIndexer(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_all_indexer_settings_event() { + let indexer_settings_json = json!({ + "id": 1, + "minimumAge": 1, + "maximumSize": 12345, + "retention": 1, + "rssSyncInterval": 60 + }); + let (mock, app, _server) = MockServarrApi::put() + .with_request_body(indexer_settings_json) + .build_for(LidarrEvent::EditAllIndexerSettings(indexer_settings())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditAllIndexerSettings(indexer_settings())) + .await + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event() { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none() + { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tags: Some(vec![1, 2]), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details() + { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details() + { + let expected_edit_indexer_params = EditIndexerParams { + indexer_id: 1, + name: Some("Test Update".to_owned()), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: Some("https://localhost:9696/1/".to_owned()), + api_key: Some("test1234".to_owned()), + seed_ratio: Some("1.3".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + priority: Some(0), + ..EditIndexerParams::default() + }; + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "priority": 0, + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(expected_edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_defaults_to_previous_values() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + ..EditIndexerParams::default() + }; + let (mock_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json.clone()) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let mock_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(edit_indexer_params)) + .await + ); + + mock_details_server.assert_async().await; + mock_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_lidarr_indexer_event_clears_tags_when_clear_tags_is_true() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1, 2], + "id": 1 + }); + let expected_edit_indexer_body = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "priority": 1, + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [], + "id": 1 + }); + let edit_indexer_params = EditIndexerParams { + indexer_id: 1, + clear_tags: true, + ..EditIndexerParams::default() + }; + + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1?forceSave=true", + LidarrEvent::EditIndexer(edit_indexer_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_edit_indexer_body)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::EditIndexer(edit_indexer_params)) + .await + ); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_lidarr_indexers_event() { + let indexers_response_json = json!([{ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "supportsRss": true, + "supportsSearch": true, + "protocol": "torrent", + "priority": 25, + "downloadClientId": 0, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "implementationName": "Torznab", + "implementation": "Torznab", + "configContract": "TorznabSettings", + "tags": [1], + "id": 1 + }]); + let response: Vec = serde_json::from_value(indexers_response_json.clone()).unwrap(); + let (async_server, app, _server) = MockServarrApi::get() + .returns(indexers_response_json) + .build_for(LidarrEvent::GetIndexers) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Indexers(indexers) = network + .handle_lidarr_event(LidarrEvent::GetIndexers) + .await + .unwrap() + else { + panic!("Expected Indexers") + }; + + async_server.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.indexers.items, + vec![indexer()] + ); + assert_eq!(indexers, response); + } + + #[tokio::test] + async fn test_handle_test_lidarr_indexer_event_error() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let response_json = json!([ + { + "isWarning": false, + "propertyName": "", + "errorMessage": "test failure", + "severity": "error" + }]); + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json.clone()) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let async_test_server = server + .mock( + "POST", + format!("/api/v1{}", LidarrEvent::TestIndexer(1).resource()).as_str(), + ) + .with_status(400) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json.clone())) + .with_body(response_json.to_string()) + .create_async() + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(vec![indexer()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Value(value) = network + .handle_lidarr_event(LidarrEvent::TestIndexer(1)) + .await + .unwrap() + else { + panic!("Expected Value") + }; + + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.indexer_test_errors, + Some("\"test failure\"".to_owned()) + ); + assert_eq!(value, response_json); + } + + #[tokio::test] + async fn test_handle_test_lidarr_indexer_event_success() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(indexer_details_json.clone()) + .path("/1") + .build_for(LidarrEvent::GetIndexers) + .await; + let async_test_server = server + .mock( + "POST", + format!("/api/v1{}", LidarrEvent::TestIndexer(1).resource()).as_str(), + ) + .with_status(200) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(indexer_details_json.clone())) + .with_body("{}") + .create_async() + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(vec![indexer()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Value(value) = network + .handle_lidarr_event(LidarrEvent::TestIndexer(1)) + .await + .unwrap() + else { + panic!("Expected Value") + }; + async_details_server.assert_async().await; + async_test_server.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.indexer_test_errors, + Some(String::new()) + ); + assert_eq!(value, json!({})); + } + + #[tokio::test] + async fn test_handle_test_all_lidarr_indexers_event() { + let indexers = vec![ + Indexer { + id: 1, + name: Some("Test 1".to_owned()), + ..Indexer::default() + }, + Indexer { + id: 2, + name: Some("Test 2".to_owned()), + ..Indexer::default() + }, + ]; + let indexer_test_results_modal_items = vec![ + IndexerTestResultModalItem { + name: "Test 1".to_owned(), + is_valid: true, + validation_failures: HorizontallyScrollableText::default(), + }, + IndexerTestResultModalItem { + name: "Test 2".to_owned(), + is_valid: false, + validation_failures: "Failure for field 'test field 1': test error message, Failure for field 'test field 2': test error message 2".into(), + }, + ]; + let response_json = json!([ + { + "id": 1, + "isValid": true, + "validationFailures": [] + }, + { + "id": 2, + "isValid": false, + "validationFailures": [ + { + "propertyName": "test field 1", + "errorMessage": "test error message", + "severity": "error" + }, + { + "propertyName": "test field 2", + "errorMessage": "test error message 2", + "severity": "error" + }, + ] + }]); + let response: Vec = serde_json::from_value(response_json.clone()).unwrap(); + let (async_server, app, _server) = MockServarrApi::post() + .returns(response_json) + .status(400) + .build_for(LidarrEvent::TestAllIndexers) + .await; + app + .lock() + .await + .data + .lidarr_data + .indexers + .set_items(indexers); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::IndexerTestResults(results) = network + .handle_lidarr_event(LidarrEvent::TestAllIndexers) + .await + .unwrap() + else { + panic!("Expected IndexerTestResults") + }; + async_server.assert_async().await; + assert_some!(&app.lock().await.data.lidarr_data.indexer_test_all_results); + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items, + indexer_test_results_modal_items + ); + assert_eq!(results, response); + } + + #[tokio::test] + async fn test_handle_test_all_lidarr_indexers_event_sets_empty_table_on_api_error() { + let (async_server, app, _server) = MockServarrApi::post() + .status(500) + .build_for(LidarrEvent::TestAllIndexers) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::TestAllIndexers) + .await; + + async_server.assert_async().await; + assert_err!(result); + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.indexer_test_all_results); + assert_is_empty!( + app + .data + .lidarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + .items + ); + } +} diff --git a/src/network/lidarr_network/indexers/mod.rs b/src/network/lidarr_network/indexers/mod.rs new file mode 100644 index 0000000..b378924 --- /dev/null +++ b/src/network/lidarr_network/indexers/mod.rs @@ -0,0 +1,419 @@ +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_models::{ + EditIndexerParams, Indexer, IndexerSettings, IndexerTestResult, +}; +use crate::models::stateful_table::StatefulTable; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::{Context, Result}; +use log::{debug, info}; +use serde_json::{Value, json}; + +#[cfg(test)] +#[path = "lidarr_indexers_network_tests.rs"] +mod lidarr_indexers_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_lidarr_indexer( + &mut self, + indexer_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteIndexer(indexer_id); + info!("Deleting Lidarr indexer with id: {indexer_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{indexer_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn edit_all_lidarr_indexer_settings( + &mut self, + params: IndexerSettings, + ) -> Result { + info!("Updating Lidarr indexer settings"); + let event = LidarrEvent::EditAllIndexerSettings(IndexerSettings::default()); + debug!("Indexer settings body: {params:?}"); + + let request_props = self + .request_props_from(event, RequestMethod::Put, Some(params), None, None) + .await; + + self + .handle_request::(request_props, |_, _| {}) + .await + } + + pub(in crate::network::lidarr_network) async fn get_all_lidarr_indexer_settings( + &mut self, + ) -> Result { + info!("Fetching Lidarr indexer settings"); + let event = LidarrEvent::GetAllIndexerSettings; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| { + if app.data.lidarr_data.indexer_settings.is_none() { + app.data.lidarr_data.indexer_settings = Some(indexer_settings); + } else { + debug!("Indexer Settings are being modified. Ignoring update..."); + } + }) + .await + } + + pub(in crate::network::lidarr_network) async fn edit_lidarr_indexer( + &mut self, + mut edit_indexer_params: EditIndexerParams, + ) -> Result<()> { + if let Some(tag_input_str) = edit_indexer_params.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + edit_indexer_params.tags = Some(tag_ids_vec); + } + let detail_event = LidarrEvent::GetIndexers; + let event = LidarrEvent::EditIndexer(EditIndexerParams::default()); + let id = edit_indexer_params.indexer_id; + info!("Updating Lidarr indexer with ID: {id}"); + info!("Fetching indexer details for indexer with ID: {id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { + response = detailed_indexer_body.to_string() + }) + .await?; + + info!("Constructing edit indexer body"); + + let mut detailed_indexer_body: Value = serde_json::from_str(&response)?; + + let ( + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + tags, + priority, + ) = { + let priority = detailed_indexer_body["priority"] + .as_i64() + .context("Failed to deserialize indexer 'priority' field")?; + let seed_ratio_field_option = detailed_indexer_body["fields"] + .as_array() + .context("Failed to get indexer 'fields' array")? + .iter() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + let name = edit_indexer_params.name.unwrap_or( + detailed_indexer_body["name"] + .as_str() + .context("Failed to deserialize indexer 'name' field")? + .to_owned(), + ); + let enable_rss = edit_indexer_params.enable_rss.unwrap_or( + detailed_indexer_body["enableRss"] + .as_bool() + .context("Failed to deserialize indexer 'enableRss' field")?, + ); + let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or( + detailed_indexer_body["enableAutomaticSearch"] + .as_bool() + .context("Failed to deserialize indexer 'enableAutomaticSearch' field")?, + ); + let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or( + detailed_indexer_body["enableInteractiveSearch"] + .as_bool() + .context("Failed to deserialize indexer 'enableInteractiveSearch' field")?, + ); + let url = edit_indexer_params.url.unwrap_or( + detailed_indexer_body["fields"] + .as_array() + .context("Failed to get indexer 'fields' array for baseUrl")? + .iter() + .find(|field| field["name"] == "baseUrl") + .context("Field 'baseUrl' was not found in the indexer fields array")? + .get("value") + .unwrap_or(&json!("")) + .as_str() + .context("Failed to deserialize indexer 'baseUrl' value")? + .to_owned(), + ); + let api_key = edit_indexer_params.api_key.unwrap_or( + detailed_indexer_body["fields"] + .as_array() + .context("Failed to get indexer 'fields' array for apiKey")? + .iter() + .find(|field| field["name"] == "apiKey") + .context("Field 'apiKey' was not found in the indexer fields array")? + .get("value") + .unwrap_or(&json!("")) + .as_str() + .context("Failed to deserialize indexer 'apiKey' value")? + .to_owned(), + ); + let seed_ratio = edit_indexer_params.seed_ratio.unwrap_or_else(|| { + if let Some(seed_ratio_field) = seed_ratio_field_option { + return seed_ratio_field + .get("value") + .unwrap_or(&json!("")) + .as_str() + .unwrap_or("") + .to_owned(); + } + + String::new() + }); + let tags = if edit_indexer_params.clear_tags { + vec![] + } else { + edit_indexer_params.tags.unwrap_or( + detailed_indexer_body["tags"] + .as_array() + .context("Failed to get indexer 'tags' array")? + .iter() + .map(|item| { + item + .as_i64() + .context("Failed to deserialize indexer tag ID") + }) + .collect::>>()?, + ) + }; + let priority = edit_indexer_params.priority.unwrap_or(priority); + + ( + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + tags, + priority, + ) + }; + + *detailed_indexer_body + .get_mut("name") + .context("Failed to get mutable reference to indexer 'name' field")? = json!(name); + *detailed_indexer_body + .get_mut("priority") + .context("Failed to get mutable reference to indexer 'priority' field")? = json!(priority); + *detailed_indexer_body + .get_mut("enableRss") + .context("Failed to get mutable reference to indexer 'enableRss' field")? = json!(enable_rss); + *detailed_indexer_body + .get_mut("enableAutomaticSearch") + .context("Failed to get mutable reference to indexer 'enableAutomaticSearch' field")? = + json!(enable_automatic_search); + *detailed_indexer_body + .get_mut("enableInteractiveSearch") + .context("Failed to get mutable reference to indexer 'enableInteractiveSearch' field")? = + json!(enable_interactive_search); + *detailed_indexer_body + .get_mut("fields") + .and_then(|f| f.as_array_mut()) + .context("Failed to get mutable reference to indexer 'fields' array")? + .iter_mut() + .find(|field| field["name"] == "baseUrl") + .context("Failed to find 'baseUrl' field in indexer fields array")? + .get_mut("value") + .context("Failed to get mutable reference to 'baseUrl' value")? = json!(url); + *detailed_indexer_body + .get_mut("fields") + .and_then(|f| f.as_array_mut()) + .context("Failed to get mutable reference to indexer 'fields' array for apiKey")? + .iter_mut() + .find(|field| field["name"] == "apiKey") + .context("Failed to find 'apiKey' field in indexer fields array")? + .get_mut("value") + .context("Failed to get mutable reference to 'apiKey' value")? = json!(api_key); + *detailed_indexer_body + .get_mut("tags") + .context("Failed to get mutable reference to indexer 'tags' field")? = json!(tags); + let seed_ratio_field_option = detailed_indexer_body + .get_mut("fields") + .and_then(|f| f.as_array_mut()) + .context("Failed to get mutable reference to indexer 'fields' array for seed ratio")? + .iter_mut() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field + .as_object_mut() + .context("Failed to get mutable reference to 'seedCriteria.seedRatio' object")? + .insert("value".to_string(), json!(seed_ratio)); + } + + debug!("Edit indexer body: {detailed_indexer_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_indexer_body), + Some(format!("/{id}")), + Some("forceSave=true".to_owned()), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_indexers( + &mut self, + ) -> Result> { + info!("Fetching Lidarr indexers"); + let event = LidarrEvent::GetIndexers; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |indexers, mut app| { + app.data.lidarr_data.indexers.set_items(indexers); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn test_lidarr_indexer( + &mut self, + indexer_id: i64, + ) -> Result { + let detail_event = LidarrEvent::GetIndexers; + let event = LidarrEvent::TestIndexer(indexer_id); + info!("Testing Lidarr indexer with ID: {indexer_id}"); + + info!("Fetching indexer details for indexer with ID: {indexer_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{indexer_id}")), + None, + ) + .await; + + let mut test_body: Value = Value::default(); + + self + .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { + test_body = detailed_indexer_body; + }) + .await?; + + info!("Testing indexer"); + + let mut request_props = self + .request_props_from(event, RequestMethod::Post, Some(test_body), None, None) + .await; + request_props.ignore_status_code = true; + + self + .handle_request::(request_props, |test_results, mut app| { + if test_results.as_object().is_none() { + let error_message = test_results + .as_array() + .and_then(|arr| arr.first()) + .and_then(|item| item.get("errorMessage")) + .map(|msg| msg.to_string()) + .unwrap_or_else(|| "Unknown indexer test error".to_string()); + app.data.lidarr_data.indexer_test_errors = Some(error_message); + } else { + app.data.lidarr_data.indexer_test_errors = Some(String::new()); + }; + }) + .await + } + + pub(in crate::network::lidarr_network) async fn test_all_lidarr_indexers( + &mut self, + ) -> Result> { + info!("Testing all Lidarr indexers"); + let event = LidarrEvent::TestAllIndexers; + + let mut request_props = self + .request_props_from(event, RequestMethod::Post, None, None, None) + .await; + request_props.ignore_status_code = true; + + let result = self + .handle_request::<(), Vec>(request_props, |test_results, mut app| { + let mut test_all_indexer_results = StatefulTable::default(); + let indexers = app.data.lidarr_data.indexers.items.clone(); + let modal_test_results = test_results + .iter() + .map(|result| { + let name = indexers + .iter() + .filter(|&indexer| indexer.id == result.id) + .map(|indexer| indexer.name.clone()) + .nth(0) + .unwrap_or_default(); + let validation_failures = result + .validation_failures + .iter() + .map(|failure| { + format!( + "Failure for field '{}': {}", + failure.property_name, failure.error_message + ) + }) + .collect::>() + .join(", "); + + IndexerTestResultModalItem { + name: name.unwrap_or_default(), + is_valid: result.is_valid, + validation_failures: validation_failures.into(), + } + }) + .collect(); + test_all_indexer_results.set_items(modal_test_results); + app.data.lidarr_data.indexer_test_all_results = Some(test_all_indexer_results); + }) + .await; + + if result.is_err() { + self + .app + .lock() + .await + .data + .lidarr_data + .indexer_test_all_results = Some(StatefulTable::default()); + } + + result + } +} diff --git a/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs b/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs new file mode 100644 index 0000000..5955ae6 --- /dev/null +++ b/src/network/lidarr_network/library/albums/lidarr_albums_network_tests.rs @@ -0,0 +1,629 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{ + Album, DeleteParams, LidarrHistoryItem, LidarrRelease, LidarrSerdeable, + }; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal; + use crate::models::stateful_table::SortOption; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + ALBUM_JSON, lidarr_history_item, torrent_release, + }; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use mockito::Matcher; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::{Value, json}; + + #[tokio::test] + async fn test_handle_get_albums_event() { + let albums_json = json!([{ + "id": 1, + "title": "Test Album", + "foreignAlbumId": "test-foreign-album-id", + "monitored": true, + "anyReleaseOk": true, + "profileId": 1, + "duration": 180, + "albumType": "Album", + "genres": ["Classical"], + "ratings": {"votes": 15, "value": 8.4}, + "releaseDate": "2023-01-01T00:00:00Z", + "statistics": { + "trackFileCount": 10, + "trackCount": 10, + "totalTrackCount": 10, + "sizeOnDisk": 1024, + "percentOfTracks": 99.9 + } + }]); + let response: Vec = serde_json::from_value(albums_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(albums_json) + .query("artistId=1") + .build_for(LidarrEvent::GetAlbums(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::GetAlbums(1)).await; + + mock.assert_async().await; + + let LidarrSerdeable::Albums(albums) = result.unwrap() else { + panic!("Expected Albums"); + }; + + assert_eq!(albums, response); + assert!(!app.lock().await.data.lidarr_data.albums.is_empty()); + } + + #[tokio::test] + async fn test_handle_delete_album_event() { + let delete_album_params = DeleteParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let (async_server, app, _server) = MockServarrApi::delete() + .path("/1") + .query("deleteFiles=true&addImportListExclusion=true") + .build_for(LidarrEvent::DeleteAlbum(delete_album_params.clone())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteAlbum(delete_album_params)) + .await + .is_ok() + ); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_toggle_album_monitoring_event() { + let mut expected_body: Value = serde_json::from_str(ALBUM_JSON).unwrap(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + let (get_mock, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(ALBUM_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetAlbums(1)) + .await; + let put_mock = server + .mock("PUT", "/api/v1/album/1") + .match_body(Matcher::Json(expected_body)) + .match_header("X-Api-Key", "test1234") + .with_status(202) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::ToggleAlbumMonitoring(1)) + .await + ); + + get_mock.assert_async().await; + put_mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_album_details_event() { + let expected_album: Album = serde_json::from_str(ALBUM_JSON).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(serde_json::from_str(ALBUM_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetAlbumDetails(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetAlbumDetails(1)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Album(album) = result.unwrap() else { + panic!("Expected Album"); + }; + + assert_eq!(album, expected_album); + } + + #[rstest] + #[tokio::test] + async fn test_handle_get_lidarr_album_history_event( + #[values(true, false)] use_custom_sorting: bool, + ) { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let mut expected_history_items = vec![ + LidarrHistoryItem { + id: 123, + artist_id: 1007, + album_id: 1007, + track_id: 1007, + source_title: "z album".into(), + ..lidarr_history_item() + }, + LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "An Album".into(), + ..lidarr_history_item() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=1&albumId=1") + .build_for(LidarrEvent::GetAlbumHistory(1, 1)) + .await; + app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + if use_custom_sorting { + let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + expected_history_items.sort_by(cmp_fn); + + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_history + .sorting(vec![history_sort_option]); + } + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_history + .sort_asc = true; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetAlbumHistory(1, 1)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .items, + expected_history_items + ); + assert!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .sort_asc + ); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_album_history_event_empty_album_details_modal() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + LidarrHistoryItem { + id: 123, + artist_id: 1007, + album_id: 1007, + track_id: 1007, + source_title: "z album".into(), + ..lidarr_history_item() + }, + LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "An Album".into(), + ..lidarr_history_item() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=1&albumId=1") + .build_for(LidarrEvent::GetAlbumHistory(1, 1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetAlbumHistory(1, 1)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.album_details_modal); + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .items, + expected_history_items + ); + assert!( + !app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .sort_asc + ); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_album_history_event_no_op_when_user_is_selecting_sort_options() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=1&albumId=1") + .build_for(LidarrEvent::GetAlbumHistory(1, 1)) + .await; + app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_history + .sort_asc = true; + app + .lock() + .await + .push_navigation_stack(ActiveLidarrBlock::AlbumHistorySortPrompt.into()); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetAlbumHistory(1, 1)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + assert_is_empty!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .items + ); + assert!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_history + .sort_asc + ); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_album_releases_event() { + let release_json = json!([ + { + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "artistName": "Alex", + "albumTitle": "Something", + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "quality": { "quality": { "name": "Lossless" }}, + "discography": true + }, + { + "guid": "4567", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "artistName": "Alex", + "albumTitle": "Something", + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "quality": { "quality": { "name": "Lossless" }}, + } + ]); + let expected_filtered_lidarr_release = LidarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }; + let expected_raw_lidarr_releases = vec![ + LidarrRelease { + discography: true, + ..torrent_release() + }, + LidarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(release_json) + .query("artistId=1&albumId=1") + .build_for(LidarrEvent::GetAlbumReleases(1, 1)) + .await; + app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Releases(releases_vec) = network + .handle_lidarr_event(LidarrEvent::GetAlbumReleases(1, 1)) + .await + .unwrap() + else { + panic!("Expected Releases") + }; + + mock.assert_async().await; + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_releases + .items, + vec![expected_filtered_lidarr_release] + ); + assert_eq!(releases_vec, expected_raw_lidarr_releases); + } + + #[tokio::test] + async fn test_handle_get_album_releases_event_empty_album_details_modal() { + let release_json = json!([ + { + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "artistName": "Alex", + "albumTitle": "Something", + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "quality": { "quality": { "name": "Lossless" }}, + "discography": true + }, + { + "guid": "4567", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "artistName": "Alex", + "albumTitle": "Something", + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "quality": { "quality": { "name": "Lossless" }}, + } + ]); + let expected_lidarr_release = LidarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }; + let (mock, app, _server) = MockServarrApi::get() + .returns(release_json) + .query("artistId=1&albumId=1") + .build_for(LidarrEvent::GetAlbumReleases(1, 1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::GetAlbumReleases(1, 1)) + .await + ); + + mock.assert_async().await; + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .album_releases + .items, + vec![expected_lidarr_release] + ); + } + + #[tokio::test] + async fn test_handle_trigger_automatic_album_search_event() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "AlbumSearch", + "albumIds": [1] + })) + .returns(json!({})) + .build_for(LidarrEvent::TriggerAutomaticAlbumSearch(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::TriggerAutomaticAlbumSearch(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } +} diff --git a/src/network/lidarr_network/library/albums/mod.rs b/src/network/lidarr_network/library/albums/mod.rs new file mode 100644 index 0000000..d99f1c5 --- /dev/null +++ b/src/network/lidarr_network/library/albums/mod.rs @@ -0,0 +1,255 @@ +use crate::models::Route; +use crate::models::lidarr_models::{ + Album, DeleteParams, LidarrCommandBody, LidarrHistoryItem, LidarrRelease, +}; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::{debug, info, warn}; +use serde_json::{Value, json}; + +#[cfg(test)] +#[path = "lidarr_albums_network_tests.rs"] +mod lidarr_albums_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn get_albums( + &mut self, + artist_id: i64, + ) -> Result> { + info!("Fetching albums for Lidarr artist with ID: {artist_id}"); + let event = LidarrEvent::GetAlbums(artist_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("artistId={artist_id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |mut albums_vec, mut app| { + albums_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.lidarr_data.albums.set_items(albums_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_album_details( + &mut self, + album_id: i64, + ) -> Result { + info!("Fetching details for Lidarr album with ID: {album_id}"); + let event = LidarrEvent::GetAlbumDetails(album_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + Some(format!("/{album_id}")), + None, + ) + .await; + + self + .handle_request::<(), Album>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_album_history( + &mut self, + artist_id: i64, + album_id: i64, + ) -> Result> { + let event = LidarrEvent::GetAlbumHistory(artist_id, album_id); + info!("Fetching history for artist with ID: {artist_id} and album with ID: {album_id}"); + + let params = format!("artistId={artist_id}&albumId={album_id}"); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), Vec>(request_props, |history_items, mut app| { + let is_sorting = matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::AlbumHistorySortPrompt, _) + ); + + if !is_sorting { + let album_details_modal = app + .data + .lidarr_data + .album_details_modal + .get_or_insert_default(); + + let mut history_vec = history_items; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + album_details_modal.album_history.set_items(history_vec); + album_details_modal + .album_history + .apply_sorting_toggle(false); + } + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_album_releases( + &mut self, + artist_id: i64, + album_id: i64, + ) -> Result> { + let event = LidarrEvent::GetAlbumReleases(artist_id, album_id); + info!("Fetching releases for artist with ID: {artist_id} and album with ID: {album_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("artistId={artist_id}&albumId={album_id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |release_vec, mut app| { + let album_details_modal = app + .data + .lidarr_data + .album_details_modal + .get_or_insert_default(); + + let album_releases_vec = release_vec + .into_iter() + .filter(|release| !release.discography) + .collect(); + + album_details_modal + .album_releases + .set_items(album_releases_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn delete_album( + &mut self, + delete_album_params: DeleteParams, + ) -> Result<()> { + let event = LidarrEvent::DeleteAlbum(DeleteParams::default()); + let DeleteParams { + id, + delete_files, + add_import_list_exclusion, + } = delete_album_params; + + info!( + "Deleting Lidarr album with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}" + ); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{id}")), + Some(format!( + "deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}" + )), + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn toggle_album_monitoring( + &mut self, + album_id: i64, + ) -> Result<()> { + let event = LidarrEvent::ToggleAlbumMonitoring(album_id); + info!("Toggling album monitoring for album with ID: {album_id}"); + info!("Fetching album details for album with ID: {album_id}"); + + let detail_event = LidarrEvent::GetAlbums(0); + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{album_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_album_body, _| { + response = detailed_album_body.to_string() + }) + .await?; + + info!("Constructing toggle album monitoring body"); + + match serde_json::from_str::(&response) { + Ok(mut detailed_album_body) => { + let monitored = detailed_album_body + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + + *detailed_album_body.get_mut("monitored").unwrap() = json!(!monitored); + + debug!("Toggle album monitoring body: {detailed_album_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_album_body), + Some(format!("/{album_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + Err(_) => { + warn!("Request for detailed album body was interrupted"); + Ok(()) + } + } + } + + pub(in crate::network::lidarr_network) async fn trigger_automatic_album_search( + &mut self, + album_id: i64, + ) -> Result { + let event = LidarrEvent::TriggerAutomaticAlbumSearch(album_id); + info!("Searching indexers for album with ID: {album_id}"); + + let body = LidarrCommandBody { + name: "AlbumSearch".to_owned(), + album_ids: Some(vec![album_id]), + ..LidarrCommandBody::default() + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs b/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs new file mode 100644 index 0000000..58e4d09 --- /dev/null +++ b/src/network/lidarr_network/library/artists/lidarr_artists_network_tests.rs @@ -0,0 +1,940 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{ + AddArtistBody, AddArtistOptions, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, + LidarrHistoryItem, LidarrRelease, LidarrSerdeable, MonitorType, NewItemMonitorType, + }; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::stateful_table::SortOption; + use crate::network::NetworkResource; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + ADD_ARTIST_SEARCH_RESULT_JSON, ARTIST_JSON, artist, lidarr_history_item, torrent_release, + }; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use bimap::BiMap; + use mockito::Matcher; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::{Value, json}; + + #[tokio::test] + async fn test_handle_list_artists_event() { + let artists_json = json!([{ + "id": 1, + "artistName": "Test Artist", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "path": "/music/test-artist", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": [], + "tags": [], + "added": "2023-01-01T00:00:00Z" + }]); + let response: Vec = serde_json::from_value(artists_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(artists_json) + .build_for(LidarrEvent::ListArtists) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::ListArtists).await; + + mock.assert_async().await; + + let LidarrSerdeable::Artists(artists) = result.unwrap() else { + panic!("Expected Artists"); + }; + + assert_eq!(artists, response); + assert!(!app.lock().await.data.lidarr_data.artists.is_empty()); + } + + #[tokio::test] + async fn test_handle_delete_artist_event() { + let delete_artist_params = DeleteParams { + id: 1, + delete_files: true, + add_import_list_exclusion: true, + }; + let (async_server, app, _server) = MockServarrApi::delete() + .path("/1") + .query("deleteFiles=true&addImportListExclusion=true") + .build_for(LidarrEvent::DeleteArtist(delete_artist_params.clone())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteArtist(delete_artist_params)) + .await + .is_ok() + ); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_artist_details_event() { + let expected_artist: Artist = serde_json::from_str(ARTIST_JSON).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(serde_json::from_str(ARTIST_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetArtistDetails(1)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Artist(artist) = result.unwrap() else { + panic!("Expected Artist"); + }; + + assert_eq!(artist, expected_artist); + } + + #[rstest] + #[tokio::test] + async fn test_handle_get_lidarr_artist_history_event( + #[values(true, false)] use_custom_sorting: bool, + ) { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let mut expected_history_items = vec![ + LidarrHistoryItem { + id: 123, + artist_id: 1007, + album_id: 1007, + track_id: 1007, + source_title: "z album".into(), + ..lidarr_history_item() + }, + LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "An Album".into(), + ..lidarr_history_item() + }, + ]; + let (async_server, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=1") + .build_for(LidarrEvent::GetArtistHistory(1)) + .await; + if use_custom_sorting { + let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + expected_history_items.sort_by(cmp_fn); + + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .artist_history + .sorting(vec![history_sort_option]); + } + app + .lock() + .await + .data + .lidarr_data + .artists + .set_items(vec![artist()]); + app.lock().await.data.lidarr_data.artist_history.sort_asc = true; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history_items) = network + .handle_lidarr_event(LidarrEvent::GetArtistHistory(1)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + async_server.assert_async().await; + let app = app.lock().await; + assert_eq!( + app.data.lidarr_data.artist_history.items, + expected_history_items + ); + assert!(app.data.lidarr_data.artist_history.sort_asc); + assert_eq!(history_items, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_artist_history_event_empty_artist_history_table() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + LidarrHistoryItem { + id: 123, + artist_id: 1007, + album_id: 1007, + track_id: 1007, + source_title: "z album".into(), + ..lidarr_history_item() + }, + LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "An Album".into(), + ..lidarr_history_item() + }, + ]; + let (async_server, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=1") + .build_for(LidarrEvent::GetArtistHistory(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .artists + .set_items(vec![artist()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history_items) = network + .handle_lidarr_event(LidarrEvent::GetArtistHistory(1)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + async_server.assert_async().await; + let app = app.lock().await; + assert_eq!( + app.data.lidarr_data.artist_history.items, + expected_history_items + ); + assert!(!app.data.lidarr_data.artist_history.sort_asc); + assert_eq!(history_items, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_artist_history_event_no_op_when_user_is_selecting_sort_options() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z album", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "An Album", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let (async_server, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=1") + .build_for(LidarrEvent::GetArtistHistory(1)) + .await; + let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .artist_history + .sorting(vec![history_sort_option]); + app.lock().await.data.lidarr_data.artist_history.sort_asc = true; + app + .lock() + .await + .data + .lidarr_data + .artists + .set_items(vec![artist()]); + app + .lock() + .await + .push_navigation_stack(ActiveLidarrBlock::ArtistHistorySortPrompt.into()); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history_items) = network + .handle_lidarr_event(LidarrEvent::GetArtistHistory(1)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + async_server.assert_async().await; + let app = app.lock().await; + assert!(app.data.lidarr_data.artist_history.is_empty()); + assert!(app.data.lidarr_data.artist_history.sort_asc); + assert_eq!(history_items, response); + } + + #[tokio::test] + async fn test_handle_get_artist_discography_releases_event() { + let release_json = json!([ + { + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "artistName": "Alex", + "albumTitle": "Something", + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "quality": { "quality": { "name": "Lossless" }}, + "discography": true + }, + { + "guid": "4567", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "artistName": "Alex", + "albumTitle": "Something", + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "quality": { "quality": { "name": "Lossless" }}, + } + ]); + let expected_filtered_lidarr_release = LidarrRelease { + discography: true, + ..torrent_release() + }; + let expected_raw_lidarr_releases = vec![ + LidarrRelease { + discography: true, + ..torrent_release() + }, + LidarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(release_json) + .query("artistId=1") + .build_for(LidarrEvent::GetDiscographyReleases(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .artists + .set_items(vec![artist()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Releases(releases_vec) = network + .handle_lidarr_event(LidarrEvent::GetDiscographyReleases(1)) + .await + .unwrap() + else { + panic!("Expected Releases") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.discography_releases.items, + vec![expected_filtered_lidarr_release] + ); + assert_eq!(releases_vec, expected_raw_lidarr_releases); + } + + #[tokio::test] + async fn test_handle_toggle_artist_monitoring_event() { + let artist_json = json!({ + "id": 1, + "artistName": "Test Artist", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "path": "/music/test-artist", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": [], + "tags": [], + "added": "2023-01-01T00:00:00Z" + }); + let mut expected_body = artist_json.clone(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + let (get_mock, app, mut server) = MockServarrApi::get() + .returns(artist_json) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + let put_mock = server + .mock("PUT", "/api/v1/artist/1") + .match_body(Matcher::Json(expected_body)) + .match_header("X-Api-Key", "test1234") + .with_status(202) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::ToggleArtistMonitoring(1)) + .await + ); + + get_mock.assert_async().await; + put_mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_update_all_artists_event() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "RefreshArtist" + })) + .returns(json!({})) + .build_for(LidarrEvent::UpdateAllArtists) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::UpdateAllArtists) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_update_and_scan_artist_event() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "RefreshArtist", + "artistId": 1 + })) + .returns(json!({})) + .build_for(LidarrEvent::UpdateAndScanArtist(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::UpdateAndScanArtist(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_trigger_automatic_artist_search_event() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "ArtistSearch", + "artistId": 1 + })) + .returns(json!({})) + .build_for(LidarrEvent::TriggerAutomaticArtistSearch(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::TriggerAutomaticArtistSearch(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_artist_event() { + let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + *expected_body.get_mut("monitorNewItems").unwrap() = json!("none"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); + *expected_body.get_mut("metadataProfileId").unwrap() = json!(2222); + *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); + *expected_body.get_mut("tags").unwrap() = json!([1, 2]); + let edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::None), + quality_profile_id: Some(1111), + metadata_profile_id: Some(2222), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tag_input_string: Some("usenet, testing".to_owned()), + ..EditArtistParams::default() + }; + + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(ARTIST_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1", + LidarrEvent::EditArtist(edit_artist_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params)) + .await + .is_ok() + ); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_artist_event_does_not_overwrite_tag_ids_vec_when_tag_input_string_is_none() + { + let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + *expected_body.get_mut("monitorNewItems").unwrap() = json!("none"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); + *expected_body.get_mut("metadataProfileId").unwrap() = json!(2222); + *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); + *expected_body.get_mut("tags").unwrap() = json!([1, 2]); + let edit_artist_params = EditArtistParams { + artist_id: 1, + monitored: Some(false), + monitor_new_items: Some(NewItemMonitorType::None), + quality_profile_id: Some(1111), + metadata_profile_id: Some(2222), + root_folder_path: Some("/nfs/Test Path".to_owned()), + tags: Some(vec![1, 2]), + ..EditArtistParams::default() + }; + + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(ARTIST_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1", + LidarrEvent::EditArtist(edit_artist_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params)) + .await + .is_ok() + ); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_artist_event_defaults_to_previous_values() { + let edit_artist_params = EditArtistParams { + artist_id: 1, + ..EditArtistParams::default() + }; + let expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap(); + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(ARTIST_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1", + LidarrEvent::EditArtist(edit_artist_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params)) + .await + .is_ok() + ); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_edit_artist_event_returns_empty_tags_vec_when_clear_tags_is_true() { + let mut expected_body: Value = serde_json::from_str(ARTIST_JSON).unwrap(); + *expected_body.get_mut("tags").unwrap() = json!([]); + + let (async_details_server, app, mut server) = MockServarrApi::get() + .returns(serde_json::from_str(ARTIST_JSON).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetArtistDetails(1)) + .await; + let edit_artist_params = EditArtistParams { + artist_id: 1, + clear_tags: true, + ..EditArtistParams::default() + }; + let async_edit_server = server + .mock( + "PUT", + format!( + "/api/v1{}/1", + LidarrEvent::EditArtist(edit_artist_params.clone()).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::EditArtist(edit_artist_params)) + .await + .is_ok() + ); + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_search_new_artist_event() { + let search_results_json = + json!([serde_json::from_str::(ADD_ARTIST_SEARCH_RESULT_JSON).unwrap()]); + let expected_results: Vec = + serde_json::from_value(search_results_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(search_results_json) + .query("term=test%20artist") + .build_for(LidarrEvent::SearchNewArtist("test artist".to_owned())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::SearchNewArtist("test artist".to_owned())) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::AddArtistSearchResults(search_results) = result.unwrap() else { + panic!("Expected AddArtistSearchResults"); + }; + + assert_eq!(search_results, expected_results); + assert_some!(&app.lock().await.data.lidarr_data.add_searched_artists); + } + + #[tokio::test] + async fn test_handle_search_new_artist_event_navigates_to_empty_results_when_empty() { + let (mock, app, _server) = MockServarrApi::get() + .returns(json!([])) + .query("term=nonexistent") + .build_for(LidarrEvent::SearchNewArtist("nonexistent".to_owned())) + .await; + app.lock().await.server_tabs.set_index(2); + app + .lock() + .await + .push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::SearchNewArtist("nonexistent".to_owned())) + .await; + + mock.assert_async().await; + + assert_ok!(result); + let app = app.lock().await; + assert_none!(&app.data.lidarr_data.add_searched_artists); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddArtistEmptySearchResults.into() + ); + } + + #[tokio::test] + async fn test_handle_search_new_artist_event_sets_empty_table_on_api_error() { + let (mock, app, _server) = MockServarrApi::get() + .status(500) + .query("term=nonexistent") + .build_for(LidarrEvent::SearchNewArtist("nonexistent".to_owned())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::SearchNewArtist("nonexistent".to_owned())) + .await; + + mock.assert_async().await; + + assert_err!(result); + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.add_searched_artists); + assert_is_empty!(app.data.lidarr_data.add_searched_artists.as_ref().unwrap()); + } + + #[tokio::test] + async fn test_handle_add_artist_event() { + let add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_owned(), + artist_name: "Test Artist".to_owned(), + monitored: true, + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: Vec::default(), + tag_input_string: Some("usenet, testing".to_owned()), + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: true, + }, + }; + let expected_body = json!({ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "monitored": true, + "rootFolderPath": "/music", + "qualityProfileId": 1, + "metadataProfileId": 1, + "tags": [1, 2], + "addOptions": { + "monitor": "all", + "monitorNewItems": "all", + "searchForMissingAlbums": true + } + }); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(expected_body) + .returns(json!({"id": 1})) + .build_for(LidarrEvent::AddArtist(AddArtistBody::default())) + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::AddArtist(add_artist_body)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_add_artist_event_does_not_overwrite_tags_vec_when_tag_input_string_is_none() + { + let add_artist_body = AddArtistBody { + foreign_artist_id: "test-foreign-id".to_owned(), + artist_name: "Test Artist".to_owned(), + monitored: true, + root_folder_path: "/music".to_owned(), + quality_profile_id: 1, + metadata_profile_id: 1, + tags: vec![1, 2], + tag_input_string: None, + add_options: AddArtistOptions { + monitor: MonitorType::All, + monitor_new_items: NewItemMonitorType::All, + search_for_missing_albums: true, + }, + }; + let expected_body = json!({ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "monitored": true, + "rootFolderPath": "/music", + "qualityProfileId": 1, + "metadataProfileId": 1, + "tags": [1, 2], + "addOptions": { + "monitor": "all", + "monitorNewItems": "all", + "searchForMissingAlbums": true + } + }); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(expected_body) + .returns(json!({"id": 1})) + .build_for(LidarrEvent::AddArtist(add_artist_body.clone())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::AddArtist(add_artist_body)) + .await + .is_ok() + ); + + mock.assert_async().await; + } +} diff --git a/src/network/lidarr_network/library/artists/mod.rs b/src/network/lidarr_network/library/artists/mod.rs new file mode 100644 index 0000000..484fa66 --- /dev/null +++ b/src/network/lidarr_network/library/artists/mod.rs @@ -0,0 +1,467 @@ +use anyhow::Result; +use log::{debug, info, warn}; +use serde_json::{Value, json}; + +use crate::models::Route; +use crate::models::lidarr_models::{ + AddArtistBody, AddArtistSearchResult, Artist, DeleteParams, EditArtistParams, LidarrCommandBody, + LidarrHistoryItem, LidarrRelease, +}; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::stateful_table::StatefulTable; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use urlencoding::encode; + +#[cfg(test)] +#[path = "lidarr_artists_network_tests.rs"] +mod lidarr_artists_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_artist( + &mut self, + delete_artist_params: DeleteParams, + ) -> Result<()> { + let event = LidarrEvent::DeleteArtist(DeleteParams::default()); + let DeleteParams { + id, + delete_files, + add_import_list_exclusion, + } = delete_artist_params; + + info!( + "Deleting Lidarr artist with ID: {id} with deleteFiles={delete_files} and addImportListExclusion={add_import_list_exclusion}" + ); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{id}")), + Some(format!( + "deleteFiles={delete_files}&addImportListExclusion={add_import_list_exclusion}" + )), + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result> { + info!("Fetching Lidarr artists"); + let event = LidarrEvent::ListArtists; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |mut artists_vec, mut app| { + if !matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _) + ) { + artists_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.lidarr_data.artists.set_items(artists_vec); + app.data.lidarr_data.artists.apply_sorting_toggle(false); + } + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_artist_details( + &mut self, + artist_id: i64, + ) -> Result { + info!("Fetching details for Lidarr artist with ID: {artist_id}"); + let event = LidarrEvent::GetArtistDetails(artist_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + Some(format!("/{artist_id}")), + None, + ) + .await; + + self + .handle_request::<(), Artist>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn toggle_artist_monitoring( + &mut self, + artist_id: i64, + ) -> Result<()> { + let event = LidarrEvent::ToggleArtistMonitoring(artist_id); + + let detail_event = LidarrEvent::GetArtistDetails(artist_id); + info!("Toggling artist monitoring for artist with ID: {artist_id}"); + info!("Fetching artist details for artist with ID: {artist_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{artist_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_artist_body, _| { + response = detailed_artist_body.to_string() + }) + .await?; + + info!("Constructing toggle artist monitoring body"); + + match serde_json::from_str::(&response) { + Ok(mut detailed_artist_body) => { + let monitored = detailed_artist_body + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + + *detailed_artist_body.get_mut("monitored").unwrap() = json!(!monitored); + + debug!("Toggle artist monitoring body: {detailed_artist_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_artist_body), + Some(format!("/{artist_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + Err(_) => { + warn!("Request for detailed artist body was interrupted"); + Ok(()) + } + } + } + + pub(in crate::network::lidarr_network) async fn update_all_artists(&mut self) -> Result { + info!("Updating all artists"); + let event = LidarrEvent::UpdateAllArtists; + let body = LidarrCommandBody { + name: "RefreshArtist".to_owned(), + ..LidarrCommandBody::default() + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn update_and_scan_artist( + &mut self, + artist_id: i64, + ) -> Result { + let event = LidarrEvent::UpdateAndScanArtist(artist_id); + info!("Updating and scanning artist with ID: {artist_id}"); + let body = LidarrCommandBody { + name: "RefreshArtist".to_owned(), + artist_id: Some(artist_id), + ..LidarrCommandBody::default() + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn trigger_automatic_artist_search( + &mut self, + artist_id: i64, + ) -> Result { + let event = LidarrEvent::TriggerAutomaticArtistSearch(artist_id); + info!("Searching indexers for artist with ID: {artist_id}"); + let body = LidarrCommandBody { + name: "ArtistSearch".to_owned(), + artist_id: Some(artist_id), + ..LidarrCommandBody::default() + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn search_artist( + &mut self, + query: String, + ) -> Result> { + info!("Searching for artist: {query}"); + let event = LidarrEvent::SearchNewArtist(String::new()); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("term={}", encode(&query))), + ) + .await; + + let result = self + .handle_request::<(), Vec>(request_props, |artist_vec, mut app| { + if artist_vec.is_empty() { + app.pop_and_push_navigation_stack(ActiveLidarrBlock::AddArtistEmptySearchResults.into()); + } else if let Some(add_searched_artists) = + app.data.lidarr_data.add_searched_artists.as_mut() + { + add_searched_artists.set_items(artist_vec); + } else { + let mut add_searched_artists = StatefulTable::default(); + add_searched_artists.set_items(artist_vec); + app.data.lidarr_data.add_searched_artists = Some(add_searched_artists); + } + }) + .await; + + if result.is_err() { + self.app.lock().await.data.lidarr_data.add_searched_artists = Some(StatefulTable::default()); + } + + result + } + + pub(in crate::network::lidarr_network) async fn add_artist( + &mut self, + mut add_artist_body: AddArtistBody, + ) -> Result { + info!("Adding Lidarr artist: {}", add_artist_body.artist_name); + if let Some(tag_input_str) = add_artist_body.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + add_artist_body.tags = tag_ids_vec; + } + let event = LidarrEvent::AddArtist(AddArtistBody::default()); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(add_artist_body), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_artist_history( + &mut self, + artist_id: i64, + ) -> Result> { + info!("Fetching Lidarr artist history for artist with ID: {artist_id}"); + let event = LidarrEvent::GetArtistHistory(artist_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("artistId={artist_id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |mut history_vec, mut app| { + let is_sorting = matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::ArtistHistorySortPrompt, _) + ); + + let artist_history = &mut app.data.lidarr_data.artist_history; + + if !is_sorting { + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + artist_history.set_items(history_vec); + artist_history.apply_sorting_toggle(false); + } + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_artist_discography_releases( + &mut self, + artist_id: i64, + ) -> Result> { + let event = LidarrEvent::GetDiscographyReleases(artist_id); + info!("Fetching discography releases for artist with ID: {artist_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("artistId={artist_id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |release_vec, mut app| { + let artist_releases_vec = release_vec + .into_iter() + .filter(|release| release.discography) + .collect(); + + app + .data + .lidarr_data + .discography_releases + .set_items(artist_releases_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn edit_artist( + &mut self, + mut edit_artist_params: EditArtistParams, + ) -> Result<()> { + info!("Editing Lidarr artist"); + if let Some(tag_input_str) = edit_artist_params.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + edit_artist_params.tags = Some(tag_ids_vec); + } + let artist_id = edit_artist_params.artist_id; + let detail_event = LidarrEvent::GetArtistDetails(artist_id); + let event = LidarrEvent::EditArtist(EditArtistParams::default()); + info!("Fetching artist details for artist with ID: {artist_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{artist_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_artist_body, _| { + response = detailed_artist_body.to_string() + }) + .await?; + + info!("Constructing edit artist body"); + + let mut detailed_artist_body: Value = serde_json::from_str(&response)?; + let ( + monitored, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tags, + ) = { + let monitored = edit_artist_params.monitored.unwrap_or( + detailed_artist_body["monitored"] + .as_bool() + .expect("Unable to deserialize 'monitored'"), + ); + let monitor_new_items = edit_artist_params.monitor_new_items.unwrap_or_else(|| { + serde_json::from_value(detailed_artist_body["monitorNewItems"].clone()) + .expect("Unable to deserialize 'monitorNewItems'") + }); + let quality_profile_id = edit_artist_params.quality_profile_id.unwrap_or_else(|| { + detailed_artist_body["qualityProfileId"] + .as_i64() + .expect("Unable to deserialize 'qualityProfileId'") + }); + let metadata_profile_id = edit_artist_params.metadata_profile_id.unwrap_or_else(|| { + detailed_artist_body["metadataProfileId"] + .as_i64() + .expect("Unable to deserialize 'metadataProfileId'") + }); + let root_folder_path = edit_artist_params.root_folder_path.unwrap_or_else(|| { + detailed_artist_body["path"] + .as_str() + .expect("Unable to deserialize 'path'") + .to_owned() + }); + let tags = if edit_artist_params.clear_tags { + vec![] + } else { + edit_artist_params.tags.unwrap_or( + detailed_artist_body["tags"] + .as_array() + .expect("Unable to deserialize 'tags'") + .iter() + .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) + .collect(), + ) + }; + + ( + monitored, + monitor_new_items, + quality_profile_id, + metadata_profile_id, + root_folder_path, + tags, + ) + }; + + *detailed_artist_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_artist_body.get_mut("monitorNewItems").unwrap() = json!(monitor_new_items); + *detailed_artist_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); + *detailed_artist_body.get_mut("metadataProfileId").unwrap() = json!(metadata_profile_id); + *detailed_artist_body.get_mut("path").unwrap() = json!(root_folder_path); + *detailed_artist_body.get_mut("tags").unwrap() = json!(tags); + + debug!("Edit artist body: {detailed_artist_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_artist_body), + Some(format!("/{artist_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/lidarr_network/library/lidarr_library_network_tests.rs b/src/network/lidarr_network/library/lidarr_library_network_tests.rs new file mode 100644 index 0000000..133d37b --- /dev/null +++ b/src/network/lidarr_network/library/lidarr_library_network_tests.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::LidarrReleaseDownloadBody; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use serde_json::json; + + #[tokio::test] + async fn test_handle_download_lidarr_release_event_uses_provided_params() { + 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!({})) + .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_ok!(result); + } +} diff --git a/src/network/lidarr_network/library/mod.rs b/src/network/lidarr_network/library/mod.rs new file mode 100644 index 0000000..f20624f --- /dev/null +++ b/src/network/lidarr_network/library/mod.rs @@ -0,0 +1,38 @@ +use crate::models::lidarr_models::LidarrReleaseDownloadBody; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; + +mod albums; +mod artists; +mod tracks; + +#[cfg(test)] +#[path = "lidarr_library_network_tests.rs"] +mod lidarr_library_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn download_lidarr_release( + &mut self, + lidarr_release_download_body: LidarrReleaseDownloadBody, + ) -> Result { + let event = LidarrEvent::DownloadRelease(LidarrReleaseDownloadBody::default()); + info!("Downloading Lidarr release with params: {lidarr_release_download_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(lidarr_release_download_body), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs b/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs new file mode 100644 index 0000000..7899095 --- /dev/null +++ b/src/network/lidarr_network/library/tracks/lidarr_tracks_network_tests.rs @@ -0,0 +1,871 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{LidarrHistoryItem, LidarrSerdeable, Track, TrackFile}; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::models::servarr_data::lidarr::modals::{AlbumDetailsModal, TrackDetailsModal}; + use crate::models::stateful_table::SortOption; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::{ + lidarr_history_item, track, track_file, + }; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use indoc::formatdoc; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::json; + + #[tokio::test] + async fn test_handle_delete_lidarr_track_file_event() { + let (async_server, app_arc, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteTrackFile(1)) + .await; + app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteTrackFile(1)) + .await + .is_ok() + ); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_track_details_event() { + let response = track(); + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(serde_json::to_value(track()).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetTrackDetails(1)) + .await; + app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal { + track_details_modal: Some(TrackDetailsModal::default()), + ..AlbumDetailsModal::default() + }); + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + let result = network + .handle_lidarr_event(LidarrEvent::GetTrackDetails(1)) + .await; + + async_server.assert_async().await; + assert_ok!(&result); + let LidarrSerdeable::Track(track) = result.unwrap() else { + panic!("Expected Track") + }; + assert_eq!(track, response); + let app = app_arc.lock().await; + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route(), + ActiveLidarrBlock::TrackDetails.into() + ); + let track_details = &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details; + assert_str_eq!( + track_details.get_text(), + formatdoc!( + " + Title: Test title + Track Number: 1 + Duration: 3:20 + Explicit: false + Quality: Lossless + File Path: /music/P!nk/TRUSTFALL/01 - When I Get There.flac + File Size: 37.40 MB + Date Added: 2023-05-20 21:29:16 UTC + Codec: FLAC + Channels: 2 + Bits: 24bit + Bit Rate: 1563 kbps + Sample Rate: 44.1kHz + " + ) + ) + } + + #[tokio::test] + async fn test_handle_get_track_details_event_empty_media_info() { + let expected_track = Track { + track_file: Some(TrackFile { + media_info: None, + ..track_file() + }), + ..track() + }; + let response = expected_track.clone(); + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(serde_json::to_value(expected_track.clone()).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetTrackDetails(1)) + .await; + app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal { + track_details_modal: Some(TrackDetailsModal::default()), + ..AlbumDetailsModal::default() + }); + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + let result = network + .handle_lidarr_event(LidarrEvent::GetTrackDetails(1)) + .await; + + async_server.assert_async().await; + assert_ok!(&result); + let LidarrSerdeable::Track(track) = result.unwrap() else { + panic!("Expected Track") + }; + assert_eq!(track, response); + let app = app_arc.lock().await; + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route(), + ActiveLidarrBlock::TrackDetails.into() + ); + let track_details = &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details; + assert_str_eq!( + track_details.get_text(), + formatdoc!( + " + Title: Test title + Track Number: 1 + Duration: 3:20 + Explicit: false + Quality: Lossless + File Path: /music/P!nk/TRUSTFALL/01 - When I Get There.flac + File Size: 37.40 MB + Date Added: 2023-05-20 21:29:16 UTC + " + ) + ) + } + + #[tokio::test] + async fn test_handle_get_track_details_event_empty_track_file() { + let expected_track = Track { + track_file: None, + ..track() + }; + let response = expected_track.clone(); + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(serde_json::to_value(expected_track.clone()).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetTrackDetails(1)) + .await; + app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal { + track_details_modal: Some(TrackDetailsModal::default()), + ..AlbumDetailsModal::default() + }); + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + let result = network + .handle_lidarr_event(LidarrEvent::GetTrackDetails(1)) + .await; + + async_server.assert_async().await; + assert_ok!(&result); + let LidarrSerdeable::Track(track) = result.unwrap() else { + panic!("Expected Track") + }; + assert_eq!(track, response); + let app = app_arc.lock().await; + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details_tabs + .get_active_route(), + ActiveLidarrBlock::TrackDetails.into() + ); + let track_details = &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_details; + assert_str_eq!( + track_details.get_text(), + formatdoc!( + " + Title: Test title + Track Number: 1 + Duration: 3:20 + Explicit: false + " + ) + ) + } + + #[tokio::test] + async fn test_handle_get_track_details_event_album_details_modal_not_required_in_cli_mode() { + let response = track(); + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(serde_json::to_value(track()).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetTrackDetails(1)) + .await; + app_arc.lock().await.cli_mode = true; + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + let result = network + .handle_lidarr_event(LidarrEvent::GetTrackDetails(1)) + .await; + + async_server.assert_async().await; + assert_ok!(&result); + let LidarrSerdeable::Track(track) = result.unwrap() else { + panic!("Expected Track") + }; + assert_eq!(track, response); + let app = app_arc.lock().await; + assert_some!(&app.data.lidarr_data.album_details_modal); + assert_some!( + &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + ); + } + + #[tokio::test] + #[should_panic(expected = "Album details modal is empty")] + async fn test_handle_get_track_details_event_requires_album_details_modal_to_be_some_when_in_tui_mode() + { + let (_async_server, app_arc, _server) = MockServarrApi::get() + .returns(serde_json::to_value(track()).unwrap()) + .path("/1") + .build_for(LidarrEvent::GetTrackDetails(1)) + .await; + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + network + .handle_lidarr_event(LidarrEvent::GetTrackDetails(1)) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_handle_get_tracks_event() { + let expected_tracks = vec![track()]; + let (mock, app, _server) = MockServarrApi::get() + .query("artistId=1&albumId=1") + .returns(json!([track()])) + .build_for(LidarrEvent::GetTracks(1, 1)) + .await; + app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetTracks(1, 1)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Tracks(tracks) = result.unwrap() else { + panic!("Expected Tracks variant") + }; + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .tracks + .items, + expected_tracks + ); + assert_eq!(tracks, expected_tracks); + } + + #[tokio::test] + async fn test_handle_get_tracks_event_empty_album_details_modal() { + let expected_tracks = vec![track()]; + let (mock, app, _server) = MockServarrApi::get() + .query("artistId=1&albumId=1") + .returns(json!([track()])) + .build_for(LidarrEvent::GetTracks(1, 1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetTracks(1, 1)) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Tracks(tracks) = result.unwrap() else { + panic!("Expected Tracks variant") + }; + + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .tracks + .items, + expected_tracks + ); + assert_eq!(tracks, expected_tracks); + } + + #[tokio::test] + async fn test_handle_get_track_files_event() { + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(json!([track_file()])) + .query("albumId=1") + .build_for(LidarrEvent::GetTrackFiles(1)) + .await; + app_arc.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + let LidarrSerdeable::TrackFiles(track_files) = network + .handle_lidarr_event(LidarrEvent::GetTrackFiles(1)) + .await + .unwrap() + else { + panic!("Expected TrackFiles") + }; + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_files + .items, + vec![track_file()] + ); + assert_eq!(track_files, vec![track_file()]); + } + + #[tokio::test] + async fn test_handle_get_track_files_event_empty_album_details_modal() { + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(json!([track_file()])) + .query("albumId=1") + .build_for(LidarrEvent::GetTrackFiles(1)) + .await; + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + let LidarrSerdeable::TrackFiles(track_files) = network + .handle_lidarr_event(LidarrEvent::GetTrackFiles(1)) + .await + .unwrap() + else { + panic!("Expected TrackFiles") + }; + async_server.assert_async().await; + let app = app_arc.lock().await; + assert_some!(&app.data.lidarr_data.album_details_modal); + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_files + .items, + vec![track_file()] + ); + assert_eq!(track_files, vec![track_file()]); + } + + #[rstest] + #[tokio::test] + async fn test_handle_get_lidarr_track_history_event( + #[values(true, false)] use_custom_sorting: bool, + ) { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z track", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "A Track", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let mut expected_history_items = vec![LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "A Track".into(), + ..lidarr_history_item() + }]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=2001&albumId=2001") + .build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await; + let album_details_modal = AlbumDetailsModal { + track_details_modal: Some(TrackDetailsModal::default()), + ..AlbumDetailsModal::default() + }; + app.lock().await.data.lidarr_data.album_details_modal = Some(album_details_modal); + if use_custom_sorting { + let cmp_fn = |a: &LidarrHistoryItem, b: &LidarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + expected_history_items.sort_by(cmp_fn); + + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_history + .sorting(vec![history_sort_option]); + } + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_history + .sort_asc = true; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .items, + expected_history_items + ); + assert!( + app + .lock() + .await + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .sort_asc + ); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_track_history_event_empty_track_details_modal() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z track", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "A Track", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "A Track".into(), + ..lidarr_history_item() + }]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=2001&albumId=2001") + .build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await; + app.lock().await.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.album_details_modal); + assert_some!( + &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + ); + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .items, + expected_history_items + ); + assert!( + !app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .sort_asc + ); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_track_history_event_empty_album_details_modal() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z track", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "A Track", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![LidarrHistoryItem { + id: 456, + artist_id: 2001, + album_id: 2001, + track_id: 2001, + source_title: "A Track".into(), + ..lidarr_history_item() + }]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=2001&albumId=2001") + .build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.album_details_modal); + assert_some!( + &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + ); + assert_eq!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .items, + expected_history_items + ); + assert!( + !app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .sort_asc + ); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_track_history_event_no_op_when_user_is_selecting_sort_options() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z track", + "albumId": 1007, + "artistId": 1007, + "trackId": 1007, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }, + { + "id": 456, + "sourceTitle": "A Track", + "albumId": 2001, + "artistId": 2001, + "trackId": 2001, + "quality": { "quality": { "name": "Lossless" } }, + "date": "2023-01-01T00:00:00Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/music/Something/cool.mp3", + "importedPath": "/nfs/music/Something/Album 1/Cool.mp3" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("artistId=2001&albumId=2001") + .build_for(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await; + let album_details_modal = AlbumDetailsModal { + track_details_modal: Some(TrackDetailsModal::default()), + ..AlbumDetailsModal::default() + }; + app.lock().await.data.lidarr_data.album_details_modal = Some(album_details_modal); + app.lock().await.server_tabs.set_index(2); + app + .lock() + .await + .push_navigation_stack(ActiveLidarrBlock::TrackHistorySortPrompt.into()); + let mut network = test_network(&app); + + let LidarrSerdeable::LidarrHistoryItems(history) = network + .handle_lidarr_event(LidarrEvent::GetTrackHistory(2001, 2001, 2001)) + .await + .unwrap() + else { + panic!("Expected LidarrHistoryItems") + }; + mock.assert_async().await; + let app = app.lock().await; + assert_some!(&app.data.lidarr_data.album_details_modal); + assert_some!( + &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + ); + assert_is_empty!( + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .items, + ); + assert!( + !app + .data + .lidarr_data + .album_details_modal + .as_ref() + .unwrap() + .track_details_modal + .as_ref() + .unwrap() + .track_history + .sort_asc + ); + assert_eq!(history, response); + } +} diff --git a/src/network/lidarr_network/library/tracks/mod.rs b/src/network/lidarr_network/library/tracks/mod.rs new file mode 100644 index 0000000..d24ddb5 --- /dev/null +++ b/src/network/lidarr_network/library/tracks/mod.rs @@ -0,0 +1,250 @@ +use crate::models::lidarr_models::{LidarrHistoryItem, MediaInfo, Track, TrackFile}; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::servarr_data::lidarr::modals::AlbumDetailsModal; +use crate::models::{Route, ScrollableText}; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use indoc::formatdoc; +use log::info; + +#[cfg(test)] +#[path = "lidarr_tracks_network_tests.rs"] +mod lidarr_tracks_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_lidarr_track_file( + &mut self, + track_file_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteTrackFile(track_file_id); + info!("Deleting Lidarr track file for track file with id: {track_file_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{track_file_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_tracks( + &mut self, + artist_id: i64, + album_id: i64, + ) -> Result> { + let event = LidarrEvent::GetTracks(artist_id, album_id); + info!("Fetching tracks for Lidarr artist with ID: {artist_id} and album with ID: {album_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("artistId={artist_id}&albumId={album_id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |mut track_vec, mut app| { + track_vec.sort_by(|a, b| a.id.cmp(&b.id)); + let album_details_modal = app + .data + .lidarr_data + .album_details_modal + .get_or_insert_default(); + + album_details_modal.tracks.set_items(track_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_track_details( + &mut self, + track_id: i64, + ) -> Result { + let event = LidarrEvent::GetTrackDetails(track_id); + info!("Fetching Lidarr track details for track with ID: {track_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + Some(format!("/{track_id}")), + None, + ) + .await; + + self + .handle_request::<(), Track>(request_props, |track_response, mut app| { + if app.cli_mode { + app.data.lidarr_data.album_details_modal = Some(AlbumDetailsModal::default()); + } + + let Track { + explicit, + track_number, + title, + duration, + track_file, + .. + } = track_response; + let duration_secs = duration / 1000; + let mins = duration_secs / 60; + let secs = duration_secs % 60; + let track_length = format!("{mins}:{secs:02}"); + let track_details_modal = app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("Album details modal is empty") + .track_details_modal + .get_or_insert_default(); + let mut details = formatdoc!( + " + Title: {title} + Track Number: {track_number} + Duration: {track_length} + Explicit: {explicit} + " + ); + + if let Some(file) = track_file { + let TrackFile { + path, + size, + quality, + date_added, + media_info, + .. + } = file; + let quality_name = quality.quality.name; + let size_mb = size as f64 / 1024f64.powi(2); + + details.push_str(&formatdoc!( + " + Quality: {quality_name} + File Path: {path} + File Size: {size_mb:.2} MB + Date Added: {date_added} + " + )); + + if let Some(info) = media_info { + let MediaInfo { + audio_bit_rate, + audio_channels, + audio_codec, + audio_bits, + audio_sample_rate, + } = info; + + details.push_str(&formatdoc!( + " + Codec: {} + Channels: {} + Bits: {} + Bit Rate: {} + Sample Rate: {} + ", + audio_codec.unwrap_or_default(), + audio_channels, + audio_bits.unwrap_or_default(), + audio_bit_rate.unwrap_or_default(), + audio_sample_rate.unwrap_or_default() + )); + } + } + + track_details_modal.track_details = ScrollableText::with_string(details); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_track_files( + &mut self, + album_id: i64, + ) -> Result> { + let event = LidarrEvent::GetTrackFiles(album_id); + info!("Fetching tracks files for Lidarr album with ID: {album_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("albumId={album_id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |track_file_vec, mut app| { + let album_details_modal = app + .data + .lidarr_data + .album_details_modal + .get_or_insert_default(); + + album_details_modal.track_files.set_items(track_file_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_track_history( + &mut self, + artist_id: i64, + album_id: i64, + track_id: i64, + ) -> Result> { + let event = LidarrEvent::GetTrackHistory(artist_id, album_id, track_id); + info!( + "Fetching history for artist with ID: {artist_id} and album with ID: {album_id} and track with ID: {track_id}" + ); + + let params = format!("artistId={artist_id}&albumId={album_id}"); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), Vec>(request_props, |history_items, mut app| { + let is_sorting = matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::TrackHistorySortPrompt, _) + ); + + let album_details_modal = app + .data + .lidarr_data + .album_details_modal + .get_or_insert_default(); + let track_details_modal = album_details_modal + .track_details_modal + .get_or_insert_default(); + + if !is_sorting { + let mut history_vec: Vec = history_items + .into_iter() + .filter(|it| it.track_id == track_id) + .collect(); + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + track_details_modal.track_history.set_items(history_vec); + track_details_modal + .track_history + .apply_sorting_toggle(false); + } + }) + .await + } +} diff --git a/src/network/lidarr_network/lidarr_network_test_utils.rs b/src/network/lidarr_network/lidarr_network_test_utils.rs new file mode 100644 index 0000000..e4b73cd --- /dev/null +++ b/src/network/lidarr_network/lidarr_network_test_utils.rs @@ -0,0 +1,501 @@ +#[cfg(test)] +#[allow(dead_code)] +pub mod test_utils { + use crate::models::lidarr_models::{ + AddArtistSearchResult, Album, AlbumStatistics, Artist, ArtistStatistics, ArtistStatus, + AudioTags, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus, DownloadsResponse, + EditArtistParams, LidarrHistoryData, LidarrHistoryEventType, LidarrHistoryItem, + LidarrHistoryWrapper, LidarrRelease, LidarrTask, LidarrTaskName, MediaInfo, Member, + MetadataProfile, NewItemMonitorType, Ratings, SystemStatus, Track, TrackFile, + }; + use crate::models::servarr_models::IndexerSettings; + use crate::models::servarr_models::{ + Indexer, IndexerField, Quality, QualityProfile, QualityWrapper, RootFolder, Tag, + }; + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use bimap::BiMap; + use chrono::DateTime; + use indoc::formatdoc; + use serde_json::{Number, json}; + + pub const ADD_ARTIST_SEARCH_RESULT_JSON: &str = r#"{ + "foreignArtistId": "test-foreign-id", + "artistName": "Test Artist", + "status": "continuing", + "overview": "some interesting description of the artist", + "artistType": "Person", + "disambiguation": "American pianist", + "genres": ["soundtrack"], + "ratings": { "votes": 15, "value": 8.4 } + }"#; + + pub const ARTIST_JSON: &str = r#"{ + "id": 1, + "artistName": "Test Artist", + "foreignArtistId": "test-foreign-id", + "status": "continuing", + "overview": "some interesting description of the artist", + "artistType": "Person", + "disambiguation": "American pianist", + "path": "/music/test-artist", + "members": [{"name": "alex", "instrument": "piano"}], + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": ["soundtrack"], + "tags": [1], + "added": "2023-01-01T00:00:00Z", + "ratings": { "votes": 15, "value": 8.4 }, + "statistics": { + "albumCount": 1, + "trackFileCount": 15, + "trackCount": 15, + "totalTrackCount": 15, + "sizeOnDisk": 12345, + "percentOfTracks": 99.9 + } + }"#; + + pub const ALBUM_JSON: &str = r#"{ + "id": 1, + "title": "Test Album", + "foreignAlbumId": "test-foreign-album-id", + "monitored": true, + "anyReleaseOk": true, + "profileId": 1, + "duration": 180, + "albumType": "Album", + "genres": ["Classical"], + "ratings": {"votes": 15, "value": 8.4}, + "releaseDate": "2023-01-01T00:00:00Z", + "statistics": { + "trackFileCount": 10, + "trackCount": 10, + "totalTrackCount": 10, + "sizeOnDisk": 1024, + "percentOfTracks": 99.9 + } + }"#; + + pub fn member() -> Member { + Member { + name: Some("alex".to_owned()), + instrument: Some("piano".to_owned()), + } + } + + pub fn ratings() -> Ratings { + Ratings { + votes: 15, + value: 8.4, + } + } + + pub fn artist_statistics() -> ArtistStatistics { + ArtistStatistics { + album_count: 1, + track_file_count: 15, + track_count: 15, + total_track_count: 15, + size_on_disk: 12345, + percent_of_tracks: 99.9, + } + } + + pub fn artist() -> Artist { + Artist { + id: 1, + artist_name: "Alex".into(), + foreign_artist_id: "test-foreign-id".to_owned(), + status: ArtistStatus::Continuing, + overview: Some("some interesting description of the artist".to_owned()), + artist_type: Some("Person".to_owned()), + disambiguation: Some("American pianist".to_owned()), + members: Some(vec![member()]), + path: "/nfs/music/test-artist".to_owned(), + quality_profile_id: quality_profile().id, + metadata_profile_id: metadata_profile().id, + monitored: true, + monitor_new_items: NewItemMonitorType::All, + genres: vec!["soundtrack".to_owned()], + tags: vec![Number::from(tag().id)], + added: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()), + ratings: Some(ratings()), + statistics: Some(artist_statistics()), + } + } + + pub fn quality_wrapper() -> QualityWrapper { + QualityWrapper { quality: quality() } + } + + pub fn quality() -> Quality { + Quality { + name: "Lossless".to_string(), + } + } + + pub fn quality_profile() -> QualityProfile { + QualityProfile { + id: 1, + name: "Lossless".to_owned(), + } + } + + pub fn quality_profile_map() -> BiMap { + let quality_profile = quality_profile(); + BiMap::from_iter(vec![(quality_profile.id, quality_profile.name)]) + } + + pub fn metadata_profile() -> MetadataProfile { + MetadataProfile { + id: 1, + name: "Standard".to_owned(), + } + } + + pub fn metadata_profile_map() -> BiMap { + let metadata_profile = metadata_profile(); + BiMap::from_iter(vec![(metadata_profile.id, metadata_profile.name)]) + } + + pub fn tag() -> Tag { + Tag { + id: 1, + label: "alex".to_owned(), + } + } + + pub fn tags_map() -> BiMap { + let tag = tag(); + BiMap::from_iter(vec![(tag.id, tag.label)]) + } + + pub fn download_record() -> DownloadRecord { + DownloadRecord { + title: "Test download title".to_owned(), + status: DownloadStatus::Downloading, + id: 1, + album_id: Some(Number::from(1i64)), + artist_id: Some(Number::from(1i64)), + size: 3543348019f64, + sizeleft: 1771674009f64, + output_path: Some(HorizontallyScrollableText::from("/nfs/music/alex/album")), + indexer: "kickass torrents".to_owned(), + download_client: Some("transmission".to_owned()), + } + } + + pub fn downloads_response() -> DownloadsResponse { + DownloadsResponse { + records: vec![download_record()], + } + } + + pub fn system_status() -> SystemStatus { + SystemStatus { + version: "1.0".to_owned(), + start_time: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()), + } + } + + pub fn root_folder() -> RootFolder { + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + } + } + + pub fn edit_artist_params() -> EditArtistParams { + EditArtistParams { + artist_id: artist().id, + monitored: Some(true), + monitor_new_items: Some(NewItemMonitorType::All), + quality_profile_id: Some(quality_profile().id), + metadata_profile_id: Some(metadata_profile().id), + root_folder_path: Some("/nfs/music/test-artist".to_owned()), + tags: Some(vec![tag().id]), + tag_input_string: Some("alex".to_owned()), + clear_tags: false, + } + } + + pub fn add_artist_search_result() -> AddArtistSearchResult { + AddArtistSearchResult { + foreign_artist_id: "test-foreign-id".to_owned(), + artist_name: "Test Artist".into(), + status: ArtistStatus::Continuing, + overview: Some("some interesting description of the artist".to_owned()), + artist_type: Some("Person".to_owned()), + disambiguation: Some("American pianist".to_owned()), + genres: vec!["soundtrack".to_owned()], + ratings: Some(ratings()), + } + } + + pub fn album_statistics() -> AlbumStatistics { + AlbumStatistics { + track_file_count: 10, + track_count: 10, + total_track_count: 10, + size_on_disk: 1024, + percent_of_tracks: 99.9, + } + } + + pub fn album() -> Album { + Album { + id: 1, + title: "Test Album".into(), + foreign_album_id: "test-foreign-album-id".to_string(), + monitored: true, + any_release_ok: true, + profile_id: 1, + duration: 180, + album_type: Some("Album".to_owned()), + genres: vec!["Classical".to_owned()], + ratings: Some(ratings()), + release_date: Some(DateTime::from( + DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap(), + )), + statistics: Some(album_statistics()), + } + } + + pub fn lidarr_history_wrapper() -> LidarrHistoryWrapper { + LidarrHistoryWrapper { + records: vec![lidarr_history_item()], + } + } + + pub fn lidarr_history_item() -> LidarrHistoryItem { + LidarrHistoryItem { + id: 1, + source_title: "Test source title".into(), + album_id: 1, + artist_id: 1, + track_id: 1, + quality: quality_wrapper(), + date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()), + event_type: LidarrHistoryEventType::Grabbed, + data: lidarr_history_data(), + } + } + + pub fn lidarr_history_data() -> LidarrHistoryData { + LidarrHistoryData { + dropped_path: Some("/nfs/nzbget/completed/music/Something/cool.mp3".to_owned()), + imported_path: Some("/nfs/music/Something/Album 1/Cool.mp3".to_owned()), + ..LidarrHistoryData::default() + } + } + + pub fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: vec![Number::from(1)], + id: 1, + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(json!("")), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), + }, + ]), + } + } + + pub fn indexer_settings() -> IndexerSettings { + IndexerSettings { + id: 1, + minimum_age: 1, + retention: 1, + maximum_size: 12345, + rss_sync_interval: 60, + } + } + + pub fn task() -> LidarrTask { + LidarrTask { + name: "Backup".to_owned(), + task_name: LidarrTaskName::Backup, + interval: 60, + last_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()), + next_execution: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T22:29:16Z").unwrap()), + } + } + + pub fn log_line() -> &'static str { + "2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process" + } + + pub fn updates() -> ScrollableText { + let line_break = "-".repeat(200); + ScrollableText::with_string(formatdoc!( + " + The latest version of Lidarr is already installed + + 4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) + {line_break} + New: + * Cool new thing + Fixed: + * Some bugs killed + + + 3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) + {line_break} + New: + * Cool new thing (old) + * Other cool new thing (old) + + + 2.1.0 - 2023-04-15 02:02:53 UTC + {line_break} + Fixed: + * Killed bug 1 + * Fixed bug 2" + )) + } + + pub fn rejections() -> Vec { + vec![ + "Unknown quality profile".to_owned(), + "Release is already mapped".to_owned(), + ] + } + + pub fn torrent_release() -> LidarrRelease { + LidarrRelease { + guid: "1234".to_owned(), + protocol: "torrent".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Test Release"), + discography: false, + artist_name: Some("Alex".to_owned()), + album_title: Some("Something".to_owned()), + indexer: "kickass torrents".to_owned(), + indexer_id: 2, + size: 1234, + rejected: true, + rejections: Some(rejections()), + seeders: Some(Number::from(2)), + leechers: Some(Number::from(1)), + quality: quality_wrapper(), + } + } + + pub fn usenet_release() -> LidarrRelease { + LidarrRelease { + guid: "1234".to_owned(), + protocol: "usenet".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Test Release"), + discography: false, + artist_name: Some("Alex".to_owned()), + album_title: Some("Something".to_owned()), + indexer: "DrunkenSlug".to_owned(), + indexer_id: 1, + size: 1234, + rejected: true, + rejections: Some(rejections()), + seeders: None, + leechers: None, + quality: quality_wrapper(), + } + } + + pub fn audio_tags() -> AudioTags { + AudioTags { + title: "When I Get There".to_string(), + artist_title: "P!nk".to_string(), + album_title: "Trustfall".to_string(), + disc_number: 1, + disc_count: 1, + year: 2023, + duration: "00:03:20.1802267".to_string(), + } + } + + pub fn media_info() -> MediaInfo { + MediaInfo { + audio_bit_rate: Some("1563 kbps".to_owned()), + audio_channels: 2, + audio_codec: Some("FLAC".to_owned()), + audio_bits: Some("24bit".to_owned()), + audio_sample_rate: Some("44.1kHz".to_owned()), + } + } + + pub fn track_file() -> TrackFile { + TrackFile { + id: 1, + path: "/music/P!nk/TRUSTFALL/01 - When I Get There.flac".to_string(), + size: 39216378, + quality: quality_wrapper(), + date_added: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()), + media_info: Some(media_info()), + audio_tags: Some(audio_tags()), + } + } + + pub fn track() -> Track { + Track { + id: 1, + artist_id: 1, + foreign_track_id: "test-foreign-track-id".to_string(), + track_file_id: 1, + album_id: 1, + explicit: false, + track_number: "1".to_string(), + title: "Test title".to_string(), + duration: 200173, + has_file: false, + ratings: ratings(), + track_file: Some(track_file()), + } + } + + pub fn blocklist_item() -> BlocklistItem { + BlocklistItem { + id: 1, + artist_id: 1, + album_ids: Some(vec![1.into()]), + source_title: "Alex - Something".to_string(), + quality: quality_wrapper(), + date: DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()), + protocol: "usenet".to_string(), + indexer: "NZBgeek (Prowlarr)".to_string(), + message: "test message".to_string(), + artist: artist(), + } + } + + pub fn blocklist_response() -> BlocklistResponse { + BlocklistResponse { + records: vec![blocklist_item()], + } + } +} diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs new file mode 100644 index 0000000..289ff9a --- /dev/null +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -0,0 +1,409 @@ +#[cfg(test)] +mod tests { + use crate::app::App; + use crate::models::lidarr_models::{ + AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile, + }; + use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag}; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent}; + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use serde_json::json; + use std::sync::Arc; + use tokio::sync::Mutex; + + #[rstest] + fn test_resource_all_indexer_settings( + #[values( + LidarrEvent::GetAllIndexerSettings, + LidarrEvent::EditAllIndexerSettings(IndexerSettings::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/config/indexer"); + } + + #[rstest] + fn test_resource_artist( + #[values( + LidarrEvent::GetArtistDetails(0), + LidarrEvent::ListArtists, + LidarrEvent::AddArtist(AddArtistBody::default()), + LidarrEvent::ToggleArtistMonitoring(0), + LidarrEvent::DeleteArtist(DeleteParams::default()), + LidarrEvent::EditArtist(EditArtistParams::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/artist"); + } + + #[rstest] + fn test_resource_downloads( + #[values(LidarrEvent::GetDownloads(0), LidarrEvent::DeleteDownload(0))] event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/queue"); + } + + #[rstest] + fn test_resource_indexer( + #[values( + LidarrEvent::GetIndexers, + LidarrEvent::DeleteIndexer(0), + LidarrEvent::EditIndexer(EditIndexerParams::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/indexer"); + } + + #[rstest] + fn test_resource_artist_history( + #[values( + LidarrEvent::GetArtistHistory(0), + LidarrEvent::GetAlbumHistory(0, 0), + LidarrEvent::GetTrackHistory(0, 0, 0) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/history/artist"); + } + + #[rstest] + fn test_resource_tag( + #[values( + LidarrEvent::AddTag(String::new()), + LidarrEvent::DeleteTag(0), + LidarrEvent::GetTags + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/tag"); + } + + #[rstest] + fn test_resource_config( + #[values(LidarrEvent::GetHostConfig, LidarrEvent::GetSecurityConfig)] event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/config/host"); + } + + #[rstest] + fn test_resource_command( + #[values( + LidarrEvent::UpdateAllArtists, + LidarrEvent::TriggerAutomaticArtistSearch(0), + LidarrEvent::TriggerAutomaticAlbumSearch(0), + LidarrEvent::UpdateAndScanArtist(0), + LidarrEvent::UpdateDownloads, + LidarrEvent::GetQueuedEvents, + LidarrEvent::StartTask(Default::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/command"); + } + + #[rstest] + fn test_resource_album( + #[values( + LidarrEvent::GetAlbums(0), + LidarrEvent::ToggleAlbumMonitoring(0), + LidarrEvent::GetAlbumDetails(0), + LidarrEvent::DeleteAlbum(DeleteParams::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/album"); + } + + #[rstest] + fn test_resource_root_folder( + #[values( + LidarrEvent::GetRootFolders, + LidarrEvent::DeleteRootFolder(0), + LidarrEvent::AddRootFolder(Default::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/rootfolder"); + } + + #[rstest] + fn test_resource_release( + #[values( + LidarrEvent::GetDiscographyReleases(0), + LidarrEvent::DownloadRelease(Default::default()), + LidarrEvent::GetAlbumReleases(0, 0) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/release"); + } + + #[rstest] + fn test_resource_track_file( + #[values(LidarrEvent::DeleteTrackFile(0), LidarrEvent::GetTrackFiles(0))] event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/trackfile"); + } + + #[rstest] + fn test_resource_track( + #[values(LidarrEvent::GetTracks(0, 0), LidarrEvent::GetTrackDetails(0))] event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/track"); + } + + #[rstest] + #[case(LidarrEvent::ClearBlocklist, "/blocklist/bulk")] + #[case(LidarrEvent::DeleteBlocklistItem(0), "/blocklist")] + #[case(LidarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] + #[case(LidarrEvent::GetDiskSpace, "/diskspace")] + #[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")] + #[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")] + #[case(LidarrEvent::GetStatus, "/system/status")] + #[case(LidarrEvent::GetTags, "/tag")] + #[case(LidarrEvent::GetLogs(500), "/log")] + #[case(LidarrEvent::GetTasks, "/system/task")] + #[case(LidarrEvent::GetUpdates, "/update")] + #[case(LidarrEvent::HealthCheck, "/health")] + #[case(LidarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")] + #[case(LidarrEvent::GetHistory(0), "/history")] + #[case(LidarrEvent::TestIndexer(0), "/indexer/test")] + #[case(LidarrEvent::TestAllIndexers, "/indexer/testall")] + fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) { + assert_str_eq!(event.resource(), expected_uri); + } + + #[test] + fn test_from_lidarr_event() { + assert_eq!( + NetworkEvent::Lidarr(LidarrEvent::HealthCheck), + NetworkEvent::from(LidarrEvent::HealthCheck) + ); + } + + #[tokio::test] + async fn test_handle_get_lidarr_healthcheck_event() { + let (mock, app, _server) = MockServarrApi::get() + .build_for(LidarrEvent::HealthCheck) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let _ = network.handle_lidarr_event(LidarrEvent::HealthCheck).await; + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_metadata_profiles_event() { + let metadata_profiles_json = json!([{ + "id": 1, + "name": "Standard" + }]); + let response: Vec = + serde_json::from_value(metadata_profiles_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(metadata_profiles_json) + .build_for(LidarrEvent::GetMetadataProfiles) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetMetadataProfiles) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else { + panic!("Expected MetadataProfiles"); + }; + + assert_eq!(metadata_profiles, response); + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .metadata_profile_map + .get_by_left(&1), + Some(&"Standard".to_owned()) + ); + } + + #[tokio::test] + async fn test_handle_get_quality_profiles_event() { + let quality_profiles_json = json!([{ + "id": 1, + "name": "Lossless" + }]); + let response: Vec = + serde_json::from_value(quality_profiles_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(quality_profiles_json) + .build_for(LidarrEvent::GetQualityProfiles) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetQualityProfiles) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else { + panic!("Expected QualityProfiles"); + }; + + assert_eq!(quality_profiles, response); + assert_eq!( + app + .lock() + .await + .data + .lidarr_data + .quality_profile_map + .get_by_left(&1), + Some(&"Lossless".to_owned()) + ); + } + + #[tokio::test] + async fn test_handle_get_tags_event() { + let tags_json = json!([{ + "id": 1, + "label": "usenet" + }]); + let response: Vec = serde_json::from_value(tags_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(tags_json) + .build_for(LidarrEvent::GetTags) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::GetTags).await; + + mock.assert_async().await; + + let LidarrSerdeable::Tags(tags) = result.unwrap() else { + panic!("Expected Tags"); + }; + + assert_eq!(tags, response); + assert_eq!( + app.lock().await.data.lidarr_data.tags_map.get_by_left(&1), + Some(&"usenet".to_owned()) + ); + } + + #[tokio::test] + async fn test_handle_add_lidarr_tag_event() { + let tag_json = json!({ + "id": 1, + "label": "usenet" + }); + let response: Tag = serde_json::from_value(tag_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ "label": "usenet" })) + .returns(tag_json) + .build_for(LidarrEvent::AddTag("usenet".to_owned())) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::AddTag("usenet".to_owned())) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::Tag(tag) = result.unwrap() else { + panic!("Expected Tag"); + }; + + assert_eq!(tag, response); + assert_eq!( + app.lock().await.data.lidarr_data.tags_map.get_by_left(&1), + Some(&"usenet".to_owned()) + ); + } + + #[tokio::test] + async fn test_handle_delete_lidarr_tag_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteTag(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::DeleteTag(1)).await; + + mock.assert_async().await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_extract_and_add_lidarr_tag_ids_vec() { + let app_arc = Arc::new(Mutex::new(App::test_default())); + let tags = " test,HI ,, usenet "; + { + let mut app = app_arc.lock().await; + app.data.lidarr_data.tags_map = BiMap::from_iter([ + (1, "usenet".to_owned()), + (2, "test".to_owned()), + (3, "hi".to_owned()), + ]); + } + app_arc.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app_arc); + + assert_eq!( + network.extract_and_add_lidarr_tag_ids_vec(tags).await, + vec![2, 3, 1] + ); + } + + #[tokio::test] + async fn test_extract_and_add_lidarr_tag_ids_vec_add_missing_tags_first() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ "label": "TESTING" })) + .returns(json!({ "id": 3, "label": "testing" })) + .build_for(LidarrEvent::GetTags) + .await; + let tags = "usenet, test, TESTING"; + { + let mut app_guard = app.lock().await; + app_guard.data.lidarr_data.edit_artist_modal = Some(EditArtistModal { + tags: tags.into(), + ..EditArtistModal::default() + }); + app_guard.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]); + } + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let tag_ids_vec = network.extract_and_add_lidarr_tag_ids_vec(tags).await; + + mock.assert_async().await; + assert_eq!(tag_ids_vec, vec![1, 2, 3]); + assert_eq!( + app.lock().await.data.lidarr_data.tags_map, + BiMap::from_iter([ + (1, "usenet".to_owned()), + (2, "test".to_owned()), + (3, "testing".to_owned()) + ]) + ); + } +} diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs new file mode 100644 index 0000000..ef52a96 --- /dev/null +++ b/src/network/lidarr_network/mod.rs @@ -0,0 +1,499 @@ +use anyhow::Result; +use log::info; + +use super::{NetworkEvent, NetworkResource}; +use crate::models::lidarr_models::{ + AddArtistBody, AddLidarrRootFolderBody, DeleteParams, EditArtistParams, + LidarrReleaseDownloadBody, LidarrSerdeable, LidarrTaskName, MetadataProfile, +}; +use crate::models::servarr_models::{EditIndexerParams, IndexerSettings, QualityProfile, Tag}; +use crate::network::{Network, RequestMethod}; + +mod blocklist; +mod downloads; +mod history; +mod indexers; +mod library; +mod root_folders; +mod system; + +#[cfg(test)] +#[path = "lidarr_network_tests.rs"] +mod lidarr_network_tests; + +#[cfg(test)] +#[path = "lidarr_network_test_utils.rs"] +pub mod lidarr_network_test_utils; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum LidarrEvent { + AddArtist(AddArtistBody), + AddRootFolder(AddLidarrRootFolderBody), + AddTag(String), + ClearBlocklist, + DeleteAlbum(DeleteParams), + DeleteArtist(DeleteParams), + DeleteBlocklistItem(i64), + DeleteDownload(i64), + DeleteIndexer(i64), + DeleteRootFolder(i64), + DeleteTag(i64), + DeleteTrackFile(i64), + DownloadRelease(LidarrReleaseDownloadBody), + EditArtist(EditArtistParams), + EditAllIndexerSettings(IndexerSettings), + EditIndexer(EditIndexerParams), + GetAlbums(i64), + GetAlbumDetails(i64), + GetAlbumHistory(i64, i64), + GetAlbumReleases(i64, i64), + GetArtistHistory(i64), + GetAllIndexerSettings, + GetArtistDetails(i64), + GetBlocklist, + GetDiscographyReleases(i64), + GetDiskSpace, + GetDownloads(u64), + GetHistory(u64), + GetHostConfig, + GetIndexers, + GetLogs(u64), + MarkHistoryItemAsFailed(i64), + GetMetadataProfiles, + GetQualityProfiles, + GetQueuedEvents, + GetRootFolders, + GetSecurityConfig, + GetStatus, + GetTrackDetails(i64), + GetTracks(i64, i64), + GetTrackFiles(i64), + GetTrackHistory(i64, i64, i64), + GetUpdates, + GetTags, + GetTasks, + HealthCheck, + ListArtists, + SearchNewArtist(String), + StartTask(LidarrTaskName), + TestIndexer(i64), + TestAllIndexers, + ToggleAlbumMonitoring(i64), + ToggleArtistMonitoring(i64), + TriggerAutomaticArtistSearch(i64), + TriggerAutomaticAlbumSearch(i64), + UpdateAllArtists, + UpdateAndScanArtist(i64), + UpdateDownloads, +} + +impl NetworkResource for LidarrEvent { + fn resource(&self) -> &'static str { + match &self { + LidarrEvent::AddTag(_) | LidarrEvent::DeleteTag(_) | LidarrEvent::GetTags => "/tag", + LidarrEvent::ClearBlocklist => "/blocklist/bulk", + LidarrEvent::DeleteTrackFile(_) | LidarrEvent::GetTrackFiles(_) => "/trackfile", + LidarrEvent::DeleteBlocklistItem(_) => "/blocklist", + LidarrEvent::GetAllIndexerSettings | LidarrEvent::EditAllIndexerSettings(_) => { + "/config/indexer" + } + LidarrEvent::DeleteArtist(_) + | LidarrEvent::EditArtist(_) + | LidarrEvent::GetArtistDetails(_) + | LidarrEvent::ListArtists + | LidarrEvent::AddArtist(_) + | LidarrEvent::ToggleArtistMonitoring(_) => "/artist", + LidarrEvent::GetAlbums(_) + | LidarrEvent::ToggleAlbumMonitoring(_) + | LidarrEvent::GetAlbumDetails(_) + | LidarrEvent::DeleteAlbum(_) => "/album", + LidarrEvent::GetArtistHistory(_) + | LidarrEvent::GetAlbumHistory(_, _) + | LidarrEvent::GetTrackHistory(_, _, _) => "/history/artist", + LidarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", + LidarrEvent::GetLogs(_) => "/log", + LidarrEvent::GetDiskSpace => "/diskspace", + LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue", + LidarrEvent::GetHistory(_) => "/history", + LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", + LidarrEvent::GetDiscographyReleases(_) + | LidarrEvent::DownloadRelease(_) + | LidarrEvent::GetAlbumReleases(_, _) => "/release", + LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host", + LidarrEvent::GetIndexers | LidarrEvent::DeleteIndexer(_) | LidarrEvent::EditIndexer(_) => { + "/indexer" + } + LidarrEvent::TriggerAutomaticArtistSearch(_) + | LidarrEvent::UpdateAllArtists + | LidarrEvent::UpdateAndScanArtist(_) + | LidarrEvent::UpdateDownloads + | LidarrEvent::GetQueuedEvents + | LidarrEvent::StartTask(_) + | LidarrEvent::TriggerAutomaticAlbumSearch(_) => "/command", + LidarrEvent::GetMetadataProfiles => "/metadataprofile", + LidarrEvent::GetQualityProfiles => "/qualityprofile", + LidarrEvent::GetRootFolders + | LidarrEvent::AddRootFolder(_) + | LidarrEvent::DeleteRootFolder(_) => "/rootfolder", + LidarrEvent::TestIndexer(_) => "/indexer/test", + LidarrEvent::TestAllIndexers => "/indexer/testall", + LidarrEvent::GetStatus => "/system/status", + LidarrEvent::GetTasks => "/system/task", + LidarrEvent::GetTracks(_, _) | LidarrEvent::GetTrackDetails(_) => "/track", + LidarrEvent::GetUpdates => "/update", + LidarrEvent::HealthCheck => "/health", + LidarrEvent::SearchNewArtist(_) => "/artist/lookup", + } + } +} + +impl From for NetworkEvent { + fn from(lidarr_event: LidarrEvent) -> Self { + NetworkEvent::Lidarr(lidarr_event) + } +} + +impl Network<'_, '_> { + pub async fn handle_lidarr_event( + &mut self, + lidarr_event: LidarrEvent, + ) -> Result { + match lidarr_event { + LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from), + LidarrEvent::AddRootFolder(path) => self + .add_lidarr_root_folder(path) + .await + .map(LidarrSerdeable::from), + LidarrEvent::ClearBlocklist => self + .clear_lidarr_blocklist() + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteAlbum(params) => { + self.delete_album(params).await.map(LidarrSerdeable::from) + } + LidarrEvent::DeleteArtist(params) => { + self.delete_artist(params).await.map(LidarrSerdeable::from) + } + LidarrEvent::DeleteBlocklistItem(blocklist_item_id) => self + .delete_lidarr_blocklist_item(blocklist_item_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteDownload(download_id) => self + .delete_lidarr_download(download_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteTrackFile(track_file_id) => self + .delete_lidarr_track_file(track_file_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::EditAllIndexerSettings(params) => self + .edit_all_lidarr_indexer_settings(params) + .await + .map(LidarrSerdeable::from), + LidarrEvent::EditIndexer(params) => self + .edit_lidarr_indexer(params) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteIndexer(indexer_id) => self + .delete_lidarr_indexer(indexer_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteRootFolder(root_folder_id) => self + .delete_lidarr_root_folder(root_folder_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DeleteTag(tag_id) => self + .delete_lidarr_tag(tag_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::DownloadRelease(lidarr_release_download_body) => self + .download_lidarr_release(lidarr_release_download_body) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetAlbums(artist_id) => { + self.get_albums(artist_id).await.map(LidarrSerdeable::from) + } + LidarrEvent::GetAllIndexerSettings => self + .get_all_lidarr_indexer_settings() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetArtistDetails(artist_id) => self + .get_artist_details(artist_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetAlbumDetails(album_id) => self + .get_album_details(album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetAlbumHistory(artist_id, album_id) => self + .get_lidarr_album_history(artist_id, album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetAlbumReleases(artist_id, album_id) => self + .get_album_releases(artist_id, album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetBlocklist => self.get_lidarr_blocklist().await.map(LidarrSerdeable::from), + LidarrEvent::GetDiscographyReleases(artist_id) => self + .get_artist_discography_releases(artist_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetDiskSpace => self.get_lidarr_diskspace().await.map(LidarrSerdeable::from), + LidarrEvent::GetDownloads(count) => self + .get_lidarr_downloads(count) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetIndexers => self.get_lidarr_indexers().await.map(LidarrSerdeable::from), + LidarrEvent::GetHistory(events) => self + .get_lidarr_history(events) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetArtistHistory(artist_id) => self + .get_lidarr_artist_history(artist_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetLogs(events) => self + .get_lidarr_logs(events) + .await + .map(LidarrSerdeable::from), + LidarrEvent::MarkHistoryItemAsFailed(history_item_id) => self + .mark_lidarr_history_item_as_failed(history_item_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetHostConfig => self + .get_lidarr_host_config() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetMetadataProfiles => self + .get_lidarr_metadata_profiles() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetQualityProfiles => self + .get_lidarr_quality_profiles() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetQueuedEvents => self + .get_queued_lidarr_events() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetRootFolders => self + .get_lidarr_root_folders() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetSecurityConfig => self + .get_lidarr_security_config() + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetStatus => self.get_lidarr_status().await.map(LidarrSerdeable::from), + LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from), + LidarrEvent::GetTasks => self.get_lidarr_tasks().await.map(LidarrSerdeable::from), + LidarrEvent::GetTrackDetails(track_id) => self + .get_track_details(track_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetTracks(artist_id, album_id) => self + .get_tracks(artist_id, album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetTrackFiles(album_id) => self + .get_track_files(album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetTrackHistory(artist_id, album_id, track_id) => self + .get_lidarr_track_history(artist_id, album_id, track_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::GetUpdates => self.get_lidarr_updates().await.map(LidarrSerdeable::from), + LidarrEvent::HealthCheck => self + .get_lidarr_healthcheck() + .await + .map(LidarrSerdeable::from), + LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from), + LidarrEvent::SearchNewArtist(query) => { + self.search_artist(query).await.map(LidarrSerdeable::from) + } + LidarrEvent::StartTask(task_name) => self + .start_lidarr_task(task_name) + .await + .map(LidarrSerdeable::from), + LidarrEvent::ToggleAlbumMonitoring(album_id) => self + .toggle_album_monitoring(album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::ToggleArtistMonitoring(artist_id) => self + .toggle_artist_monitoring(artist_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::TriggerAutomaticArtistSearch(artist_id) => self + .trigger_automatic_artist_search(artist_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::TriggerAutomaticAlbumSearch(album_id) => self + .trigger_automatic_album_search(album_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::UpdateAllArtists => self.update_all_artists().await.map(LidarrSerdeable::from), + LidarrEvent::UpdateAndScanArtist(artist_id) => self + .update_and_scan_artist(artist_id) + .await + .map(LidarrSerdeable::from), + LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from), + LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from), + LidarrEvent::UpdateDownloads => self + .update_lidarr_downloads() + .await + .map(LidarrSerdeable::from), + LidarrEvent::TestAllIndexers => self + .test_all_lidarr_indexers() + .await + .map(LidarrSerdeable::from), + LidarrEvent::TestIndexer(indexer_id) => self + .test_lidarr_indexer(indexer_id) + .await + .map(LidarrSerdeable::from), + } + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_healthcheck(&mut self) -> Result<()> { + info!("Performing Lidarr health check"); + let event = LidarrEvent::HealthCheck; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + async fn get_lidarr_metadata_profiles(&mut self) -> Result> { + info!("Fetching Lidarr metadata profiles"); + let event = LidarrEvent::GetMetadataProfiles; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |metadata_profiles, mut app| { + app.data.lidarr_data.metadata_profile_map = metadata_profiles + .into_iter() + .map(|profile| (profile.id, profile.name)) + .collect(); + }) + .await + } + + async fn get_lidarr_quality_profiles(&mut self) -> Result> { + info!("Fetching Lidarr quality profiles"); + let event = LidarrEvent::GetQualityProfiles; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |quality_profiles, mut app| { + app.data.lidarr_data.quality_profile_map = quality_profiles + .into_iter() + .map(|profile| (profile.id, profile.name)) + .collect(); + }) + .await + } + + async fn get_lidarr_tags(&mut self) -> Result> { + info!("Fetching Lidarr tags"); + let event = LidarrEvent::GetTags; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |tags_vec, mut app| { + app.data.lidarr_data.tags_map = tags_vec + .into_iter() + .map(|tag| (tag.id, tag.label)) + .collect(); + }) + .await + } + + async fn add_lidarr_tag(&mut self, tag: String) -> Result { + info!("Adding a new Lidarr tag"); + let event = LidarrEvent::AddTag(String::new()); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(serde_json::json!({ "label": tag })), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |tag, mut app| { + app.data.lidarr_data.tags_map.insert(tag.id, tag.label); + }) + .await + } + + async fn delete_lidarr_tag(&mut self, id: i64) -> Result<()> { + info!("Deleting Lidarr tag with ID: {id}"); + let event = LidarrEvent::DeleteTag(id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn extract_and_add_lidarr_tag_ids_vec( + &mut self, + edit_tags: &str, + ) -> Vec { + let missing_tags_vec = { + let tags_map = &self.app.lock().await.data.lidarr_data.tags_map; + edit_tags + .split(',') + .filter(|&tag| { + !tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none() + }) + .collect::>() + }; + + for tag in missing_tags_vec { + self + .add_lidarr_tag(tag.trim().to_owned()) + .await + .expect("Unable to add tag"); + } + + let app = self.app.lock().await; + edit_tags + .split(',') + .filter(|tag| !tag.is_empty()) + .map(|tag| { + *app + .data + .lidarr_data + .tags_map + .get_by_right(tag.to_lowercase().trim()) + .unwrap() + }) + .collect() + } +} diff --git a/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs b/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs new file mode 100644 index 0000000..9a1b95a --- /dev/null +++ b/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs @@ -0,0 +1,148 @@ +#[cfg(test)] +mod tests { + use crate::models::lidarr_models::{ + AddLidarrRootFolderBody, LidarrSerdeable, MonitorType, NewItemMonitorType, + }; + use crate::models::servarr_models::RootFolder; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use bimap::BiMap; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_add_lidarr_root_folder_event() { + let expected_add_root_folder_body = AddLidarrRootFolderBody { + name: "Music".to_owned(), + path: "/nfs/test".to_owned(), + default_quality_profile_id: 1, + default_metadata_profile_id: 1, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: vec![], + tag_input_string: Some("usenet, testing".to_owned()), + }; + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "Music", + "path": "/nfs/test", + "defaultQualityProfileId": 1, + "defaultMetadataProfileId": 1, + "defaultMonitorOption": "all", + "defaultNewItemMonitorOption": "all", + "defaultTags": [1, 2] + })) + .returns(json!({})) + .build_for(LidarrEvent::AddRootFolder( + expected_add_root_folder_body.clone(), + )) + .await; + app.lock().await.data.lidarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::AddRootFolder(expected_add_root_folder_body)) + .await + ); + + mock.assert_async().await; + assert_none!(app.lock().await.data.lidarr_data.add_root_folder_modal); + } + + #[tokio::test] + async fn test_handle_add_lidarr_root_folder_event_does_not_overwrite_default_tags_vec_when_tag_input_string_is_none() + { + let expected_add_root_folder_body = AddLidarrRootFolderBody { + name: "Music".to_owned(), + path: "/nfs/test".to_owned(), + default_quality_profile_id: 1, + default_metadata_profile_id: 1, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: vec![1, 2], + tag_input_string: None, + }; + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "Music", + "path": "/nfs/test", + "defaultQualityProfileId": 1, + "defaultMetadataProfileId": 1, + "defaultMonitorOption": "all", + "defaultNewItemMonitorOption": "all", + "defaultTags": [1, 2] + })) + .returns(json!({})) + .build_for(LidarrEvent::AddRootFolder( + expected_add_root_folder_body.clone(), + )) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert_ok!( + network + .handle_lidarr_event(LidarrEvent::AddRootFolder(expected_add_root_folder_body)) + .await + ); + + mock.assert_async().await; + assert_none!(app.lock().await.data.lidarr_data.add_root_folder_modal); + } + + #[tokio::test] + async fn test_handle_delete_lidarr_root_folder_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteRootFolder(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteRootFolder(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_handle_get_root_folders_event() { + let root_folders_json = json!([{ + "id": 1, + "path": "/nfs", + "accessible": true, + "freeSpace": 219902325555200i64 + }]); + let response: Vec = serde_json::from_value(root_folders_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(root_folders_json) + .build_for(LidarrEvent::GetRootFolders) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetRootFolders) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else { + panic!("Expected RootFolders"); + }; + + assert_eq!(root_folders, response); + assert_eq!( + app.lock().await.data.lidarr_data.root_folders.items, + vec![root_folder()] + ); + } +} diff --git a/src/network/lidarr_network/root_folders/mod.rs b/src/network/lidarr_network/root_folders/mod.rs new file mode 100644 index 0000000..1415980 --- /dev/null +++ b/src/network/lidarr_network/root_folders/mod.rs @@ -0,0 +1,80 @@ +use crate::models::lidarr_models::AddLidarrRootFolderBody; +use crate::models::servarr_models::RootFolder; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::{debug, info}; +use serde_json::Value; + +#[cfg(test)] +#[path = "lidarr_root_folders_network_tests.rs"] +mod lidarr_root_folders_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn add_lidarr_root_folder( + &mut self, + mut add_root_folder_body: AddLidarrRootFolderBody, + ) -> Result { + info!("Adding new root folder to Lidarr"); + if let Some(tag_input_str) = add_root_folder_body.tag_input_string.as_ref() { + let tag_ids_vec = self.extract_and_add_lidarr_tag_ids_vec(tag_input_str).await; + add_root_folder_body.default_tags = tag_ids_vec; + } + let event = LidarrEvent::AddRootFolder(AddLidarrRootFolderBody::default()); + + debug!("Add root folder body: {add_root_folder_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(add_root_folder_body), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn delete_lidarr_root_folder( + &mut self, + root_folder_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteRootFolder(root_folder_id); + info!("Deleting Lidarr root folder for folder with id: {root_folder_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{root_folder_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders( + &mut self, + ) -> Result> { + info!("Fetching Lidarr root folders"); + let event = LidarrEvent::GetRootFolders; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |root_folders, mut app| { + app.data.lidarr_data.root_folders.set_items(root_folders); + }) + .await + } +} diff --git a/src/network/lidarr_network/system/lidarr_system_network_tests.rs b/src/network/lidarr_network/system/lidarr_system_network_tests.rs new file mode 100644 index 0000000..ab97ab8 --- /dev/null +++ b/src/network/lidarr_network/system/lidarr_system_network_tests.rs @@ -0,0 +1,415 @@ +#[cfg(test)] +mod tests { + use crate::models::HorizontallyScrollableText; + use crate::models::lidarr_models::{LidarrSerdeable, LidarrTask, LidarrTaskName, SystemStatus}; + use crate::models::servarr_models::{ + DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update, + }; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::updates; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use chrono::DateTime; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[tokio::test] + async fn test_handle_get_diskspace_event() { + let diskspace_json = json!([ + { + "freeSpace": 1111, + "totalSpace": 2222, + }, + { + "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 result = network.handle_lidarr_event(LidarrEvent::GetDiskSpace).await; + + mock.assert_async().await; + + let LidarrSerdeable::DiskSpaces(disk_spaces) = result.unwrap() else { + panic!("Expected DiskSpaces"); + }; + + assert_eq!(disk_spaces, response); + assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty()); + } + + #[tokio::test] + async fn test_handle_get_host_config_event() { + let host_config_json = json!({ + "bindAddress": "*", + "port": 8686, + "urlBase": "some.test.site/lidarr", + "instanceName": "Lidarr", + "applicationUrl": "https://some.test.site:8686/lidarr", + "enableSsl": true, + "sslPort": 6868, + "sslCertPath": "/app/lidarr.pfx", + "sslCertPassword": "test" + }); + let response: HostConfig = serde_json::from_value(host_config_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(host_config_json) + .build_for(LidarrEvent::GetHostConfig) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetHostConfig) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::HostConfig(host_config) = result.unwrap() else { + panic!("Expected HostConfig"); + }; + + assert_eq!(host_config, response); + } + + #[tokio::test] + async fn test_handle_get_security_config_event() { + let security_config_json = json!({ + "authenticationMethod": "forms", + "authenticationRequired": "disabledForLocalAddresses", + "username": "test", + "password": "some password", + "apiKey": "someApiKey12345", + "certificateValidation": "disabledForLocalAddresses" + }); + let response: SecurityConfig = serde_json::from_value(security_config_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(security_config_json) + .build_for(LidarrEvent::GetSecurityConfig) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network + .handle_lidarr_event(LidarrEvent::GetSecurityConfig) + .await; + + mock.assert_async().await; + + let LidarrSerdeable::SecurityConfig(security_config) = result.unwrap() else { + panic!("Expected SecurityConfig"); + }; + + assert_eq!(security_config, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_logs_event() { + let expected_logs = vec![ + HorizontallyScrollableText::from( + "2023-05-20 21:29:16 UTC|FATAL|LidarrError|Some.Big.Bad.Exception|test exception", + ), + HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), + ]; + let logs_response_json = json!({ + "page": 1, + "pageSize": 500, + "sortKey": "time", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "time": "2023-05-20T21:29:16Z", + "level": "info", + "logger": "TestLogger", + "message": "test message", + "id": 1 + }, + { + "time": "2023-05-20T21:29:16Z", + "level": "fatal", + "logger": "LidarrError", + "exception": "test exception", + "exceptionType": "Some.Big.Bad.Exception", + "id": 2 + } + ] + }); + let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(logs_response_json) + .query("pageSize=500&sortDirection=descending&sortKey=time") + .build_for(LidarrEvent::GetLogs(500)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::LogResponse(logs) = network + .handle_lidarr_event(LidarrEvent::GetLogs(500)) + .await + .unwrap() + else { + panic!("Expected LogResponse") + }; + mock.assert_async().await; + assert_eq!(app.lock().await.data.lidarr_data.logs.items, expected_logs); + assert!( + app + .lock() + .await + .data + .lidarr_data + .logs + .current_selection() + .text + .contains("INFO") + ); + assert_eq!(logs, response); + } + + #[tokio::test] + async fn test_handle_get_queued_lidarr_events_event() { + let queued_events_json = json!([{ + "name": "RefreshMonitoredDownloads", + "commandName": "Refresh Monitored Downloads", + "status": "completed", + "queued": "2023-05-20T21:29:16Z", + "started": "2023-05-20T21:29:16Z", + "ended": "2023-05-20T21:29:16Z", + "duration": "00:00:00.5111547", + "trigger": "scheduled", + }]); + let response: Vec = serde_json::from_value(queued_events_json.clone()).unwrap(); + let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); + let expected_event = QueueEvent { + name: "RefreshMonitoredDownloads".to_owned(), + command_name: "Refresh Monitored Downloads".to_owned(), + status: "completed".to_owned(), + queued: timestamp, + started: Some(timestamp), + ended: Some(timestamp), + duration: Some("00:00:00.5111547".to_owned()), + trigger: "scheduled".to_owned(), + }; + + let (mock, app, _server) = MockServarrApi::get() + .returns(queued_events_json) + .build_for(LidarrEvent::GetQueuedEvents) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::QueueEvents(events) = network + .handle_lidarr_event(LidarrEvent::GetQueuedEvents) + .await + .unwrap() + else { + panic!("Expected QueueEvents") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.queued_events.items, + vec![expected_event] + ); + assert_eq!(events, response); + } + + #[tokio::test] + async fn test_handle_get_status_event() { + let status_json = json!({ + "version": "1.0.0", + "startTime": "2023-01-01T00:00:00Z" + }); + let response: SystemStatus = serde_json::from_value(status_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(status_json) + .build_for(LidarrEvent::GetStatus) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let result = network.handle_lidarr_event(LidarrEvent::GetStatus).await; + + mock.assert_async().await; + + let LidarrSerdeable::SystemStatus(status) = result.unwrap() else { + panic!("Expected SystemStatus"); + }; + + assert_eq!(status, response); + assert_eq!(app.lock().await.data.lidarr_data.version, "1.0.0"); + } + + #[tokio::test] + async fn test_handle_get_lidarr_tasks_event() { + let tasks_json = json!([{ + "name": "Application Update Check", + "taskName": "ApplicationUpdateCheck", + "interval": 360, + "lastExecution": "2023-05-20T21:29:16Z", + "nextExecution": "2023-05-20T21:29:16Z", + }, + { + "name": "Backup", + "taskName": "Backup", + "interval": 10080, + "lastExecution": "2023-05-20T21:29:16Z", + "nextExecution": "2023-05-20T21:29:16Z", + }]); + let response: Vec = serde_json::from_value(tasks_json.clone()).unwrap(); + let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap()); + let expected_tasks = vec![ + LidarrTask { + name: "Application Update Check".to_owned(), + task_name: LidarrTaskName::ApplicationUpdateCheck, + interval: 360, + last_execution: timestamp, + next_execution: timestamp, + }, + LidarrTask { + name: "Backup".to_owned(), + task_name: LidarrTaskName::Backup, + interval: 10080, + last_execution: timestamp, + next_execution: timestamp, + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(tasks_json) + .build_for(LidarrEvent::GetTasks) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Tasks(tasks) = network + .handle_lidarr_event(LidarrEvent::GetTasks) + .await + .unwrap() + else { + panic!("Expected Tasks") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.lidarr_data.tasks.items, + expected_tasks + ); + assert_eq!(tasks, response); + } + + #[tokio::test] + async fn test_handle_get_lidarr_updates_event() { + let updates_json = json!([{ + "version": "4.3.2.1", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": true, + "installedOn": "2023-04-15T02:02:53Z", + "latest": true, + "changes": { + "new": [ + "Cool new thing" + ], + "fixed": [ + "Some bugs killed" + ] + }, + }, + { + "version": "3.2.1.0", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": false, + "installedOn": "2023-04-15T02:02:53Z", + "latest": false, + "changes": { + "new": [ + "Cool new thing (old)", + "Other cool new thing (old)" + ], + }, + }, + { + "version": "2.1.0", + "releaseDate": "2023-04-15T02:02:53Z", + "installed": false, + "latest": false, + "changes": { + "fixed": [ + "Killed bug 1", + "Fixed bug 2" + ] + }, + }]); + let response: Vec = serde_json::from_value(updates_json.clone()).unwrap(); + let expected_text = updates(); + let (mock, app, _server) = MockServarrApi::get() + .returns(updates_json) + .build_for(LidarrEvent::GetUpdates) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Updates(updates) = network + .handle_lidarr_event(LidarrEvent::GetUpdates) + .await + .unwrap() + else { + panic!("Expected Updates") + }; + mock.assert_async().await; + let actual_text = app.lock().await.data.lidarr_data.updates.get_text(); + let expected = expected_text.get_text(); + + // Trim trailing whitespace from each line for comparison + let actual_trimmed: Vec<&str> = actual_text.lines().map(|l| l.trim_end()).collect(); + let expected_trimmed: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect(); + + assert_eq!( + actual_trimmed, expected_trimmed, + "Updates text mismatch (after trimming trailing whitespace)" + ); + assert_eq!(updates, response); + } + + #[tokio::test] + async fn test_handle_start_lidarr_task_event() { + let response = json!({ "test": "test"}); + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "ApplicationUpdateCheck" + })) + .returns(response.clone()) + .build_for(LidarrEvent::StartTask( + LidarrTaskName::ApplicationUpdateCheck, + )) + .await; + app + .lock() + .await + .data + .lidarr_data + .tasks + .set_items(vec![LidarrTask { + task_name: LidarrTaskName::default(), + ..LidarrTask::default() + }]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + let LidarrSerdeable::Value(value) = network + .handle_lidarr_event(LidarrEvent::StartTask( + LidarrTaskName::ApplicationUpdateCheck, + )) + .await + .unwrap() + else { + panic!("Expected Value") + }; + mock.assert_async().await; + assert_eq!(value, response); + } +} diff --git a/src/network/lidarr_network/system/mod.rs b/src/network/lidarr_network/system/mod.rs new file mode 100644 index 0000000..fb1b1cf --- /dev/null +++ b/src/network/lidarr_network/system/mod.rs @@ -0,0 +1,278 @@ +use crate::models::lidarr_models::{LidarrTask, LidarrTaskName, SystemStatus}; +use crate::models::servarr_models::{ + CommandBody, DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update, +}; +use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText}; +use crate::network::lidarr_network::LidarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use indoc::formatdoc; +use log::info; +use serde_json::Value; + +#[cfg(test)] +#[path = "lidarr_system_network_tests.rs"] +mod lidarr_system_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn get_lidarr_host_config( + &mut self, + ) -> Result { + info!("Fetching Lidarr host config"); + let event = LidarrEvent::GetHostConfig; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), HostConfig>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_logs( + &mut self, + events: u64, + ) -> Result { + info!("Fetching Lidarr logs"); + let event = LidarrEvent::GetLogs(events); + + let params = format!("pageSize={events}&sortDirection=descending&sortKey=time"); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), LogResponse>(request_props, |log_response, mut app| { + let mut logs = log_response.records; + logs.reverse(); + + let log_lines = logs + .into_iter() + .map(|log| { + if log.exception.is_some() { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log + .logger + .as_ref() + .expect("logger must exist when exception is present"), + log + .exception_type + .as_ref() + .expect("exception_type must exist when exception is present"), + log + .exception + .as_ref() + .expect("exception must exist in this branch") + )) + } else { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().expect("logger must exist in log entry"), + log + .message + .as_ref() + .expect("message must exist when exception is not present") + )) + } + }) + .collect(); + + app.data.lidarr_data.logs.set_items(log_lines); + app.data.lidarr_data.logs.scroll_to_bottom(); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_diskspace( + &mut self, + ) -> Result> { + info!("Fetching Lidarr disk space"); + let event = LidarrEvent::GetDiskSpace; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |disk_space_vec, mut app| { + app.data.lidarr_data.disk_space_vec = disk_space_vec; + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_queued_lidarr_events( + &mut self, + ) -> Result> { + info!("Fetching Lidarr queued events"); + let event = LidarrEvent::GetQueuedEvents; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |queued_events_vec, mut app| { + app + .data + .lidarr_data + .queued_events + .set_items(queued_events_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_security_config( + &mut self, + ) -> Result { + info!("Fetching Lidarr security config"); + let event = LidarrEvent::GetSecurityConfig; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), SecurityConfig>(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_status( + &mut self, + ) -> Result { + info!("Fetching Lidarr system status"); + let event = LidarrEvent::GetStatus; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), SystemStatus>(request_props, |system_status, mut app| { + app.data.lidarr_data.version = system_status.version; + app.data.lidarr_data.start_time = system_status.start_time; + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_tasks( + &mut self, + ) -> Result> { + info!("Fetching Lidarr tasks"); + let event = LidarrEvent::GetTasks; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |tasks_vec, mut app| { + app.data.lidarr_data.tasks.set_items(tasks_vec); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn get_lidarr_updates( + &mut self, + ) -> Result> { + info!("Fetching Lidarr updates"); + let event = LidarrEvent::GetUpdates; + + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, None) + .await; + + self + .handle_request::<(), Vec>(request_props, |updates_vec, mut app| { + let latest_installed = if updates_vec + .iter() + .any(|update| update.latest && update.installed_on.is_some()) + { + "already".to_owned() + } else { + "not".to_owned() + }; + let updates = updates_vec + .into_iter() + .map(|update| { + let install_status = if update.installed_on.is_some() { + if update.installed { + " (Currently Installed)".to_owned() + } else { + " (Previously Installed)".to_owned() + } + } else { + String::new() + }; + let vec_to_bullet_points = |vec: Vec| { + vec + .iter() + .map(|change| format!(" * {change}")) + .collect::>() + .join("\n") + }; + + let mut update_info = formatdoc!( + "{} - {}{install_status} + {}", + update.version, + update.release_date, + "-".repeat(200) + ); + + if let Some(new_changes) = update.changes.new { + let changes = vec_to_bullet_points(new_changes); + update_info = formatdoc!( + "{update_info} + New: + {changes}" + ) + } + + if let Some(fixes) = update.changes.fixed { + let fixes = vec_to_bullet_points(fixes); + update_info = formatdoc!( + "{update_info} + Fixed: + {fixes}" + ); + } + + update_info + }) + .reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}")) + .unwrap(); + + app.data.lidarr_data.updates = ScrollableText::with_string(formatdoc!( + "The latest version of Lidarr is {latest_installed} installed + + {updates}" + )); + }) + .await + } + + pub(in crate::network::lidarr_network) async fn start_lidarr_task( + &mut self, + task: LidarrTaskName, + ) -> Result { + let event = LidarrEvent::StartTask(task); + let task_name = task.to_string(); + info!("Starting Lidarr task: {task_name}"); + + let body = CommandBody { name: task_name }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/mod.rs b/src/network/mod.rs index b3bf0f4..dc8359f 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::{Result, anyhow}; use async_trait::async_trait; +use lidarr_network::LidarrEvent; use log::{debug, error, warn}; use regex::Regex; use reqwest::{Client, RequestBuilder}; @@ -21,6 +22,7 @@ use crate::network::radarr_network::RadarrEvent; use mockall::automock; use reqwest::header::HeaderMap; +pub mod lidarr_network; pub mod radarr_network; pub mod sonarr_network; mod utils; @@ -44,6 +46,7 @@ pub trait NetworkResource { pub enum NetworkEvent { Radarr(RadarrEvent), Sonarr(SonarrEvent), + Lidarr(LidarrEvent), } #[derive(Clone)] @@ -65,6 +68,10 @@ impl NetworkTrait for Network<'_, '_> { .handle_sonarr_event(sonarr_event) .await .map(Serdeable::from), + NetworkEvent::Lidarr(lidarr_event) => self + .handle_lidarr_event(lidarr_event) + .await + .map(Serdeable::from), }; let mut app = self.app.lock().await; @@ -229,12 +236,14 @@ impl<'a, 'b> Network<'a, 'b> { .get_active_config() .as_ref() .expect("Servarr config is undefined"); - let default_port = match network_event.into() { - NetworkEvent::Radarr(_) => 7878, - NetworkEvent::Sonarr(_) => 8989, + let network_event_type = network_event.into(); + let (default_port, api_version) = match &network_event_type { + NetworkEvent::Radarr(_) => (7878, "v3"), + NetworkEvent::Sonarr(_) => (8989, "v3"), + NetworkEvent::Lidarr(_) => (8686, "v1"), }; let mut uri = if let Some(servarr_uri) = uri { - format!("{servarr_uri}/api/v3{resource}") + format!("{servarr_uri}/api/{api_version}{resource}") } else { let protocol = if ssl_cert_path.is_some() { "https" @@ -243,7 +252,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let host = host.as_ref().unwrap(); format!( - "{protocol}://{host}:{}/api/v3{resource}", + "{protocol}://{host}:{}/api/{api_version}{resource}", port.unwrap_or(default_port) ) }; diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index 7b83b9c..871e9d6 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -18,6 +18,7 @@ mod tests { use crate::app::{App, AppConfig, ServarrConfig}; use crate::models::HorizontallyScrollableText; use crate::network::NetworkResource; + use crate::network::lidarr_network::LidarrEvent; use crate::network::network_tests::test_utils::test_network; use crate::network::radarr_network::RadarrEvent; use crate::network::sonarr_network::SonarrEvent; @@ -421,11 +422,13 @@ mod tests { } #[rstest] - #[case(RadarrEvent::GetMovies, 7878)] - #[case(SonarrEvent::ListSeries, 8989)] + #[case(RadarrEvent::GetMovies, "v3", 7878)] + #[case(SonarrEvent::ListSeries, "v3", 8989)] + #[case(LidarrEvent::ListArtists, "v1", 8686)] #[tokio::test] async fn test_request_props_from_default_config( #[case] network_event: impl Into + NetworkResource, + #[case] api_version: &str, #[case] default_port: u16, ) { let app_arc = Arc::new(Mutex::new(App::test_default())); @@ -435,6 +438,7 @@ mod tests { let mut app = app_arc.lock().await; app.server_tabs.tabs[0].config = Some(ServarrConfig::default()); app.server_tabs.tabs[1].config = Some(ServarrConfig::default()); + app.server_tabs.tabs[2].config = Some(ServarrConfig::default()); } let request_props = network @@ -443,7 +447,7 @@ mod tests { assert_str_eq!( request_props.uri, - format!("http://localhost:{default_port}/api/v3{resource}") + format!("http://localhost:{default_port}/api/{api_version}{resource}") ); assert_eq!(request_props.method, RequestMethod::Get); assert_eq!(request_props.body, None); @@ -564,11 +568,13 @@ mod tests { } #[rstest] - #[case(RadarrEvent::GetMovies, 7878)] - #[case(SonarrEvent::ListSeries, 8989)] + #[case(RadarrEvent::GetMovies, "v3", 7878)] + #[case(SonarrEvent::ListSeries, "v3", 8989)] + #[case(LidarrEvent::ListArtists, "v1", 8686)] #[tokio::test] async fn test_request_props_from_default_config_with_path_and_query_params( #[case] network_event: impl Into + NetworkResource, + #[case] api_version: &str, #[case] default_port: u16, ) { let app_arc = Arc::new(Mutex::new(App::test_default())); @@ -578,6 +584,7 @@ mod tests { let mut app = app_arc.lock().await; app.server_tabs.tabs[0].config = Some(ServarrConfig::default()); app.server_tabs.tabs[1].config = Some(ServarrConfig::default()); + app.server_tabs.tabs[2].config = Some(ServarrConfig::default()); } let request_props = network @@ -592,7 +599,7 @@ mod tests { assert_str_eq!( request_props.uri, - format!("http://localhost:{default_port}/api/v3{resource}/test?id=1") + format!("http://localhost:{default_port}/api/{api_version}{resource}/test?id=1") ); assert_eq!(request_props.method, RequestMethod::Get); assert_eq!(request_props.body, None); @@ -810,11 +817,16 @@ pub(in crate::network) mod test_utils { network_event: E, ) -> (Mock, Arc>>, ServerGuard) where - E: Into + NetworkResource, + E: Into + NetworkResource + Clone, { let resource = network_event.resource(); + let network_event_clone: NetworkEvent = network_event.clone().into(); + let api_version = match &network_event_clone { + NetworkEvent::Lidarr(_) => "v1", + _ => "v3", + }; let mut server = Server::new_async().await; - let mut uri = format!("/api/v3{resource}"); + let mut uri = format!("/api/{api_version}{resource}"); if let Some(path) = &self.path { uri = format!("{uri}{path}"); @@ -853,9 +865,10 @@ pub(in crate::network) mod test_utils { ..ServarrConfig::default() }; - match network_event.into() { + match network_event_clone { NetworkEvent::Radarr(_) => app.server_tabs.tabs[0].config = Some(servarr_config), NetworkEvent::Sonarr(_) => app.server_tabs.tabs[1].config = Some(servarr_config), + NetworkEvent::Lidarr(_) => app.server_tabs.tabs[2].config = Some(servarr_config), } let app_arc = Arc::new(Mutex::new(app)); diff --git a/src/network/radarr_network/history/mod.rs b/src/network/radarr_network/history/mod.rs new file mode 100644 index 0000000..429b7dd --- /dev/null +++ b/src/network/radarr_network/history/mod.rs @@ -0,0 +1,63 @@ +use crate::models::Route; +use crate::models::radarr_models::RadarrHistoryWrapper; +use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; +use crate::network::radarr_network::RadarrEvent; +use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; + +#[cfg(test)] +#[path = "radarr_history_network_tests.rs"] +mod radarr_history_network_tests; + +impl Network<'_, '_> { + pub(in crate::network::radarr_network) async fn get_radarr_history( + &mut self, + events: u64, + ) -> Result { + info!("Fetching all Radarr history events"); + let event = RadarrEvent::GetHistory(events); + + let params = format!("pageSize={events}&sortDirection=descending&sortKey=date"); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), RadarrHistoryWrapper>(request_props, |history_response, mut app| { + if !matches!( + app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::HistorySortPrompt, _) + ) { + let mut history_vec = history_response.records; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.radarr_data.history.set_items(history_vec); + app.data.radarr_data.history.apply_sorting_toggle(false); + } + }) + .await + } + + pub(in crate::network::radarr_network) async fn mark_radarr_history_item_as_failed( + &mut self, + history_item_id: i64, + ) -> Result { + info!("Marking the Radarr history item with ID: {history_item_id} as 'failed'"); + let event = RadarrEvent::MarkHistoryItemAsFailed(history_item_id); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + None, + Some(format!("/{history_item_id}")), + None, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |_, _| ()) + .await + } +} diff --git a/src/network/radarr_network/history/radarr_history_network_tests.rs b/src/network/radarr_network/history/radarr_history_network_tests.rs new file mode 100644 index 0000000..b11a1bd --- /dev/null +++ b/src/network/radarr_network/history/radarr_history_network_tests.rs @@ -0,0 +1,196 @@ +#[cfg(test)] +mod tests { + use crate::models::radarr_models::{RadarrHistoryItem, RadarrHistoryWrapper, RadarrSerdeable}; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::stateful_table::SortOption; + use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::radarr_network::RadarrEvent; + use crate::network::radarr_network::radarr_network_test_utils::test_utils::radarr_history_item; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::json; + + #[rstest] + #[tokio::test] + async fn test_handle_get_radarr_history_event(#[values(true, false)] use_custom_sorting: bool) { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z movie", + "movieId": 1007, + "quality": { "quality": { "name": "HD - 1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2022-12-30T07:37:56Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS", + "downloadClient": "transmission", + } + }, + { + "id": 456, + "sourceTitle": "A Movie", + "movieId": 2001, + "quality": { "quality": { "name": "HD - 1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2022-12-30T07:37:56Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS", + "downloadClient": "transmission", + } + }]}); + let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let mut expected_history_items = vec![ + RadarrHistoryItem { + id: 123, + movie_id: 1007, + source_title: "z movie".into(), + ..radarr_history_item() + }, + RadarrHistoryItem { + id: 456, + movie_id: 2001, + source_title: "A Movie".into(), + ..radarr_history_item() + }, + ]; + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("pageSize=500&sortDirection=descending&sortKey=date") + .build_for(RadarrEvent::GetHistory(500)) + .await; + app.lock().await.data.radarr_data.history.sort_asc = true; + if use_custom_sorting { + let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + expected_history_items.sort_by(cmp_fn); + + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .radarr_data + .history + .sorting(vec![history_sort_option]); + } + let mut network = test_network(&app); + + let RadarrSerdeable::HistoryWrapper(history) = network + .handle_radarr_event(RadarrEvent::GetHistory(500)) + .await + .unwrap() + else { + panic!("Expected HistoryWrapper") + }; + mock.assert_async().await; + assert_eq!( + app.lock().await.data.radarr_data.history.items, + expected_history_items + ); + assert!(app.lock().await.data.radarr_data.history.sort_asc); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_get_radarr_history_event_no_op_when_user_is_selecting_sort_options() { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z movie", + "movieId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS" + } + }, + { + "id": 456, + "sourceTitle": "A Movie", + "movieId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "indexer": "DrunkenSlug (Prowlarr)", + "releaseGroup": "SPARKS" + } + }]}); + let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("pageSize=500&sortDirection=descending&sortKey=date") + .build_for(RadarrEvent::GetHistory(500)) + .await; + app.lock().await.data.radarr_data.history.sort_asc = true; + app + .lock() + .await + .push_navigation_stack(ActiveRadarrBlock::HistorySortPrompt.into()); + let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let history_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app + .lock() + .await + .data + .radarr_data + .history + .sorting(vec![history_sort_option]); + let mut network = test_network(&app); + + let RadarrSerdeable::HistoryWrapper(history) = network + .handle_radarr_event(RadarrEvent::GetHistory(500)) + .await + .unwrap() + else { + panic!("Expected HistoryWrapper") + }; + mock.assert_async().await; + assert_is_empty!(app.lock().await.data.radarr_data.history); + assert!(app.lock().await.data.radarr_data.history.sort_asc); + assert_eq!(history, response); + } + + #[tokio::test] + async fn test_handle_mark_radarr_history_item_as_failed_event() { + let expected_history_item_id = 1; + let (mock, app, _server) = MockServarrApi::post() + .returns(json!({})) + .path("/1") + .build_for(RadarrEvent::MarkHistoryItemAsFailed( + expected_history_item_id, + )) + .await; + let mut network = test_network(&app); + + let result = network + .handle_radarr_event(RadarrEvent::MarkHistoryItemAsFailed( + expected_history_item_id, + )) + .await; + + mock.assert_async().await; + assert_ok!(result); + } +} diff --git a/src/network/radarr_network/indexers/mod.rs b/src/network/radarr_network/indexers/mod.rs index e9c67ee..5e78413 100644 --- a/src/network/radarr_network/indexers/mod.rs +++ b/src/network/radarr_network/indexers/mod.rs @@ -368,7 +368,7 @@ impl Network<'_, '_> { .await; request_props.ignore_status_code = true; - self + let result = self .handle_request::<(), Vec>(request_props, |test_results, mut app| { let mut test_all_indexer_results = StatefulTable::default(); let indexers = app.data.radarr_data.indexers.items.clone(); @@ -403,6 +403,18 @@ impl Network<'_, '_> { test_all_indexer_results.set_items(modal_test_results); app.data.radarr_data.indexer_test_all_results = Some(test_all_indexer_results); }) - .await + .await; + + if result.is_err() { + self + .app + .lock() + .await + .data + .radarr_data + .indexer_test_all_results = Some(StatefulTable::default()); + } + + result } } diff --git a/src/network/radarr_network/indexers/radarr_indexers_network_tests.rs b/src/network/radarr_network/indexers/radarr_indexers_network_tests.rs index 4c0c228..7a8b411 100644 --- a/src/network/radarr_network/indexers/radarr_indexers_network_tests.rs +++ b/src/network/radarr_network/indexers/radarr_indexers_network_tests.rs @@ -925,4 +925,31 @@ mod tests { ); assert_eq!(results, response); } + + #[tokio::test] + async fn test_handle_test_all_radarr_indexers_event_sets_empty_table_on_api_error() { + let (async_server, app, _server) = MockServarrApi::post() + .status(500) + .build_for(RadarrEvent::TestAllIndexers) + .await; + let mut network = test_network(&app); + + let result = network + .handle_radarr_event(RadarrEvent::TestAllIndexers) + .await; + + async_server.assert_async().await; + assert_err!(result); + assert_some!(&app.lock().await.data.radarr_data.indexer_test_all_results); + assert_is_empty!( + app + .lock() + .await + .data + .radarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + ); + } } diff --git a/src/network/radarr_network/library/mod.rs b/src/network/radarr_network/library/mod.rs index 6d08957..c68a63c 100644 --- a/src/network/radarr_network/library/mod.rs +++ b/src/network/radarr_network/library/mod.rs @@ -498,7 +498,7 @@ impl Network<'_, '_> { ) .await; - self + let result = self .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { if movie_vec.is_empty() { app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into()); @@ -511,7 +511,13 @@ impl Network<'_, '_> { app.data.radarr_data.add_searched_movies = Some(add_searched_movies); } }) - .await + .await; + + if result.is_err() { + self.app.lock().await.data.radarr_data.add_searched_movies = Some(StatefulTable::default()); + } + + result } pub(in crate::network) async fn toggle_movie_monitoring(&mut self, movie_id: i64) -> Result<()> { 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 24288c2..26215e4 100644 --- a/src/network/radarr_network/library/radarr_library_network_tests.rs +++ b/src/network/radarr_network/library/radarr_library_network_tests.rs @@ -2,7 +2,7 @@ mod tests { use crate::models::radarr_models::{ AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams, - MinimumAvailability, Movie, MovieHistoryItem, RadarrReleaseDownloadBody, + MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrReleaseDownloadBody, }; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; @@ -50,13 +50,13 @@ mod tests { tmdb_id: 1234, title: "Test".to_owned(), root_folder_path: "/nfs2".to_owned(), - minimum_availability: "announced".to_owned(), + minimum_availability: MinimumAvailability::Announced, monitored: true, quality_profile_id: 2222, tags: vec![1, 2], tag_input_string: Some("usenet, testing".into()), add_options: AddMovieOptions { - monitor: "movieOnly".to_owned(), + monitor: MovieMonitor::MovieOnly, search_for_movie: true, }, }; @@ -99,13 +99,13 @@ mod tests { tmdb_id: 1234, title: "Test".to_owned(), root_folder_path: "/nfs2".to_owned(), - minimum_availability: "announced".to_owned(), + minimum_availability: MinimumAvailability::Announced, monitored: true, quality_profile_id: 2222, tags: vec![1, 2], tag_input_string: None, add_options: AddMovieOptions { - monitor: "movieOnly".to_owned(), + monitor: MovieMonitor::MovieOnly, search_for_movie: true, }, }; @@ -981,18 +981,39 @@ mod tests { ); async_server.assert_async().await; - assert!( + assert_none!(&app_arc.lock().await.data.radarr_data.add_searched_movies); + assert_eq!( + app_arc.lock().await.get_current_route(), + ActiveRadarrBlock::AddMovieEmptySearchResults.into() + ); + } + + #[tokio::test] + async fn test_handle_search_new_movie_event_sets_empty_table_on_api_error() { + let (async_server, app_arc, _server) = MockServarrApi::get() + .returns(json!([])) + .status(500) + .query("term=test%20term") + .build_for(RadarrEvent::SearchNewMovie("test term".into())) + .await; + let mut network = test_network(&app_arc); + + let result = network + .handle_radarr_event(RadarrEvent::SearchNewMovie("test term".into())) + .await; + + async_server.assert_async().await; + assert_err!(result); + assert_some!(&app_arc.lock().await.data.radarr_data.add_searched_movies); + assert_is_empty!( app_arc .lock() .await .data .radarr_data .add_searched_movies - .is_none() - ); - assert_eq!( - app_arc.lock().await.get_current_route(), - ActiveRadarrBlock::AddMovieEmptySearchResults.into() + .as_ref() + .unwrap() ); } diff --git a/src/network/radarr_network/mod.rs b/src/network/radarr_network/mod.rs index 72ff7d9..207a4ca 100644 --- a/src/network/radarr_network/mod.rs +++ b/src/network/radarr_network/mod.rs @@ -16,6 +16,7 @@ use super::NetworkResource; mod blocklist; mod collections; mod downloads; +mod history; mod indexers; mod library; mod root_folders; @@ -47,10 +48,12 @@ pub enum RadarrEvent { GetBlocklist, GetCollections, GetDownloads(u64), + GetHistory(u64), GetHostConfig, GetIndexers, GetAllIndexerSettings, GetLogs(u64), + MarkHistoryItemAsFailed(i64), GetMovieCredits(i64), GetMovieDetails(i64), GetMovieHistory(i64), @@ -86,7 +89,9 @@ impl NetworkResource for RadarrEvent { RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection", RadarrEvent::GetDownloads(_) | RadarrEvent::DeleteDownload(_) => "/queue", + RadarrEvent::GetHistory(_) => "/history", RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host", + RadarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => { "/indexer" } @@ -199,6 +204,10 @@ impl Network<'_, '_> { .get_radarr_downloads(count) .await .map(RadarrSerdeable::from), + RadarrEvent::GetHistory(events) => self + .get_radarr_history(events) + .await + .map(RadarrSerdeable::from), RadarrEvent::GetHostConfig => self .get_radarr_host_config() .await @@ -208,6 +217,10 @@ impl Network<'_, '_> { .get_radarr_logs(events) .await .map(RadarrSerdeable::from), + RadarrEvent::MarkHistoryItemAsFailed(history_item_id) => self + .mark_radarr_history_item_as_failed(history_item_id) + .await + .map(RadarrSerdeable::from), RadarrEvent::GetMovieCredits(movie_id) => { self.get_credits(movie_id).await.map(RadarrSerdeable::from) } diff --git a/src/network/radarr_network/radarr_network_test_utils.rs b/src/network/radarr_network/radarr_network_test_utils.rs index 8cc37d6..b361f34 100644 --- a/src/network/radarr_network/radarr_network_test_utils.rs +++ b/src/network/radarr_network/radarr_network_test_utils.rs @@ -3,8 +3,8 @@ pub mod test_utils { use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit, CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability, - Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, - Rating, RatingsList, + Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType, + RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList, }; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, @@ -313,6 +313,24 @@ pub mod test_utils { } } + pub fn radarr_history_item() -> RadarrHistoryItem { + RadarrHistoryItem { + id: 1, + source_title: HorizontallyScrollableText::from("Test"), + movie_id: 1, + quality: quality_wrapper(), + languages: vec![language()], + date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()), + event_type: RadarrHistoryEventType::Grabbed, + data: RadarrHistoryData { + indexer: Some("DrunkenSlug (Prowlarr)".to_owned()), + release_group: Some("SPARKS".to_owned()), + download_client: Some("transmission".to_owned()), + ..RadarrHistoryData::default() + }, + } + } + pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), diff --git a/src/network/radarr_network/radarr_network_tests.rs b/src/network/radarr_network/radarr_network_tests.rs index cfa10b0..e6f2c54 100644 --- a/src/network/radarr_network/radarr_network_tests.rs +++ b/src/network/radarr_network/radarr_network_tests.rs @@ -136,7 +136,9 @@ mod test { #[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")] #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] + #[case(RadarrEvent::GetHistory(500), "/history")] #[case(RadarrEvent::GetLogs(500), "/log")] + #[case(RadarrEvent::MarkHistoryItemAsFailed(1), "/history/failed")] #[case(RadarrEvent::SearchNewMovie(String::new()), "/movie/lookup")] #[case(RadarrEvent::GetMovieCredits(0), "/credit")] #[case(RadarrEvent::GetMovieHistory(0), "/history/movie")] diff --git a/src/network/servarr_test_utils.rs b/src/network/servarr_test_utils.rs index 0f79be9..38ea73c 100644 --- a/src/network/servarr_test_utils.rs +++ b/src/network/servarr_test_utils.rs @@ -1,5 +1,5 @@ use crate::models::servarr_data::modals::IndexerTestResultModalItem; -use crate::models::servarr_models::{DiskSpace, QueueEvent}; +use crate::models::servarr_models::{DiskSpace, IndexerSettings, QueueEvent}; use chrono::DateTime; pub fn diskspace() -> DiskSpace { @@ -9,6 +9,16 @@ pub fn diskspace() -> DiskSpace { } } +pub fn indexer_settings() -> IndexerSettings { + IndexerSettings { + id: 1, + minimum_age: 1, + retention: 1, + maximum_size: 12345, + rss_sync_interval: 60, + } +} + pub fn indexer_test_result() -> IndexerTestResultModalItem { IndexerTestResultModalItem { name: "DrunkenSlug".to_owned(), diff --git a/src/network/sonarr_network/history/sonarr_history_network_tests.rs b/src/network/sonarr_network/history/sonarr_history_network_tests.rs index 7bb1dae..8aa6e72 100644 --- a/src/network/sonarr_network/history/sonarr_history_network_tests.rs +++ b/src/network/sonarr_network/history/sonarr_history_network_tests.rs @@ -5,7 +5,7 @@ mod tests { use crate::models::stateful_table::SortOption; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::sonarr_network::SonarrEvent; - use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::history_item; + use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::sonarr_history_item; use pretty_assertions::assert_eq; use rstest::rstest; use serde_json::json; @@ -45,13 +45,13 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (mock, app, _server) = MockServarrApi::get() diff --git a/src/network/sonarr_network/indexers/mod.rs b/src/network/sonarr_network/indexers/mod.rs index 7200d47..a085fa6 100644 --- a/src/network/sonarr_network/indexers/mod.rs +++ b/src/network/sonarr_network/indexers/mod.rs @@ -1,6 +1,6 @@ use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_models::IndexerSettings; use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult}; -use crate::models::sonarr_models::IndexerSettings; use crate::models::stateful_table::StatefulTable; use crate::network::sonarr_network::SonarrEvent; use crate::network::{Network, RequestMethod}; @@ -366,7 +366,7 @@ impl Network<'_, '_> { .await; request_props.ignore_status_code = true; - self + let result = self .handle_request::<(), Vec>(request_props, |test_results, mut app| { let mut test_all_indexer_results = StatefulTable::default(); let indexers = app.data.sonarr_data.indexers.items.clone(); @@ -401,6 +401,18 @@ impl Network<'_, '_> { test_all_indexer_results.set_items(modal_test_results); app.data.sonarr_data.indexer_test_all_results = Some(test_all_indexer_results); }) - .await + .await; + + if result.is_err() { + self + .app + .lock() + .await + .data + .sonarr_data + .indexer_test_all_results = Some(StatefulTable::default()); + } + + result } } diff --git a/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs b/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs index 3bd86bd..2eeaa8a 100644 --- a/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs +++ b/src/network/sonarr_network/indexers/sonarr_indexers_network_tests.rs @@ -6,10 +6,9 @@ mod tests { use crate::models::sonarr_models::SonarrSerdeable; use crate::network::NetworkResource; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; + use crate::network::servarr_test_utils::indexer_settings; use crate::network::sonarr_network::SonarrEvent; - use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - indexer, indexer_settings, - }; + use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::indexer; use bimap::BiMap; use mockito::Matcher; use pretty_assertions::assert_eq; @@ -31,11 +30,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::DeleteIndexer(1)) .await - .is_ok() ); mock.assert_async().await; @@ -58,11 +56,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditAllIndexerSettings(indexer_settings())) .await - .is_ok() ); mock.assert_async().await; @@ -153,11 +150,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -248,11 +244,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -338,11 +333,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -435,11 +429,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(expected_edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -497,11 +490,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params)) .await - .is_ok() ); mock_details_server.assert_async().await; @@ -584,11 +576,10 @@ mod tests { app.lock().await.server_tabs.next(); let mut network = test_network(&app); - assert!( + assert_ok!( network .handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params)) .await - .is_ok() ); async_details_server.assert_async().await; @@ -642,6 +633,7 @@ mod tests { else { panic!("Expected Indexers") }; + async_server.assert_async().await; assert_eq!( app.lock().await.data.sonarr_data.indexers.items, @@ -714,6 +706,7 @@ mod tests { else { panic!("Expected Value") }; + async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( @@ -780,6 +773,7 @@ mod tests { else { panic!("Expected Value") }; + async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( @@ -860,16 +854,9 @@ mod tests { else { panic!("Expected IndexerTestResults") }; + async_server.assert_async().await; - assert!( - app - .lock() - .await - .data - .sonarr_data - .indexer_test_all_results - .is_some() - ); + assert_some!(&app.lock().await.data.sonarr_data.indexer_test_all_results); assert_eq!( app .lock() @@ -884,4 +871,31 @@ mod tests { ); assert_eq!(results, response); } + + #[tokio::test] + async fn test_handle_test_all_sonarr_indexers_event_sets_empty_table_on_api_error() { + let (async_server, app, _server) = MockServarrApi::post() + .status(500) + .build_for(SonarrEvent::TestAllIndexers) + .await; + let mut network = test_network(&app); + app.lock().await.server_tabs.next(); + + let result = network + .handle_sonarr_event(SonarrEvent::TestAllIndexers) + .await; + + async_server.assert_async().await; + assert_err!(result); + let app = app.lock().await; + assert_some!(&app.data.sonarr_data.indexer_test_all_results); + assert_is_empty!( + app + .data + .sonarr_data + .indexer_test_all_results + .as_ref() + .unwrap() + ); + } } diff --git a/src/network/sonarr_network/library/episodes/mod.rs b/src/network/sonarr_network/library/episodes/mod.rs index 1973c70..518c872 100644 --- a/src/network/sonarr_network/library/episodes/mod.rs +++ b/src/network/sonarr_network/library/episodes/mod.rs @@ -1,4 +1,4 @@ -use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal}; +use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_models::Language; use crate::models::sonarr_models::{ @@ -65,10 +65,6 @@ impl Network<'_, '_> { app.get_current_route(), Route::Sonarr(ActiveSonarrBlock::EpisodesSortPrompt, _) ) { - if app.data.sonarr_data.season_details_modal.is_none() { - app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - } - let season_episodes_vec = if !app.data.sonarr_data.seasons.is_empty() { let season_number = app .data @@ -85,22 +81,14 @@ impl Network<'_, '_> { episode_vec }; - app + let season_details_modal = app .data .sonarr_data .season_details_modal - .as_mut() - .unwrap() - .episodes - .set_items(season_episodes_vec); - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episodes - .apply_sorting_toggle(false); + .get_or_insert_default(); + + season_details_modal.episodes.set_items(season_episodes_vec); + season_details_modal.episodes.apply_sorting_toggle(false); } }) .await @@ -125,16 +113,13 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |episode_file_vec, mut app| { - if app.data.sonarr_data.season_details_modal.is_none() { - app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - } - - app + let season_details_modal = app .data .sonarr_data .season_details_modal - .as_mut() - .unwrap() + .get_or_insert_default(); + + season_details_modal .episode_files .set_items(episode_file_vec); }) @@ -156,50 +141,19 @@ impl Network<'_, '_> { self .handle_request::<(), SonarrHistoryWrapper>(request_props, |history_response, mut app| { - if app.data.sonarr_data.season_details_modal.is_none() { - app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - } - - if app + let season_details_modal = app .data .sonarr_data .season_details_modal - .as_ref() - .unwrap() + .get_or_insert_default(); + let episode_details_modal = season_details_modal .episode_details_modal - .is_none() - { - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal = Some(EpisodeDetailsModal::default()); - } + .get_or_insert_default(); let mut history_vec = history_response.records; history_vec.sort_by(|a, b| a.id.cmp(&b.id)); - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal - .as_mut() - .unwrap() - .episode_history - .set_items(history_vec); - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal - .as_mut() - .unwrap() + episode_details_modal.episode_history.set_items(history_vec); + episode_details_modal .episode_history .apply_sorting_toggle(false); }) @@ -231,24 +185,6 @@ impl Network<'_, '_> { app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); } - if app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("Season details modal is empty") - .episode_details_modal - .is_none() - { - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal = Some(EpisodeDetailsModal::default()); - } - let Episode { id, title, @@ -271,10 +207,9 @@ impl Network<'_, '_> { .sonarr_data .season_details_modal .as_mut() - .unwrap() + .expect("Season details modal is empty") .episode_details_modal - .as_mut() - .unwrap(); + .get_or_insert_default(); episode_details_modal.episode_details = ScrollableText::with_string(formatdoc!( " Title: {} @@ -366,39 +301,23 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |release_vec, mut app| { - if app.data.sonarr_data.season_details_modal.is_none() { - app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - } - - if app + let season_details_modal = app .data .sonarr_data .season_details_modal - .as_mut() - .unwrap() + .get_or_insert_default(); + let episode_details_modal = season_details_modal .episode_details_modal - .is_none() - { - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal = Some(EpisodeDetailsModal::default()); - } + .get_or_insert_default(); - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .episode_details_modal - .as_mut() - .unwrap() + let episode_releases_vec = release_vec + .into_iter() + .filter(|release| !release.full_season) + .collect(); + + episode_details_modal .episode_releases - .set_items(release_vec); + .set_items(episode_releases_vec); }) .await } diff --git a/src/network/sonarr_network/library/episodes/sonarr_episodes_network_tests.rs b/src/network/sonarr_network/library/episodes/sonarr_episodes_network_tests.rs index 4de541a..62b2530 100644 --- a/src/network/sonarr_network/library/episodes/sonarr_episodes_network_tests.rs +++ b/src/network/sonarr_network/library/episodes/sonarr_episodes_network_tests.rs @@ -4,7 +4,7 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::sonarr_models::{ DownloadRecord, DownloadStatus, Episode, MonitorEpisodeBody, Season, Series, SonarrHistoryItem, - SonarrHistoryWrapper, SonarrSerdeable, + SonarrHistoryWrapper, SonarrRelease, SonarrSerdeable, }; use crate::models::stateful_table::SortOption; use crate::network::NetworkResource; @@ -12,7 +12,7 @@ mod tests { use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::library::episodes::get_episode_status; use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - EPISODE_JSON, episode, episode_file, history_item, torrent_release, + EPISODE_JSON, episode, episode_file, sonarr_history_item, torrent_release, }; use indoc::formatdoc; use mockito::Matcher; @@ -522,13 +522,13 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (async_server, app_arc, _server) = MockServarrApi::get() @@ -649,13 +649,13 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (async_server, app_arc, _server) = MockServarrApi::get() @@ -754,13 +754,13 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (async_server, app_arc, _server) = MockServarrApi::get() @@ -821,8 +821,7 @@ mod tests { .path("/1") .build_for(SonarrEvent::GetEpisodeDetails(1)) .await; - let mut episode_details_modal = EpisodeDetailsModal::default(); - episode_details_modal.episode_details_tabs.next(); + let episode_details_modal = EpisodeDetailsModal::default(); let mut season_details_modal = SeasonDetailsModal::default(); season_details_modal.episodes.set_items(vec![episode()]); season_details_modal.episode_details_modal = Some(episode_details_modal); @@ -868,7 +867,7 @@ mod tests { .unwrap() .episode_details_tabs .get_active_route(), - ActiveSonarrBlock::EpisodeHistory.into() + ActiveSonarrBlock::EpisodeDetails.into() ); assert_eq!(episode, response); @@ -1067,21 +1066,53 @@ mod tests { #[tokio::test] async fn test_handle_get_episode_releases_event() { - let release_json = json!([{ - "guid": "1234", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }} - }]); + let release_json = json!([ + { + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "id": 1, "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }}, + "fullSeason": true + }, + { + "guid": "4567", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "id": 1, "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }}, + } + ]); + let expected_filtered_sonarr_release = SonarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }; + let expected_raw_sonarr_releases = vec![ + SonarrRelease { + full_season: true, + ..torrent_release() + }, + SonarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }, + ]; let (async_server, app_arc, _server) = MockServarrApi::get() .returns(release_json) .query("episodeId=1") @@ -1124,28 +1155,60 @@ mod tests { .unwrap() .episode_releases .items, - vec![torrent_release()] + vec![expected_filtered_sonarr_release] ); - assert_eq!(releases_vec, vec![torrent_release()]); + assert_eq!(releases_vec, expected_raw_sonarr_releases); } #[tokio::test] async fn test_handle_get_episode_releases_event_empty_episode_details_modal() { - let release_json = json!([{ - "guid": "1234", - "protocol": "torrent", - "age": 1, - "title": "Test Release", - "indexer": "kickass torrents", - "indexerId": 2, - "size": 1234, - "rejected": true, - "rejections": [ "Unknown quality profile", "Release is already mapped" ], - "seeders": 2, - "leechers": 1, - "languages": [ { "id": 1, "name": "English" } ], - "quality": { "quality": { "name": "Bluray-1080p" }} - }]); + let release_json = json!([ + { + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "id": 1, "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }}, + "fullSeason": true + }, + { + "guid": "4567", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "id": 1, "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }}, + } + ]); + let expected_filtered_sonarr_release = SonarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }; + let expected_raw_sonarr_releases = vec![ + SonarrRelease { + full_season: true, + ..torrent_release() + }, + SonarrRelease { + guid: "4567".to_owned(), + ..torrent_release() + }, + ]; let (async_server, app_arc, _server) = MockServarrApi::get() .returns(release_json) .query("episodeId=1") @@ -1179,9 +1242,9 @@ mod tests { .unwrap() .episode_releases .items, - vec![torrent_release()] + vec![expected_filtered_sonarr_release] ); - assert_eq!(releases_vec, vec![torrent_release()]); + assert_eq!(releases_vec, expected_raw_sonarr_releases); } #[tokio::test] diff --git a/src/network/sonarr_network/library/seasons/mod.rs b/src/network/sonarr_network/library/seasons/mod.rs index 7ed2912..d165dff 100644 --- a/src/network/sonarr_network/library/seasons/mod.rs +++ b/src/network/sonarr_network/library/seasons/mod.rs @@ -1,4 +1,5 @@ -use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal; +use crate::models::Route; +use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::sonarr_models::{SonarrCommandBody, SonarrHistoryItem, SonarrRelease}; use crate::network::sonarr_network::SonarrEvent; use crate::network::{Network, RequestMethod}; @@ -13,10 +14,10 @@ mod sonarr_seasons_network_tests; impl Network<'_, '_> { pub(in crate::network::sonarr_network) async fn toggle_sonarr_season_monitoring( &mut self, - series_id_season_number_tuple: (i64, i64), + series_id: i64, + season_number: i64, ) -> Result<()> { - let event = SonarrEvent::ToggleSeasonMonitoring(series_id_season_number_tuple); - let (series_id, season_number) = series_id_season_number_tuple; + let event = SonarrEvent::ToggleSeasonMonitoring(series_id, season_number); let detail_event = SonarrEvent::GetSeriesDetails(series_id); info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); @@ -93,10 +94,10 @@ impl Network<'_, '_> { pub(in crate::network::sonarr_network) async fn get_season_releases( &mut self, - series_season_id_tuple: (i64, i64), + series_id: i64, + season_number: i64, ) -> Result> { - let event = SonarrEvent::GetSeasonReleases(series_season_id_tuple); - let (series_id, season_number) = series_season_id_tuple; + let event = SonarrEvent::GetSeasonReleases(series_id, season_number); info!("Fetching releases for series with ID: {series_id} and season number: {season_number}"); let request_props = self @@ -111,21 +112,18 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |release_vec, mut app| { - if app.data.sonarr_data.season_details_modal.is_none() { - app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - } + let season_details_modal = app + .data + .sonarr_data + .season_details_modal + .get_or_insert_default(); let season_releases_vec = release_vec .into_iter() .filter(|release| release.full_season) .collect(); - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() + season_details_modal .season_releases .set_items(season_releases_vec); }) @@ -134,10 +132,10 @@ impl Network<'_, '_> { pub(in crate::network::sonarr_network) async fn get_sonarr_season_history( &mut self, - series_season_id_tuple: (i64, i64), + series_id: i64, + season_number: i64, ) -> Result> { - let event = SonarrEvent::GetSeasonHistory(series_season_id_tuple); - let (series_id, season_number) = series_season_id_tuple; + let event = SonarrEvent::GetSeasonHistory(series_id, season_number); info!("Fetching history for series with ID: {series_id} and season number: {season_number}"); let params = format!("seriesId={series_id}&seasonNumber={season_number}",); @@ -147,38 +145,35 @@ impl Network<'_, '_> { self .handle_request::<(), Vec>(request_props, |history_items, mut app| { - if app.data.sonarr_data.season_details_modal.is_none() { - app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); - } + let is_sorting = matches!( + app.get_current_route(), + Route::Sonarr(ActiveSonarrBlock::SeasonHistorySortPrompt, _) + ); - let mut history_vec = history_items; - history_vec.sort_by(|a, b| a.id.cmp(&b.id)); - app + let season_details_modal = app .data .sonarr_data .season_details_modal - .as_mut() - .unwrap() - .season_history - .set_items(history_vec); - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .unwrap() - .season_history - .apply_sorting_toggle(false); + .get_or_insert_default(); + + if !is_sorting { + let mut history_vec = history_items; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + season_details_modal.season_history.set_items(history_vec); + season_details_modal + .season_history + .apply_sorting_toggle(false); + } }) .await } pub(in crate::network::sonarr_network) async fn trigger_automatic_season_search( &mut self, - series_season_id_tuple: (i64, i64), + series_id: i64, + season_number: i64, ) -> Result { - let event = SonarrEvent::TriggerAutomaticSeasonSearch(series_season_id_tuple); - let (series_id, season_number) = series_season_id_tuple; + let event = SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number); info!("Searching indexers for series with ID: {series_id} and season number: {season_number}"); let body = SonarrCommandBody { diff --git a/src/network/sonarr_network/library/seasons/sonarr_seasons_network_tests.rs b/src/network/sonarr_network/library/seasons/sonarr_seasons_network_tests.rs index 80427cc..2d1b035 100644 --- a/src/network/sonarr_network/library/seasons/sonarr_seasons_network_tests.rs +++ b/src/network/sonarr_network/library/seasons/sonarr_seasons_network_tests.rs @@ -1,12 +1,13 @@ #[cfg(test)] mod tests { use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::sonarr_models::{SonarrHistoryItem, SonarrRelease, SonarrSerdeable}; use crate::network::NetworkResource; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - SERIES_JSON, history_item, season, series, torrent_release, + SERIES_JSON, season, series, sonarr_history_item, torrent_release, }; use mockito::Matcher; use pretty_assertions::assert_eq; @@ -36,7 +37,7 @@ mod tests { "PUT", format!( "/api/v3{}/1", - SonarrEvent::ToggleSeasonMonitoring((1, 1)).resource() + SonarrEvent::ToggleSeasonMonitoring(1, 1).resource() ) .as_str(), ) @@ -55,7 +56,7 @@ mod tests { assert!( network - .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring((1, 1))) + .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(1, 1)) .await .is_ok() ); @@ -116,7 +117,7 @@ mod tests { let (mock, app, _server) = MockServarrApi::get() .returns(release_json) .query("seriesId=1&seasonNumber=1") - .build_for(SonarrEvent::GetSeasonReleases((1, 1))) + .build_for(SonarrEvent::GetSeasonReleases(1, 1)) .await; app .lock() @@ -137,7 +138,7 @@ mod tests { let mut network = test_network(&app); let SonarrSerdeable::Releases(releases_vec) = network - .handle_sonarr_event(SonarrEvent::GetSeasonReleases((1, 1))) + .handle_sonarr_event(SonarrEvent::GetSeasonReleases(1, 1)) .await .unwrap() else { @@ -202,7 +203,7 @@ mod tests { let (mock, app, _server) = MockServarrApi::get() .returns(release_json) .query("seriesId=1&seasonNumber=1") - .build_for(SonarrEvent::GetSeasonReleases((1, 1))) + .build_for(SonarrEvent::GetSeasonReleases(1, 1)) .await; app .lock() @@ -223,7 +224,7 @@ mod tests { assert!( network - .handle_sonarr_event(SonarrEvent::GetSeasonReleases((1, 1))) + .handle_sonarr_event(SonarrEvent::GetSeasonReleases(1, 1)) .await .is_ok() ); @@ -278,19 +279,19 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (mock, app, _server) = MockServarrApi::get() .returns(history_json) .query("seriesId=1&seasonNumber=1") - .build_for(SonarrEvent::GetSeasonHistory((1, 1))) + .build_for(SonarrEvent::GetSeasonHistory(1, 1)) .await; app.lock().await.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); app @@ -321,7 +322,7 @@ mod tests { let mut network = test_network(&app); let SonarrSerdeable::SonarrHistoryItems(history) = network - .handle_sonarr_event(SonarrEvent::GetSeasonHistory((1, 1))) + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(1, 1)) .await .unwrap() else { @@ -390,19 +391,19 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (mock, app, _server) = MockServarrApi::get() .returns(history_json) .query("seriesId=1&seasonNumber=1") - .build_for(SonarrEvent::GetSeasonHistory((1, 1))) + .build_for(SonarrEvent::GetSeasonHistory(1, 1)) .await; app .lock() @@ -422,7 +423,7 @@ mod tests { let mut network = test_network(&app); let SonarrSerdeable::SonarrHistoryItems(history) = network - .handle_sonarr_event(SonarrEvent::GetSeasonHistory((1, 1))) + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(1, 1)) .await .unwrap() else { @@ -466,6 +467,93 @@ mod tests { assert_eq!(history, response); } + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event_no_op_when_user_is_selecting_sort_option() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let (mock, app, _server) = MockServarrApi::get() + .returns(history_json) + .query("seriesId=1&seasonNumber=1") + .build_for(SonarrEvent::GetSeasonHistory(1, 1)) + .await; + app.lock().await.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + app + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .sort_asc = true; + app.lock().await.server_tabs.next(); + app + .lock() + .await + .push_navigation_stack(ActiveSonarrBlock::SeasonHistorySortPrompt.into()); + let mut network = test_network(&app); + + let SonarrSerdeable::SonarrHistoryItems(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(1, 1)) + .await + .unwrap() + else { + panic!("Expected SonarrHistoryItems") + }; + mock.assert_async().await; + assert_is_empty!( + app + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items + ); + assert!( + app + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + #[tokio::test] async fn test_handle_trigger_automatic_season_search_event() { let (mock, app, _server) = MockServarrApi::post() @@ -475,14 +563,14 @@ mod tests { "seasonNumber": 1 })) .returns(json!({})) - .build_for(SonarrEvent::TriggerAutomaticSeasonSearch((1, 1))) + .build_for(SonarrEvent::TriggerAutomaticSeasonSearch(1, 1)) .await; app.lock().await.server_tabs.next(); let mut network = test_network(&app); assert!( network - .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch((1, 1))) + .handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch(1, 1)) .await .is_ok() ); diff --git a/src/network/sonarr_network/library/series/mod.rs b/src/network/sonarr_network/library/series/mod.rs index 100a162..151cbe6 100644 --- a/src/network/sonarr_network/library/series/mod.rs +++ b/src/network/sonarr_network/library/series/mod.rs @@ -20,7 +20,7 @@ impl Network<'_, '_> { pub(in crate::network::sonarr_network) async fn add_sonarr_series( &mut self, mut add_series_body: AddSeriesBody, - ) -> anyhow::Result { + ) -> Result { info!("Adding new series to Sonarr"); let event = SonarrEvent::AddSeries(AddSeriesBody::default()); if let Some(tag_input_str) = add_series_body.tag_input_string.as_ref() { @@ -362,20 +362,26 @@ impl Network<'_, '_> { ) .await; - self + let result = self .handle_request::<(), Vec>(request_props, |series_vec, mut app| { if series_vec.is_empty() { app.pop_and_push_navigation_stack(ActiveSonarrBlock::AddSeriesEmptySearchResults.into()); - } else if let Some(add_searched_seriess) = app.data.sonarr_data.add_searched_series.as_mut() + } else if let Some(add_searched_series) = app.data.sonarr_data.add_searched_series.as_mut() { - add_searched_seriess.set_items(series_vec); + add_searched_series.set_items(series_vec); } else { - let mut add_searched_seriess = StatefulTable::default(); - add_searched_seriess.set_items(series_vec); - app.data.sonarr_data.add_searched_series = Some(add_searched_seriess); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(series_vec); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); } }) - .await + .await; + + if result.is_err() { + self.app.lock().await.data.sonarr_data.add_searched_series = Some(StatefulTable::default()); + } + + result } pub(in crate::network::sonarr_network) async fn trigger_automatic_series_search( diff --git a/src/network/sonarr_network/library/series/sonarr_series_network_tests.rs b/src/network/sonarr_network/library/series/sonarr_series_network_tests.rs index fabd110..2342c1a 100644 --- a/src/network/sonarr_network/library/series/sonarr_series_network_tests.rs +++ b/src/network/sonarr_network/library/series/sonarr_series_network_tests.rs @@ -2,15 +2,15 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::sonarr_models::{ - AddSeriesBody, AddSeriesOptions, DeleteSeriesParams, EditSeriesParams, Series, SeriesType, - SonarrHistoryItem, SonarrSerdeable, + AddSeriesBody, AddSeriesOptions, DeleteSeriesParams, EditSeriesParams, Series, SeriesMonitor, + SeriesType, SonarrHistoryItem, SonarrSerdeable, }; use crate::models::stateful_table::{SortOption, StatefulTable}; use crate::network::NetworkResource; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{ - SERIES_JSON, add_series_search_result, history_item, season, series, + SERIES_JSON, add_series_search_result, season, series, sonarr_history_item, }; use bimap::BiMap; use mockito::Matcher; @@ -27,12 +27,12 @@ mod tests { root_folder_path: "/nfs2".to_owned(), quality_profile_id: 2222, language_profile_id: 2222, - series_type: "standard".to_owned(), + series_type: SeriesType::Standard, season_folder: true, tags: Vec::new(), tag_input_string: Some("usenet, testing".to_owned()), add_options: AddSeriesOptions { - monitor: "all".to_owned(), + monitor: SeriesMonitor::All, search_for_cutoff_unmet_episodes: true, search_for_missing_episodes: true, }, @@ -82,12 +82,12 @@ mod tests { root_folder_path: "/nfs2".to_owned(), quality_profile_id: 2222, language_profile_id: 2222, - series_type: "standard".to_owned(), + series_type: SeriesType::Standard, season_folder: true, tags: vec![1, 2], tag_input_string: None, add_options: AddSeriesOptions { - monitor: "all".to_owned(), + monitor: SeriesMonitor::All, search_for_cutoff_unmet_episodes: true, search_for_missing_episodes: true, }, @@ -457,13 +457,13 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (async_server, app, _server) = MockServarrApi::get() @@ -570,13 +570,13 @@ mod tests { id: 123, episode_id: 1007, source_title: "z episode".into(), - ..history_item() + ..sonarr_history_item() }, SonarrHistoryItem { id: 456, episode_id: 2001, source_title: "A Episode".into(), - ..history_item() + ..sonarr_history_item() }, ]; let (async_server, app, _server) = MockServarrApi::get() @@ -873,7 +873,6 @@ mod tests { .query("term=test%20term") .build_for(SonarrEvent::SearchNewSeries("test term".into())) .await; - app.lock().await.data.sonarr_data.add_series_search = Some("test term".into()); app.lock().await.server_tabs.next(); let mut network = test_network(&app); @@ -943,6 +942,27 @@ mod tests { ); } + #[tokio::test] + async fn test_handle_search_new_series_event_sets_empty_table_on_api_error() { + let (async_server, app, _server) = MockServarrApi::get() + .status(500) + .query("term=test%20term") + .build_for(SonarrEvent::SearchNewSeries("test term".into())) + .await; + app.lock().await.server_tabs.next(); + let mut network = test_network(&app); + + let result = network + .handle_sonarr_event(SonarrEvent::SearchNewSeries("test term".into())) + .await; + + async_server.assert_async().await; + assert_err!(result); + let app = app.lock().await; + assert_some!(&app.data.sonarr_data.add_searched_series); + assert_is_empty!(app.data.sonarr_data.add_searched_series.as_ref().unwrap()); + } + #[tokio::test] async fn test_handle_trigger_automatic_series_search_event() { let (async_server, app, _server) = MockServarrApi::post() 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 0323dd2..68562d3 100644 --- a/src/network/sonarr_network/library/sonarr_library_network_tests.rs +++ b/src/network/sonarr_network/library/sonarr_library_network_tests.rs @@ -32,6 +32,6 @@ mod tests { .await; mock.assert_async().await; - assert!(result.is_ok()); + assert_ok!(result); } } diff --git a/src/network/sonarr_network/mod.rs b/src/network/sonarr_network/mod.rs index b0f314c..9425d2f 100644 --- a/src/network/sonarr_network/mod.rs +++ b/src/network/sonarr_network/mod.rs @@ -5,10 +5,12 @@ use serde_json::{Value, json}; use super::{Network, NetworkEvent, NetworkResource}; use crate::{ models::{ - servarr_models::{AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag}, + servarr_models::{ + AddRootFolderBody, EditIndexerParams, IndexerSettings, Language, QualityProfile, Tag, + }, sonarr_models::{ - AddSeriesBody, DeleteSeriesParams, EditSeriesParams, IndexerSettings, - SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTaskName, + AddSeriesBody, DeleteSeriesParams, EditSeriesParams, SonarrReleaseDownloadBody, + SonarrSerdeable, SonarrTaskName, }, }, network::RequestMethod, @@ -63,8 +65,8 @@ pub enum SonarrEvent { GetQueuedEvents, GetRootFolders, GetEpisodeReleases(i64), - GetSeasonHistory((i64, i64)), - GetSeasonReleases((i64, i64)), + GetSeasonHistory(i64, i64), + GetSeasonReleases(i64, i64), GetSecurityConfig, GetSeriesDetails(i64), GetSeriesHistory(i64), @@ -79,11 +81,11 @@ pub enum SonarrEvent { StartTask(SonarrTaskName), TestIndexer(i64), TestAllIndexers, - ToggleSeasonMonitoring((i64, i64)), + ToggleSeasonMonitoring(i64, i64), ToggleSeriesMonitoring(i64), ToggleEpisodeMonitoring(i64), TriggerAutomaticEpisodeSearch(i64), - TriggerAutomaticSeasonSearch((i64, i64)), + TriggerAutomaticSeasonSearch(i64, i64), TriggerAutomaticSeriesSearch(i64), UpdateAllSeries, UpdateAndScanSeries(i64), @@ -116,7 +118,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetQueuedEvents | SonarrEvent::StartTask(_) | SonarrEvent::TriggerAutomaticSeriesSearch(_) - | SonarrEvent::TriggerAutomaticSeasonSearch(_) + | SonarrEvent::TriggerAutomaticSeasonSearch(_, _) | SonarrEvent::TriggerAutomaticEpisodeSearch(_) | SonarrEvent::UpdateAllSeries | SonarrEvent::UpdateAndScanSeries(_) @@ -124,8 +126,8 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetRootFolders | SonarrEvent::DeleteRootFolder(_) | SonarrEvent::AddRootFolder(_) => "/rootfolder", - SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release", - SonarrEvent::GetSeriesHistory(_) | SonarrEvent::GetSeasonHistory(_) => "/history/series", + SonarrEvent::GetSeasonReleases(_, _) | SonarrEvent::GetEpisodeReleases(_) => "/release", + SonarrEvent::GetSeriesHistory(_) | SonarrEvent::GetSeasonHistory(_, _) => "/history/series", SonarrEvent::GetStatus => "/system/status", SonarrEvent::GetTasks => "/system/task", SonarrEvent::GetUpdates => "/update", @@ -135,7 +137,7 @@ impl NetworkResource for SonarrEvent { | SonarrEvent::GetSeriesDetails(_) | SonarrEvent::DeleteSeries(_) | SonarrEvent::EditSeries(_) - | SonarrEvent::ToggleSeasonMonitoring(_) + | SonarrEvent::ToggleSeasonMonitoring(_, _) | SonarrEvent::ToggleSeriesMonitoring(_) => "/series", SonarrEvent::SearchNewSeries(_) => "/series/lookup", SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", @@ -273,12 +275,12 @@ impl Network<'_, '_> { .get_episode_releases(params) .await .map(SonarrSerdeable::from), - SonarrEvent::GetSeasonHistory(params) => self - .get_sonarr_season_history(params) + SonarrEvent::GetSeasonHistory(series_id, season_number) => self + .get_sonarr_season_history(series_id, season_number) .await .map(SonarrSerdeable::from), - SonarrEvent::GetSeasonReleases(params) => self - .get_season_releases(params) + SonarrEvent::GetSeasonReleases(series_id, season_number) => self + .get_season_releases(series_id, season_number) .await .map(SonarrSerdeable::from), SonarrEvent::GetSecurityConfig => self @@ -326,16 +328,16 @@ impl Network<'_, '_> { .toggle_sonarr_episode_monitoring(episode_id) .await .map(SonarrSerdeable::from), - SonarrEvent::ToggleSeasonMonitoring(params) => self - .toggle_sonarr_season_monitoring(params) + SonarrEvent::ToggleSeasonMonitoring(series_id, season_number) => self + .toggle_sonarr_season_monitoring(series_id, season_number) .await .map(SonarrSerdeable::from), SonarrEvent::ToggleSeriesMonitoring(series_id) => self .toggle_sonarr_series_monitoring(series_id) .await .map(SonarrSerdeable::from), - SonarrEvent::TriggerAutomaticSeasonSearch(params) => self - .trigger_automatic_season_search(params) + SonarrEvent::TriggerAutomaticSeasonSearch(series_id, season_number) => self + .trigger_automatic_season_search(series_id, season_number) .await .map(SonarrSerdeable::from), SonarrEvent::TriggerAutomaticSeriesSearch(series_id) => self diff --git a/src/network/sonarr_network/sonarr_network_test_utils.rs b/src/network/sonarr_network/sonarr_network_test_utils.rs index e3fc28c..0a9fab3 100644 --- a/src/network/sonarr_network/sonarr_network_test_utils.rs +++ b/src/network/sonarr_network/sonarr_network_test_utils.rs @@ -5,10 +5,9 @@ pub mod test_utils { }; use crate::models::sonarr_models::{ AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord, - DownloadStatus, DownloadsResponse, Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, - Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, - SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask, - SonarrTaskName, + DownloadStatus, DownloadsResponse, Episode, EpisodeFile, MediaInfo, Rating, Season, + SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData, + SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrTask, SonarrTaskName, }; use crate::models::{HorizontallyScrollableText, ScrollableText}; use bimap::BiMap; @@ -204,7 +203,7 @@ pub mod test_utils { } } - pub fn history_item() -> SonarrHistoryItem { + pub fn sonarr_history_item() -> SonarrHistoryItem { SonarrHistoryItem { id: 1, source_title: "Test source".into(), @@ -250,16 +249,6 @@ pub mod test_utils { } } - pub fn indexer_settings() -> IndexerSettings { - IndexerSettings { - id: 1, - minimum_age: 1, - retention: 1, - maximum_size: 12345, - rss_sync_interval: 60, - } - } - pub fn language() -> Language { Language { id: 1, @@ -284,6 +273,7 @@ pub mod test_utils { subtitles: Some("English".to_owned()), } } + pub fn quality() -> Quality { Quality { name: "Bluray-1080p".to_owned(), diff --git a/src/network/sonarr_network/sonarr_network_tests.rs b/src/network/sonarr_network/sonarr_network_tests.rs index 87aa4ce..123441c 100644 --- a/src/network/sonarr_network/sonarr_network_tests.rs +++ b/src/network/sonarr_network/sonarr_network_tests.rs @@ -3,11 +3,9 @@ mod test { use crate::app::App; use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_models::{ - AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag, - }; - use crate::models::sonarr_models::{ - AddSeriesBody, EditSeriesParams, IndexerSettings, SonarrTaskName, + AddRootFolderBody, EditIndexerParams, IndexerSettings, Language, QualityProfile, Tag, }; + use crate::models::sonarr_models::{AddSeriesBody, EditSeriesParams, SonarrTaskName}; use crate::models::sonarr_models::{DeleteSeriesParams, SonarrSerdeable}; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::tag; @@ -45,8 +43,8 @@ mod test { SonarrEvent::GetSeriesDetails(0), SonarrEvent::DeleteSeries(DeleteSeriesParams::default()), SonarrEvent::EditSeries(EditSeriesParams::default()), - SonarrEvent::ToggleSeasonMonitoring((0, 0)), - SonarrEvent::ToggleSeriesMonitoring(0), + SonarrEvent::ToggleSeasonMonitoring(0, 0), + SonarrEvent::ToggleSeriesMonitoring(0) )] event: SonarrEvent, ) { @@ -78,7 +76,7 @@ mod test { SonarrEvent::GetQueuedEvents, SonarrEvent::StartTask(SonarrTaskName::default()), SonarrEvent::TriggerAutomaticEpisodeSearch(0), - SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)), + SonarrEvent::TriggerAutomaticSeasonSearch(0, 0), SonarrEvent::TriggerAutomaticSeriesSearch(0), SonarrEvent::UpdateAllSeries, SonarrEvent::UpdateAndScanSeries(0), @@ -110,10 +108,7 @@ mod test { #[rstest] fn test_resource_series_history( - #[values( - SonarrEvent::GetSeriesHistory(0), - SonarrEvent::GetSeasonHistory((0, 0)) - )] + #[values(SonarrEvent::GetSeriesHistory(0), SonarrEvent::GetSeasonHistory(0, 0))] event: SonarrEvent, ) { assert_str_eq!(event.resource(), "/history/series"); @@ -141,7 +136,7 @@ mod test { #[rstest] fn test_resource_release( #[values( - SonarrEvent::GetSeasonReleases((0, 0)), + SonarrEvent::GetSeasonReleases(0, 0), SonarrEvent::GetEpisodeReleases(0) )] event: SonarrEvent, diff --git a/src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs b/src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs new file mode 100644 index 0000000..b0810ef --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs @@ -0,0 +1,74 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::blocklist::BlocklistUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_blocklist_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_lidarr_block) { + assert!(BlocklistUi::accepts(active_lidarr_block.into())); + } else { + assert!(!BlocklistUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_blocklist_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + BlocklistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_blocklist_ui_renders_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Blocklist.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + BlocklistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_blocklist_ui_renders( + #[values( + ActiveLidarrBlock::Blocklist, + ActiveLidarrBlock::BlocklistItemDetails, + ActiveLidarrBlock::DeleteBlocklistItemPrompt, + ActiveLidarrBlock::BlocklistClearAllItemsPrompt, + ActiveLidarrBlock::BlocklistSortPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + BlocklistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("blocklist_tab_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/blocklist/mod.rs b/src/ui/lidarr_ui/blocklist/mod.rs new file mode 100644 index 0000000..6c2f055 --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/mod.rs @@ -0,0 +1,156 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::BlocklistItem; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, BLOCKLIST_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::styles::{ManagarrStyle, secondary_style}; +use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use ratatui::Frame; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::style::Stylize; +use ratatui::text::{Line, Text}; +use ratatui::widgets::{Cell, Row}; + +#[cfg(test)] +#[path = "blocklist_ui_tests.rs"] +mod blocklist_ui_tests; + +pub(super) struct BlocklistUi; + +impl DrawUi for BlocklistUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return BLOCKLIST_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_blocklist_table(f, app, area); + + match active_lidarr_block { + ActiveLidarrBlock::BlocklistItemDetails => { + draw_blocklist_item_details_popup(f, app); + } + ActiveLidarrBlock::DeleteBlocklistItemPrompt => { + let prompt = format!( + "Do you want to remove this item from your blocklist: \n{}?", + app + .data + .lidarr_data + .blocklist + .current_selection() + .source_title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Remove Item from Blocklist") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::BlocklistClearAllItemsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Clear Blocklist") + .prompt("Do you want to clear your blocklist?") + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::SmallPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { + let BlocklistItem { + source_title, + artist, + quality, + date, + .. + } = blocklist_item; + + let title = artist.artist_name.text.to_owned(); + + Row::new(vec![ + Cell::from(title), + Cell::from(source_title.to_owned()), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let blocklist_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.blocklist), + blocklist_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_lidarr_block == ActiveLidarrBlock::BlocklistSortPrompt) + .headers(["Artist Name", "Source Title", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(27), + Constraint::Percentage(43), + Constraint::Percentage(13), + Constraint::Percentage(17), + ]); + + f.render_widget(blocklist_table, area); + } +} + +fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.lidarr_data.blocklist.items.is_empty() { + BlocklistItem::default() + } else { + app.data.lidarr_data.blocklist.current_selection().clone() + }; + let BlocklistItem { + source_title, + protocol, + indexer, + message, + .. + } = current_selection; + let text = Text::from(vec![ + Line::from(vec![ + "Name: ".bold().secondary(), + source_title.to_owned().secondary(), + ]), + Line::from(vec![ + "Protocol: ".bold().secondary(), + protocol.to_owned().secondary(), + ]), + Line::from(vec![ + "Indexer: ".bold().secondary(), + indexer.to_owned().secondary(), + ]), + Line::from(vec![ + "Message: ".bold().secondary(), + message.to_owned().secondary(), + ]), + ]); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); +} diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_Blocklist.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_Blocklist.snap new file mode 100644 index 0000000..8a1326b --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_Blocklist.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Artist Name ▼ Source Title Quality Date +=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistClearAllItemsPrompt.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistClearAllItemsPrompt.snap new file mode 100644 index 0000000..639316b --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistClearAllItemsPrompt.snap @@ -0,0 +1,34 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Artist Name ▼ Source Title Quality Date +=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC + + + + + + + + + + + + + + + + + + ╭────── Clear Blocklist ──────╮ + │ Do you want to clear your │ + │ blocklist? │ + │ │ + │ │ + │ │ + │╭──────────────╮╭─────────────╮│ + ││ Yes ││ No ││ + │╰──────────────╯╰─────────────╯│ + ╰───────────────────────────────╯ diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistItemDetails.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistItemDetails.snap new file mode 100644 index 0000000..3359023 --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistItemDetails.snap @@ -0,0 +1,34 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Artist Name ▼ Source Title Quality Date +=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC + + + + + + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Name: Alex - Something │ + │Protocol: usenet │ + │Indexer: NZBgeek (Prowlarr) │ + │Message: test message │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistSortPrompt.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistSortPrompt.snap new file mode 100644 index 0000000..22d3ad6 --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_BlocklistSortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Artist Name Source Title Quality Date +=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC + + + + + + + + + + + ╭───────────────────────────────╮ + │Something │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_DeleteBlocklistItemPrompt.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_DeleteBlocklistItemPrompt.snap new file mode 100644 index 0000000..0b417ef --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_tab_DeleteBlocklistItemPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Artist Name ▼ Source Title Quality Date +=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC + + + + + + + + + + + + + + ╭────────────── Remove Item from Blocklist ───────────────╮ + │ Do you want to remove this item from your blocklist: │ + │ Alex - Something? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_ui_renders_empty.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_ui_renders_empty.snap new file mode 100644 index 0000000..3863f67 --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_ui_renders_empty.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_ui_renders_loading.snap b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_ui_renders_loading.snap new file mode 100644 index 0000000..c681094 --- /dev/null +++ b/src/ui/lidarr_ui/blocklist/snapshots/managarr__ui__lidarr_ui__blocklist__blocklist_ui_tests__tests__snapshot_tests__blocklist_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/blocklist/blocklist_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/downloads/downloads_ui_tests.rs b/src/ui/lidarr_ui/downloads/downloads_ui_tests.rs new file mode 100644 index 0000000..f6be857 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/downloads_ui_tests.rs @@ -0,0 +1,72 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::downloads::DownloadsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_downloads_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) { + assert!(DownloadsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!DownloadsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_downloads_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DownloadsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_downloads_ui_renders_empty_downloads() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DownloadsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_downloads_ui_renders( + #[values( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DownloadsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("downloads_ui_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/downloads/mod.rs b/src/ui/lidarr_ui/downloads/mod.rs new file mode 100644 index 0000000..2e0ed1f --- /dev/null +++ b/src/ui/lidarr_ui/downloads/mod.rs @@ -0,0 +1,146 @@ +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; + +use crate::app::App; +use crate::models::lidarr_models::DownloadRecord; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; +use crate::models::{HorizontallyScrollableText, Route}; +use crate::ui::DrawUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::utils::convert_f64_to_gb; + +#[cfg(test)] +#[path = "downloads_ui_tests.rs"] +mod downloads_ui_tests; + +pub(super) struct DownloadsUi; + +impl DrawUi for DownloadsUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return DOWNLOADS_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_downloads(f, app, area); + + match active_lidarr_block { + ActiveLidarrBlock::DeleteDownloadPrompt => { + let prompt = format!( + "Do you really want to delete this download: \n{}?", + app.data.lidarr_data.downloads.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Cancel Download") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::UpdateDownloadsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update Downloads") + .prompt("Do you want to update your downloads?") + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.lidarr_data.downloads.items.is_empty() { + DownloadRecord::default() + } else { + app.data.lidarr_data.downloads.current_selection().clone() + }; + + let downloads_row_mapping = |download_record: &DownloadRecord| { + let DownloadRecord { + title, + size, + sizeleft, + download_client, + indexer, + output_path, + .. + } = download_record; + + if output_path.is_some() { + output_path.as_ref().unwrap().scroll_left_or_reset( + get_width_from_percentage(area, 18), + current_selection == *download_record, + app.ui_scroll_tick_count == 0, + ); + } + + let percent = if *size == 0.0 { + 0.0 + } else { + 1f64 - (*sizeleft / *size) + }; + let file_size: f64 = convert_f64_to_gb(*size); + + Row::new(vec![ + Cell::from(title.to_owned()), + Cell::from(format!("{:.0}%", percent * 100.0)), + Cell::from(format!("{file_size:.2} GB")), + Cell::from( + output_path + .as_ref() + .unwrap_or(&HorizontallyScrollableText::default()) + .to_string(), + ), + Cell::from(indexer.to_owned()), + Cell::from( + download_client + .as_ref() + .unwrap_or(&String::new()) + .to_owned(), + ), + ]) + .primary() + }; + let downloads_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.downloads), + downloads_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers([ + "Title", + "Percent Complete", + "Size", + "Output Path", + "Indexer", + "Download Client", + ]) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(18), + Constraint::Percentage(17), + Constraint::Percentage(13), + ]); + + f.render_widget(downloads_table, area); +} diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap new file mode 100644 index 0000000..fae0b18 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Title Percent Complete Size Output Path Indexer Download Client +=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission + + + + + + + + + + + + + + ╭──────────────────── Cancel Download ────────────────────╮ + │ Do you really want to delete this download: │ + │ Test download title? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap new file mode 100644 index 0000000..1be9a81 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Title Percent Complete Size Output Path Indexer Download Client +=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap new file mode 100644 index 0000000..992b606 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Title Percent Complete Size Output Path Indexer Download Client +=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission + + + + + + + + + + + + + + ╭─────────────────── Update Downloads ────────────────────╮ + │ Do you want to update your downloads? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap new file mode 100644 index 0000000..d20a8d6 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap new file mode 100644 index 0000000..7146ea5 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/history/history_ui_tests.rs b/src/ui/lidarr_ui/history/history_ui_tests.rs new file mode 100644 index 0000000..f46b50f --- /dev/null +++ b/src/ui/lidarr_ui/history/history_ui_tests.rs @@ -0,0 +1,79 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, HISTORY_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::history::HistoryUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_history_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if HISTORY_BLOCKS.contains(&active_lidarr_block) { + assert!(HistoryUi::accepts(active_lidarr_block.into())); + } else { + assert!(!HistoryUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_history_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::History.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_history_ui_renders_empty( + #[values(ActiveLidarrBlock::History, ActiveLidarrBlock::HistoryItemDetails)] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("loading_history_tab_{active_lidarr_block}"), output); + } + + #[rstest] + fn test_history_ui_renders( + #[values( + ActiveLidarrBlock::History, + ActiveLidarrBlock::HistoryItemDetails, + ActiveLidarrBlock::HistorySortPrompt, + ActiveLidarrBlock::FilterHistory, + ActiveLidarrBlock::FilterHistoryError, + ActiveLidarrBlock::SearchHistory, + ActiveLidarrBlock::SearchHistoryError + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("history_tab_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/history/mod.rs b/src/ui/lidarr_ui/history/mod.rs new file mode 100644 index 0000000..fb6b8a8 --- /dev/null +++ b/src/ui/lidarr_ui/history/mod.rs @@ -0,0 +1,120 @@ +use super::lidarr_ui_utils::create_history_event_details; +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::LidarrHistoryItem; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, HISTORY_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::styles::{ManagarrStyle, secondary_style}; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use ratatui::Frame; +use ratatui::layout::{Alignment, Rect}; +use ratatui::prelude::Constraint; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; + +#[cfg(test)] +#[path = "history_ui_tests.rs"] +mod history_ui_tests; + +pub(super) struct HistoryUi; + +impl DrawUi for HistoryUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return HISTORY_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_history_table(f, app, area); + + if active_lidarr_block == ActiveLidarrBlock::HistoryItemDetails { + draw_history_item_details_popup(f, app); + } + } + } +} + +fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.lidarr_data.history.items.is_empty() { + LidarrHistoryItem::default() + } else { + app.data.lidarr_data.history.current_selection().clone() + }; + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &LidarrHistoryItem| { + let LidarrHistoryItem { + source_title, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 50), + current_selection == *history_item, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let history_table = + ManagarrTable::new(Some(&mut app.data.lidarr_data.history), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_lidarr_block == ActiveLidarrBlock::HistorySortPrompt) + .searching(active_lidarr_block == ActiveLidarrBlock::SearchHistory) + .search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchHistoryError) + .filtering(active_lidarr_block == ActiveLidarrBlock::FilterHistory) + .filter_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::FilterHistoryError) + .headers(["Source Title", "Event Type", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(50), + Constraint::Percentage(18), + Constraint::Percentage(12), + Constraint::Percentage(20), + ]); + + if [ + ActiveLidarrBlock::SearchHistory, + ActiveLidarrBlock::FilterHistory, + ] + .contains(&active_lidarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.lidarr_data.history.items.is_empty() { + LidarrHistoryItem::default() + } else { + app.data.lidarr_data.history.current_selection().clone() + }; + + let line_vec = create_history_event_details(current_selection); + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); +} diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap new file mode 100644 index 0000000..657ae85 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC + + + + + + + + + + + + + + + + + + + ╭───────────────── Filter ──────────────────╮ + │test filter │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap new file mode 100644 index 0000000..e8ff449 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │The given filter produced empty results│ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap new file mode 100644 index 0000000..6ed3c67 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap new file mode 100644 index 0000000..791f73c --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC + + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Source Title: Test source title │ + │Event Type: grabbed │ + │Quality: Lossless │ + │Date: 2023-01-01 00:00:00 UTC │ + │Indexer: │ + │NZB Info URL: │ + │Release Group: │ + │Age: 0 days │ + │Published Date: 1970-01-01 00:00:00 UTC │ + │Download Client: │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap new file mode 100644 index 0000000..37c8e24 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC + + + + + + + + + + + ╭───────────────────────────────╮ + │Date │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap new file mode 100644 index 0000000..68de9a8 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC + + + + + + + + + + + + + + + + + + + ╭───────────────── Search ──────────────────╮ + │test search │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap new file mode 100644 index 0000000..c1f02a7 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Quality Date +=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ No items found matching search │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap new file mode 100644 index 0000000..7d9fc6c --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap new file mode 100644 index 0000000..42913ba --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap new file mode 100644 index 0000000..0095337 --- /dev/null +++ b/src/ui/lidarr_ui/history/snapshots/managarr__ui__lidarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Source Title: │ + │Event Type: unknown │ + │Quality: │ + │Date: 1970-01-01 00:00:00 UTC │ + │No additional details available. │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/edit_indexer_ui.rs b/src/ui/lidarr_ui/indexers/edit_indexer_ui.rs new file mode 100644 index 0000000..6327d35 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/edit_indexer_ui.rs @@ -0,0 +1,169 @@ +use std::sync::atomic::Ordering; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::render_selectable_input_box; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::popup::Size; +use crate::ui::{DrawUi, draw_popup}; +use ratatui::Frame; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; + +#[cfg(test)] +#[path = "edit_indexer_ui_tests.rs"] +mod edit_indexer_ui_tests; + +pub(super) struct EditIndexerUi; + +impl DrawUi for EditIndexerUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_prompt, Size::WideLargePrompt); + } +} + +fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block_centered("Edit Indexer"); + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::EditIndexerConfirmPrompt; + let edit_indexer_modal_option = &app.data.lidarr_data.edit_indexer_modal; + let protocol = &app.data.lidarr_data.indexers.current_selection().protocol; + + if edit_indexer_modal_option.is_some() { + f.render_widget(block, area); + let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); + + let [settings_area, buttons_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]) + .margin(1) + .areas(area); + let [left_side_area, right_side_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) + .margin(1) + .areas(settings_area); + let [ + name_area, + rss_area, + auto_search_area, + interactive_search_area, + priority_area, + ] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(left_side_area); + let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(right_side_area); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let priority = edit_indexer_modal.priority.to_string(); + let name_input_box = InputBox::new(&edit_indexer_modal.name.text) + .offset(edit_indexer_modal.name.offset.load(Ordering::SeqCst)) + .label("Name") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerNameInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerNameInput); + let url_input_box = InputBox::new(&edit_indexer_modal.url.text) + .offset(edit_indexer_modal.url.offset.load(Ordering::SeqCst)) + .label("URL") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerUrlInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerUrlInput); + let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text) + .offset(edit_indexer_modal.api_key.offset.load(Ordering::SeqCst)) + .label("API Key") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerApiKeyInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerApiKeyInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerTagsInput); + let priority_input_box = InputBox::new(&priority) + .cursor_after_string(false) + .label("Indexer Priority ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerPriorityInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerPriorityInput); + + render_selectable_input_box!(name_input_box, f, name_area); + render_selectable_input_box!(url_input_box, f, url_area); + render_selectable_input_box!(api_key_input_box, f, api_key_area); + + if protocol == "torrent" { + let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text) + .offset(edit_indexer_modal.seed_ratio.offset.load(Ordering::SeqCst)) + .label("Seed Ratio") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerSeedRatioInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerSeedRatioInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditIndexerTagsInput); + + render_selectable_input_box!(seed_ratio_input_box, f, seed_ratio_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + render_selectable_input_box!(priority_input_box, f, priority_area); + } else { + render_selectable_input_box!(tags_input_box, f, seed_ratio_area); + render_selectable_input_box!(priority_input_box, f, tags_area); + } + + let rss_checkbox = Checkbox::new("Enable RSS") + .checked(edit_indexer_modal.enable_rss.unwrap_or_default()) + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableRss); + let auto_search_checkbox = Checkbox::new("Enable Automatic Search") + .checked( + edit_indexer_modal + .enable_automatic_search + .unwrap_or_default(), + ) + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableAutomaticSearch); + let interactive_search_checkbox = Checkbox::new("Enable Interactive Search") + .checked( + edit_indexer_modal + .enable_interactive_search + .unwrap_or_default(), + ) + .highlighted(selected_block == ActiveLidarrBlock::EditIndexerToggleEnableInteractiveSearch); + + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) + .flex(Flex::Center) + .areas(buttons_area); + + let save_button = Button::default() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(rss_checkbox, rss_area); + f.render_widget(auto_search_checkbox, auto_search_area); + f.render_widget(interactive_search_checkbox, interactive_search_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} diff --git a/src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs b/src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs new file mode 100644 index 0000000..66ffa51 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs @@ -0,0 +1,81 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::edit_indexer_ui::EditIndexerUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_edit_indexer_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_lidarr_block) { + assert!(EditIndexerUi::accepts(active_lidarr_block.into())); + } else { + assert!(!EditIndexerUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::models::servarr_data::lidarr::lidarr_data::EDIT_INDEXER_NZB_SELECTION_BLOCKS; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_edit_indexer_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.data.lidarr_data.edit_indexer_modal = None; + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditIndexerUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_edit_indexer_ui_renders_edit_torrent_indexer() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditIndexerUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_edit_indexer_ui_renders_edit_usenet_indexer() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + protocol: "usenet".into(), + ..indexer() + }]); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditIndexerUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/indexers/indexer_settings_ui.rs b/src/ui/lidarr_ui/indexers/indexer_settings_ui.rs new file mode 100644 index 0000000..684e2ef --- /dev/null +++ b/src/ui/lidarr_ui/indexers/indexer_settings_ui.rs @@ -0,0 +1,117 @@ +use ratatui::Frame; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::render_selectable_input_box; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::popup::Size; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "indexer_settings_ui_tests.rs"] +mod indexer_settings_ui_tests; + +pub(super) struct IndexerSettingsUi; + +impl DrawUi for IndexerSettingsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_settings_prompt, Size::LargePrompt); + } +} + +fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block_centered("Configure All Indexer Settings"); + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::IndexerSettingsConfirmPrompt; + let indexer_settings_option = &app.data.lidarr_data.indexer_settings; + + if indexer_settings_option.is_some() { + f.render_widget(block, area); + let indexer_settings = indexer_settings_option.as_ref().unwrap(); + + let [ + _, + min_age_area, + retention_area, + max_size_area, + rss_sync_area, + _, + buttons_area, + ] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let min_age = indexer_settings.minimum_age.to_string(); + let retention = indexer_settings.retention.to_string(); + let max_size = indexer_settings.maximum_size.to_string(); + let rss_sync_interval = indexer_settings.rss_sync_interval.to_string(); + + let min_age_text_box = InputBox::new(&min_age) + .cursor_after_string(false) + .label("Minimum Age (minutes) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsMinimumAgeInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsMinimumAgeInput); + let retention_input_box = InputBox::new(&retention) + .cursor_after_string(false) + .label("Retention (days) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsRetentionInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsRetentionInput); + let max_size_input_box = InputBox::new(&max_size) + .cursor_after_string(false) + .label("Maximum Size (MB) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsMaximumSizeInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsMaximumSizeInput); + let rss_sync_interval_input_box = InputBox::new(&rss_sync_interval) + .cursor_after_string(false) + .label("RSS Sync Interval (minutes) ▴▾") + .highlighted(selected_block == ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput) + .selected(active_lidarr_block == ActiveLidarrBlock::IndexerSettingsRssSyncIntervalInput); + + render_selectable_input_box!(min_age_text_box, f, min_age_area); + render_selectable_input_box!(retention_input_box, f, retention_area); + render_selectable_input_box!(max_size_input_box, f, max_size_area); + render_selectable_input_box!(rss_sync_interval_input_box, f, rss_sync_area); + } + + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) + .flex(Flex::Center) + .areas(buttons_area); + + let save_button = Button::default() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} diff --git a/src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs b/src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs new file mode 100644 index 0000000..6052463 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_indexer_settings_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_lidarr_block) { + assert!(IndexerSettingsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!IndexerSettingsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_indexer_settings_ui_renders_indexer_settings() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::IndexerSettingsMinimumAgeInput.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexerSettingsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/indexers/indexers_ui_tests.rs b/src/ui/lidarr_ui/indexers/indexers_ui_tests.rs new file mode 100644 index 0000000..8019188 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/indexers_ui_tests.rs @@ -0,0 +1,156 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_INDEXER_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXERS_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::IndexersUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_indexers_ui_accepts() { + let mut indexers_blocks = Vec::new(); + indexers_blocks.extend(INDEXERS_BLOCKS); + indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveLidarrBlock::TestAllIndexers); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if indexers_blocks.contains(&active_lidarr_block) { + assert!(IndexersUi::accepts(active_lidarr_block.into())); + } else { + assert!(!IndexersUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::indexer; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_indexers_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_loading_test_results() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TestIndexer.into()); + app.data.lidarr_data.indexer_test_errors = None; + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_empty_indexers() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Indexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_indexers_ui_renders( + #[values( + ActiveLidarrBlock::DeleteIndexerPrompt, + ActiveLidarrBlock::Indexers, + ActiveLidarrBlock::TestIndexer + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("indexers_ui_{active_lidarr_block}"), output); + } + + #[test] + fn test_indexers_ui_renders_test_all_over_indexers() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_edit_usenet_indexer_over_indexers() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.lidarr_data.indexers.set_items(vec![Indexer { + protocol: "usenet".into(), + ..indexer() + }]); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_indexers_ui_renders_edit_torrent_indexer_over_indexers() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::EditIndexerPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + IndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/indexers/mod.rs b/src/ui/lidarr_ui/indexers/mod.rs new file mode 100644 index 0000000..2968537 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/mod.rs @@ -0,0 +1,184 @@ +use crate::ui::styles::success_style; +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, INDEXERS_BLOCKS}; +use crate::models::servarr_models::Indexer; +use crate::ui::DrawUi; +use crate::ui::lidarr_ui::indexers::edit_indexer_ui::EditIndexerUi; +use crate::ui::lidarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; +use crate::ui::lidarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{layout_block_top_border, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; + +mod edit_indexer_ui; +mod indexer_settings_ui; +mod test_all_indexers_ui; + +#[cfg(test)] +#[path = "indexers_ui_tests.rs"] +mod indexers_ui_tests; + +pub(super) struct IndexersUi; + +impl DrawUi for IndexersUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return EditIndexerUi::accepts(route) + || IndexerSettingsUi::accepts(route) + || TestAllIndexersUi::accepts(route) + || INDEXERS_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_indexers(f, app, area); + + match route { + _ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, area), + _ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, area), + _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), + Route::Lidarr(active_lidarr_block, _) => match active_lidarr_block { + ActiveLidarrBlock::TestIndexer => { + if app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none() { + let loading_popup = Popup::new(LoadingBlock::new( + app.is_loading || app.data.lidarr_data.indexer_test_errors.is_none(), + title_block("Testing Indexer"), + )) + .size(Size::LargeMessage); + f.render_widget(loading_popup, f.area()); + } else { + let popup = { + let result = app + .data + .lidarr_data + .indexer_test_errors + .as_ref() + .expect("Test result is unpopulated"); + + if !result.is_empty() { + Popup::new(Message::new(result.clone())).size(Size::LargeMessage) + } else { + let message = Message::new("Indexer test succeeded!") + .title("Success") + .style(success_style().bold()); + Popup::new(message).size(Size::Message) + } + }; + + f.render_widget(popup, f.area()); + } + } + ActiveLidarrBlock::DeleteIndexerPrompt => { + let prompt = format!( + "Do you really want to delete this indexer: \n{}?", + app + .data + .lidarr_data + .indexers + .current_selection() + .name + .clone() + .unwrap_or_default() + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Indexer") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + }, + _ => (), + } + } +} + +fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let indexers_row_mapping = |indexer: &'_ Indexer| { + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + priority, + tags, + .. + } = indexer; + let bool_to_text = |flag: bool| { + if flag { + return Text::from("Enabled").success(); + } + + Text::from("Disabled").failure() + }; + + let rss = bool_to_text(*enable_rss); + let automatic_search = bool_to_text(*enable_automatic_search); + let interactive_search = bool_to_text(*enable_interactive_search); + let empty_tag = String::new(); + let tags: String = tags + .iter() + .map(|tag_id| { + app + .data + .lidarr_data + .tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap_or(&empty_tag) + .clone() + }) + .collect::>() + .join(", "); + + Row::new(vec![ + Cell::from(name.clone().unwrap_or_default()), + Cell::from(rss), + Cell::from(automatic_search), + Cell::from(interactive_search), + Cell::from(priority.to_string()), + Cell::from(tags), + ]) + .primary() + }; + let indexers_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.indexers), + indexers_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers([ + "Indexer", + "RSS", + "Automatic Search", + "Interactive Search", + "Priority", + "Tags", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(23), + ]); + + f.render_widget(indexers_table, area); +} diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap new file mode 100644 index 0000000..83a1c5a --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_torrent_indexer.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Seed Ratio: │ratio │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭─────────────────────────╮ │ + │ Indexer Priority ▴▾: │1 │ │ + │ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap new file mode 100644 index 0000000..cbb9e35 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_edit_usenet_indexer.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Indexer Priority ▴▾: │1 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap new file mode 100644 index 0000000..17a099f --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__edit_indexer_ui__edit_indexer_ui_tests__tests__snapshot_tests__edit_indexer_ui_renders_loading.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/edit_indexer_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap new file mode 100644 index 0000000..7f37593 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexer_settings_ui__indexer_settings_ui_tests__tests__snapshot_tests__indexer_settings_ui_renders_indexer_settings.snap @@ -0,0 +1,40 @@ +--- +source: src/ui/lidarr_ui/indexers/indexer_settings_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + ╭─────────────────── Configure All Indexer Settings ───────────────────╮ + │ │ + │ │ + │ │ + │ ╭────────────────────────────────╮ │ + │ Minimum Age (minutes) ▴▾: │1 │ │ + │ ╰────────────────────────────────╯ │ + │ ╭────────────────────────────────╮ │ + │ Retention (days) ▴▾: │1 │ │ + │ ╰────────────────────────────────╯ │ + │ ╭────────────────────────────────╮ │ + │ Maximum Size (MB) ▴▾: │12345 │ │ + │ ╰────────────────────────────────╯ │ + │ ╭────────────────────────────────╮ │ + │ RSS Sync Interval (minutes) ▴▾: │60 │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ ╭────────────────╮╭────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰────────────────╯╰────────────────╯ │ + ╰────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap new file mode 100644 index 0000000..608058e --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_DeleteIndexerPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + ╭──────────────────── Delete Indexer ─────────────────────╮ + │ Do you really want to delete this indexer: │ + │ Test Indexer? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap new file mode 100644 index 0000000..4972808 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_Indexers.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap new file mode 100644 index 0000000..8b56ae6 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_TestIndexer.snap @@ -0,0 +1,35 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ error │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap new file mode 100644 index 0000000..f2ab6e8 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_torrent_indexer_over_indexers.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Seed Ratio: │ratio │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭─────────────────────────╮ │ + │ Indexer Priority ▴▾: │1 │ │ + │ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap new file mode 100644 index 0000000..54ef1b1 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_edit_usenet_indexer_over_indexers.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + ╭──────────────────────────────────────────────── Edit Indexer ─────────────────────────────────────────────────╮ + │ │ + │ ╭─────────────────────────╮ ╭─────────────────────────╮ │ + │ Name: │DrunkenSlug │ URL: │http://127.0.0.1:9696/1/ │ │ + │ ╰─────────────────────────╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable RSS: │ ✔ │ API Key: │someApiKey │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Automatic Search: │ ✔ │ Tags: │25 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ ╭───╮ ╭─────────────────────────╮ │ + │ Enable Interactive Search: │ ✔ │ Indexer Priority ▴▾: │1 │ │ + │ ╰───╯ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────────────────╮╭──────────────────────────╮ │ + │ │ Save ││ Cancel │ │ + │ ╰───────────────────────────╯╰──────────────────────────╯ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap new file mode 100644 index 0000000..1cee723 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_empty_indexers.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap new file mode 100644 index 0000000..c164738 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap new file mode 100644 index 0000000..3cf1800 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results.snap @@ -0,0 +1,35 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... + + + + + + + + + + + + + + + + ╭ Testing Indexer ────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap new file mode 100644 index 0000000..bab738d --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_loading_test_results_when_indexer_test_errors_is_none.snap @@ -0,0 +1,35 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + + + + + + + + + + + + + + ╭ Testing Indexer ────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap new file mode 100644 index 0000000..63c5b17 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__indexers_ui_tests__tests__snapshot_tests__indexers_ui_renders_test_all_over_indexers.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/indexers/indexers_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Indexer RSS Automatic Search Interactive Search Priority Tags +=> Test Indexer Enabled Enabled Enabled 25 alex + + + + ╭ Test All Indexers ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Indexer Pass/Fail Failure Messages │ + │=> DrunkenSlug x Some failure │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap new file mode 100644 index 0000000..9b8832e --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs +expression: output +--- + + + + + + + ╭ Test All Indexers ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Indexer Pass/Fail Failure Messages │ + │=> DrunkenSlug x Some failure │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap new file mode 100644 index 0000000..418a025 --- /dev/null +++ b/src/ui/lidarr_ui/indexers/snapshots/managarr__ui__lidarr_ui__indexers__test_all_indexers_ui__test_all_indexers_ui_tests__tests__snapshot_tests__test_all_indexers_ui_renders_loading.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs +expression: output +--- + + + + + + + ╭ Test All Indexers ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs new file mode 100644 index 0000000..6a3b30e --- /dev/null +++ b/src/ui/lidarr_ui/indexers/test_all_indexers_ui.rs @@ -0,0 +1,79 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, title_block}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::Size; +use crate::ui::{DrawUi, draw_popup}; +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; + +#[cfg(test)] +#[path = "test_all_indexers_ui_tests.rs"] +mod test_all_indexers_ui_tests; + +pub(super) struct TestAllIndexersUi; + +impl DrawUi for TestAllIndexersUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return active_lidarr_block == ActiveLidarrBlock::TestAllIndexers; + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_test_all_indexers_test_results, Size::Large); + } +} + +fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let is_loading = app.is_loading || app.data.lidarr_data.indexer_test_all_results.is_none(); + let current_selection = if let Some(test_all_results) = + app.data.lidarr_data.indexer_test_all_results.as_ref() + && !test_all_results.is_empty() + { + test_all_results.current_selection().clone() + } else { + IndexerTestResultModalItem::default() + }; + f.render_widget(title_block("Test All Indexers"), area); + let test_results_row_mapping = |result: &IndexerTestResultModalItem| { + result.validation_failures.scroll_left_or_reset( + get_width_from_percentage(area, 86), + *result == current_selection, + app.ui_scroll_tick_count == 0, + ); + let pass_fail = if result.is_valid { "+" } else { "x" }; + let row = Row::new(vec![ + Cell::from(result.name.to_owned()), + Cell::from(pass_fail.to_owned()), + Cell::from(result.validation_failures.to_string()), + ]); + + if result.is_valid { + row.success() + } else { + row.failure() + } + }; + + let indexers_test_results_table = ManagarrTable::new( + app.data.lidarr_data.indexer_test_all_results.as_mut(), + test_results_row_mapping, + ) + .loading(is_loading) + .margin(1) + .headers(["Indexer", "Pass/Fail", "Failure Messages"]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(70), + ]); + + f.render_widget(indexers_test_results_table, area); +} diff --git a/src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs b/src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs new file mode 100644 index 0000000..9565f4d --- /dev/null +++ b/src/ui/lidarr_ui/indexers/test_all_indexers_ui_tests.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_test_all_indexers_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if active_lidarr_block == ActiveLidarrBlock::TestAllIndexers { + assert!(TestAllIndexersUi::accepts(active_lidarr_block.into())); + } else { + assert!(!TestAllIndexersUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_test_all_indexers_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TestAllIndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_test_all_indexers_ui_renders() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TestAllIndexers.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TestAllIndexersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/add_artist_ui.rs b/src/ui/lidarr_ui/library/add_artist_ui.rs new file mode 100644 index 0000000..bb9ab21 --- /dev/null +++ b/src/ui/lidarr_ui/library/add_artist_ui.rs @@ -0,0 +1,417 @@ +use std::sync::atomic::Ordering; + +use ratatui::Frame; +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::widgets::{Cell, ListItem, Row}; + +use crate::App; +use crate::models::Route; +use crate::models::lidarr_models::AddArtistSearchResult; +use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock}; +use crate::models::servarr_data::lidarr::modals::AddArtistModal; +use crate::render_selectable_input_box; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, layout_block, title_block_centered}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "add_artist_ui_tests.rs"] +mod add_artist_ui_tests; + +pub(super) struct AddArtistUi; + +impl DrawUi for AddArtistUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_popup(f, app, draw_add_artist_search, Size::Large); + + match active_lidarr_block { + ActiveLidarrBlock::AddArtistPrompt + | ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder + | ActiveLidarrBlock::AddArtistTagsInput => { + draw_popup(f, app, draw_confirmation_popup, Size::Long); + } + ActiveLidarrBlock::AddArtistAlreadyInLibrary => { + f.render_widget( + Popup::new(Message::new("This artist is already in your library")).size(Size::Message), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_add_artist_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let is_loading = app.is_loading || app.data.lidarr_data.add_searched_artists.is_none(); + let current_selection = if let Some(add_searched_artists) = + app.data.lidarr_data.add_searched_artists.as_ref() + && !add_searched_artists.is_empty() + { + add_searched_artists.current_selection().clone() + } else { + AddArtistSearchResult::default() + }; + + let [search_box_area, results_area] = + Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) + .margin(1) + .areas(area); + let block_content = &app + .data + .lidarr_data + .add_artist_search + .as_ref() + .expect("add_artist_search must be populated") + .text; + let offset = app + .data + .lidarr_data + .add_artist_search + .as_ref() + .expect("add_artist_search must be populated") + .offset + .load(Ordering::SeqCst); + + let search_results_row_mapping = |artist: &AddArtistSearchResult| { + let rating = artist + .ratings + .as_ref() + .map_or(String::new(), |r| format!("{:.1}", r.value)); + let in_library = if app + .data + .lidarr_data + .artists + .items + .iter() + .any(|a| a.foreign_artist_id == artist.foreign_artist_id) + { + "✔" + } else { + "" + }; + + artist.artist_name.scroll_left_or_reset( + get_width_from_percentage(area, 27), + *artist == current_selection, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(in_library), + Cell::from(artist.artist_name.to_string()), + Cell::from(artist.artist_type.clone().unwrap_or_default()), + Cell::from(artist.status.to_display_str()), + Cell::from(rating), + Cell::from(artist.genres.join(", ")), + ]) + .primary() + }; + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + match active_lidarr_block { + ActiveLidarrBlock::AddArtistSearchInput => { + let search_box = InputBox::new(block_content) + .offset(offset) + .block(title_block_centered("Add Artist")); + + search_box.show_cursor(f, search_box_area); + f.render_widget(layout_block().default_color(), results_area); + f.render_widget(search_box, search_box_area); + } + ActiveLidarrBlock::AddArtistEmptySearchResults => { + let error_message = Message::new("No artists found matching your query!"); + let error_message_popup = Popup::new(error_message).size(Size::Message); + + f.render_widget(layout_block().default_color(), results_area); + f.render_widget(error_message_popup, f.area()); + } + ActiveLidarrBlock::AddArtistSearchResults + | ActiveLidarrBlock::AddArtistPrompt + | ActiveLidarrBlock::AddArtistSelectMonitor + | ActiveLidarrBlock::AddArtistSelectMonitorNewItems + | ActiveLidarrBlock::AddArtistSelectQualityProfile + | ActiveLidarrBlock::AddArtistSelectMetadataProfile + | ActiveLidarrBlock::AddArtistSelectRootFolder + | ActiveLidarrBlock::AddArtistAlreadyInLibrary + | ActiveLidarrBlock::AddArtistTagsInput => { + let search_results_table = ManagarrTable::new( + app.data.lidarr_data.add_searched_artists.as_mut(), + search_results_row_mapping, + ) + .loading(is_loading) + .block(layout_block().default_color()) + .headers(["✔", "Name", "Type", "Status", "Rating", "Genres"]) + .constraints([ + Constraint::Percentage(3), + Constraint::Percentage(27), + Constraint::Percentage(12), + Constraint::Percentage(12), + Constraint::Percentage(8), + Constraint::Percentage(38), + ]); + + f.render_widget(search_results_table, results_area); + } + _ => (), + } + } + + f.render_widget( + InputBox::new(block_content) + .offset(offset) + .block(title_block_centered("Add Artist")), + search_box_area, + ); +} + +fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + match active_lidarr_block { + ActiveLidarrBlock::AddArtistSelectMonitor => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_monitor_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectMonitorNewItems => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_monitor_new_items_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectQualityProfile => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_quality_profile_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectMetadataProfile => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_metadata_profile_popup(f, app); + } + ActiveLidarrBlock::AddArtistSelectRootFolder => { + draw_confirmation_prompt(f, app, area); + draw_add_artist_select_root_folder_popup(f, app); + } + ActiveLidarrBlock::AddArtistPrompt | ActiveLidarrBlock::AddArtistTagsInput => { + draw_confirmation_prompt(f, app, area) + } + _ => (), + } + } +} + +fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let searched_artist = app + .data + .lidarr_data + .add_searched_artists + .as_ref() + .expect("add_searched_artists must be populated") + .current_selection(); + let artist_name = &searched_artist.artist_name.text; + let artist_disambiguation = searched_artist.disambiguation.clone().unwrap_or_default(); + + let title = if artist_disambiguation.is_empty() { + format!("Add - {artist_name}") + } else { + format!("Add - {artist_name} ({artist_disambiguation})") + }; + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::AddArtistConfirmPrompt; + let AddArtistModal { + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + root_folder_list, + tags, + .. + } = app + .data + .lidarr_data + .add_artist_modal + .as_ref() + .expect("add_artist_modal must exist in this context"); + + let selected_monitor = monitor_list.current_selection(); + let selected_monitor_new_items = monitor_new_items_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_metadata_profile = metadata_profile_list.current_selection(); + let selected_root_folder = root_folder_list.current_selection(); + + f.render_widget(title_block_centered(&title), area); + + let [ + _, + root_folder_area, + monitor_area, + monitor_new_items_area, + quality_profile_area, + metadata_profile_area, + tags_area, + _, + buttons_area, + ] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + + let [add_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let root_folder_drop_down_button = Button::default() + .title(&selected_root_folder.path) + .label("Root Folder") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectRootFolder); + let monitor_drop_down_button = Button::default() + .title(selected_monitor.to_display_str()) + .label("Monitor") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectMonitor); + let monitor_new_items_drop_down_button = Button::default() + .title(selected_monitor_new_items.to_display_str()) + .label("Monitor New Items") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectMonitorNewItems); + let quality_profile_drop_down_button = Button::default() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectQualityProfile); + let metadata_profile_drop_down_button = Button::default() + .title(selected_metadata_profile) + .label("Metadata Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddArtistSelectMetadataProfile); + + f.render_widget(root_folder_drop_down_button, root_folder_area); + f.render_widget(monitor_drop_down_button, monitor_area); + f.render_widget(monitor_new_items_drop_down_button, monitor_new_items_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(metadata_profile_drop_down_button, metadata_profile_area); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::AddArtistTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::AddArtistTagsInput); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let add_button = Button::default() + .title("Add") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(add_button, add_area); + f.render_widget(cancel_button, cancel_area); +} + +fn draw_add_artist_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .monitor_list, + |monitor| ListItem::new(monitor.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_new_items_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .monitor_new_items_list, + |monitor_new_items| ListItem::new(monitor_new_items.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_new_items_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .quality_profile_list, + |quality_profile| ListItem::new(quality_profile.clone()), + ); + let popup = Popup::new(quality_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let metadata_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .metadata_profile_list, + |metadata_profile| ListItem::new(metadata_profile.clone()), + ); + let popup = Popup::new(metadata_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_artist_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let root_folder_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_artist_modal + .as_mut() + .expect("add_artist_modal must exist in this context") + .root_folder_list, + |root_folder| ListItem::new(root_folder.path.to_owned()), + ); + let popup = Popup::new(root_folder_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} diff --git a/src/ui/lidarr_ui/library/add_artist_ui_tests.rs b/src/ui/lidarr_ui/library/add_artist_ui_tests.rs new file mode 100644 index 0000000..285d575 --- /dev/null +++ b/src/ui/lidarr_ui/library/add_artist_ui_tests.rs @@ -0,0 +1,124 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ARTIST_BLOCKS, ActiveLidarrBlock}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::add_artist_ui::AddArtistUi; + + #[test] + fn test_add_artist_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ADD_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(AddArtistUi::accepts(active_lidarr_block.into())); + } else { + assert!(!AddArtistUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use super::*; + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ARTIST_SELECTION_BLOCKS; + use crate::models::{BlockSelectionState, HorizontallyScrollableText}; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + use rstest::rstest; + + #[test] + fn test_add_artist_ui_renders_loading_for_search() { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.add_artist_search = Some(HorizontallyScrollableText::default()); + app.data.lidarr_data.add_searched_artists = None; + app.push_navigation_stack(ActiveLidarrBlock::AddArtistSearchResults.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_add_artist_ui_renders( + #[values( + ActiveLidarrBlock::AddArtistSearchInput, + ActiveLidarrBlock::AddArtistSearchResults, + ActiveLidarrBlock::AddArtistEmptySearchResults + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.add_artist_search = Some("Test Artist".into()); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("add_artist_ui_{active_lidarr_block}"), output); + } + + #[rstest] + #[case(ActiveLidarrBlock::AddArtistPrompt)] + #[case(ActiveLidarrBlock::AddArtistConfirmPrompt)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitor)] + #[case(ActiveLidarrBlock::AddArtistSelectMonitorNewItems)] + #[case(ActiveLidarrBlock::AddArtistSelectQualityProfile)] + #[case(ActiveLidarrBlock::AddArtistSelectMetadataProfile)] + #[case(ActiveLidarrBlock::AddArtistSelectRootFolder)] + #[case(ActiveLidarrBlock::AddArtistTagsInput)] + fn test_add_artist_modal_ui_renders(#[case] active_lidarr_block: ActiveLidarrBlock) { + use crate::models::lidarr_models::{MonitorType, NewItemMonitorType}; + use crate::models::servarr_data::lidarr::modals::AddArtistModal; + use crate::models::servarr_models::RootFolder; + use strum::IntoEnumIterator; + + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(ADD_ARTIST_SELECTION_BLOCKS); + + let mut add_artist_modal = AddArtistModal { + tags: "test".into(), + ..AddArtistModal::default() + }; + add_artist_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_artist_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + add_artist_modal + .quality_profile_list + .set_items(vec!["Any".to_owned()]); + add_artist_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned()]); + add_artist_modal + .root_folder_list + .set_items(vec![RootFolder { + path: "/nfs/music".to_owned(), + ..RootFolder::default() + }]); + app.data.lidarr_data.add_artist_modal = Some(add_artist_modal); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("add_artist_modal_{active_lidarr_block}"), output); + } + + #[test] + fn test_add_artist_already_in_library_ui_renders() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::AddArtistAlreadyInLibrary.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/album_details_ui.rs b/src/ui/lidarr_ui/library/album_details_ui.rs new file mode 100644 index 0000000..58f6ee6 --- /dev/null +++ b/src/ui/lidarr_ui/library/album_details_ui.rs @@ -0,0 +1,511 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::{LidarrHistoryItem, LidarrRelease, Track}; +use crate::models::servarr_data::lidarr::lidarr_data::{ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock}; +use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi; +use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details; +use crate::ui::styles::{ManagarrStyle, secondary_style}; +use crate::ui::utils::{ + borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_top_border, +}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{DrawUi, draw_popup, draw_tabs}; +use crate::utils::convert_to_gb; +use ratatui::Frame; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::prelude::{Line, Stylize, Text}; +use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; +use serde_json::Number; + +#[cfg(test)] +#[path = "album_details_ui_tests.rs"] +mod album_details_ui_tests; + +pub(super) struct AlbumDetailsUi; + +impl DrawUi for AlbumDetailsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + TrackDetailsUi::accepts(route) || ALBUM_DETAILS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + let route = app.get_current_route(); + if app.data.lidarr_data.album_details_modal.is_some() + && let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() + { + let draw_album_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { + let content_area = draw_tabs( + f, + popup_area, + &format!( + "{} Details", + app.data.lidarr_data.albums.current_selection().title.text + ), + &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal must exist in this context") + .album_details_tabs, + ); + draw_album_details(f, app, content_area); + + match active_lidarr_block { + ActiveLidarrBlock::AutomaticallySearchAlbumPrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for the album: {}?", + app.data.lidarr_data.albums.current_selection().title.text + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Album Search") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::DeleteTrackFilePrompt => { + let prompt = format!( + "Do you really want to delete this track file: \n{}?", + app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal must exist in this context") + .tracks + .current_selection() + .title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Track File") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt => { + draw_manual_album_search_confirm_prompt(f, app); + } + ActiveLidarrBlock::AlbumHistoryDetails => { + draw_history_item_details_popup(f, app); + } + _ => (), + } + }; + + draw_popup(f, app, draw_album_details_popup, Size::XLarge); + + if TrackDetailsUi::accepts(route) { + TrackDetailsUi::draw(f, app, _area); + } + } + } +} + +pub fn draw_album_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref() + && let Route::Lidarr(active_lidarr_block, _) = + album_details_modal.album_details_tabs.get_active_route() + { + match active_lidarr_block { + ActiveLidarrBlock::AlbumDetails => draw_tracks_table(f, app, area), + ActiveLidarrBlock::AlbumHistory => draw_album_history_table(f, app, area), + ActiveLidarrBlock::ManualAlbumSearch => draw_album_releases(f, app, area), + _ => (), + } + } +} + +fn draw_tracks_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let track_files = app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("Album details modal is unpopulated") + .track_files + .items + .clone(); + let content = Some( + &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("Album details modal is unpopulated") + .tracks, + ); + + let track_row_mapping = |track: &Track| { + let Track { + track_number, + title, + duration, + track_file_id, + has_file, + .. + } = track; + + let track_file = track_files + .iter() + .find(|track_file| track_file.id == *track_file_id); + let quality = if let Some(track_file) = track_file { + track_file.quality.quality.name.to_owned() + } else { + String::new() + }; + + let audio_info = track_file + .and_then(|tf| tf.media_info.as_ref()) + .map(|mi| { + let codec = mi.audio_codec.as_deref().unwrap_or(""); + let channels = format!("{}.0", mi.audio_channels); + let bitrate = mi.audio_bit_rate.as_deref().unwrap_or(""); + let sample_rate = mi.audio_sample_rate.as_deref().unwrap_or(""); + let bits = mi.audio_bits.as_deref().unwrap_or(""); + format!("{codec} - {channels} - {bitrate} - {sample_rate} - {bits}") + }) + .unwrap_or_default(); + + let duration_secs = duration / 1000; + let mins = duration_secs / 60; + let secs = duration_secs % 60; + let duration_str = format!("{mins}:{secs:02}"); + + let row = Row::new(vec![ + Cell::from(track_number.clone()), + Cell::from(title.clone()), + Cell::from(duration_str), + Cell::from(audio_info), + Cell::from(quality), + ]); + + if *has_file { + row.downloaded() + } else { + row.missing() + } + }; + + let is_searching = active_lidarr_block == ActiveLidarrBlock::SearchTracks; + let tracks_table = ManagarrTable::new(content, track_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .searching(is_searching) + .search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchTracksError) + .headers(["#", "Title", "Duration", "Audio Info", "Quality"]) + .constraints([ + Constraint::Percentage(5), + Constraint::Percentage(35), + Constraint::Percentage(8), + Constraint::Percentage(37), + Constraint::Percentage(15), + ]); + + if is_searching { + tracks_table.show_cursor(f, area); + } + + f.render_widget(tracks_table, area); + } +} + +fn draw_album_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.lidarr_data.album_details_modal.as_ref() { + Some(album_details_modal) if !app.is_loading => { + let current_selection = if album_details_modal.album_history.is_empty() { + LidarrHistoryItem::default() + } else { + album_details_modal + .album_history + .current_selection() + .clone() + }; + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &LidarrHistoryItem| { + let LidarrHistoryItem { + source_title, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let mut album_history_table = &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("album_details_modal must exist in this context") + .album_history; + let history_table = ManagarrTable::new(Some(&mut album_history_table), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_lidarr_block == ActiveLidarrBlock::AlbumHistorySortPrompt) + .searching(active_lidarr_block == ActiveLidarrBlock::SearchAlbumHistory) + .search_produced_empty_results( + active_lidarr_block == ActiveLidarrBlock::SearchAlbumHistoryError, + ) + .filtering(active_lidarr_block == ActiveLidarrBlock::FilterAlbumHistory) + .filter_produced_empty_results( + active_lidarr_block == ActiveLidarrBlock::FilterAlbumHistoryError, + ) + .headers(["Source Title", "Event Type", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(20), + Constraint::Percentage(15), + Constraint::Percentage(25), + ]); + + if [ + ActiveLidarrBlock::SearchAlbumHistory, + ActiveLidarrBlock::FilterAlbumHistory, + ] + .contains(&active_lidarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.lidarr_data.album_details_modal.is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_album_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.lidarr_data.album_details_modal.as_ref() { + Some(album_details_modal) if !app.is_loading => { + let (current_selection, is_empty) = if album_details_modal.album_releases.is_empty() { + (LidarrRelease::default(), true) + } else { + ( + album_details_modal + .album_releases + .current_selection() + .clone(), + album_details_modal.album_releases.is_empty(), + ) + }; + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let album_release_row_mapping = |release: &LidarrRelease| { + let LidarrRelease { + protocol, + age, + title, + indexer, + size, + rejected, + seeders, + leechers, + quality, + .. + } = release; + + let age = format!("{age} days"); + title.scroll_left_or_reset( + get_width_from_percentage(area, 35), + current_selection == *release + && active_lidarr_block != ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, + app.ui_scroll_tick_count == 0, + ); + let size = convert_to_gb(*size); + let rejected_str = if *rejected { "⛔" } else { "" }; + let peers = if seeders.is_none() || leechers.is_none() { + Text::from("") + } else { + let seeders = seeders + .clone() + .unwrap_or(Number::from(0u64)) + .as_u64() + .unwrap(); + let leechers = leechers + .clone() + .unwrap_or(Number::from(0u64)) + .as_u64() + .unwrap(); + + decorate_peer_style( + seeders, + leechers, + Text::from(format!("{seeders} / {leechers}")), + ) + }; + + let quality_name = quality.quality.name.clone(); + + Row::new(vec![ + Cell::from(protocol.clone()), + Cell::from(age), + Cell::from(rejected_str), + Cell::from(title.to_string()), + Cell::from(indexer.clone()), + Cell::from(format!("{size:.1} GB")), + Cell::from(peers), + Cell::from(quality_name), + ]) + .primary() + }; + let mut album_release_table = &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("album_details_modal must exist in this context") + .album_releases; + let release_table = + ManagarrTable::new(Some(&mut album_release_table), album_release_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading || is_empty) + .sorting(active_lidarr_block == ActiveLidarrBlock::ManualAlbumSearchSortPrompt) + .headers([ + "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Quality", + ]) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(35), + Constraint::Percentage(15), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(10), + ]); + + f.render_widget(release_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.lidarr_data.album_details_modal.is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_manual_album_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal must exist in this context") + .album_releases + .current_selection(); + let title = if current_selection.rejected { + "Download Rejected Release" + } else { + "Download Release" + }; + let prompt = if current_selection.rejected { + format!( + "Do you really want to download the rejected release: {}?", + ¤t_selection.title.text + ) + } else { + format!( + "Do you want to download the release: {}?", + ¤t_selection.title.text + ) + }; + + if current_selection.rejected { + let mut lines_vec = vec![Line::from("Rejection reasons: ".primary().bold())]; + let mut rejections_spans = current_selection + .rejections + .clone() + .unwrap_or_default() + .iter() + .map(|item| Line::from(format!("• {item}").primary().bold())) + .collect::>>(); + lines_vec.append(&mut rejections_spans); + + let content_paragraph = Paragraph::new(lines_vec) + .block(borderless_block()) + .wrap(Wrap { trim: false }) + .left_aligned(); + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .content(content_paragraph) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.area()); + } else { + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = + if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref() { + if album_details_modal.album_history.is_empty() { + LidarrHistoryItem::default() + } else { + album_details_modal + .album_history + .current_selection() + .clone() + } + } else { + LidarrHistoryItem::default() + }; + + let line_vec = create_history_event_details(current_selection); + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); +} diff --git a/src/ui/lidarr_ui/library/album_details_ui_tests.rs b/src/ui/lidarr_ui/library/album_details_ui_tests.rs new file mode 100644 index 0000000..874ba33 --- /dev/null +++ b/src/ui/lidarr_ui/library/album_details_ui_tests.rs @@ -0,0 +1,148 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ALBUM_DETAILS_BLOCKS, ActiveLidarrBlock, TRACK_DETAILS_BLOCKS, + }; + use crate::models::stateful_table::StatefulTable; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::album_details_ui::AlbumDetailsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_album_details_ui_accepts() { + let mut album_details_blocks = ALBUM_DETAILS_BLOCKS.to_vec(); + album_details_blocks.extend(TRACK_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if album_details_blocks.contains(&active_lidarr_block) { + assert!(AlbumDetailsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!AlbumDetailsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(ActiveLidarrBlock::AlbumDetails, 0)] + #[case(ActiveLidarrBlock::AlbumHistory, 1)] + #[case(ActiveLidarrBlock::SearchTracks, 0)] + #[case(ActiveLidarrBlock::SearchTracksError, 0)] + #[case(ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, 0)] + #[case(ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, 1)] + #[case(ActiveLidarrBlock::AutomaticallySearchAlbumPrompt, 2)] + #[case(ActiveLidarrBlock::SearchAlbumHistory, 1)] + #[case(ActiveLidarrBlock::SearchAlbumHistoryError, 1)] + #[case(ActiveLidarrBlock::FilterAlbumHistory, 1)] + #[case(ActiveLidarrBlock::FilterAlbumHistoryError, 1)] + #[case(ActiveLidarrBlock::AlbumHistorySortPrompt, 1)] + #[case(ActiveLidarrBlock::AlbumHistoryDetails, 1)] + #[case(ActiveLidarrBlock::ManualAlbumSearch, 2)] + #[case(ActiveLidarrBlock::ManualAlbumSearchConfirmPrompt, 2)] + #[case(ActiveLidarrBlock::ManualAlbumSearchSortPrompt, 2)] + #[case(ActiveLidarrBlock::DeleteTrackFilePrompt, 0)] + fn test_album_details_ui_renders( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .album_details_tabs + .set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AlbumDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("album_details_renders_{active_lidarr_block}_{index}"), + output + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::AlbumDetails, 0)] + #[case(ActiveLidarrBlock::AlbumHistory, 1)] + #[case(ActiveLidarrBlock::AlbumHistoryDetails, 1)] + #[case(ActiveLidarrBlock::ManualAlbumSearch, 2)] + fn test_album_details_ui_renders_loading( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + { + let album_details_modal = app.data.lidarr_data.album_details_modal.as_mut().unwrap(); + album_details_modal.album_releases = StatefulTable::default(); + album_details_modal.album_history = StatefulTable::default(); + album_details_modal.tracks = StatefulTable::default(); + album_details_modal.album_details_tabs.set_index(index); + } + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AlbumDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("loading_album_details_{active_lidarr_block}_{index}"), + output + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::AlbumDetails, 0)] + #[case(ActiveLidarrBlock::AlbumHistory, 1)] + #[case(ActiveLidarrBlock::AlbumHistoryDetails, 1)] + #[case(ActiveLidarrBlock::ManualAlbumSearch, 2)] + fn test_album_details_ui_renders_empty( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + { + let album_details_modal = app.data.lidarr_data.album_details_modal.as_mut().unwrap(); + album_details_modal.album_releases = StatefulTable::default(); + album_details_modal.album_history = StatefulTable::default(); + album_details_modal.tracks = StatefulTable::default(); + album_details_modal.album_details_tabs.set_index(index); + } + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AlbumDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("empty_album_details_{active_lidarr_block}_{index}"), + output + ); + } + + #[test] + fn test_album_details_ui_renders_track_details_over_album_details() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::TrackDetails.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AlbumDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/artist_details_ui.rs b/src/ui/lidarr_ui/library/artist_details_ui.rs new file mode 100644 index 0000000..9a7d381 --- /dev/null +++ b/src/ui/lidarr_ui/library/artist_details_ui.rs @@ -0,0 +1,611 @@ +use chrono::Utc; +use deunicode::deunicode; +use ratatui::Frame; +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; +use regex::Regex; + +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::{Album, LidarrHistoryItem, LidarrRelease}; +use crate::models::servarr_data::lidarr::lidarr_data::{ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock}; +use crate::ui::lidarr_ui::library::album_details_ui::AlbumDetailsUi; +use crate::ui::lidarr_ui::library::delete_album_ui::DeleteAlbumUi; +use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details; +use crate::ui::styles::{ManagarrStyle, secondary_style}; +use crate::ui::utils::decorate_peer_style; +use crate::ui::utils::{ + borderless_block, get_width_from_percentage, layout_block_top_border, title_block, +}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{DrawUi, draw_popup, draw_tabs}; +use crate::utils::convert_to_gb; +use ratatui::layout::Alignment; +use ratatui::text::Text; +use serde_json::Number; + +#[cfg(test)] +#[path = "artist_details_ui_tests.rs"] +mod artist_details_ui_tests; + +pub(super) struct ArtistDetailsUi; + +impl DrawUi for ArtistDetailsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + AlbumDetailsUi::accepts(route) + || DeleteAlbumUi::accepts(route) + || ARTIST_DETAILS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + if let Route::Lidarr(active_lidarr_block, _) = route { + let draw_artist_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { + f.render_widget( + title_block( + &app + .data + .lidarr_data + .artists + .current_selection() + .artist_name + .text, + ), + popup_area, + ); + let [description_area, detail_area] = + Layout::vertical([Constraint::Length(14), Constraint::Fill(0)]) + .margin(1) + .areas(popup_area); + draw_artist_description(f, app, description_area); + let content_area = draw_tabs( + f, + detail_area, + "Artist Details", + &app.data.lidarr_data.artist_info_tabs, + ); + draw_artist_details(f, app, content_area); + + match active_lidarr_block { + _ if DeleteAlbumUi::accepts(route) => DeleteAlbumUi::draw(f, app, area), + ActiveLidarrBlock::ArtistHistoryDetails => { + draw_artist_history_item_details_popup(f, app); + } + ActiveLidarrBlock::AutomaticallySearchArtistPrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for all monitored album(s) for the artist: {}?", + app.data.lidarr_data.artists.current_selection().artist_name + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Artist Search") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::UpdateAndScanArtistPrompt => { + let prompt = format!( + "Do you want to trigger an update and disk scan for the artist: {}?", + app.data.lidarr_data.artists.current_selection().artist_name + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update and Scan") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::ManualArtistSearchConfirmPrompt => { + draw_manual_artist_search_confirm_prompt(f, app); + } + _ => (), + } + }; + + draw_popup(f, app, draw_artist_details_popup, Size::XXLarge); + + if AlbumDetailsUi::accepts(route) { + AlbumDetailsUi::draw(f, app, area); + } + } + } +} + +fn draw_artist_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = app.data.lidarr_data.artists.current_selection(); + let monitored = if current_selection.monitored { + "Yes" + } else { + "No" + }; + let quality_profile = app + .data + .lidarr_data + .quality_profile_map + .get_by_left(¤t_selection.quality_profile_id) + .cloned() + .unwrap_or_default(); + let metadata_profile = app + .data + .lidarr_data + .metadata_profile_map + .get_by_left(¤t_selection.metadata_profile_id) + .cloned() + .unwrap_or_default(); + let overview = Regex::new(r"[\r\n\t]") + .unwrap() + .replace_all( + &deunicode( + current_selection + .overview + .as_ref() + .unwrap_or(&String::new()), + ), + "", + ) + .to_string(); + + let mut artist_description = vec![ + Line::from(vec![ + "Artist: ".primary().bold(), + current_selection.artist_name.text.clone().primary().bold(), + ]), + Line::from(vec![ + "Overview: ".primary().bold(), + overview.default_color(), + ]), + Line::from(vec![ + "Disambiguation: ".primary().bold(), + current_selection + .disambiguation + .clone() + .unwrap_or_default() + .default_color(), + ]), + Line::from(vec![ + "Type: ".primary().bold(), + current_selection + .artist_type + .clone() + .unwrap_or_default() + .default_color(), + ]), + Line::from(vec![ + "Status: ".primary().bold(), + current_selection.status.to_display_str().default_color(), + ]), + Line::from(vec![ + "Genres: ".primary().bold(), + current_selection.genres.join(", ").default_color(), + ]), + Line::from(vec![ + "Rating: ".primary().bold(), + current_selection + .ratings + .as_ref() + .map_or_else( + || "N/A".to_owned(), + |r| format!("{}%", (r.value * 10.0) as i32), + ) + .default_color(), + ]), + Line::from(vec![ + "Path: ".primary().bold(), + current_selection.path.clone().default_color(), + ]), + Line::from(vec![ + "Quality Profile: ".primary().bold(), + quality_profile.default_color(), + ]), + Line::from(vec![ + "Metadata Profile: ".primary().bold(), + metadata_profile.default_color(), + ]), + Line::from(vec![ + "Monitored: ".primary().bold(), + monitored.default_color(), + ]), + ]; + + if let Some(stats) = current_selection.statistics.as_ref() { + let size = convert_to_gb(stats.size_on_disk); + artist_description.extend(vec![ + Line::from(vec![ + "Albums: ".primary().bold(), + stats.album_count.to_string().default_color(), + ]), + Line::from(vec![ + "Tracks: ".primary().bold(), + format!("{}/{}", stats.track_file_count, stats.total_track_count).default_color(), + ]), + Line::from(vec![ + "Size on Disk: ".primary().bold(), + format!("{size:.2} GB").default_color(), + ]), + ]); + } + + let description_paragraph = Paragraph::new(artist_description) + .block(borderless_block()) + .wrap(Wrap { trim: true }); + f.render_widget(description_paragraph, area); +} + +fn draw_artist_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = + app.data.lidarr_data.artist_info_tabs.get_active_route() + { + match active_lidarr_block { + ActiveLidarrBlock::ArtistDetails => draw_albums_table(f, app, area), + ActiveLidarrBlock::ArtistHistory => draw_artist_history_table(f, app, area), + ActiveLidarrBlock::ManualArtistSearch => draw_artist_releases(f, app, area), + _ => (), + } + } +} + +fn draw_albums_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let current_selection = if app.data.lidarr_data.albums.is_empty() { + Album::default() + } else { + app.data.lidarr_data.albums.current_selection().clone() + }; + let content = Some(&mut app.data.lidarr_data.albums); + let album_row_mapping = |album: &Album| { + album.title.scroll_left_or_reset( + get_width_from_percentage(area, 33), + *album == current_selection, + app.ui_scroll_tick_count == 0, + ); + let monitored = if album.monitored { "🏷" } else { "" }; + let album_type = album.album_type.clone().unwrap_or_default(); + let release_date = album + .release_date + .map_or_else(|| "N/A".to_owned(), |d| d.format("%Y-%m-%d").to_string()); + let track_count = album.statistics.as_ref().map_or_else( + || "0/0".to_owned(), + |s| format!("{}/{}", s.track_file_count, s.total_track_count), + ); + let size = album + .statistics + .as_ref() + .map_or(0f64, |s| convert_to_gb(s.size_on_disk)); + let duration_mins = album.duration / 60000; + + let row = Row::new(vec![ + Cell::from(monitored.to_owned()), + Cell::from(album.title.to_string()), + Cell::from(album_type), + Cell::from(track_count), + Cell::from(format!("{duration_mins} min")), + Cell::from(release_date), + Cell::from(format!("{size:.2} GB")), + ]); + + if !album.monitored { + row.unmonitored() + } else if let Some(stats) = album.statistics.as_ref() { + if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 { + row.downloaded() + } else if let Some(release_date) = album.release_date.as_ref() { + if release_date > &Utc::now() { + row.unreleased() + } else { + row.missing() + } + } else { + row.missing() + } + } else { + row.indeterminate() + } + }; + + let is_searching = active_lidarr_block == ActiveLidarrBlock::SearchAlbums; + let album_table = ManagarrTable::new(content, album_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .searching(is_searching) + .search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchAlbumsError) + .headers([ + "Monitored", + "Title", + "Type", + "Tracks", + "Duration", + "Release Date", + "Size", + ]) + .constraints([ + Constraint::Percentage(7), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(13), + Constraint::Percentage(15), + ]); + + if is_searching { + album_table.show_cursor(f, area); + } + + f.render_widget(album_table, area); + } +} + +fn draw_artist_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if !app.is_loading { + let current_selection = if app.data.lidarr_data.artist_history.is_empty() { + LidarrHistoryItem::default() + } else { + app + .data + .lidarr_data + .artist_history + .current_selection() + .clone() + }; + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &LidarrHistoryItem| { + let LidarrHistoryItem { + source_title, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let history_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.artist_history), + history_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_lidarr_block == ActiveLidarrBlock::ArtistHistorySortPrompt) + .searching(active_lidarr_block == ActiveLidarrBlock::SearchArtistHistory) + .search_produced_empty_results( + active_lidarr_block == ActiveLidarrBlock::SearchArtistHistoryError, + ) + .filtering(active_lidarr_block == ActiveLidarrBlock::FilterArtistHistory) + .filter_produced_empty_results( + active_lidarr_block == ActiveLidarrBlock::FilterArtistHistoryError, + ) + .headers(["Source Title", "Event Type", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(20), + Constraint::Percentage(15), + Constraint::Percentage(25), + ]); + + if [ + ActiveLidarrBlock::SearchArtistHistory, + ActiveLidarrBlock::FilterArtistHistory, + ] + .contains(&active_lidarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } + } else { + f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.lidarr_data.albums.is_empty(), + layout_block_top_border(), + ), + area, + ); + } +} + +fn draw_artist_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.lidarr_data.artist_history.is_empty() { + LidarrHistoryItem::default() + } else { + app + .data + .lidarr_data + .artist_history + .current_selection() + .clone() + }; + + let line_vec = create_history_event_details(current_selection); + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); +} + +fn draw_artist_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let (current_selection, is_empty) = if app.data.lidarr_data.discography_releases.is_empty() { + (LidarrRelease::default(), true) + } else { + ( + app + .data + .lidarr_data + .discography_releases + .current_selection() + .clone(), + app.data.lidarr_data.discography_releases.is_empty(), + ) + }; + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let release_row_mapping = |release: &LidarrRelease| { + let LidarrRelease { + protocol, + age, + title, + indexer, + size, + rejected, + seeders, + leechers, + quality, + .. + } = release; + + let age = format!("{age} days"); + title.scroll_left_or_reset( + get_width_from_percentage(area, 35), + current_selection == *release + && active_lidarr_block != ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, + app.ui_scroll_tick_count == 0, + ); + let size = convert_to_gb(*size); + let rejected_str = if *rejected { "⛔" } else { "" }; + let peers = if seeders.is_none() || leechers.is_none() { + Text::from("") + } else { + let seeders = seeders + .clone() + .unwrap_or(Number::from(0u64)) + .as_u64() + .unwrap(); + let leechers = leechers + .clone() + .unwrap_or(Number::from(0u64)) + .as_u64() + .unwrap(); + + decorate_peer_style( + seeders, + leechers, + Text::from(format!("{seeders} / {leechers}")), + ) + }; + + let quality_name = quality.quality.name.clone(); + + Row::new(vec![ + Cell::from(protocol.clone()), + Cell::from(age), + Cell::from(rejected_str), + Cell::from(title.to_string()), + Cell::from(indexer.clone()), + Cell::from(format!("{size:.1} GB")), + Cell::from(peers), + Cell::from(quality_name), + ]) + .primary() + }; + let mut release_table = &mut app.data.lidarr_data.discography_releases; + let artist_release_table = ManagarrTable::new(Some(&mut release_table), release_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading || is_empty) + .sorting(active_lidarr_block == ActiveLidarrBlock::ManualArtistSearchSortPrompt) + .headers([ + "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Quality", + ]) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(35), + Constraint::Percentage(15), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(10), + ]); + + f.render_widget(artist_release_table, area); + } +} + +fn draw_manual_artist_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = app + .data + .lidarr_data + .discography_releases + .current_selection(); + let title = if current_selection.rejected { + "Download Rejected Release" + } else { + "Download Release" + }; + let prompt = if current_selection.rejected { + format!( + "Do you really want to download the rejected release: {}?", + ¤t_selection.title.text + ) + } else { + format!( + "Do you want to download the release: {}?", + ¤t_selection.title.text + ) + }; + + if current_selection.rejected { + let mut lines_vec = vec![Line::from("Rejection reasons: ".primary().bold())]; + let mut rejections_spans = current_selection + .rejections + .clone() + .unwrap_or_default() + .iter() + .map(|item| Line::from(format!("• {item}").primary().bold())) + .collect::>>(); + lines_vec.append(&mut rejections_spans); + + let content_paragraph = Paragraph::new(lines_vec) + .block(borderless_block()) + .wrap(Wrap { trim: false }) + .left_aligned(); + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .content(content_paragraph) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.area()); + } else { + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} diff --git a/src/ui/lidarr_ui/library/artist_details_ui_tests.rs b/src/ui/lidarr_ui/library/artist_details_ui_tests.rs new file mode 100644 index 0000000..0ce22c5 --- /dev/null +++ b/src/ui/lidarr_ui/library/artist_details_ui_tests.rs @@ -0,0 +1,167 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::lidarr::lidarr_data::{ + ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, + TRACK_DETAILS_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi; + + #[test] + fn test_artist_details_ui_accepts() { + let mut blocks = ARTIST_DETAILS_BLOCKS.clone().to_vec(); + blocks.extend(DELETE_ALBUM_BLOCKS); + blocks.extend(ALBUM_DETAILS_BLOCKS); + blocks.extend(TRACK_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if blocks.contains(&active_lidarr_block) { + assert!(ArtistDetailsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!ArtistDetailsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use rstest::rstest; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ALBUM_SELECTION_BLOCKS, + }; + use crate::models::stateful_table::StatefulTable; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + + #[rstest] + #[case(ActiveLidarrBlock::ArtistDetails, 0)] + #[case(ActiveLidarrBlock::ArtistHistory, 1)] + #[case(ActiveLidarrBlock::ManualArtistSearch, 2)] + #[case(ActiveLidarrBlock::SearchAlbums, 0)] + #[case(ActiveLidarrBlock::SearchAlbumsError, 0)] + #[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 0)] + #[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 1)] + #[case(ActiveLidarrBlock::UpdateAndScanArtistPrompt, 2)] + #[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 0)] + #[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 1)] + #[case(ActiveLidarrBlock::AutomaticallySearchArtistPrompt, 2)] + #[case(ActiveLidarrBlock::SearchArtistHistory, 1)] + #[case(ActiveLidarrBlock::SearchArtistHistoryError, 1)] + #[case(ActiveLidarrBlock::FilterArtistHistory, 1)] + #[case(ActiveLidarrBlock::FilterArtistHistoryError, 1)] + #[case(ActiveLidarrBlock::ArtistHistorySortPrompt, 1)] + #[case(ActiveLidarrBlock::ArtistHistoryDetails, 1)] + #[case(ActiveLidarrBlock::ManualArtistSearchConfirmPrompt, 2)] + #[case(ActiveLidarrBlock::ManualArtistSearchSortPrompt, 2)] + fn test_artist_details_ui_renders( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.artist_info_tabs.set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ArtistDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("artist_details_{active_lidarr_block}_{index}"), + output + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::ArtistDetails, 0)] + #[case(ActiveLidarrBlock::ArtistHistory, 1)] + #[case(ActiveLidarrBlock::ManualArtistSearch, 2)] + fn test_artist_details_ui_renders_artist_details_loading( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.artist_info_tabs.set_index(index); + app.is_loading = true; + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ArtistDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("loading_artist_details_{active_lidarr_block}"), + output + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::ArtistDetails, 0)] + #[case(ActiveLidarrBlock::ArtistHistory, 1)] + #[case(ActiveLidarrBlock::ArtistHistoryDetails, 1)] + #[case(ActiveLidarrBlock::ManualArtistSearch, 2)] + fn test_artist_details_ui_renders_artist_details_empty( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.albums = StatefulTable::default(); + app.data.lidarr_data.discography_releases = StatefulTable::default(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.artist_info_tabs.set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ArtistDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("empty_artist_details_{active_lidarr_block}"), + output + ); + } + + #[test] + fn test_artist_details_ui_renders_delete_album_prompt_over_artist_details() { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ArtistDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_artist_details_ui_renders_update_and_scan_prompt_over_artist_details() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAndScanArtistPrompt.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ArtistDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_artist_details_ui_renders_automatic_search_prompt_over_artist_details() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::AutomaticallySearchArtistPrompt.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + ArtistDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/delete_album_ui.rs b/src/ui/lidarr_ui/library/delete_album_ui.rs new file mode 100644 index 0000000..cf8fab2 --- /dev/null +++ b/src/ui/lidarr_ui/library/delete_album_ui.rs @@ -0,0 +1,57 @@ +use ratatui::Frame; +use ratatui::layout::Rect; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ALBUM_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::popup::{Popup, Size}; + +#[cfg(test)] +#[path = "delete_album_ui_tests.rs"] +mod delete_album_ui_tests; + +pub(in crate::ui::lidarr_ui) struct DeleteAlbumUi; + +impl DrawUi for DeleteAlbumUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::DeleteAlbumPrompt, _) + ) { + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let prompt = format!( + "Do you really want to delete the album: \n{}?", + app.data.lidarr_data.albums.current_selection().title.text + ); + let checkboxes = vec![ + Checkbox::new("Delete Album Files") + .checked(app.data.lidarr_data.delete_files) + .highlighted(selected_block == ActiveLidarrBlock::DeleteAlbumToggleDeleteFile), + Checkbox::new("Add List Exclusion") + .checked(app.data.lidarr_data.add_import_list_exclusion) + .highlighted(selected_block == ActiveLidarrBlock::DeleteAlbumToggleAddListExclusion), + ]; + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Album") + .prompt(&prompt) + .checkboxes(checkboxes) + .yes_no_highlighted(selected_block == ActiveLidarrBlock::DeleteAlbumConfirmPrompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + } +} diff --git a/src/ui/lidarr_ui/library/delete_album_ui_tests.rs b/src/ui/lidarr_ui/library/delete_album_ui_tests.rs new file mode 100644 index 0000000..81adf0f --- /dev/null +++ b/src/ui/lidarr_ui/library/delete_album_ui_tests.rs @@ -0,0 +1,43 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::delete_album_ui::DeleteAlbumUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_delete_album_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DELETE_ALBUM_BLOCKS.contains(&active_lidarr_block) { + assert!(DeleteAlbumUi::accepts(active_lidarr_block.into())); + } else { + assert!(!DeleteAlbumUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_delete_album_ui_renders_delete_album() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteAlbumPrompt.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(DELETE_ALBUM_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DeleteAlbumUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/delete_artist_ui.rs b/src/ui/lidarr_ui/library/delete_artist_ui.rs new file mode 100644 index 0000000..a271844 --- /dev/null +++ b/src/ui/lidarr_ui/library/delete_artist_ui.rs @@ -0,0 +1,63 @@ +use ratatui::Frame; +use ratatui::layout::Rect; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DELETE_ARTIST_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::popup::{Popup, Size}; + +#[cfg(test)] +#[path = "delete_artist_ui_tests.rs"] +mod delete_artist_ui_tests; + +pub(in crate::ui::lidarr_ui) struct DeleteArtistUi; + +impl DrawUi for DeleteArtistUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::DeleteArtistPrompt, _) + ) { + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let prompt = format!( + "Do you really want to delete the artist: \n{}?", + app + .data + .lidarr_data + .artists + .current_selection() + .artist_name + .text + ); + let checkboxes = vec![ + Checkbox::new("Delete Artist Files") + .checked(app.data.lidarr_data.delete_files) + .highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleDeleteFile), + Checkbox::new("Add List Exclusion") + .checked(app.data.lidarr_data.add_import_list_exclusion) + .highlighted(selected_block == ActiveLidarrBlock::DeleteArtistToggleAddListExclusion), + ]; + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Artist") + .prompt(&prompt) + .checkboxes(checkboxes) + .yes_no_highlighted(selected_block == ActiveLidarrBlock::DeleteArtistConfirmPrompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + } +} diff --git a/src/ui/lidarr_ui/library/delete_artist_ui_tests.rs b/src/ui/lidarr_ui/library/delete_artist_ui_tests.rs new file mode 100644 index 0000000..9ccb8ba --- /dev/null +++ b/src/ui/lidarr_ui/library/delete_artist_ui_tests.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::delete_artist_ui::DeleteArtistUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_delete_artist_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DELETE_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(DeleteArtistUi::accepts(active_lidarr_block.into())); + } else { + assert!(!DeleteArtistUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + + use super::*; + + #[test] + fn test_delete_artist_ui_renders_delete_artist() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DeleteArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/edit_artist_ui.rs b/src/ui/lidarr_ui/library/edit_artist_ui.rs new file mode 100644 index 0000000..304c822 --- /dev/null +++ b/src/ui/lidarr_ui/library/edit_artist_ui.rs @@ -0,0 +1,242 @@ +use std::sync::atomic::Ordering; + +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::prelude::Layout; +use ratatui::widgets::ListItem; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, +}; +use crate::models::servarr_data::lidarr::modals::EditArtistModal; +use crate::render_selectable_input_box; + +use crate::ui::lidarr_ui::library::artist_details_ui::ArtistDetailsUi; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "edit_artist_ui_tests.rs"] +mod edit_artist_ui_tests; + +pub(super) struct EditArtistUi; + +impl DrawUi for EditArtistUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Lidarr(active_lidarr_block, context_option) = app.get_current_route() { + if let Some(context) = context_option + && ARTIST_DETAILS_BLOCKS.contains(&context) + { + draw_popup(f, app, ArtistDetailsUi::draw, Size::Large); + } + + let draw_edit_artist_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| { + draw_edit_artist_confirmation_prompt(f, app, prompt_area); + + match active_lidarr_block { + ActiveLidarrBlock::EditArtistSelectMonitorNewItems => { + draw_edit_artist_select_monitor_new_items_popup(f, app); + } + ActiveLidarrBlock::EditArtistSelectQualityProfile => { + draw_edit_artist_select_quality_profile_popup(f, app); + } + ActiveLidarrBlock::EditArtistSelectMetadataProfile => { + draw_edit_artist_select_metadata_profile_popup(f, app); + } + _ => (), + } + }; + + draw_popup(f, app, draw_edit_artist_prompt, Size::Long); + } + } +} + +fn draw_edit_artist_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let artist_name = app + .data + .lidarr_data + .artists + .current_selection() + .artist_name + .text + .clone(); + let artist_disambiguation = app + .data + .lidarr_data + .artists + .current_selection() + .disambiguation + .clone() + .unwrap_or_default(); + let title = if artist_disambiguation.is_empty() { + format!("Edit - {artist_name}") + } else { + format!("Edit - {artist_name} ({artist_disambiguation})") + }; + f.render_widget(title_block_centered(&title), area); + + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::EditArtistConfirmPrompt; + let EditArtistModal { + monitor_list, + quality_profile_list, + metadata_profile_list, + monitored, + path, + tags, + } = app + .data + .lidarr_data + .edit_artist_modal + .as_ref() + .expect("edit_artist_modal must exist in this context"); + let selected_monitor_new_items = monitor_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_metadata_profile = metadata_profile_list.current_selection(); + + let [ + _, + monitored_area, + monitor_new_items_area, + quality_profile_area, + metadata_profile_area, + path_area, + tags_area, + _, + buttons_area, + ] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let monitored_checkbox = Checkbox::new("Monitored") + .checked(monitored.unwrap_or_default()) + .highlighted(selected_block == ActiveLidarrBlock::EditArtistToggleMonitored); + let monitor_new_items_drop_down_button = Button::default() + .title(selected_monitor_new_items.to_display_str()) + .label("Monitor New Albums") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::EditArtistSelectMonitorNewItems); + let quality_profile_drop_down_button = Button::default() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::EditArtistSelectQualityProfile); + let metadata_profile_drop_down_button = Button::default() + .title(selected_metadata_profile) + .label("Metadata Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::EditArtistSelectMetadataProfile); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let path_input_box = InputBox::new(&path.text) + .offset(path.offset.load(Ordering::SeqCst)) + .label("Path") + .highlighted(selected_block == ActiveLidarrBlock::EditArtistPathInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditArtistPathInput); + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::EditArtistTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::EditArtistTagsInput); + + match active_lidarr_block { + ActiveLidarrBlock::EditArtistPathInput => path_input_box.show_cursor(f, path_area), + ActiveLidarrBlock::EditArtistTagsInput => tags_input_box.show_cursor(f, tags_area), + _ => (), + } + + render_selectable_input_box!(path_input_box, f, path_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let save_button = Button::default() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(monitored_checkbox, monitored_area); + f.render_widget(monitor_new_items_drop_down_button, monitor_new_items_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(metadata_profile_drop_down_button, metadata_profile_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); +} + +fn draw_edit_artist_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_list = SelectableList::new( + &mut app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .expect("edit_artist_modal must exist in this context") + .monitor_list, + |monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_edit_artist_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .expect("edit_artist_modal must exist in this context") + .quality_profile_list, + |quality_profile| ListItem::new(quality_profile.clone()), + ); + let popup = Popup::new(quality_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_edit_artist_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let metadata_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .edit_artist_modal + .as_mut() + .expect("edit_artist_modal must exist in this context") + .metadata_profile_list, + |metadata_profile| ListItem::new(metadata_profile.clone()), + ); + let popup = Popup::new(metadata_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} diff --git a/src/ui/lidarr_ui/library/edit_artist_ui_tests.rs b/src/ui/lidarr_ui/library/edit_artist_ui_tests.rs new file mode 100644 index 0000000..f6a12cc --- /dev/null +++ b/src/ui/lidarr_ui/library/edit_artist_ui_tests.rs @@ -0,0 +1,49 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::edit_artist_ui::EditArtistUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_edit_artist_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if EDIT_ARTIST_BLOCKS.contains(&active_lidarr_block) { + assert!(EditArtistUi::accepts(active_lidarr_block.into())); + } else { + assert!(!EditArtistUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(ActiveLidarrBlock::EditArtistPrompt)] + #[case(ActiveLidarrBlock::EditArtistConfirmPrompt)] + #[case(ActiveLidarrBlock::EditArtistSelectMetadataProfile)] + #[case(ActiveLidarrBlock::EditArtistSelectMonitorNewItems)] + #[case(ActiveLidarrBlock::EditArtistSelectQualityProfile)] + fn test_edit_artist_ui_renders(#[case] active_lidarr_block: ActiveLidarrBlock) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + EditArtistUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("edit_artist_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/library/library_ui_tests.rs b/src/ui/lidarr_ui/library/library_ui_tests.rs new file mode 100644 index 0000000..c2505e5 --- /dev/null +++ b/src/ui/lidarr_ui/library/library_ui_tests.rs @@ -0,0 +1,338 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus}; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ARTIST_BLOCKS, ALBUM_DETAILS_BLOCKS, ARTIST_DETAILS_BLOCKS, ActiveLidarrBlock, + DELETE_ALBUM_BLOCKS, DELETE_ARTIST_BLOCKS, EDIT_ARTIST_BLOCKS, LIBRARY_BLOCKS, + TRACK_DETAILS_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::{LibraryUi, decorate_artist_row_with_style}; + use crate::ui::styles::ManagarrStyle; + use pretty_assertions::assert_eq; + use ratatui::widgets::{Cell, Row}; + + #[test] + fn test_library_ui_accepts() { + let mut library_ui_blocks = Vec::new(); + library_ui_blocks.extend(LIBRARY_BLOCKS); + library_ui_blocks.extend(DELETE_ARTIST_BLOCKS); + library_ui_blocks.extend(DELETE_ALBUM_BLOCKS); + library_ui_blocks.extend(EDIT_ARTIST_BLOCKS); + library_ui_blocks.extend(ADD_ARTIST_BLOCKS); + library_ui_blocks.extend(ARTIST_DETAILS_BLOCKS); + library_ui_blocks.extend(ALBUM_DETAILS_BLOCKS); + library_ui_blocks.extend(TRACK_DETAILS_BLOCKS); + + for active_lidarr_block in ActiveLidarrBlock::iter() { + if library_ui_blocks.contains(&active_lidarr_block) { + assert!( + LibraryUi::accepts(active_lidarr_block.into()), + "{active_lidarr_block} is not accepted by the LibraryUi" + ); + } else { + assert!( + !LibraryUi::accepts(active_lidarr_block.into()), + "{active_lidarr_block} should not be accepted by LibraryUi" + ); + } + } + } + + #[test] + fn test_decorate_row_with_style_unmonitored() { + let artist = Artist::default(); + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.unmonitored()); + } + + #[test] + fn test_decorate_row_with_style_downloaded_when_ended_and_all_tracks_present() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: Some(ArtistStatistics { + track_file_count: 10, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.downloaded()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_ended_and_tracks_are_missing() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: Some(ArtistStatistics { + track_file_count: 5, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_no_statistics() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: None, + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_total_track_count_is_zero() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Ended, + statistics: Some(ArtistStatistics { + track_file_count: 0, + total_track_count: 0, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_continuing_and_all_tracks_present() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Continuing, + statistics: Some(ArtistStatistics { + track_file_count: 10, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_continuing_and_tracks_are_missing() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Continuing, + statistics: Some(ArtistStatistics { + track_file_count: 5, + total_track_count: 10, + ..ArtistStatistics::default() + }), + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_continuing_and_no_statistics() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Continuing, + statistics: None, + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_defaults_to_indeterminate_for_deleted_status() { + let artist = Artist { + monitored: true, + status: ArtistStatus::Deleted, + ..Artist::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_artist_row_with_style(&artist, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + mod snapshot_tests { + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, DELETE_ARTIST_SELECTION_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, + }; + use rstest::rstest; + + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::LibraryUi; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + + #[rstest] + fn test_library_ui_renders( + #[values( + ActiveLidarrBlock::Artists, + ActiveLidarrBlock::ArtistsSortPrompt, + ActiveLidarrBlock::SearchArtists, + ActiveLidarrBlock::SearchArtistsError, + ActiveLidarrBlock::FilterArtists, + ActiveLidarrBlock::FilterArtistsError + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("lidarr_library_{active_lidarr_block}"), output); + } + + #[test] + fn test_library_ui_renders_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_delete_artist_over_library() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::DeleteArtistPrompt.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(DELETE_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_update_all_artists_prompt() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::UpdateAllArtistsPrompt.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_library_ui_renders_artist_details_over_library() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + #[case(ActiveLidarrBlock::ArtistDetails, 0)] + #[case(ActiveLidarrBlock::ArtistHistory, 1)] + #[case(ActiveLidarrBlock::ManualArtistSearch, 2)] + fn test_library_ui_renders_edit_artist_over_artist_details( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack( + ( + ActiveLidarrBlock::EditArtistPrompt, + Some(active_lidarr_block), + ) + .into(), + ); + app.data.lidarr_data.artist_info_tabs.set_index(index); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("edit_artist_renders_over_{active_lidarr_block}"), + output + ); + } + + #[test] + fn test_library_ui_renders_dropdown_over_edit_artist_over_artist_details() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); + app.push_navigation_stack(ActiveLidarrBlock::ArtistDetails.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistPrompt.into()); + app.push_navigation_stack(ActiveLidarrBlock::EditArtistSelectMetadataProfile.into()); + app.data.lidarr_data.selected_block = BlockSelectionState::new(EDIT_ARTIST_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + } +} diff --git a/src/ui/lidarr_ui/library/mod.rs b/src/ui/lidarr_ui/library/mod.rs new file mode 100644 index 0000000..449a4f9 --- /dev/null +++ b/src/ui/lidarr_ui/library/mod.rs @@ -0,0 +1,222 @@ +use add_artist_ui::AddArtistUi; +use artist_details_ui::ArtistDetailsUi; +use delete_artist_ui::DeleteArtistUi; +use edit_artist_ui::EditArtistUi; +use ratatui::{ + Frame, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, +}; + +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::{ + confirmation_prompt::ConfirmationPrompt, + popup::{Popup, Size}, +}; +use crate::utils::convert_to_gb; +use crate::{ + app::App, + models::{ + Route, + lidarr_models::{Artist, ArtistStatus}, + servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS}, + }, + ui::{ + DrawUi, + styles::ManagarrStyle, + utils::{get_width_from_percentage, layout_block_top_border}, + }, +}; + +mod add_artist_ui; +mod album_details_ui; +mod artist_details_ui; +mod delete_album_ui; +mod delete_artist_ui; +mod edit_artist_ui; +mod track_details_ui; + +#[cfg(test)] +#[path = "library_ui_tests.rs"] +mod library_ui_tests; + +pub(super) struct LibraryUi; + +impl DrawUi for LibraryUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return AddArtistUi::accepts(route) + || DeleteArtistUi::accepts(route) + || EditArtistUi::accepts(route) + || ArtistDetailsUi::accepts(route) + || LIBRARY_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_library(f, app, area); + + match route { + _ if AddArtistUi::accepts(route) => AddArtistUi::draw(f, app, area), + _ if DeleteArtistUi::accepts(route) => DeleteArtistUi::draw(f, app, area), + _ if ArtistDetailsUi::accepts(route) => ArtistDetailsUi::draw(f, app, area), + _ if EditArtistUi::accepts(route) => EditArtistUi::draw(f, app, area), + Route::Lidarr(ActiveLidarrBlock::UpdateAllArtistsPrompt, _) => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update All Artists") + .prompt("Do you want to update info and scan your disks for all of your artists?") + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } +} + +fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let current_selection = if !app.data.lidarr_data.artists.items.is_empty() { + app.data.lidarr_data.artists.current_selection().clone() + } else { + Artist::default() + }; + let quality_profile_map = &app.data.lidarr_data.quality_profile_map; + let metadata_profile_map = &app.data.lidarr_data.metadata_profile_map; + let tags_map = &app.data.lidarr_data.tags_map; + let content = Some(&mut app.data.lidarr_data.artists); + + let artists_table_row_mapping = |artist: &Artist| { + artist.artist_name.scroll_left_or_reset( + get_width_from_percentage(area, 25), + *artist == current_selection, + app.ui_scroll_tick_count == 0, + ); + let monitored = if artist.monitored { "🏷" } else { "" }; + let artist_type = artist.artist_type.clone().unwrap_or_default(); + let size = artist + .statistics + .as_ref() + .map_or(0f64, |stats| convert_to_gb(stats.size_on_disk)); + let quality_profile = quality_profile_map + .get_by_left(&artist.quality_profile_id) + .cloned() + .unwrap_or_default(); + let metadata_profile = metadata_profile_map + .get_by_left(&artist.metadata_profile_id) + .cloned() + .unwrap_or_default(); + let albums = artist + .statistics + .as_ref() + .map_or(0, |stats| stats.album_count); + let tracks = artist.statistics.as_ref().map_or(String::new(), |stats| { + format!("{}/{}", stats.track_file_count, stats.total_track_count) + }); + let tags = artist + .tags + .iter() + .filter_map(|tag_id| { + let id = tag_id.as_i64()?; + tags_map.get_by_left(&id).cloned() + }) + .collect::>() + .join(", "); + + decorate_artist_row_with_style( + artist, + Row::new(vec![ + Cell::from(artist.artist_name.to_string()), + Cell::from(artist_type), + Cell::from(artist.status.to_display_str()), + Cell::from(quality_profile), + Cell::from(metadata_profile), + Cell::from(albums.to_string()), + Cell::from(tracks), + Cell::from(format!("{size:.2} GB")), + Cell::from(monitored.to_owned()), + Cell::from(tags), + ]), + ) + }; + let artists_table = ManagarrTable::new(content, artists_table_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_lidarr_block == ActiveLidarrBlock::ArtistsSortPrompt) + .searching(active_lidarr_block == ActiveLidarrBlock::SearchArtists) + .filtering(active_lidarr_block == ActiveLidarrBlock::FilterArtists) + .search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchArtistsError) + .filter_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::FilterArtistsError) + .headers([ + "Name", + "Type", + "Status", + "Quality Profile", + "Metadata Profile", + "Albums", + "Tracks", + "Size", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(22), + Constraint::Percentage(8), + Constraint::Percentage(8), + Constraint::Percentage(12), + Constraint::Percentage(12), + Constraint::Percentage(6), + Constraint::Percentage(8), + Constraint::Percentage(7), + Constraint::Percentage(6), + Constraint::Percentage(11), + ]); + + if [ + ActiveLidarrBlock::SearchArtists, + ActiveLidarrBlock::FilterArtists, + ] + .contains(&active_lidarr_block) + { + artists_table.show_cursor(f, area); + } + + f.render_widget(artists_table, area); + } +} + +fn decorate_artist_row_with_style<'a>(artist: &Artist, row: Row<'a>) -> Row<'a> { + if !artist.monitored { + return row.unmonitored(); + } + + match artist.status { + ArtistStatus::Ended => { + if let Some(ref stats) = artist.statistics { + return if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 { + row.downloaded() + } else { + row.missing() + }; + } + row.indeterminate() + } + ArtistStatus::Continuing => { + if let Some(ref stats) = artist.statistics { + return if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 { + row.unreleased() + } else { + row.missing() + }; + } + row.indeterminate() + } + _ => row.indeterminate(), + } +} diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap new file mode 100644 index 0000000..1d69061 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_already_in_library_ui_renders.snap @@ -0,0 +1,47 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ ✔ Name Type Status Rating Genres │ + │=> ✔ Test Artist Person Continuing 8.4 soundtrack │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────── Error ───────────────╮ │ + │ │This artist is already in your library │ │ + │ │ │ │ + │ ╰───────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap new file mode 100644 index 0000000..2b8ab65 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistConfirmPrompt.snap @@ -0,0 +1,14 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap new file mode 100644 index 0000000..3b97587 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor New Items: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Quality Profile: │Any ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Metadata Profile: │Standard ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Tags: │test │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap new file mode 100644 index 0000000..78471ed --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMetadataProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │Standard │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap new file mode 100644 index 0000000..a11a32c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitor.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │All Albums │ ▼ │ │ │ + │ │ │Future Albums │──────────────────────────────╯ │ │ + │ │ │Missing Albums │──────────────────────────────╮ │ │ + │ │ Monito│Existing Albums │ ▼ │ │ │ + │ │ │First Album │──────────────────────────────╯ │ │ + │ │ │Latest Album │──────────────────────────────╮ │ │ + │ │ Qual│None │ ▼ │ │ │ + │ │ │Unknown │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap new file mode 100644 index 0000000..269177c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectMonitorNewItems.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │All Albums │ ▼ │ │ │ + │ │ │No New Albums │──────────────────────────────╯ │ │ + │ │ │New Albums │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap new file mode 100644 index 0000000..3f94c56 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectQualityProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │Any │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap new file mode 100644 index 0000000..a947912 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistSelectRootFolder.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭───────────────────────────────╮──────────────────────────────╮ │ │ + │ │ │/nfs/music │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Monito│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Qual│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ Metad│ │ ▼ │ │ │ + │ │ │ │──────────────────────────────╯ │ │ + │ │ │ │──────────────────────────────╮ │ │ + │ │ │ │ │ │ │ + │ │ ╰───────────────────────────────╯──────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap new file mode 100644 index 0000000..3b97587 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_modal_AddArtistTagsInput.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭──────────────────────────────── Add - Test Artist (American pianist) ─────────────────────────────────╮ + ╭──────│ │───────╮ + │Test A│ │ │ + ╰──────│ │───────╯ + ╭──────│ │───────╮ + │ ✔ │ │ │ + │=> ✔ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Root Folder: │/nfs/music ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Monitor New Items: │All Albums ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Quality Profile: │Any ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Metadata Profile: │Standard ▼ │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ ╭─────────────────────────────────────────────────╮ │ │ + │ │ Tags: │test │ │ │ + │ │ ╰─────────────────────────────────────────────────╯ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ │ + │ ││ Add ││ Cancel ││ │ + ╰──────│╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│───────╯ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistEmptySearchResults.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistEmptySearchResults.snap new file mode 100644 index 0000000..68d5e7f --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistEmptySearchResults.snap @@ -0,0 +1,47 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────── Error ───────────────╮ │ + │ │ No artists found matching your query! │ │ + │ │ │ │ + │ ╰───────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistSearchInput.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistSearchInput.snap new file mode 100644 index 0000000..1f81b1d --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistSearchInput.snap @@ -0,0 +1,47 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistSearchResults.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistSearchResults.snap new file mode 100644 index 0000000..890828a --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_AddArtistSearchResults.snap @@ -0,0 +1,47 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │Test Artist │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ ✔ Name Type Status Rating Genres │ + │=> ✔ Test Artist Person Continuing 8.4 soundtrack │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_renders_loading_for_search.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_renders_loading_for_search.snap new file mode 100644 index 0000000..3c61ebc --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__add_artist_ui__add_artist_ui_tests__tests__snapshot_tests__add_artist_ui_renders_loading_for_search.snap @@ -0,0 +1,47 @@ +--- +source: src/ui/lidarr_ui/library/add_artist_ui_tests.rs +expression: output +--- + + + + + + + + ╭───────────────────────────────────────────────────── Add Artist ─────────────────────────────────────────────────────╮ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumDetails_0.snap new file mode 100644 index 0000000..90b98a0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumDetails_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ # Title Duration Audio Info Quality │ + │=> 1 Test title 3:20 FLAC - 2.0 - 1563 kbps - 44.1kHz - 24bit Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistoryDetails_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistoryDetails_1.snap new file mode 100644 index 0000000..5a09cac --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistoryDetails_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: Test source title │ │ + │ │Event Type: grabbed │ │ + │ │Quality: Lossless │ │ + │ │Date: 2023-01-01 00:00:00 UTC │ │ + │ │Indexer: │ │ + │ │NZB Info URL: │ │ + │ │Release Group: │ │ + │ │Age: 0 days │ │ + │ │Published Date: 1970-01-01 00:00:00 UTC │ │ + │ │Download Client: │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistorySortPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistorySortPrompt_1.snap new file mode 100644 index 0000000..4c3f4bc --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistorySortPrompt_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────╮ │ + │ │Something │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistory_1.snap new file mode 100644 index 0000000..5c90284 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AlbumHistory_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_0.snap new file mode 100644 index 0000000..515aa5e --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ # Title Duration Audio Info Quality │ + │=> 1 Test title 3:20 FLAC - 2.0 - 1563 kbps - 44.1kHz - 24bit Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭──────────────── Automatic Album Search ─────────────────╮ │ + │ │Do you want to trigger an automatic search of your indexers│ │ + │ │ for the album: Test Album? │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭────────────────────────────╮╭───────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰────────────────────────────╯╰───────────────────────────╯│ │ + │ ╰───────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_1.snap new file mode 100644 index 0000000..9e73f3f --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭──────────────── Automatic Album Search ─────────────────╮ │ + │ │Do you want to trigger an automatic search of your indexers│ │ + │ │ for the album: Test Album? │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭────────────────────────────╮╭───────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰────────────────────────────╯╰───────────────────────────╯│ │ + │ ╰───────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_2.snap new file mode 100644 index 0000000..70a533e --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_AutomaticallySearchAlbumPrompt_2.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source ▼ Age ⛔ Title Indexer Size Peers Quality │ + │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ + │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭──────────────── Automatic Album Search ─────────────────╮ │ + │ │Do you want to trigger an automatic search of your indexers│ │ + │ │ for the album: Test Album? │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭────────────────────────────╮╭───────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰────────────────────────────╯╰───────────────────────────╯│ │ + │ ╰───────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_DeleteTrackFilePrompt_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_DeleteTrackFilePrompt_0.snap new file mode 100644 index 0000000..a0de373 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_DeleteTrackFilePrompt_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ # Title Duration Audio Info Quality │ + │=> 1 Test title 3:20 FLAC - 2.0 - 1563 kbps - 44.1kHz - 24bit Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────── Delete Track File ───────────────────╮ │ + │ │ Do you really want to delete this track file: │ │ + │ │ Test title? │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭────────────────────────────╮╭───────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰────────────────────────────╯╰───────────────────────────╯│ │ + │ ╰───────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_FilterAlbumHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_FilterAlbumHistoryError_1.snap new file mode 100644 index 0000000..3125c1d --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_FilterAlbumHistoryError_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────── Error ────────────╮ │ + │ │ The given filter produced empty│ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_FilterAlbumHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_FilterAlbumHistory_1.snap new file mode 100644 index 0000000..f1b8013 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_FilterAlbumHistory_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────── Filter ──────────────╮ │ + │ │album history filter │ │ + │ ╰─────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearchConfirmPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearchConfirmPrompt_2.snap new file mode 100644 index 0000000..6791f5c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearchConfirmPrompt_2.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source ▼ Age ⛔ Title Indexer Size Peers Quality │ + │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ + │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────────── Download Rejected Release ──────────────────╮ │ + │ │ Do you really want to download the rejected release: Test │ │ + │ │ Release? │ │ + │ │ │ │ + │ │ │ │ + │ │Rejection reasons: │ │ + │ │• Unknown quality profile │ │ + │ │• Release is already mapped │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭──────────────────────────────╮╭──────────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰──────────────────────────────╯╰──────────────────────────────╯│ │ + │ ╰────────────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearchSortPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearchSortPrompt_2.snap new file mode 100644 index 0000000..9bfe80d --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearchSortPrompt_2.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Age ⛔ Title Indexer Size Peers Quality │ + │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ + │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────╮ │ + │ │Something │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearch_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearch_2.snap new file mode 100644 index 0000000..9c73808 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_ManualAlbumSearch_2.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source ▼ Age ⛔ Title Indexer Size Peers Quality │ + │=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless │ + │ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchAlbumHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchAlbumHistoryError_1.snap new file mode 100644 index 0000000..888ef32 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchAlbumHistoryError_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────── Error ────────────╮ │ + │ │ No items found matching search │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchAlbumHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchAlbumHistory_1.snap new file mode 100644 index 0000000..14a212c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchAlbumHistory_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────── Search ──────────────╮ │ + │ │album history search │ │ + │ ╰─────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchTracksError_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchTracksError_0.snap new file mode 100644 index 0000000..534115d --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchTracksError_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ # Title Duration Audio Info Quality │ + │=> 1 Test title 3:20 FLAC - 2.0 - 1563 kbps - 44.1kHz - 24bit Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────── Error ────────────╮ │ + │ │ No items found matching search │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchTracks_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchTracks_0.snap new file mode 100644 index 0000000..526377f --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_renders_SearchTracks_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ # Title Duration Audio Info Quality │ + │=> 1 Test title 3:20 FLAC - 2.0 - 1563 kbps - 44.1kHz - 24bit Lossless │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───────────── Search ──────────────╮ │ + │ │album search │ │ + │ ╰─────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_ui_renders_track_details_over_album_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_ui_renders_track_details_over_album_details.snap new file mode 100644 index 0000000..ec6133e --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__album_details_ui_renders_track_details_over_album_details.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │──────╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮─────│ + │ # │ Track Details │ History │ │ + │=> 1 │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ + │ │Some details: │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumDetails_0.snap new file mode 100644 index 0000000..cadad28 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumDetails_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumHistoryDetails_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumHistoryDetails_1.snap new file mode 100644 index 0000000..161a3fb --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumHistoryDetails_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: │ │ + │ │Event Type: unknown │ │ + │ │Quality: │ │ + │ │Date: 1970-01-01 00:00:00 UTC │ │ + │ │No additional details available. │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumHistory_1.snap new file mode 100644 index 0000000..cadad28 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_AlbumHistory_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_ManualAlbumSearch_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_ManualAlbumSearch_2.snap new file mode 100644 index 0000000..1debade --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__empty_album_details_ManualAlbumSearch_2.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumDetails_0.snap new file mode 100644 index 0000000..1debade --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumDetails_0.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumHistoryDetails_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumHistoryDetails_1.snap new file mode 100644 index 0000000..e68a2f4 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumHistoryDetails_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: │ │ + │ │Event Type: unknown │ │ + │ │Quality: │ │ + │ │Date: 1970-01-01 00:00:00 UTC │ │ + │ │No additional details available. │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumHistory_1.snap new file mode 100644 index 0000000..1debade --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_AlbumHistory_1.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_ManualAlbumSearch_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_ManualAlbumSearch_2.snap new file mode 100644 index 0000000..1debade --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__album_details_ui__album_details_ui_tests__tests__snapshot_tests__loading_album_details_ManualAlbumSearch_2.snap @@ -0,0 +1,50 @@ +--- +source: src/ui/lidarr_ui/library/album_details_ui_tests.rs +expression: output +--- + + + + + ╭ Test Album Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Tracks │ History │ Manual Search │ + │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistDetails_0.snap new file mode 100644 index 0000000..b94d5ba --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistDetails_0.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Monitored Title Type Tracks Duration Release Date Size ││ + ││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistoryDetails_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistoryDetails_1.snap new file mode 100644 index 0000000..77e46b0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistoryDetails_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │Size on Disk: 0.00 GB │Source Title: Test source title │ │ + │╭ Artist Details ─────────────│Event Type: grabbed │───────────────────────────────╮│ + ││ Albums │ History │ Manual Sear│Quality: Lossless │ ││ + ││───────────────────────────────│Date: 2023-01-01 00:00:00 UTC │───────────────────────────────││ + ││ Source Title ▼ │Indexer: │ ││ + ││=> Test source title │NZB Info URL: │-01-01 00:00:00 UTC ││ + ││ │Release Group: │ ││ + ││ │Age: 0 days │ ││ + ││ │Published Date: 1970-01-01 00:00:00 UTC │ ││ + ││ │Download Client: │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistorySortPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistorySortPrompt_1.snap new file mode 100644 index 0000000..6dc4f79 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistorySortPrompt_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭───────────────────────────╮ ││ + ││ │Something │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ ╰───────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistory_1.snap new file mode 100644 index 0000000..b9881f6 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ArtistHistory_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title ▼ Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_0.snap new file mode 100644 index 0000000..e636169 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_0.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │ + │Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │ + │╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Monitored Title │ │ Release Date Size ││ + ││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_1.snap new file mode 100644 index 0000000..773f42d --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │ + │Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │ + │╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Source Title ▼ │ │ Date ││ + ││=> Test source title │ │ 2023-01-01 00:00:00 UTC ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_2.snap new file mode 100644 index 0000000..3dc5c23 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_AutomaticallySearchArtistPrompt_2.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │ + │Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │ + │╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││ + ││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││ + ││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_FilterArtistHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_FilterArtistHistoryError_1.snap new file mode 100644 index 0000000..e384a50 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_FilterArtistHistoryError_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title ▼ Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭──────────── Error ─────────────╮ ││ + ││ │ The given filter produced empty │ ││ + ││ ╰──────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_FilterArtistHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_FilterArtistHistory_1.snap new file mode 100644 index 0000000..b335474 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_FilterArtistHistory_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title ▼ Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭────────────── Filter ───────────────╮ ││ + ││ │artist history filter │ ││ + ││ ╰───────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearchConfirmPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearchConfirmPrompt_2.snap new file mode 100644 index 0000000..4d2c9e6 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearchConfirmPrompt_2.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 ╭───────────────── Download Rejected Release ──────────────────╮ │ + │Tracks: 15/15 │ Do you really want to download the rejected release: Test │ │ + │Size on Disk: 0.00 GB │ Release? │ │ + │╭ Artist Details ──────────────────────│ │───────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││────────────────────────────────────────│Rejection reasons: │───────────────────────────────────────││ + ││ Source ▼ Age ⛔ Title │• Unknown quality profile │e Peers Quality ││ + ││=> torrent 1 days ⛔ Test Relea│• Release is already mapped │ GB 2 / 1 Lossless ││ + ││ usenet 1 days ⛔ Test Relea│ │ GB Lossless ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭──────────────────────────────╮╭──────────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰──────────────────────────────╯╰──────────────────────────────╯│ ││ + ││ ╰────────────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearchSortPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearchSortPrompt_2.snap new file mode 100644 index 0000000..07ceddf --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearchSortPrompt_2.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Age ⛔ Title Indexer Size Peers Quality ││ + ││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││ + ││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭───────────────────────────╮ ││ + ││ │Something │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ ╰───────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearch_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearch_2.snap new file mode 100644 index 0000000..684e4f6 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ManualArtistSearch_2.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source ▼ Age ⛔ Title Indexer Size Peers Quality ││ + ││=> torrent 1 days ⛔ Test Release kickass torrents 0.0 GB 2 / 1 Lossless ││ + ││ usenet 1 days ⛔ Test Release DrunkenSlug 0.0 GB Lossless ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchAlbumsError_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchAlbumsError_0.snap new file mode 100644 index 0000000..93b238e --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchAlbumsError_0.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Monitored Title Type Tracks Duration Release Date Size ││ + ││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭──────────── Error ─────────────╮ ││ + ││ │ No items found matching search │ ││ + ││ ╰──────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchAlbums_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchAlbums_0.snap new file mode 100644 index 0000000..a940bc3 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchAlbums_0.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Monitored Title Type Tracks Duration Release Date Size ││ + ││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭────────────── Search ───────────────╮ ││ + ││ │album search │ ││ + ││ ╰───────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchArtistHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchArtistHistoryError_1.snap new file mode 100644 index 0000000..4c2d8f7 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchArtistHistoryError_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title ▼ Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭──────────── Error ─────────────╮ ││ + ││ │ No items found matching search │ ││ + ││ ╰──────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchArtistHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchArtistHistory_1.snap new file mode 100644 index 0000000..42e9e6a --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_SearchArtistHistory_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title ▼ Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ╭────────────── Search ───────────────╮ ││ + ││ │artist history search │ ││ + ││ ╰───────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_0.snap new file mode 100644 index 0000000..929cfa0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_0.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │ + │Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │ + │╭ Artist Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Monitored Title │ │ Release Date Size ││ + ││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_1.snap new file mode 100644 index 0000000..2876cdf --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_1.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │ + │Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │ + │╭ Artist Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Source Title ▼ │ │ Date ││ + ││=> Test source title │ │ 2023-01-01 00:00:00 UTC ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_2.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_2.snap new file mode 100644 index 0000000..b717f47 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_UpdateAndScanArtistPrompt_2.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │ + │Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │ + │╭ Artist Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Source ▼ Age ⛔ Title │ │ Size Peers Quality ││ + ││=> torrent 1 days ⛔ Test Releas│ │ 0.0 GB 2 / 1 Lossless ││ + ││ usenet 1 days ⛔ Test Releas│ │ 0.0 GB Lossless ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_automatic_search_prompt_over_artist_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_automatic_search_prompt_over_artist_details.snap new file mode 100644 index 0000000..e636169 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_automatic_search_prompt_over_artist_details.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────── Automatic Artist Search ────────────────╮ │ + │Size on Disk: 0.00 GB │Do you want to trigger an automatic search of your indexers│ │ + │╭ Artist Details ───────────────────────│ for all monitored album(s) for the artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Monitored Title │ │ Release Date Size ││ + ││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_delete_album_prompt_over_artist_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_delete_album_prompt_over_artist_details.snap new file mode 100644 index 0000000..6d7f6e9 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_delete_album_prompt_over_artist_details.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭───────────────────── Delete Album ──────────────────────╮ │ + │Size on Disk: 0.00 GB │ Do you really want to delete the album: │ │ + │╭ Artist Details ───────────────────────│ Test Album? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Monitored Title │ ╭───╮ │ Release Date Size ││ + ││=> 🏷 Test Album │ Delete Album Files: │ ✔ │ │ 2023-01-01 0.00 GB ││ + ││ │ ╰───╯ │ ││ + ││ │ ╭───╮ │ ││ + ││ │ Add List Exclusion: │ │ │ ││ + ││ │ ╰───╯ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_update_and_scan_prompt_over_artist_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_update_and_scan_prompt_over_artist_details.snap new file mode 100644 index 0000000..929cfa0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__artist_details_ui_renders_update_and_scan_prompt_over_artist_details.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭──────────────────── Update and Scan ────────────────────╮ │ + │Size on Disk: 0.00 GB │ Do you want to trigger an update and disk scan for the │ │ + │╭ Artist Details ───────────────────────│ artist: Alex? │───────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search │ │ ││ + ││─────────────────────────────────────────│ │───────────────────────────────────────────││ + ││ Monitored Title │ │ Release Date Size ││ + ││=> 🏷 Test Album │ │ 2023-01-01 0.00 GB ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭────────────────────────────╮╭───────────────────────────╮│ ││ + ││ ││ Yes ││ No ││ ││ + ││ │╰────────────────────────────╯╰───────────────────────────╯│ ││ + ││ ╰───────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistDetails.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistDetails.snap new file mode 100644 index 0000000..89ee956 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistDetails.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistHistory.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistHistory.snap new file mode 100644 index 0000000..b9881f6 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistHistory.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Source Title ▼ Event Type Quality Date ││ + ││=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistHistoryDetails.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistHistoryDetails.snap new file mode 100644 index 0000000..77e46b0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ArtistHistoryDetails.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │Size on Disk: 0.00 GB │Source Title: Test source title │ │ + │╭ Artist Details ─────────────│Event Type: grabbed │───────────────────────────────╮│ + ││ Albums │ History │ Manual Sear│Quality: Lossless │ ││ + ││───────────────────────────────│Date: 2023-01-01 00:00:00 UTC │───────────────────────────────││ + ││ Source Title ▼ │Indexer: │ ││ + ││=> Test source title │NZB Info URL: │-01-01 00:00:00 UTC ││ + ││ │Release Group: │ ││ + ││ │Age: 0 days │ ││ + ││ │Published Date: 1970-01-01 00:00:00 UTC │ ││ + ││ │Download Client: │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ManualArtistSearch.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ManualArtistSearch.snap new file mode 100644 index 0000000..896b706 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__empty_artist_details_ManualArtistSearch.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ ││ + ││ ││ + ││ Loading ... ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ArtistDetails.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ArtistDetails.snap new file mode 100644 index 0000000..896b706 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ArtistDetails.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ ││ + ││ ││ + ││ Loading ... ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ArtistHistory.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ArtistHistory.snap new file mode 100644 index 0000000..896b706 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ArtistHistory.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ ││ + ││ ││ + ││ Loading ... ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ManualArtistSearch.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ManualArtistSearch.snap new file mode 100644 index 0000000..896b706 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__artist_details_ui__artist_details_ui_tests__tests__snapshot_tests__loading_artist_details_ManualArtistSearch.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/artist_details_ui_tests.rs +expression: output +--- + + + + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ ││ + ││ ││ + ││ Loading ... ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_album_ui__delete_album_ui_tests__tests__snapshot_tests__delete_album_ui_renders_delete_album.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_album_ui__delete_album_ui_tests__tests__snapshot_tests__delete_album_ui_renders_delete_album.snap new file mode 100644 index 0000000..d7f91f4 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_album_ui__delete_album_ui_tests__tests__snapshot_tests__delete_album_ui_renders_delete_album.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/library/delete_album_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭───────────────────── Delete Album ──────────────────────╮ + │ Do you really want to delete the album: │ + │ Test Album? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Album Files: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap new file mode 100644 index 0000000..98bd800 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__delete_artist_ui__delete_artist_ui_tests__tests__snapshot_tests__delete_artist_ui_renders_delete_artist.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/library/delete_artist_ui_tests.rs +expression: output +--- + + + + + + + + + + + + + + + + + ╭───────────────────── Delete Artist ─────────────────────╮ + │ Do you really want to delete the artist: │ + │ Alex? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Artist Files: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistConfirmPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistConfirmPrompt.snap new file mode 100644 index 0000000..460dad4 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistConfirmPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/edit_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───╮ │ + │ Monitored: │ ✔ │ │ + │ ╰───╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor New Albums: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Quality Profile: │Lossless ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Metadata Profile: │Standard ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Tags: │alex │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistPrompt.snap new file mode 100644 index 0000000..460dad4 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/edit_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───╮ │ + │ Monitored: │ ✔ │ │ + │ ╰───╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor New Albums: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Quality Profile: │Lossless ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Metadata Profile: │Standard ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Tags: │alex │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectMetadataProfile.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectMetadataProfile.snap new file mode 100644 index 0000000..709d31c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectMetadataProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/edit_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───╮ │ + │ Monitored: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───────────────────────────────╮──────────────────────────────╮ │ + │ Monitor│Standard │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ ╰───────────────────────────────╯──────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectMonitorNewItems.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectMonitorNewItems.snap new file mode 100644 index 0000000..9b232a2 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectMonitorNewItems.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/edit_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───╮ │ + │ Monitored: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───────────────────────────────╮──────────────────────────────╮ │ + │ Monitor│All Albums │ ▼ │ │ + │ │No New Albums │──────────────────────────────╯ │ + │ │New Albums │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ ╰───────────────────────────────╯──────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectQualityProfile.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectQualityProfile.snap new file mode 100644 index 0000000..45a5e12 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__edit_artist_ui__edit_artist_ui_tests__tests__snapshot_tests__edit_artist_EditArtistSelectQualityProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/edit_artist_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───╮ │ + │ Monitored: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───────────────────────────────╮──────────────────────────────╮ │ + │ Monitor│Lossless │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ ╰───────────────────────────────╯──────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ArtistDetails.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ArtistDetails.snap new file mode 100644 index 0000000..1da6ab4 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ArtistDetails.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: Ame╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ │ + │Type: Person │ │ │ + │Status: Continuing │ │ │ + │Genres: soundtrack │ │ │ + │Rating: 84% │ │ │ + │Path: /nfs/music/te│ │ │ + │Quality Profile: Lo│ │ │ + │Metadata Profile: S│ │ │ + │Monitored: Yes │ │ │ + │Albums: 1 │ ╭───╮ │ │ + │Tracks: 15/15 │ Monitored: │ ✔ │ │ │ + │Size on Disk: 0.00 │ ╰───╯ │ │ + │╭ Artist Details │ ╭─────────────────────────────────────────────────╮ │────────────────────╮│ + ││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││ + ││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││ + ││ Monitored Titl│ ╭─────────────────────────────────────────────────╮ │ize ││ + ││=> 🏷 Test│ Quality Profile: │Lossless ▼ │ │.00 GB ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Metadata Profile: │Standard ▼ │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Path: │/nfs/music │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Tags: │alex │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ ││ + ││ ││ Save ││ Cancel ││ ││ + ││ │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ArtistHistory.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ArtistHistory.snap new file mode 100644 index 0000000..f27ce3c --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ArtistHistory.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: Ame╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ │ + │Type: Person │ │ │ + │Status: Continuing │ │ │ + │Genres: soundtrack │ │ │ + │Rating: 84% │ │ │ + │Path: /nfs/music/te│ │ │ + │Quality Profile: Lo│ │ │ + │Metadata Profile: S│ │ │ + │Monitored: Yes │ │ │ + │Albums: 1 │ ╭───╮ │ │ + │Tracks: 15/15 │ Monitored: │ ✔ │ │ │ + │Size on Disk: 0.00 │ ╰───╯ │ │ + │╭ Artist Details │ ╭─────────────────────────────────────────────────╮ │────────────────────╮│ + ││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││ + ││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││ + ││ Source Title ▼ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││=> Test source tit│ Quality Profile: │Lossless ▼ │ │0:00 UTC ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Metadata Profile: │Standard ▼ │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Path: │/nfs/music │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Tags: │alex │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ ││ + ││ ││ Save ││ Cancel ││ ││ + ││ │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ManualArtistSearch.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ManualArtistSearch.snap new file mode 100644 index 0000000..b510a18 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__edit_artist_renders_over_ManualArtistSearch.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: Ame╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ │ + │Type: Person │ │ │ + │Status: Continuing │ │ │ + │Genres: soundtrack │ │ │ + │Rating: 84% │ │ │ + │Path: /nfs/music/te│ │ │ + │Quality Profile: Lo│ │ │ + │Metadata Profile: S│ │ │ + │Monitored: Yes │ │ │ + │Albums: 1 │ ╭───╮ │ │ + │Tracks: 15/15 │ Monitored: │ ✔ │ │ │ + │Size on Disk: 0.00 │ ╰───╯ │ │ + │╭ Artist Details │ ╭─────────────────────────────────────────────────╮ │────────────────────╮│ + ││ Albums │ History │ Monitor New Albums: │All Albums ▼ │ │ ││ + ││──────────────────│ ╰─────────────────────────────────────────────────╯ │────────────────────││ + ││ Source ▼ Age │ ╭─────────────────────────────────────────────────╮ │ Quality ││ + ││=> torrent 1 day│ Quality Profile: │Lossless ▼ │ │ Lossless ││ + ││ usenet 1 day│ ╰─────────────────────────────────────────────────╯ │ Lossless ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Metadata Profile: │Standard ▼ │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Path: │/nfs/music │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ ╭─────────────────────────────────────────────────╮ │ ││ + ││ │ Tags: │alex │ │ ││ + ││ │ ╰─────────────────────────────────────────────────╯ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ ││ + ││ ││ Save ││ Cancel ││ ││ + ││ │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_artist_details_over_library.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_artist_details_over_library.snap new file mode 100644 index 0000000..b60cb0f --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_artist_details_over_library.snap @@ -0,0 +1,52 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + ╭ Alex ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Artist: Alex │ + │Overview: some interesting description of the artist │ + │Disambiguation: American pianist │ + │Type: Person │ + │Status: Continuing │ + │Genres: soundtrack │ + │Rating: 84% │ + │Path: /nfs/music/test-artist │ + │Quality Profile: Lossless │ + │Metadata Profile: Standard │ + │Monitored: Yes │ + │Albums: 1 │ + │Tracks: 15/15 │ + │Size on Disk: 0.00 GB │ + │╭ Artist Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ + ││ Albums │ History │ Manual Search ││ + ││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────││ + ││ Monitored Title Type Tracks Duration Release Date Size ││ + ││=> 🏷 Test Album Album 10/10 0 min 2023-01-01 0.00 GB ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + ││ ││ + │╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap new file mode 100644 index 0000000..c4602f8 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_delete_artist_over_library.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + + + + ╭───────────────────── Delete Artist ─────────────────────╮ + │ Do you really want to delete the artist: │ + │ Alex? │ + │ │ + │ │ + │ ╭───╮ │ + │ Delete Artist Files: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───╮ │ + │ Add List Exclusion: │ │ │ + │ ╰───╯ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_dropdown_over_edit_artist_over_artist_details.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_dropdown_over_edit_artist_over_artist_details.snap new file mode 100644 index 0000000..311a053 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_dropdown_over_edit_artist_over_artist_details.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + ╭─────────────────────────────────── Edit - Alex (American pianist) ────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭───╮ │ + │ Monitored: │ ✔ │ │ + │ ╰───╯ │ + │ ╭───────────────────────────────╮──────────────────────────────╮ │ + │ Monitor│Standard │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ │ │ │ │ + │ ╰───────────────────────────────╯──────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap new file mode 100644 index 0000000..e17e3c0 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_empty.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap new file mode 100644 index 0000000..b697d89 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_update_all_artists_prompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_update_all_artists_prompt.snap new file mode 100644 index 0000000..d20a2fa --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_update_all_artists_prompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + + + + ╭────────────────── Update All Artists ───────────────────╮ + │ Do you want to update info and scan your disks for all of │ + │ your artists? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap new file mode 100644 index 0000000..1443a4f --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_Artists.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap new file mode 100644 index 0000000..c2d2087 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_ArtistsSortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + ╭───────────────────────────────╮ + │Name │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap new file mode 100644 index 0000000..24b5f60 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtists.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + ╭───────────────── Filter ──────────────────╮ + │artist filter │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap new file mode 100644 index 0000000..2b5b359 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_FilterArtistsError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │The given filter produced empty results│ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap new file mode 100644 index 0000000..1c21242 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtists.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + ╭───────────────── Search ──────────────────╮ + │artist search │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap new file mode 100644 index 0000000..6af1685 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__library_ui_tests__tests__snapshot_tests__lidarr_library_SearchArtistsError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags +=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ No items found matching search │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackDetails_0.snap new file mode 100644 index 0000000..4a35aea --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackDetails_0.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackHistory_1.snap new file mode 100644 index 0000000..4a35aea --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__empty_track_details_TrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackDetails_0.snap new file mode 100644 index 0000000..feb5ccc --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackDetails_0.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackHistory_1.snap new file mode 100644 index 0000000..feb5ccc --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__loading_track_details_TrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistoryError_1.snap new file mode 100644 index 0000000..3d6a1c1 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistoryError_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭────────── Error ──────────╮ │ + │ │ The given filter produced │ │ + │ ╰─────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistory_1.snap new file mode 100644 index 0000000..305bdb2 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_FilterTrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────── Filter ───────────╮ │ + │ │track history filter │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistoryError_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistoryError_1.snap new file mode 100644 index 0000000..5e7404b --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistoryError_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭────────── Error ──────────╮ │ + │ │ No items found matching │ │ + │ ╰─────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistory_1.snap new file mode 100644 index 0000000..06e43e1 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_SearchTrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────── Search ───────────╮ │ + │ │track history search │ │ + │ ╰────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackDetails_0.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackDetails_0.snap new file mode 100644 index 0000000..021e7df --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackDetails_0.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │Some details: │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistoryDetails_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistoryDetails_1.snap new file mode 100644 index 0000000..bffdaef --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistoryDetails_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: Test source title │ │ + │ │Event Type: grabbed │ │ + │ │Quality: Lossless │ │ + │ │Date: 2023-01-01 00:00:00 UTC │ │ + │ │Indexer: │ │ + │ │NZB Info URL: │ │ + │ │Release Group: │ │ + │ │Age: 0 days │ │ + │ │Published Date: 1970-01-01 00:00:00 UTC │ │ + │ │Download Client: │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistorySortPrompt_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistorySortPrompt_1.snap new file mode 100644 index 0000000..9936c81 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistorySortPrompt_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭──────────────────────╮ │ + │ │Something │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰──────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistory_1.snap b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistory_1.snap new file mode 100644 index 0000000..902df42 --- /dev/null +++ b/src/ui/lidarr_ui/library/snapshots/managarr__ui__lidarr_ui__library__track_details_ui__track_details_ui_tests__tests__snapshot_tests__track_details_TrackHistory_1.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/library/track_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Track Details ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Track Details │ History │ + │──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ + │ Source Title ▼ Event Type Quality Date │ + │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/library/track_details_ui.rs b/src/ui/lidarr_ui/library/track_details_ui.rs new file mode 100644 index 0000000..cf330cc --- /dev/null +++ b/src/ui/lidarr_ui/library/track_details_ui.rs @@ -0,0 +1,259 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::{LidarrHistoryItem, Track}; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS}; +use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details; +use crate::ui::styles::ManagarrStyle; +use crate::ui::styles::{downloaded_style, missing_style, secondary_style}; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{DrawUi, draw_popup, draw_tabs}; +use ratatui::Frame; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; + +#[cfg(test)] +#[path = "track_details_ui_tests.rs"] +mod track_details_ui_tests; + +pub(super) struct TrackDetailsUi; + +impl DrawUi for TrackDetailsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref() + && album_details_modal.track_details_modal.is_some() + && let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() + { + let draw_track_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { + let content_area = draw_tabs( + f, + popup_area, + "Track Details", + &app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal must exist in this context") + .track_details_modal + .as_ref() + .expect("track_details_modal must exist in this context") + .track_details_tabs, + ); + draw_track_details_tabs(f, app, content_area); + + if active_lidarr_block == ActiveLidarrBlock::TrackHistoryDetails { + draw_history_item_details_popup(f, app); + } + }; + + draw_popup(f, app, draw_track_details_popup, Size::Large); + } + } +} + +pub fn draw_track_details_tabs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref() + && let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() + && let Route::Lidarr(active_lidarr_block, _) = + track_details_modal.track_details_tabs.get_active_route() + { + match active_lidarr_block { + ActiveLidarrBlock::TrackDetails => draw_track_details(f, app, area), + ActiveLidarrBlock::TrackHistory => draw_track_history_table(f, app, area), + _ => (), + } + } +} + +fn draw_track_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = layout_block_top_border(); + + match app.data.lidarr_data.album_details_modal.as_ref() { + Some(album_details_modal) if !app.is_loading => { + if let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() { + let track = album_details_modal.tracks.current_selection().clone(); + let track_details = &track_details_modal.track_details; + let text = Text::from( + track_details + .items + .iter() + .filter(|it| !it.is_empty()) + .map(|line| { + let split = line.split(':').collect::>(); + let title = format!("{}:", split[0]); + let style = style_from_status(&track); + + Line::from(vec![ + title.bold().style(style), + Span::styled(split[1..].join(":"), style), + ]) + }) + .collect::>>(), + ); + + let paragraph = Paragraph::new(text) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((track_details.offset, 0)); + + f.render_widget(paragraph, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading + || app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal must exist in this context") + .track_details_modal + .is_none(), + block, + ), + area, + ), + } +} + +fn draw_track_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.lidarr_data.album_details_modal.as_ref() { + Some(album_details_modal) if !app.is_loading => { + let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() else { + panic!("Non-Lidarr route is being used"); + }; + if let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() { + let current_selection = if track_details_modal.track_history.is_empty() { + LidarrHistoryItem::default() + } else { + track_details_modal + .track_history + .current_selection() + .clone() + }; + + let history_row_mapping = |history_item: &LidarrHistoryItem| { + let LidarrHistoryItem { + source_title, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let mut track_history_table = &mut app + .data + .lidarr_data + .album_details_modal + .as_mut() + .expect("album_details_modal must exist in this context") + .track_details_modal + .as_mut() + .expect("track_details_modal must exist in this context") + .track_history; + let history_table = ManagarrTable::new(Some(&mut track_history_table), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_lidarr_block == ActiveLidarrBlock::TrackHistorySortPrompt) + .searching(active_lidarr_block == ActiveLidarrBlock::SearchTrackHistory) + .search_produced_empty_results( + active_lidarr_block == ActiveLidarrBlock::SearchTrackHistoryError, + ) + .filtering(active_lidarr_block == ActiveLidarrBlock::FilterTrackHistory) + .filter_produced_empty_results( + active_lidarr_block == ActiveLidarrBlock::FilterTrackHistoryError, + ) + .headers(["Source Title", "Event Type", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(20), + Constraint::Percentage(15), + Constraint::Percentage(25), + ]); + + f.render_widget(history_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading + || app + .data + .lidarr_data + .album_details_modal + .as_ref() + .expect("album_details_modal must exist in this context") + .track_details_modal + .is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = + if let Some(album_details_modal) = app.data.lidarr_data.album_details_modal.as_ref() { + if let Some(track_details_modal) = album_details_modal.track_details_modal.as_ref() { + if track_details_modal.track_history.is_empty() { + LidarrHistoryItem::default() + } else { + track_details_modal + .track_history + .current_selection() + .clone() + } + } else { + LidarrHistoryItem::default() + } + } else { + LidarrHistoryItem::default() + }; + + let line_vec = create_history_event_details(current_selection); + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); +} + +fn style_from_status(track: &Track) -> Style { + if !track.has_file { + return missing_style(); + } + + downloaded_style() +} diff --git a/src/ui/lidarr_ui/library/track_details_ui_tests.rs b/src/ui/lidarr_ui/library/track_details_ui_tests.rs new file mode 100644 index 0000000..cacbb4b --- /dev/null +++ b/src/ui/lidarr_ui/library/track_details_ui_tests.rs @@ -0,0 +1,132 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, TRACK_DETAILS_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::library::track_details_ui::TrackDetailsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_track_details_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if TRACK_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(TrackDetailsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!TrackDetailsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(ActiveLidarrBlock::TrackDetails, 0)] + #[case(ActiveLidarrBlock::TrackHistory, 1)] + #[case(ActiveLidarrBlock::TrackHistoryDetails, 1)] + #[case(ActiveLidarrBlock::SearchTrackHistory, 1)] + #[case(ActiveLidarrBlock::SearchTrackHistoryError, 1)] + #[case(ActiveLidarrBlock::FilterTrackHistory, 1)] + #[case(ActiveLidarrBlock::FilterTrackHistoryError, 1)] + #[case(ActiveLidarrBlock::TrackHistorySortPrompt, 1)] + #[case(ActiveLidarrBlock::TrackHistoryDetails, 1)] + fn test_track_details_ui_renders( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_details_tabs + .set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TrackDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("track_details_{active_lidarr_block}_{index}"), + output + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::TrackDetails, 0)] + #[case(ActiveLidarrBlock::TrackHistory, 1)] + fn test_track_details_ui_renders_loading( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap() + .track_details_tabs + .set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TrackDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("loading_track_details_{active_lidarr_block}_{index}"), + output + ); + } + + #[rstest] + #[case(ActiveLidarrBlock::TrackDetails, 0)] + #[case(ActiveLidarrBlock::TrackHistory, 1)] + fn test_track_details_ui_renders_empty( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + { + let track_details_modal = app + .data + .lidarr_data + .album_details_modal + .as_mut() + .unwrap() + .track_details_modal + .as_mut() + .unwrap(); + track_details_modal.track_details_tabs.set_index(index); + track_details_modal.track_details = Default::default(); + track_details_modal.track_history = Default::default(); + } + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + TrackDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("empty_track_details_{active_lidarr_block}_{index}"), + output + ); + } + } +} diff --git a/src/ui/lidarr_ui/lidarr_ui_tests.rs b/src/ui/lidarr_ui/lidarr_ui_tests.rs new file mode 100644 index 0000000..1241729 --- /dev/null +++ b/src/ui/lidarr_ui/lidarr_ui_tests.rs @@ -0,0 +1,46 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::Route; + use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::LidarrUi; + + #[test] + fn test_lidarr_ui_accepts() { + for lidarr_block in ActiveLidarrBlock::iter() { + assert!(LidarrUi::accepts(Route::Lidarr(lidarr_block, None))); + } + } + + mod snapshot_tests { + use super::*; + use crate::app::App; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + use rstest::rstest; + + #[rstest] + #[case(ActiveLidarrBlock::Artists, 0)] + #[case(ActiveLidarrBlock::Downloads, 1)] + #[case(ActiveLidarrBlock::Blocklist, 2)] + #[case(ActiveLidarrBlock::History, 3)] + #[case(ActiveLidarrBlock::RootFolders, 4)] + #[case(ActiveLidarrBlock::Indexers, 5)] + #[case(ActiveLidarrBlock::System, 6)] + fn test_lidarr_ui_renders_lidarr_tabs( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.main_tabs.set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LidarrUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("lidarr_tabs_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/lidarr_ui_utils.rs b/src/ui/lidarr_ui/lidarr_ui_utils.rs new file mode 100644 index 0000000..b507cba --- /dev/null +++ b/src/ui/lidarr_ui/lidarr_ui_utils.rs @@ -0,0 +1,162 @@ +use crate::models::lidarr_models::{LidarrHistoryData, LidarrHistoryEventType, LidarrHistoryItem}; +use ratatui::text::Line; + +#[cfg(test)] +#[path = "lidarr_ui_utils_tests.rs"] +mod lidarr_ui_utils_tests; + +pub(super) fn create_history_event_details(history_item: LidarrHistoryItem) -> Vec> { + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item; + let LidarrHistoryData { + indexer, + nzb_info_url, + release_group, + age, + published_date, + download_client_name, + download_client, + message, + reason, + dropped_path, + imported_path, + source_path, + path, + status_messages, + } = data; + + let mut lines = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!("Quality: {}", quality.quality.name.trim_start())), + Line::from(format!("Date: {date}")), + ]; + + match event_type { + LidarrHistoryEventType::Grabbed => { + lines.push(Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Age: {} days", + age.unwrap_or("0".to_owned()).trim_start() + ))); + lines.push(Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default() + ))); + lines.push(Line::from(format!( + "Download Client: {}", + download_client_name + .unwrap_or(download_client.unwrap_or_default()) + .trim_start() + ))); + } + LidarrHistoryEventType::DownloadImported => { + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + } + LidarrHistoryEventType::DownloadFailed => { + lines.push(Line::from(format!( + "Download Client: {}", + download_client_name + .unwrap_or(download_client.unwrap_or_default()) + .trim_start() + ))); + lines.push(Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start() + ))); + } + LidarrHistoryEventType::TrackFileDeleted => { + lines.push(Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + } + LidarrHistoryEventType::TrackFileImported => { + lines.push(Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Download Client: {}", + download_client_name + .unwrap_or(download_client.unwrap_or_default()) + .trim_start() + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + } + LidarrHistoryEventType::TrackFileRenamed => { + lines.push(Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Path: {}", + path.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + } + LidarrHistoryEventType::TrackFileRetagged => { + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + } + LidarrHistoryEventType::AlbumImportIncomplete => { + lines.push(Line::from(format!( + "Status Messages: {}", + status_messages.unwrap_or_default().trim_start() + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + ))); + } + _ => { + lines.push(Line::from("No additional details available.".to_owned())); + } + } + + lines +} diff --git a/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs b/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs new file mode 100644 index 0000000..aad67b8 --- /dev/null +++ b/src/ui/lidarr_ui/lidarr_ui_utils_tests.rs @@ -0,0 +1,458 @@ +#[cfg(test)] +mod tests { + use chrono::Utc; + use pretty_assertions::assert_eq; + use ratatui::text::Line; + + use crate::models::lidarr_models::{ + LidarrHistoryData, LidarrHistoryEventType, LidarrHistoryItem, + }; + use crate::models::servarr_models::{Quality, QualityWrapper}; + use crate::ui::lidarr_ui::lidarr_ui_utils::create_history_event_details; + + #[test] + fn test_create_history_event_details_grabbed() { + let history_item = lidarr_history_item(LidarrHistoryEventType::Grabbed); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!("Indexer: {}", data.indexer.unwrap().trim_start())) + ); + assert_eq!( + result[5], + Line::from(format!( + "NZB Info URL: {}", + data.nzb_info_url.unwrap().trim_start() + )) + ); + assert_eq!( + result[6], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!( + result[7], + Line::from(format!("Age: {} days", data.age.unwrap().trim_start())) + ); + assert_eq!( + result[8], + Line::from(format!("Published Date: {}", data.published_date.unwrap())) + ); + assert_eq!( + result[9], + Line::from(format!( + "Download Client: {}", + data.download_client_name.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 10); + } + + #[test] + fn test_create_history_event_details_grabbed_uses_download_client_as_fallback() { + let mut history_item = lidarr_history_item(LidarrHistoryEventType::Grabbed); + history_item.data.download_client_name = None; + history_item.data.download_client = Some("\nFallback Client".to_owned()); + + let result = create_history_event_details(history_item); + + assert_eq!(result[9], Line::from("Download Client: Fallback Client")); + } + + #[test] + fn test_create_history_event_details_download_imported() { + let history_item = lidarr_history_item(LidarrHistoryEventType::DownloadImported); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 5); + } + + #[test] + fn test_create_history_event_details_download_failed() { + let history_item = lidarr_history_item(LidarrHistoryEventType::DownloadFailed); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!( + "Download Client: {}", + data.download_client_name.unwrap().trim_start() + )) + ); + assert_eq!( + result[5], + Line::from(format!("Message: {}", data.message.unwrap().trim_start())) + ); + assert_eq!( + result[6], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!( + result[7], + Line::from(format!("Indexer: {}", data.indexer.unwrap().trim_start())) + ); + assert_eq!(result.len(), 8); + } + + #[test] + fn test_create_history_event_details_track_file_deleted() { + let history_item = lidarr_history_item(LidarrHistoryEventType::TrackFileDeleted); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!("Reason: {}", data.reason.unwrap().trim_start())) + ); + assert_eq!( + result[5], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 6); + } + + #[test] + fn test_create_history_event_details_track_file_imported() { + let history_item = lidarr_history_item(LidarrHistoryEventType::TrackFileImported); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!( + "Dropped Path: {}", + data.dropped_path.unwrap().trim_start() + )) + ); + assert_eq!( + result[5], + Line::from(format!( + "Imported Path: {}", + data.imported_path.unwrap().trim_start() + )) + ); + assert_eq!( + result[6], + Line::from(format!( + "Download Client: {}", + data.download_client_name.unwrap().trim_start() + )) + ); + assert_eq!( + result[7], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 8); + } + + #[test] + fn test_create_history_event_details_track_file_renamed() { + let history_item = lidarr_history_item(LidarrHistoryEventType::TrackFileRenamed); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!( + "Source Path: {}", + data.source_path.unwrap().trim_start() + )) + ); + assert_eq!( + result[5], + Line::from(format!("Path: {}", data.path.unwrap().trim_start())) + ); + assert_eq!( + result[6], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 7); + } + + #[test] + fn test_create_history_event_details_track_file_retagged() { + let history_item = lidarr_history_item(LidarrHistoryEventType::TrackFileRetagged); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 5); + } + + #[test] + fn test_create_history_event_details_album_import_incomplete() { + let history_item = lidarr_history_item(LidarrHistoryEventType::AlbumImportIncomplete); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + data, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!( + result[4], + Line::from(format!( + "Status Messages: {}", + data.status_messages.unwrap().trim_start() + )) + ); + assert_eq!( + result[5], + Line::from(format!( + "Release Group: {}", + data.release_group.unwrap().trim_start() + )) + ); + assert_eq!(result.len(), 6); + } + + #[test] + fn test_create_history_event_details_unknown() { + let history_item = lidarr_history_item(LidarrHistoryEventType::Unknown); + let LidarrHistoryItem { + source_title, + event_type, + quality, + date, + .. + } = history_item.clone(); + + let result = create_history_event_details(history_item); + + assert_eq!( + result[0], + Line::from(format!("Source Title: {}", source_title.text.trim_start())) + ); + assert_eq!(result[1], Line::from(format!("Event Type: {event_type}"))); + assert_eq!( + result[2], + Line::from(format!("Quality: {}", quality.quality.name.trim_start())) + ); + assert_eq!(result[3], Line::from(format!("Date: {date}"))); + assert_eq!(result[4], Line::from("No additional details available.")); + assert_eq!(result.len(), 5); + } + + #[test] + fn test_create_history_event_details_with_empty_optional_fields() { + let mut history_item = lidarr_history_item(LidarrHistoryEventType::Grabbed); + history_item.data = LidarrHistoryData::default(); + + let result = create_history_event_details(history_item); + + assert_eq!(result[4], Line::from("Indexer: ")); + assert_eq!(result[5], Line::from("NZB Info URL: ")); + assert_eq!(result[6], Line::from("Release Group: ")); + assert_eq!(result[7], Line::from("Age: 0 days")); + assert!(result[8].to_string().starts_with("Published Date:")); + assert_eq!(result[9], Line::from("Download Client: ")); + } + + fn lidarr_history_item(event_type: LidarrHistoryEventType) -> LidarrHistoryItem { + LidarrHistoryItem { + id: 1, + source_title: "\nTest Album - Artist Name".into(), + album_id: 100, + artist_id: 10, + track_id: 1, + event_type, + quality: QualityWrapper { + quality: Quality { + name: "\nFLAC".to_owned(), + }, + }, + date: Utc::now(), + data: lidarr_history_data(), + } + } + + fn lidarr_history_data() -> LidarrHistoryData { + LidarrHistoryData { + indexer: Some("\nTest Indexer".to_owned()), + release_group: Some("\nTest Release Group".to_owned()), + nzb_info_url: Some("\nhttps://test.url".to_owned()), + download_client_name: Some("\nTest Download Client".to_owned()), + download_client: Some("\nFallback Download Client".to_owned()), + age: Some("\n7".to_owned()), + published_date: Some(Utc::now()), + message: Some("\nTest failure message".to_owned()), + reason: Some("\nTest deletion reason".to_owned()), + dropped_path: Some("\n/downloads/completed/album".to_owned()), + imported_path: Some("\n/music/artist/album".to_owned()), + source_path: Some("\n/music/artist/old_album_name".to_owned()), + path: Some("\n/music/artist/new_album_name".to_owned()), + status_messages: Some("\nMissing tracks: 1, 2, 3".to_owned()), + } + } +} diff --git a/src/ui/lidarr_ui/mod.rs b/src/ui/lidarr_ui/mod.rs new file mode 100644 index 0000000..a6aba0d --- /dev/null +++ b/src/ui/lidarr_ui/mod.rs @@ -0,0 +1,229 @@ +use std::{cmp, iter}; + +#[cfg(test)] +use crate::ui::ui_test_utils::test_utils::Utc; +use chrono::Duration; +#[cfg(not(test))] +use chrono::Utc; +use history::HistoryUi; +use library::LibraryUi; +use ratatui::{ + Frame, + layout::{Constraint, Layout, Rect}, + style::Stylize, + text::Text, + widgets::Paragraph, +}; + +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::lidarr_ui::blocklist::BlocklistUi; +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::{ + app::App, + logos::LIDARR_LOGO, + models::{ + Route, + lidarr_models::DownloadRecord, + servarr_data::lidarr::lidarr_data::LidarrData, + servarr_models::{DiskSpace, RootFolder}, + }, + utils::convert_to_gb, +}; + +mod blocklist; +mod downloads; +mod history; +mod indexers; +mod library; +mod lidarr_ui_utils; +mod root_folders; +mod system; + +#[cfg(test)] +#[path = "lidarr_ui_tests.rs"] +mod lidarr_ui_tests; + +pub(super) struct LidarrUi; + +impl DrawUi for LidarrUi { + fn accepts(route: Route) -> bool { + matches!(route, Route::Lidarr(_, _)) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let content_area = draw_tabs(f, area, "Artists", &app.data.lidarr_data.main_tabs); + let route = app.get_current_route(); + + match route { + _ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area), + _ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area), + _ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area), + _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), + _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), + _ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area), + _ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area), + _ => (), + } + } + + fn draw_context_row(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let [main_area, logo_area] = + Layout::horizontal([Constraint::Fill(0), Constraint::Length(20)]).areas(area); + + let [stats_area, downloads_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(main_area); + + draw_stats_context(f, app, stats_area); + draw_downloads_context(f, app, downloads_area); + draw_lidarr_logo(f, logo_area); + } +} + +fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = title_block("Stats"); + + if !app.data.lidarr_data.version.is_empty() { + f.render_widget(block, area); + let LidarrData { + root_folders, + disk_space_vec, + start_time, + .. + } = &app.data.lidarr_data; + + let mut constraints = vec![ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]; + + constraints.append( + &mut iter::repeat_n( + Constraint::Length(1), + disk_space_vec.len() + root_folders.items.len() + 1, + ) + .collect(), + ); + + let stat_item_areas = Layout::vertical(constraints).margin(1).split(area); + + let version_paragraph = Paragraph::new(Text::from(format!( + "Lidarr Version: {}", + app.data.lidarr_data.version + ))) + .block(borderless_block()) + .bold(); + + let uptime = Utc::now() - start_time.to_owned(); + let days = uptime.num_days(); + let day_difference = uptime - Duration::days(days); + let hours = day_difference.num_hours(); + let hour_difference = day_difference - Duration::hours(hours); + let minutes = hour_difference.num_minutes(); + let seconds = (hour_difference - Duration::minutes(minutes)).num_seconds(); + + let uptime_paragraph = Paragraph::new(Text::from(format!( + "Uptime: {days}d {hours:0width$}:{minutes:0width$}:{seconds:0width$}", + width = 2 + ))) + .block(borderless_block()) + .bold(); + + let storage = Paragraph::new(Text::from("Storage:")).block(borderless_block().bold()); + let folders = Paragraph::new(Text::from("Root Folders:")).block(borderless_block().bold()); + + f.render_widget(version_paragraph, stat_item_areas[0]); + 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() { + let DiskSpace { + free_space, + total_space, + } = &disk_space_vec[i]; + let title = format!("Disk {}", i + 1); + let ratio = if *total_space == 0 { + 0f64 + } else { + 1f64 - (*free_space as f64 / *total_space as f64) + }; + + let space_gauge = line_gauge_with_label(title.as_str(), ratio); + + f.render_widget(space_gauge, stat_item_areas[i + 3]); + } + + f.render_widget(folders, stat_item_areas[disk_space_vec.len() + 3]); + + for i in 0..root_folders.items.len() { + let RootFolder { + path, free_space, .. + } = &root_folders.items[i]; + let space: f64 = convert_to_gb(*free_space); + let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) + .block(borderless_block()) + .default_color(); + + f.render_widget( + root_folder_space, + stat_item_areas[i + disk_space_vec.len() + 4], + ) + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} + +fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = title_block("Downloads"); + let downloads_vec = &app.data.lidarr_data.downloads.items; + + if !downloads_vec.is_empty() { + f.render_widget(block, area); + + let max_items = ((((area.height as f64 / 2.0).floor() * 2.0) as i64) / 2) - 1; + let items = cmp::min(downloads_vec.len(), max_items.unsigned_abs() as usize); + let download_item_areas = + Layout::vertical(iter::repeat_n(Constraint::Length(2), items).collect::>()) + .margin(1) + .split(area); + + for i in 0..items { + let DownloadRecord { + title, + sizeleft, + size, + .. + } = &downloads_vec[i]; + let percent = if *size == 0.0 { + 0.0 + } else { + 1f64 - (*sizeleft / *size) + }; + let download_gauge = line_gauge_with_title(title, percent); + + f.render_widget(download_gauge, download_item_areas[i]); + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} + +fn draw_lidarr_logo(f: &mut Frame<'_>, area: Rect) { + let logo_text = Text::from(LIDARR_LOGO); + let logo = Paragraph::new(logo_text) + .light_green() + .block(layout_block().default_color()) + .centered(); + f.render_widget(logo, area); +} diff --git a/src/ui/lidarr_ui/root_folders/add_root_folder_ui.rs b/src/ui/lidarr_ui/root_folders/add_root_folder_ui.rs new file mode 100644 index 0000000..120f0f2 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/add_root_folder_ui.rs @@ -0,0 +1,245 @@ +use std::sync::atomic::Ordering; + +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::prelude::Layout; +use ratatui::widgets::ListItem; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock}; +use crate::models::servarr_data::lidarr::modals::AddRootFolderModal; +use crate::render_selectable_input_box; + +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "add_root_folder_ui_tests.rs"] +mod add_root_folder_ui_tests; + +pub(super) struct AddRootFolderUi; + +impl DrawUi for AddRootFolderUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let draw_add_root_folder_prompt = + |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| { + draw_add_root_folder_confirmation_prompt(f, app, prompt_area); + + match active_lidarr_block { + ActiveLidarrBlock::AddRootFolderSelectMonitor => { + draw_add_root_folder_select_monitor_popup(f, app); + } + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems => { + draw_add_root_folder_select_monitor_new_items_popup(f, app); + } + ActiveLidarrBlock::AddRootFolderSelectQualityProfile => { + draw_add_root_folder_select_quality_profile_popup(f, app); + } + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile => { + draw_add_root_folder_select_metadata_profile_popup(f, app); + } + _ => (), + } + }; + + draw_popup(f, app, draw_add_root_folder_prompt, Size::Long); + } + } +} + +fn draw_add_root_folder_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let title = "Add Root Folder"; + f.render_widget(title_block_centered(title), area); + + let yes_no_value = app.data.lidarr_data.prompt_confirm; + let selected_block = app.data.lidarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveLidarrBlock::AddRootFolderConfirmPrompt; + let AddRootFolderModal { + name, + path, + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + tags, + } = app + .data + .lidarr_data + .add_root_folder_modal + .as_ref() + .expect("add_root_folder_modal must exist in this context"); + let selected_monitor = monitor_list.current_selection(); + let selected_monitor_new_items = monitor_new_items_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_metadata_profile = metadata_profile_list.current_selection(); + + let [ + _, + name_area, + path_area, + monitor_area, + monitor_new_items_area, + quality_profile_area, + metadata_profile_area, + tags_area, + _, + buttons_area, + ] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let monitor_drop_down_button = Button::default() + .title(selected_monitor.to_display_str()) + .label("Monitor") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectMonitor); + let monitor_new_items_drop_down_button = Button::default() + .title(selected_monitor_new_items.to_display_str()) + .label("Monitor New Albums") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems); + let quality_profile_drop_down_button = Button::default() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectQualityProfile); + let metadata_profile_drop_down_button = Button::default() + .title(selected_metadata_profile) + .label("Metadata Profile") + .icon("▼") + .selected(selected_block == ActiveLidarrBlock::AddRootFolderSelectMetadataProfile); + + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + let name_input_box = InputBox::new(&name.text) + .offset(name.offset.load(Ordering::SeqCst)) + .label("Name") + .highlighted(selected_block == ActiveLidarrBlock::AddRootFolderNameInput) + .selected(active_lidarr_block == ActiveLidarrBlock::AddRootFolderNameInput); + let path_input_box = InputBox::new(&path.text) + .offset(path.offset.load(Ordering::SeqCst)) + .label("Path") + .highlighted(selected_block == ActiveLidarrBlock::AddRootFolderPathInput) + .selected(active_lidarr_block == ActiveLidarrBlock::AddRootFolderPathInput); + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveLidarrBlock::AddRootFolderTagsInput) + .selected(active_lidarr_block == ActiveLidarrBlock::AddRootFolderTagsInput); + + match active_lidarr_block { + ActiveLidarrBlock::AddRootFolderNameInput => name_input_box.show_cursor(f, name_area), + ActiveLidarrBlock::AddRootFolderPathInput => path_input_box.show_cursor(f, path_area), + ActiveLidarrBlock::AddRootFolderTagsInput => tags_input_box.show_cursor(f, tags_area), + _ => (), + } + + render_selectable_input_box!(name_input_box, f, name_area); + render_selectable_input_box!(path_input_box, f, path_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let save_button = Button::default() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::default() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(monitor_drop_down_button, monitor_area); + f.render_widget(monitor_new_items_drop_down_button, monitor_new_items_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(metadata_profile_drop_down_button, metadata_profile_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); +} + +fn draw_add_root_folder_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .expect("add_root_folder_modal must exist in this context") + .monitor_list, + |monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_root_folder_select_monitor_new_items_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_new_items_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .expect("add_root_folder_modal must exist in this context") + .monitor_new_items_list, + |monitor_type| ListItem::new(monitor_type.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_new_items_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_root_folder_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .expect("add_root_folder_modal must exist in this context") + .quality_profile_list, + |quality_profile| ListItem::new(quality_profile.clone()), + ); + let popup = Popup::new(quality_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_root_folder_select_metadata_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let metadata_profile_list = SelectableList::new( + &mut app + .data + .lidarr_data + .add_root_folder_modal + .as_mut() + .expect("add_root_folder_modal must exist in this context") + .metadata_profile_list, + |metadata_profile| ListItem::new(metadata_profile.clone()), + ); + let popup = Popup::new(metadata_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} diff --git a/src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs b/src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs new file mode 100644 index 0000000..14b3a29 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs @@ -0,0 +1,51 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ADD_ROOT_FOLDER_SELECTION_BLOCKS, ActiveLidarrBlock, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::root_folders::add_root_folder_ui::AddRootFolderUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_add_root_folder_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) { + assert!(AddRootFolderUi::accepts(active_lidarr_block.into())); + } else { + assert!(!AddRootFolderUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(ActiveLidarrBlock::AddRootFolderPrompt)] + #[case(ActiveLidarrBlock::AddRootFolderConfirmPrompt)] + #[case(ActiveLidarrBlock::AddRootFolderSelectMonitor)] + #[case(ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems)] + #[case(ActiveLidarrBlock::AddRootFolderSelectQualityProfile)] + #[case(ActiveLidarrBlock::AddRootFolderSelectMetadataProfile)] + fn test_add_root_folder_ui_renders(#[case] active_lidarr_block: ActiveLidarrBlock) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + AddRootFolderUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("add_root_folder_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/root_folders/mod.rs b/src/ui/lidarr_ui/root_folders/mod.rs new file mode 100644 index 0000000..ac2afa1 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/mod.rs @@ -0,0 +1,102 @@ +use add_root_folder_ui::AddRootFolderUi; +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS, +}; +use crate::models::servarr_models::RootFolder; +use crate::ui::DrawUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::utils::convert_to_gb; + +mod add_root_folder_ui; + +#[cfg(test)] +#[path = "root_folders_ui_tests.rs"] +mod root_folders_ui_tests; + +pub(super) struct RootFoldersUi; + +impl DrawUi for RootFoldersUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block) + || ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_root_folders(f, app, area); + + if ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) { + AddRootFolderUi::draw(f, app, area); + } else if active_lidarr_block == ActiveLidarrBlock::DeleteRootFolderPrompt { + let prompt = format!( + "Do you really want to delete this root folder: \n{}?", + app.data.lidarr_data.root_folders.current_selection().path + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Root Folder") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + } + } +} + +fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let root_folders_row_mapping = |root_folders: &RootFolder| { + let RootFolder { + path, + free_space, + unmapped_folders, + .. + } = root_folders; + + let space: f64 = convert_to_gb(*free_space); + + Row::new(vec![ + Cell::from(path.to_owned()), + Cell::from(format!("{space:.2} GB")), + Cell::from( + unmapped_folders + .as_ref() + .unwrap_or(&Vec::new()) + .len() + .to_string(), + ), + ]) + .primary() + }; + + let root_folders_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.root_folders), + root_folders_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers(["Path", "Free Space", "Unmapped Folders"]) + .constraints([ + Constraint::Ratio(3, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + ]); + + f.render_widget(root_folders_table, area); +} diff --git a/src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs b/src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs new file mode 100644 index 0000000..0122c56 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs @@ -0,0 +1,101 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::root_folders::RootFoldersUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_root_folders_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block) + || ADD_ROOT_FOLDER_BLOCKS.contains(&active_lidarr_block) + { + assert!(RootFoldersUi::accepts(active_lidarr_block.into())); + } else { + assert!(!RootFoldersUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use super::*; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::lidarr::lidarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + #[test] + fn test_root_folders_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_root_folders_ui_renders_empty_root_folders() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_root_folders_ui_renders_root_folders_tab( + #[values( + ActiveLidarrBlock::RootFolders, + ActiveLidarrBlock::DeleteRootFolderPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(active_lidarr_block.to_string(), output); + } + + #[rstest] + fn test_root_folders_ui_renders_add_root_folders_popup_over_root_folders_table( + #[values( + ActiveLidarrBlock::AddRootFolderPrompt, + ActiveLidarrBlock::AddRootFolderConfirmPrompt, + ActiveLidarrBlock::AddRootFolderSelectMonitor, + ActiveLidarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveLidarrBlock::AddRootFolderSelectQualityProfile, + ActiveLidarrBlock::AddRootFolderSelectMetadataProfile + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.data.lidarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(active_lidarr_block.to_string(), output); + } + } +} diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderConfirmPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderConfirmPrompt.snap new file mode 100644 index 0000000..5e77e26 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderConfirmPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor New Albums: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Quality Profile: │Lossless ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Metadata Profile: │Standard ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Tags: │test │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderPrompt.snap new file mode 100644 index 0000000..5e77e26 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor New Albums: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Quality Profile: │Lossless ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Metadata Profile: │Standard ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Tags: │test │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMetadataProfile.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMetadataProfile.snap new file mode 100644 index 0000000..6e742d5 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMetadataProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │Standard │──────────────────────────────╮ │ + │ │ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Monitor│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMonitor.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMonitor.snap new file mode 100644 index 0000000..5248c8f --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMonitor.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │All Albums │──────────────────────────────╮ │ + │ │Future Albums │ ▼ │ │ + │ │Missing Albums │──────────────────────────────╯ │ + │ │Existing Albums │──────────────────────────────╮ │ + │ Monitor│First Album │ ▼ │ │ + │ │Latest Album │──────────────────────────────╯ │ + │ │None │──────────────────────────────╮ │ + │ Qual│Unknown │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMonitorNewItems.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMonitorNewItems.snap new file mode 100644 index 0000000..81c9f69 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectMonitorNewItems.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │All Albums │──────────────────────────────╮ │ + │ │No New Albums │ ▼ │ │ + │ │New Albums │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Monitor│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectQualityProfile.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectQualityProfile.snap new file mode 100644 index 0000000..5e30a57 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__add_root_folder_ui__add_root_folder_ui_tests__tests__snapshot_tests__add_root_folder_AddRootFolderSelectQualityProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/add_root_folder_ui_tests.rs +expression: output +--- + + + + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │Lossless │──────────────────────────────╮ │ + │ │ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Monitor│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderConfirmPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderConfirmPrompt.snap new file mode 100644 index 0000000..ecf8a92 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderConfirmPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor New Albums: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Quality Profile: │Lossless ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Metadata Profile: │Standard ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Tags: │test │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderPrompt.snap new file mode 100644 index 0000000..ecf8a92 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Monitor New Albums: │All Albums ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Quality Profile: │Lossless ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Metadata Profile: │Standard ▼ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Tags: │test │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMetadataProfile.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMetadataProfile.snap new file mode 100644 index 0000000..988c072 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMetadataProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │Standard │──────────────────────────────╮ │ + │ │ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Monitor│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMonitor.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMonitor.snap new file mode 100644 index 0000000..6d4fcb6 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMonitor.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │All Albums │──────────────────────────────╮ │ + │ │Future Albums │ ▼ │ │ + │ │Missing Albums │──────────────────────────────╯ │ + │ │Existing Albums │──────────────────────────────╮ │ + │ Monitor│First Album │ ▼ │ │ + │ │Latest Album │──────────────────────────────╯ │ + │ │None │──────────────────────────────╮ │ + │ Qual│Unknown │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMonitorNewItems.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMonitorNewItems.snap new file mode 100644 index 0000000..61962f6 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectMonitorNewItems.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │All Albums │──────────────────────────────╮ │ + │ │No New Albums │ ▼ │ │ + │ │New Albums │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Monitor│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectQualityProfile.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectQualityProfile.snap new file mode 100644 index 0000000..45b3258 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderSelectQualityProfile.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + ╭─────────────────────────────────────────── Add Root Folder ───────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Name: │Test Root Folder │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ ╭─────────────────────────────────────────────────╮ │ + │ Path: │/nfs/music │ │ + │ ╭───────────────────────────────╮──────────────────────────────╯ │ + │ │Lossless │──────────────────────────────╮ │ + │ │ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Monitor│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Qual│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ Metad│ │ ▼ │ │ + │ │ │──────────────────────────────╯ │ + │ │ │──────────────────────────────╮ │ + │ ╰───────────────────────────────╯ │ │ + │ ╰─────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭───────────────────────────────────────────────────╮╭──────────────────────────────────────────────────╮│ + ││ Save ││ Cancel ││ + │╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯│ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__DeleteRootFolderPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__DeleteRootFolderPrompt.snap new file mode 100644 index 0000000..65b7259 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__DeleteRootFolderPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + + + + + + + + + + + ╭────────────────── Delete Root Folder ───────────────────╮ + │ Do you really want to delete this root folder: │ + │ /nfs? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__RootFolders.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__RootFolders.snap new file mode 100644 index 0000000..4e2666f --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__RootFolders.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_empty_root_folders.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_empty_root_folders.snap new file mode 100644 index 0000000..5d3f79d --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_empty_root_folders.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_loading.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_loading.snap new file mode 100644 index 0000000..fca4f13 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap new file mode 100644 index 0000000..9ca7db1 --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ 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 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Blocklist.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Blocklist.snap new file mode 100644 index 0000000..0dd3d49 --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Blocklist.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Artist Name ▼ Source Title Quality Date │ +│=> Alex Alex - Something Lossless 2023-05-20 21:29:16 UTC │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap new file mode 100644 index 0000000..297f4ac --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Title Percent Complete Size Output Path Indexer Download Client │ +│=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap new file mode 100644 index 0000000..032ebad --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Source Title ▼ Event Type Quality Date │ +│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap new file mode 100644 index 0000000..068286c --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Indexers.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Indexer RSS Automatic Search Interactive Search Priority Tags │ +│=> Test Indexer Enabled Enabled Enabled 25 alex │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap new file mode 100644 index 0000000..65268a6 --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Path Free Space Unmapped Folders │ +│=> /nfs 204800.00 GB 0 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_System.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_System.snap new file mode 100644 index 0000000..e18c7c9 --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_System.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│╭ Tasks ───────────────────────────────────────────────────────────────────────╮╭ Queued Events ──────────────────────────────────────────────────────────────╮│ +││Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration ││ +││Backup 1 hour now 59 minutes ││manual completed Refresh Monitored 4 minutes ago 4 minutes a 00:03:03 ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +││ ││ ││ +│╰────────────────────────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────────────────────╯│ +│╭ Logs ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ +││2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +││ ││ +│╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/mod.rs b/src/ui/lidarr_ui/system/mod.rs new file mode 100644 index 0000000..70e89f1 --- /dev/null +++ b/src/ui/lidarr_ui/system/mod.rs @@ -0,0 +1,204 @@ +use crate::ui::styles::default_style; +use std::ops::Sub; + +#[cfg(test)] +use crate::ui::ui_test_utils::test_utils::Utc; +#[cfg(not(test))] +use chrono::Utc; +use ratatui::layout::Layout; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Cell, Row}; +use ratatui::{ + Frame, + layout::{Constraint, Rect}, + widgets::ListItem, +}; + +use crate::app::App; +use crate::models::lidarr_models::LidarrTask; +use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; +use crate::models::servarr_models::QueueEvent; +use crate::ui::lidarr_ui::system::system_details_ui::SystemDetailsUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{convert_to_minutes_hours_days, style_log_list_item}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::{ + models::Route, + ui::{DrawUi, utils::title_block}, +}; + +mod system_details_ui; + +#[cfg(test)] +#[path = "system_ui_tests.rs"] +mod system_ui_tests; + +pub(super) const TASK_TABLE_HEADERS: [&str; 4] = + ["Name", "Interval", "Last Execution", "Next Execution"]; + +pub(super) const TASK_TABLE_CONSTRAINTS: [Constraint; 4] = [ + Constraint::Percentage(30), + Constraint::Percentage(23), + Constraint::Percentage(23), + Constraint::Percentage(23), +]; + +pub(super) struct SystemUi; + +impl DrawUi for SystemUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + SystemDetailsUi::accepts(route) || active_lidarr_block == ActiveLidarrBlock::System + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_system_ui_layout(f, app, area); + + if SystemDetailsUi::accepts(route) { + SystemDetailsUi::draw(f, app, area); + } + } +} + +fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let [activities_area, logs_area] = + Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(area); + + let [tasks_area, events_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_area); + + draw_tasks(f, app, tasks_area); + draw_queued_events(f, app, events_area); + draw_logs(f, app, logs_area); +} + +fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let tasks_row_mapping = |task: &LidarrTask| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.lidarr_data.tasks), tasks_row_mapping) + .block(title_block("Tasks")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(tasks_table, area); +} + +pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let events_row_mapping = |event: &QueueEvent| { + let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes()); + let queued_string = if queued != "now" { + format!("{queued} ago") + } else { + queued + }; + let started_string = if event.started.is_some() { + let started = + convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes()); + + if started != "now" { + format!("{started} ago") + } else { + started + } + } else { + String::new() + }; + + let duration = if event.duration.is_some() { + &event.duration.as_ref().unwrap()[..8] + } else { + "" + }; + + Row::new(vec![ + Cell::from(event.trigger.clone()), + Cell::from(event.status.clone()), + Cell::from(event.command_name.clone()), + Cell::from(queued_string), + Cell::from(started_string), + Cell::from(duration.to_owned()), + ]) + .primary() + }; + let events_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.queued_events), + events_row_mapping, + ) + .block(title_block("Queued Events")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) + .constraints([ + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(30), + Constraint::Percentage(16), + Constraint::Percentage(14), + Constraint::Percentage(14), + ]); + + f.render_widget(events_table, area); +} + +fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block("Logs"); + + if app.data.lidarr_data.logs.items.is_empty() { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + return; + } + + let logs_box = SelectableList::new(&mut app.data.lidarr_data.logs, |log| { + let log_line = log.to_string(); + let level = log_line.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(block) + .highlight_style(default_style()); + + f.render_widget(logs_box, area); +} + +pub(super) struct TaskProps { + pub(super) name: String, + pub(super) interval: String, + pub(super) last_execution: String, + pub(super) next_execution: String, +} + +pub(super) fn extract_task_props(task: &LidarrTask) -> TaskProps { + let interval = convert_to_minutes_hours_days(task.interval); + let next_execution = + convert_to_minutes_hours_days((task.next_execution - Utc::now()).num_minutes()); + let last_execution = + convert_to_minutes_hours_days((Utc::now() - task.last_execution).num_minutes()); + let last_execution_string = if last_execution != "now" { + format!("{last_execution} ago") + } else { + last_execution + }; + + TaskProps { + name: task.name.clone(), + interval, + last_execution: last_execution_string, + next_execution, + } +} diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemLogs.snap new file mode 100644 index 0000000..1781cc6 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemLogs.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemQueuedEvents.snap new file mode 100644 index 0000000..2b79fe3 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemQueuedEvents.snap @@ -0,0 +1,44 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + + + + + ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemTasks.snap new file mode 100644 index 0000000..4d3c667 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__empty_SystemTasks.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemLogs.snap new file mode 100644 index 0000000..a465625 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemLogs.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemQueuedEvents.snap new file mode 100644 index 0000000..d3f6c60 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemQueuedEvents.snap @@ -0,0 +1,44 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + + + + + ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemTasks.snap new file mode 100644 index 0000000..5f810a9 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemTasks.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemUpdates.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemUpdates.snap new file mode 100644 index 0000000..e51c393 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__loading_SystemUpdates.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemLogs.snap new file mode 100644 index 0000000..c98c968 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemLogs.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemQueuedEvents.snap new file mode 100644 index 0000000..d469187 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemQueuedEvents.snap @@ -0,0 +1,44 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + + + + + ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ + │Trigger Status Name Queued Started Duration │ + │manual completed Refresh Monitored Downlo 4 minutes ago 4 minutes ago 00:03:03 │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTaskStartConfirmPrompt.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTaskStartConfirmPrompt.snap new file mode 100644 index 0000000..bfb2655 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTaskStartConfirmPrompt.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Name Interval Last Execution Next Execution │ + │=> Backup 1 hour now 59 minutes │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ╭────────────────────── Start Task ───────────────────────╮ │ + │ │ Do you want to manually start this task: Backup? │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │╭────────────────────────────╮╭───────────────────────────╮│ │ + │ ││ Yes ││ No ││ │ + │ │╰────────────────────────────╯╰───────────────────────────╯│ │ + │ ╰───────────────────────────────────────────────────────────╯ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTasks.snap new file mode 100644 index 0000000..231d62e --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemTasks.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ Name Interval Last Execution Next Execution │ + │=> Backup 1 hour now 59 minutes │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemUpdates.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemUpdates.snap new file mode 100644 index 0000000..c5585a1 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__popup_SystemUpdates.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │The latest version of Sonarr is already installed │ + │ │ + │4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) │ + │--------------------------------------------------------------------------------------------------------------------------│ + │New: │ + │ * Cool new thing │ + │Fixed: │ + │ * Some bugs killed │ + │ │ + │ │ + │3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) │ + │--------------------------------------------------------------------------------------------------------------------------│ + │New: │ + │ * Cool new thing (old) │ + │ * Other cool new thing (old) │ + │ │ + │ │ + │2.1.0 - 2023-04-15 02:02:53 UTC │ + │--------------------------------------------------------------------------------------------------------------------------│ + │Fixed: │ + │ * Killed bug 1 │ + │ * Fixed bug 2 │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__system_details_ui_updates_popup_loading_when_empty.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__system_details_ui_updates_popup_loading_when_empty.snap new file mode 100644 index 0000000..e51c393 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_details_ui__system_details_ui_tests__tests__snapshot_tests__system_details_ui_updates_popup_loading_when_empty.snap @@ -0,0 +1,48 @@ +--- +source: src/ui/lidarr_ui/system/system_details_ui_tests.rs +expression: output +--- + + + + + + + ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ + │ │ + │ Loading ... │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemLogs.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemLogs.snap new file mode 100644 index 0000000..dcadb23 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemLogs.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Log Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +╰───────────────────│ │────────────────────╯ +╭ Logs ───────────│ │────────────────────╮ +│2025-12-16 16:40:59│ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemQueuedEvents.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemQueuedEvents.snap new file mode 100644 index 0000000..4e39a5a --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemQueuedEvents.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Queued Events ────────────────────────────────────────────────────────────────────────────────╮ │ +│ │Trigger Status Name Queued Started Duration │ │ +│ │manual completed Refresh Monitored Downlo 4 minutes ago 4 minutes ago 00:03:03 │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +╰────────────────────────────────│ │────────────────────────────────╯ +╭ Logs ────────────────────────│ │────────────────────────────────╮ +│2025-12-16 16:40:59 UTC|INFO|Imp│ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTaskStartConfirmPrompt.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTaskStartConfirmPrompt.snap new file mode 100644 index 0000000..c0c6907 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTaskStartConfirmPrompt.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Name Interval Last Execution Next Execution │ │ +│ │=> Backup 1 hour now 59 minutes │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ ╭────────────────────── Start Task ───────────────────────╮ │ │ +│ │ │ Do you want to manually start this task: Backup? │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +╰───────────────────│ │ │ │────────────────────╯ +╭ Logs ───────────│ │ │ │────────────────────╮ +│2025-12-16 16:40:59│ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ │╭────────────────────────────╮╭───────────────────────────╮│ │ │ +│ │ ││ Yes ││ No ││ │ │ +│ │ │╰────────────────────────────╯╰───────────────────────────╯│ │ │ +│ │ ╰───────────────────────────────────────────────────────────╯ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTasks.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTasks.snap new file mode 100644 index 0000000..db69cb7 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemTasks.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Tasks ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Name Interval Last Execution Next Execution │ │ +│ │=> Backup 1 hour now 59 minutes │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +╰───────────────────│ │────────────────────╯ +╭ Logs ───────────│ │────────────────────╮ +│2025-12-16 16:40:59│ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemUpdates.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemUpdates.snap new file mode 100644 index 0000000..935bdcb --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__popups_over_system_ui_SystemUpdates.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ╭ Updates ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │The latest version of Sonarr is already installed │ │ +│ │ │ │ +│ │4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed) │ │ +│ │--------------------------------------------------------------------------------------------------------------------------│ │ +│ │New: │ │ +│ │ * Cool new thing │ │ +│ │Fixed: │ │ +│ │ * Some bugs killed │ │ +│ │ │ │ +│ │ │ │ +│ │3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed) │ │ +│ │--------------------------------------------------------------------------------------------------------------------------│ │ +│ │New: │ │ +│ │ * Cool new thing (old) │ │ +│ │ * Other cool new thing (old) │ │ +│ │ │ │ +│ │ │ │ +╰───────────────────│2.1.0 - 2023-04-15 02:02:53 UTC │────────────────────╯ +╭ Logs ───────────│--------------------------------------------------------------------------------------------------------------------------│────────────────────╮ +│2025-12-16 16:40:59│Fixed: │ │ +│ │ * Killed bug 1 │ │ +│ │ * Fixed bug 2 │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_logs_loading.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_logs_loading.snap new file mode 100644 index 0000000..b6c7090 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_logs_loading.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ Loading ... ││ Loading ... │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ │ +│ Loading ... │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab.snap new file mode 100644 index 0000000..b7f4872 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│Name Interval Last Execution Next Execution ││Trigger Status Name Queued Started Duration │ +│Backup 1 hour now 59 minutes ││manual completed Refresh Monitored D 4 minutes ago 4 minutes a 00:03:03 │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_empty.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_empty.snap new file mode 100644 index 0000000..1bf7297 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_empty.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_loading.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_loading.snap new file mode 100644 index 0000000..b6c7090 --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_loading.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ Loading ... ││ Loading ... │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ │ +│ Loading ... │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_task_and_events_loading.snap b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_task_and_events_loading.snap new file mode 100644 index 0000000..19eb0cd --- /dev/null +++ b/src/ui/lidarr_ui/system/snapshots/managarr__ui__lidarr_ui__system__system_ui_tests__tests__snapshot_tests__system_ui_renders_system_tab_task_and_events_loading.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/system/system_ui_tests.rs +expression: output +--- +╭ Tasks ────────────────────────────────────────────────────────────────────────╮╭ Queued Events ───────────────────────────────────────────────────────────────╮ +│ ││ │ +│ ││ │ +│ Loading ... ││ Loading ... │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +╰─────────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ +╭ Logs ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│2025-12-16 16:40:59 UTC|INFO|ImportListSyncService|No list items to process │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/system/system_details_ui.rs b/src/ui/lidarr_ui/system/system_details_ui.rs new file mode 100644 index 0000000..0af1350 --- /dev/null +++ b/src/ui/lidarr_ui/system/system_details_ui.rs @@ -0,0 +1,144 @@ +use ratatui::Frame; +use ratatui::layout::Rect; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; + +use crate::app::App; +use crate::models::Route; +use crate::models::lidarr_models::LidarrTask; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::ui::lidarr_ui::system::{ + TASK_TABLE_CONSTRAINTS, TASK_TABLE_HEADERS, draw_queued_events, extract_task_props, +}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, style_log_list_item, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{DrawUi, draw_popup}; + +#[cfg(test)] +#[path = "system_details_ui_tests.rs"] +mod system_details_ui_tests; + +pub(super) struct SystemDetailsUi; + +impl DrawUi for SystemDetailsUi { + fn accepts(route: Route) -> bool { + let Route::Lidarr(active_lidarr_block, _) = route else { + return false; + }; + SYSTEM_DETAILS_BLOCKS.contains(&active_lidarr_block) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + match active_lidarr_block { + ActiveLidarrBlock::SystemLogs => { + draw_logs_popup(f, app); + } + ActiveLidarrBlock::SystemTasks | ActiveLidarrBlock::SystemTaskStartConfirmPrompt => { + draw_popup(f, app, draw_tasks_popup, Size::Large) + } + ActiveLidarrBlock::SystemQueuedEvents => { + draw_popup(f, app, draw_queued_events, Size::Medium) + } + ActiveLidarrBlock::SystemUpdates => { + draw_updates_popup(f, app); + } + _ => (), + } + } + } +} + +fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let block = title_block("Log Details"); + + if app.data.lidarr_data.log_details.items.is_empty() { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading).size(Size::Large).block(block).margin(1); + + f.render_widget(popup, f.area()); + return; + } + + let logs_list = SelectableList::new(&mut app.data.lidarr_data.log_details, |log| { + let log_line = log.to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(borderless_block()); + let popup = Popup::new(logs_list) + .size(Size::Large) + .block(block) + .margin(1); + + f.render_widget(popup, f.area()); +} + +fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let tasks_row_mapping = |task: &LidarrTask| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.lidarr_data.tasks), tasks_row_mapping) + .loading(app.is_loading) + .margin(1) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(title_block("Tasks"), area); + f.render_widget(tasks_table, area); + + if matches!( + app.get_current_route(), + Route::Lidarr(ActiveLidarrBlock::SystemTaskStartConfirmPrompt, _) + ) { + let prompt = format!( + "Do you want to manually start this task: {}?", + app.data.lidarr_data.tasks.current_selection().name + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Start Task") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} + +fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let updates = app.data.lidarr_data.updates.get_text(); + let block = title_block("Updates"); + + if !updates.is_empty() && !app.is_loading { + let updates_paragraph = Paragraph::new(Text::from(updates)) + .block(borderless_block()) + .scroll((app.data.lidarr_data.updates.offset, 0)); + let popup = Popup::new(updates_paragraph) + .size(Size::Large) + .block(block) + .margin(1); + + f.render_widget(popup, f.area()); + } else { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading).size(Size::Large).block(block).margin(1); + + f.render_widget(popup, f.area()); + } +} diff --git a/src/ui/lidarr_ui/system/system_details_ui_tests.rs b/src/ui/lidarr_ui/system/system_details_ui_tests.rs new file mode 100644 index 0000000..a3fafc1 --- /dev/null +++ b/src/ui/lidarr_ui/system/system_details_ui_tests.rs @@ -0,0 +1,106 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::system::system_details_ui::SystemDetailsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_system_details_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if SYSTEM_DETAILS_BLOCKS.contains(&active_lidarr_block) { + assert!(SystemDetailsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!SystemDetailsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use super::*; + use crate::models::ScrollableText; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + #[rstest] + fn test_system_details_ui_popups( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("popup_{active_lidarr_block}"), output); + } + + #[rstest] + fn test_system_details_ui_loading( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("loading_{active_lidarr_block}"), output); + } + + #[test] + fn test_system_details_ui_updates_popup_loading_when_empty() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::SystemUpdates.into()); + app.data.lidarr_data.updates = ScrollableText::with_string("".to_string()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_system_details_ui_popups_empty( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemDetailsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("empty_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/system/system_ui_tests.rs b/src/ui/lidarr_ui/system/system_ui_tests.rs new file mode 100644 index 0000000..58d9fcf --- /dev/null +++ b/src/ui/lidarr_ui/system/system_ui_tests.rs @@ -0,0 +1,123 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ + ActiveLidarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::system::SystemUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_system_ui_accepts() { + let mut system_ui_blocks = Vec::new(); + system_ui_blocks.push(ActiveLidarrBlock::System); + system_ui_blocks.extend(SYSTEM_DETAILS_BLOCKS); + + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if system_ui_blocks.contains(&active_lidarr_block) { + assert!(SystemUi::accepts(active_lidarr_block.into())); + } else { + assert!(!SystemUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::models::stateful_list::StatefulList; + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_system_ui_renders_system_tab_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_logs_loading() { + let mut app = App::test_default_fully_populated(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.data.lidarr_data.logs = StatefulList::default(); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_system_tab_task_and_events_loading() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + app.is_loading = true; + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_system_tab() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_system_ui_renders_system_tab_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::System.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_system_details_ui_renders_popups_over_system_ui( + #[values( + ActiveLidarrBlock::SystemLogs, + ActiveLidarrBlock::SystemQueuedEvents, + ActiveLidarrBlock::SystemTasks, + ActiveLidarrBlock::SystemTaskStartConfirmPrompt, + ActiveLidarrBlock::SystemUpdates + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + SystemUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!( + format!("popups_over_system_ui_{active_lidarr_block}"), + output + ); + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d974eee..9842a5f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,10 @@ use std::cell::Cell; use std::sync::atomic::Ordering; +use lidarr_ui::LidarrUi; use ratatui::Frame; use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; +use ratatui::style::Stylize; use ratatui::text::{Line, Text}; use ratatui::widgets::Paragraph; use ratatui::widgets::Tabs; @@ -16,7 +17,7 @@ use crate::app::App; use crate::models::servarr_models::KeybindingItem; use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::ui::radarr_ui::RadarrUi; -use crate::ui::styles::ManagarrStyle; +use crate::ui::styles::{ManagarrStyle, secondary_style}; use crate::ui::theme::Theme; use crate::ui::utils::{ background_block, borderless_block, centered_rect, logo_block, title_block, title_block_centered, @@ -27,6 +28,7 @@ use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::Size; mod builtin_themes; +mod lidarr_ui; mod radarr_ui; mod sonarr_ui; mod styles; @@ -86,6 +88,10 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { SonarrUi::draw_context_row(f, app, context_area); SonarrUi::draw(f, app, table_area); } + route if LidarrUi::accepts(route) => { + LidarrUi::draw_context_row(f, app, context_area); + LidarrUi::draw(f, app, table_area); + } _ => (), } @@ -110,7 +116,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .map(|tab| Line::from(tab.title.clone().bold())); let tabs = Tabs::new(titles) .block(borderless_block()) - .highlight_style(Style::new().secondary()) + .highlight_style(secondary_style()) .select(app.server_tabs.index); let help = Paragraph::new(help_text) .block(borderless_block()) @@ -179,7 +185,7 @@ pub fn draw_help_popup(f: &mut Frame<'_>, app: &mut App<'_>) { fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect { if title.is_empty() { - f.render_widget(layout_block().default(), area); + f.render_widget(layout_block().default_color(), area); } else { f.render_widget(title_block(title), area); } @@ -194,7 +200,7 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) - .map(|tab_route| Line::from(tab_route.title.clone().bold())); let tabs = Tabs::new(titles) .block(borderless_block()) - .highlight_style(Style::new().secondary()) + .highlight_style(secondary_style()) .select(tab_state.index); f.render_widget(tabs, header_area); diff --git a/src/ui/radarr_ui/blocklist/mod.rs b/src/ui/radarr_ui/blocklist/mod.rs index 4301221..3fb243b 100644 --- a/src/ui/radarr_ui/blocklist/mod.rs +++ b/src/ui/radarr_ui/blocklist/mod.rs @@ -3,7 +3,7 @@ use crate::models::Route; use crate::models::radarr_models::BlocklistItem; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::ui::DrawUi; -use crate::ui::styles::ManagarrStyle; +use crate::ui::styles::{ManagarrStyle, secondary_style}; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; @@ -11,7 +11,7 @@ use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::style::{Style, Stylize}; +use ratatui::style::Stylize; use ratatui::text::{Line, Text}; use ratatui::widgets::{Cell, Row}; @@ -186,7 +186,7 @@ fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let message = Message::new(text) .title("Details") - .style(Style::new().secondary()) + .style(secondary_style()) .alignment(Alignment::Left); f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 9567523..3daeaa4 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -150,7 +150,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .overview .clone() .unwrap_or_default() - .default(), + .default_color(), ]), Line::from(vec![ "Root Folder Path: ".primary().bold(), @@ -158,20 +158,23 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .root_folder_path .clone() .unwrap_or_default() - .default(), + .default_color(), ]), Line::from(vec![ "Quality Profile: ".primary().bold(), - quality_profile.default(), + quality_profile.default_color(), ]), Line::from(vec![ "Minimum Availability: ".primary().bold(), - minimum_availability.default(), + minimum_availability.default_color(), + ]), + Line::from(vec![ + "Monitored: ".primary().bold(), + monitored.default_color(), ]), - Line::from(vec!["Monitored: ".primary().bold(), monitored.default()]), Line::from(vec![ "Search on Add: ".primary().bold(), - search_on_add.default(), + search_on_add.default_color(), ]), ]); @@ -225,7 +228,7 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .clone() .overview, ) - .default(); + .default_color(); let paragraph = Paragraph::new(overview) .block(borderless_block()) diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index 38ef03c..f6702fa 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -11,7 +11,6 @@ use crate::models::servarr_data::radarr::radarr_data::{ }; use crate::render_selectable_input_box; use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -129,12 +128,12 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> let monitored_checkbox = Checkbox::new("Monitored") .highlighted(selected_block == ActiveRadarrBlock::EditCollectionToggleMonitored) .checked(monitored.unwrap_or_default()); - let min_availability_drop_down_button = Button::new() + let min_availability_drop_down_button = Button::default() .title(selected_minimum_availability.to_display_str()) .label("Minimum Availability") .icon("▼") .selected(selected_block == ActiveRadarrBlock::EditCollectionSelectMinimumAvailability); - let quality_profile_drop_down_button = Button::new() + let quality_profile_drop_down_button = Button::default() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") @@ -152,10 +151,10 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> let search_on_add_checkbox = Checkbox::new("Search on Add") .highlighted(selected_block == ActiveRadarrBlock::EditCollectionToggleSearchOnAdd) .checked(search_on_add.unwrap_or_default()); - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/radarr_ui/history/history_ui_tests.rs b/src/ui/radarr_ui/history/history_ui_tests.rs new file mode 100644 index 0000000..99b7196 --- /dev/null +++ b/src/ui/radarr_ui/history/history_ui_tests.rs @@ -0,0 +1,79 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::radarr_ui::history::HistoryUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_history_ui_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if HISTORY_BLOCKS.contains(&active_radarr_block) { + assert!(HistoryUi::accepts(active_radarr_block.into())); + } else { + assert!(!HistoryUi::accepts(active_radarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_history_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveRadarrBlock::History.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_history_ui_renders_empty( + #[values(ActiveRadarrBlock::History, ActiveRadarrBlock::HistoryItemDetails)] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_radarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("loading_history_tab_{active_radarr_block}"), output); + } + + #[rstest] + fn test_history_ui_renders( + #[values( + ActiveRadarrBlock::History, + ActiveRadarrBlock::HistoryItemDetails, + ActiveRadarrBlock::HistorySortPrompt, + ActiveRadarrBlock::FilterHistory, + ActiveRadarrBlock::FilterHistoryError, + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::SearchHistoryError + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_radarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + HistoryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("history_tab_{active_radarr_block}"), output); + } + } +} diff --git a/src/ui/radarr_ui/history/mod.rs b/src/ui/radarr_ui/history/mod.rs new file mode 100644 index 0000000..797ee58 --- /dev/null +++ b/src/ui/radarr_ui/history/mod.rs @@ -0,0 +1,129 @@ +use crate::app::App; +use crate::models::Route; +use crate::models::radarr_models::RadarrHistoryItem; +use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, HISTORY_BLOCKS}; +use crate::ui::DrawUi; +use crate::ui::styles::{ManagarrStyle, secondary_style}; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use ratatui::Frame; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; + +use super::radarr_ui_utils::create_history_event_details; + +#[cfg(test)] +#[path = "history_ui_tests.rs"] +mod history_ui_tests; + +pub(super) struct HistoryUi; + +impl DrawUi for HistoryUi { + fn accepts(route: Route) -> bool { + if let Route::Radarr(active_radarr_block, _) = route { + return HISTORY_BLOCKS.contains(&active_radarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + draw_history_table(f, app, area); + + if active_radarr_block == ActiveRadarrBlock::HistoryItemDetails { + draw_history_item_details_popup(f, app); + } + } + } +} + +fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.radarr_data.history.items.is_empty() { + RadarrHistoryItem::default() + } else { + app.data.radarr_data.history.current_selection().clone() + }; + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &RadarrHistoryItem| { + let RadarrHistoryItem { + source_title, + languages, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.ui_scroll_tick_count == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let history_table = + ManagarrTable::new(Some(&mut app.data.radarr_data.history), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .sorting(active_radarr_block == ActiveRadarrBlock::HistorySortPrompt) + .searching(active_radarr_block == ActiveRadarrBlock::SearchHistory) + .search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchHistoryError) + .filtering(active_radarr_block == ActiveRadarrBlock::FilterHistory) + .filter_produced_empty_results(active_radarr_block == ActiveRadarrBlock::FilterHistoryError) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); + + if [ + ActiveRadarrBlock::SearchHistory, + ActiveRadarrBlock::FilterHistory, + ] + .contains(&active_radarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.radarr_data.history.items.is_empty() { + RadarrHistoryItem::default() + } else { + app.data.radarr_data.history.current_selection().clone() + }; + + let line_vec = create_history_event_details(current_selection); + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(secondary_style()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); +} diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap new file mode 100644 index 0000000..08eb332 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistory.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + ╭───────────────── Filter ──────────────────╮ + │Something │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap new file mode 100644 index 0000000..b2bc3c7 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_FilterHistoryError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │The given filter produced empty results│ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap new file mode 100644 index 0000000..a50a68b --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_History.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap new file mode 100644 index 0000000..020a121 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Source Title: Test │ + │Event Type: grabbed │ + │Indexer: DrunkenSlug (Prowlarr) │ + │Release Group: SPARKS │ + │NZB Info URL: │ + │Download Client: transmission │ + │Age: 0 days │ + │Published Date: 1970-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap new file mode 100644 index 0000000..2839cc9 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistorySortPrompt.snap @@ -0,0 +1,42 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + ╭───────────────────────────────╮ + │Something │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap new file mode 100644 index 0000000..9097f94 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistory.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + ╭───────────────── Search ──────────────────╮ + │Something │ + ╰─────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap new file mode 100644 index 0000000..ef6ccda --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_SearchHistoryError.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Source Title ▼ Event Type Language Quality Date +=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC + + + + + + + + + + + + + + + + + + + + + ╭─────────────── Error ───────────────╮ + │ No items found matching search │ + │ │ + ╰───────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap new file mode 100644 index 0000000..2b84f5c --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__history_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap new file mode 100644 index 0000000..c9acf0e --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_History.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap new file mode 100644 index 0000000..510fcf8 --- /dev/null +++ b/src/ui/radarr_ui/history/snapshots/managarr__ui__radarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/radarr_ui/history/history_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + ╭─────────────────────────────────── Details ───────────────────────────────────╮ + │Source Title: │ + │Event Type: unknown │ + │ │ + │No additional data available │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index cd7f52a..1a98be2 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -4,7 +4,6 @@ use crate::app::App; use crate::models::Route; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::render_selectable_input_box; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -152,10 +151,10 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .flex(Flex::Center) .areas(buttons_area); - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index 677f17f..842b440 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -9,7 +9,6 @@ use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, }; use crate::render_selectable_input_box; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -154,10 +153,10 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: .flex(Flex::Center) .areas(buttons_area); - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index bc63c90..3f9cee1 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -1,6 +1,6 @@ +use crate::ui::styles::success_style; use ratatui::Frame; use ratatui::layout::{Constraint, Rect}; -use ratatui::style::{Style, Stylize}; use ratatui::text::Text; use ratatui::widgets::{Cell, Row}; @@ -73,7 +73,7 @@ impl DrawUi for IndexersUi { } else { let message = Message::new("Indexer test succeeded!") .title("Success") - .style(Style::new().success().bold()); + .style(success_style().bold()); Popup::new(message).size(Size::Message) } }; diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index 734fc15..3fc509d 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -34,12 +34,14 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are let is_loading = app.is_loading || app.data.radarr_data.indexer_test_all_results.is_none(); let block = title_block("Test All Indexers"); - let current_selection = - if let Some(test_all_results) = app.data.radarr_data.indexer_test_all_results.as_ref() { - test_all_results.current_selection().clone() - } else { - IndexerTestResultModalItem::default() - }; + let current_selection = if let Some(test_all_results) = + app.data.radarr_data.indexer_test_all_results.as_ref() + && !test_all_results.is_empty() + { + test_all_results.current_selection().clone() + } else { + IndexerTestResultModalItem::default() + }; f.render_widget(block, area); let test_results_row_mapping = |result: &IndexerTestResultModalItem| { result.validation_failures.scroll_left_or_reset( diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index f3ad937..f3afa39 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -69,12 +69,14 @@ impl DrawUi for AddMovieUi { fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let is_loading = app.is_loading || app.data.radarr_data.add_searched_movies.is_none(); - let current_selection = - if let Some(add_searched_movies) = app.data.radarr_data.add_searched_movies.as_ref() { - add_searched_movies.current_selection().clone() - } else { - AddMovieSearchResult::default() - }; + let current_selection = if let Some(add_searched_movies) = + app.data.radarr_data.add_searched_movies.as_ref() + && !add_searched_movies.is_empty() + { + add_searched_movies.current_selection().clone() + } else { + AddMovieSearchResult::default() + }; let [search_box_area, results_area] = Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) @@ -162,14 +164,14 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .block(title_block_centered("Add Movie")); search_box.show_cursor(f, search_box_area); - f.render_widget(layout_block().default(), results_area); + f.render_widget(layout_block().default_color(), results_area); f.render_widget(search_box, search_box_area); } ActiveRadarrBlock::AddMovieEmptySearchResults => { let error_message = Message::new("No movies found matching your query!"); let error_message_popup = Popup::new(error_message).size(Size::Message); - f.render_widget(layout_block().default(), results_area); + f.render_widget(layout_block().default_color(), results_area); f.render_widget(error_message_popup, f.area()); } ActiveRadarrBlock::AddMovieSearchResults @@ -185,7 +187,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { search_results_row_mapping, ) .loading(is_loading) - .block(layout_block().default()) + .block(layout_block().default_color()) .headers([ "✔", "Title", @@ -336,22 +338,22 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - let root_folder_drop_down_button = Button::new() + let root_folder_drop_down_button = Button::default() .title(&selected_root_folder.path) .label("Root Folder") .icon("▼") .selected(selected_block == ActiveRadarrBlock::AddMovieSelectRootFolder); - let monitor_drop_down_button = Button::new() + let monitor_drop_down_button = Button::default() .title(selected_monitor.to_display_str()) .label("Monitor") .icon("▼") .selected(selected_block == ActiveRadarrBlock::AddMovieSelectMonitor); - let min_availability_drop_down_button = Button::new() + let min_availability_drop_down_button = Button::default() .title(selected_minimum_availability.to_display_str()) .label("Minimum Availability") .icon("▼") .selected(selected_block == ActiveRadarrBlock::AddMovieSelectMinimumAvailability); - let quality_profile_drop_down_button = Button::new() + let quality_profile_drop_down_button = Button::default() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") @@ -371,10 +373,10 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { render_selectable_input_box!(tags_input_box, f, tags_area); } - let add_button = Button::new() + let add_button = Button::default() .title("Add") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index f944e81..e759a8a 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -14,7 +14,6 @@ use crate::models::servarr_data::radarr::radarr_data::{ use crate::render_selectable_input_box; use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -125,12 +124,12 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are let monitored_checkbox = Checkbox::new("Monitored") .checked(monitored.unwrap_or_default()) .highlighted(selected_block == ActiveRadarrBlock::EditMovieToggleMonitored); - let min_availability_drop_down_button = Button::new() + let min_availability_drop_down_button = Button::default() .title(selected_minimum_availability.to_display_str()) .label("Minimum Availability") .icon("▼") .selected(selected_block == ActiveRadarrBlock::EditMovieSelectMinimumAvailability); - let quality_profile_drop_down_button = Button::new() + let quality_profile_drop_down_button = Button::default() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") @@ -158,10 +157,10 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are render_selectable_input_box!(tags_input_box, f, tags_area); } - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index 7823d2b..e36ecad 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -1,3 +1,7 @@ +use crate::ui::styles::{ + awaiting_import_style, downloaded_style, downloading_style, missing_style, + unmonitored_missing_style, unreleased_style, +}; use std::iter; use ratatui::Frame; @@ -529,12 +533,12 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { fn style_from_download_status(download_status: &str, is_monitored: bool, status: String) -> Style { match download_status { - "Downloaded" => Style::new().downloaded(), - "Awaiting Import" => Style::new().awaiting_import(), - "Downloading" => Style::new().downloading(), - _ if !is_monitored && download_status == "Missing" => Style::new().unmonitored_missing(), - _ if status != "released" && download_status == "Missing" => Style::new().unreleased(), - "Missing" => Style::new().missing(), - _ => Style::new().downloaded(), + "Downloaded" => downloaded_style(), + "Awaiting Import" => awaiting_import_style(), + "Downloading" => downloading_style(), + _ if !is_monitored && download_status == "Missing" => unmonitored_missing_style(), + _ if status != "released" && download_status == "Missing" => unreleased_style(), + "Missing" => missing_style(), + _ => downloaded_style(), } } diff --git a/src/ui/radarr_ui/library/movie_details_ui_tests.rs b/src/ui/radarr_ui/library/movie_details_ui_tests.rs index 46e8417..551297d 100644 --- a/src/ui/radarr_ui/library/movie_details_ui_tests.rs +++ b/src/ui/radarr_ui/library/movie_details_ui_tests.rs @@ -11,7 +11,10 @@ mod tests { use crate::ui::radarr_ui::library::movie_details_ui::{ MovieDetailsUi, style_from_download_status, }; - use crate::ui::styles::ManagarrStyle; + use crate::ui::styles::{ + awaiting_import_style, downloaded_style, downloading_style, missing_style, + unmonitored_missing_style, + }; use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; #[test] @@ -26,13 +29,13 @@ mod tests { } #[rstest] - #[case("Downloading", true, "", Style::new().downloading())] - #[case("Downloaded", true, "", Style::new().downloaded())] - #[case("Awaiting Import", true, "", Style::new().awaiting_import())] - #[case("Missing", false, "", Style::new().unmonitored_missing())] - #[case("Missing", false, "", Style::new().unmonitored_missing())] - #[case("Missing", true, "released", Style::new().missing())] - #[case("", true, "", Style::new().downloaded())] + #[case("Downloading", true, "", downloading_style())] + #[case("Downloaded", true, "", downloaded_style())] + #[case("Awaiting Import", true, "", awaiting_import_style())] + #[case("Missing", false, "", unmonitored_missing_style())] + #[case("Missing", false, "", unmonitored_missing_style())] + #[case("Missing", true, "released", missing_style())] + #[case("", true, "", downloaded_style())] fn test_style_from_download_status( #[case] download_status: &str, #[case] is_monitored: bool, diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 991aa6e..882d85a 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -21,6 +21,7 @@ use crate::ui::draw_tabs; use crate::ui::radarr_ui::blocklist::BlocklistUi; use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::radarr_ui::downloads::DownloadsUi; +use crate::ui::radarr_ui::history::HistoryUi; use crate::ui::radarr_ui::indexers::IndexersUi; use crate::ui::radarr_ui::library::LibraryUi; use crate::ui::radarr_ui::root_folders::RootFoldersUi; @@ -35,10 +36,12 @@ use crate::utils::convert_to_gb; mod blocklist; mod collections; mod downloads; +mod history; mod indexers; mod library; #[cfg(test)] mod radarr_ui_tests; +mod radarr_ui_utils; mod root_folders; mod system; @@ -61,6 +64,7 @@ impl DrawUi for RadarrUi { _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), _ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area), _ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area), + _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), _ => (), } } @@ -161,7 +165,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { let space: f64 = convert_to_gb(*free_space); let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) .block(borderless_block()) - .default(); + .default_color(); f.render_widget( root_folder_space, @@ -249,7 +253,7 @@ fn draw_radarr_logo(f: &mut Frame<'_>, area: Rect) { let logo_text = Text::from(RADARR_LOGO); let logo = Paragraph::new(logo_text) .light_yellow() - .block(layout_block().default()) + .block(layout_block().default_color()) .centered(); f.render_widget(logo, area); } diff --git a/src/ui/radarr_ui/radarr_ui_tests.rs b/src/ui/radarr_ui/radarr_ui_tests.rs index ab43f32..4c0838f 100644 --- a/src/ui/radarr_ui/radarr_ui_tests.rs +++ b/src/ui/radarr_ui/radarr_ui_tests.rs @@ -29,9 +29,10 @@ mod tests { #[case(ActiveRadarrBlock::Collections, 1)] #[case(ActiveRadarrBlock::Downloads, 2)] #[case(ActiveRadarrBlock::Blocklist, 3)] - #[case(ActiveRadarrBlock::RootFolders, 4)] - #[case(ActiveRadarrBlock::Indexers, 5)] - #[case(ActiveRadarrBlock::System, 6)] + #[case(ActiveRadarrBlock::History, 4)] + #[case(ActiveRadarrBlock::RootFolders, 5)] + #[case(ActiveRadarrBlock::Indexers, 6)] + #[case(ActiveRadarrBlock::System, 7)] fn test_radarr_ui_renders_radarr_tabs( #[case] active_radarr_block: ActiveRadarrBlock, #[case] index: usize, diff --git a/src/ui/radarr_ui/radarr_ui_utils.rs b/src/ui/radarr_ui/radarr_ui_utils.rs new file mode 100644 index 0000000..ac51c5b --- /dev/null +++ b/src/ui/radarr_ui/radarr_ui_utils.rs @@ -0,0 +1,121 @@ +use ratatui::text::Line; + +use crate::models::radarr_models::{RadarrHistoryData, RadarrHistoryEventType, RadarrHistoryItem}; + +#[cfg(test)] +#[path = "radarr_ui_utils_tests.rs"] +mod radarr_ui_utils_tests; + +pub(super) fn create_history_event_details(history_item: RadarrHistoryItem) -> Vec> { + let RadarrHistoryItem { + source_title, + event_type, + data, + .. + } = history_item; + let RadarrHistoryData { + indexer, + release_group, + nzb_info_url, + download_client, + download_client_name, + age, + published_date, + dropped_path, + imported_path, + message, + reason, + source_path, + path, + .. + } = data; + + let mut lines = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {}", event_type)), + ]; + + match event_type { + RadarrHistoryEventType::Grabbed => { + lines.push(Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start() + ))); + lines.push(Line::from(format!( + "Age: {} days", + age.unwrap_or("0".to_owned()).trim_start(), + ))); + lines.push(Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default().to_string().trim_start(), + ))); + } + RadarrHistoryEventType::DownloadFolderImported => { + lines.push(Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start() + ))); + } + RadarrHistoryEventType::DownloadFailed => { + lines.push(Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start(), + ))); + lines.push(Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start(), + ))); + } + RadarrHistoryEventType::MovieFileDeleted => { + lines.push(Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start(), + ))); + } + RadarrHistoryEventType::MovieFileRenamed => { + lines.push(Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Destination Path: {}", + path.unwrap_or_default().trim_start(), + ))); + } + _ => { + lines.push(Line::from(String::new())); + lines.push(Line::from("No additional data available")); + } + } + + lines +} diff --git a/src/ui/radarr_ui/radarr_ui_utils_tests.rs b/src/ui/radarr_ui/radarr_ui_utils_tests.rs new file mode 100644 index 0000000..070dafd --- /dev/null +++ b/src/ui/radarr_ui/radarr_ui_utils_tests.rs @@ -0,0 +1,236 @@ +#[cfg(test)] +mod tests { + use chrono::Utc; + use ratatui::text::Line; + + use crate::models::radarr_models::RadarrHistoryEventType; + use crate::models::radarr_models::{RadarrHistoryData, RadarrHistoryItem}; + use crate::ui::radarr_ui::radarr_ui_utils::create_history_event_details; + use pretty_assertions::assert_eq; + + #[test] + fn test_create_grabbed_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::Grabbed); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + indexer, + release_group, + nzb_info_url, + download_client, + download_client_name, + age, + published_date, + .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start(), + )), + Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + )), + Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Download Client: {}", + download_client + .or(download_client_name) + .unwrap_or_default() + .trim_start() + )), + Line::from(format!( + "Age: {} days", + age.unwrap_or("0".to_owned()).trim_start(), + )), + Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default().to_string().trim_start() + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_download_folder_imported_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::DownloadFolderImported); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + dropped_path, + imported_path, + download_client, + .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Download Client: {}", + download_client.unwrap_or_default().trim_start(), + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_download_failed_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::DownloadFailed); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + message, + download_client, + .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Download Client: {}", + download_client.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start() + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_movie_file_deleted_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::MovieFileDeleted); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { reason, .. } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start(), + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_movie_file_renamed_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::MovieFileRenamed); + let RadarrHistoryItem { + source_title, + data, + event_type, + .. + } = history_item.clone(); + let RadarrHistoryData { + source_path, path, .. + } = data; + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start(), + )), + Line::from(format!( + "Destination Path: {}", + path.unwrap_or_default().trim_start(), + )), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_no_data_history_event_details() { + let history_item = radarr_history_item(RadarrHistoryEventType::Unknown); + let RadarrHistoryItem { + source_title, + event_type, + .. + } = history_item.clone(); + let expected_vec = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start(),)), + Line::from(format!("Event Type: {event_type}")), + Line::from(String::new()), + Line::from("No additional data available"), + ]; + + let history_details_vec = create_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + fn radarr_history_item(event_type: RadarrHistoryEventType) -> RadarrHistoryItem { + RadarrHistoryItem { + source_title: "test.source.title".into(), + event_type, + data: radarr_history_data(), + ..RadarrHistoryItem::default() + } + } + + fn radarr_history_data() -> RadarrHistoryData { + RadarrHistoryData { + dropped_path: Some("/dropped/test".into()), + imported_path: Some("/imported/test".into()), + indexer: Some("Test Indexer".into()), + release_group: Some("test release group".into()), + nzb_info_url: Some("test url".into()), + download_client: Some("test download client".into()), + download_client_name: None, + age: Some("1".into()), + published_date: Some(Utc::now()), + message: Some("test message".into()), + reason: Some("test reason".into()), + source_path: Some("/source/path".into()), + path: Some("/path".into()), + } + } +} diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap index c502692..97b29eb 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Blocklist.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Movie Title ▼ Source Title Languages Quality Formats Date │ │=> Test z movie English HD - 1080p English 2024-02-10 07:28:45 UTC │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap index 983cf62..87f3b8e 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Collections.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Collection ▼ Number of Movies Root Folder Path Quality Profile Search on Add Monitored │ │=> Test Collection 1 /nfs/movies HD - 1080p Yes 🏷 │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap index 16641df..70721de 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Downloads.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title Percent Complete Size Output Path Indexer Download Client │ │=> Test Download Title 50% 3.30 GB /nfs/movies/Test kickass torrents transmission │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__History.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__History.snap new file mode 100644 index 0000000..baa99cf --- /dev/null +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__History.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/radarr_ui/radarr_ui_tests.rs +expression: output +--- +╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Source Title ▼ Event Type Language Quality Date │ +│=> Test grabbed English HD - 1080p 2022-12-30 07:37:56 UTC │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap index 0576c5c..b5aaaf8 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Indexers.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Indexer ▼ RSS Automatic Search Interactive Search Priority Tags │ │=> Test Indexer Enabled Enabled Enabled 25 alex │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap index 82fca3e..7e9fd6f 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__Movies.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ 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 │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap index 10d0cfe..5ba427d 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__RootFolders.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Path Free Space Unmapped Folders │ │=> /nfs 204800.00 GB 0 │ diff --git a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap index 5f3c163..16c2abc 100644 --- a/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap +++ b/src/ui/radarr_ui/snapshots/managarr__ui__radarr_ui__radarr_ui_tests__tests__snapshot_tests__System.snap @@ -3,7 +3,7 @@ source: src/ui/radarr_ui/radarr_ui_tests.rs expression: output --- ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ Library │ Collections │ Downloads │ Blocklist │ History │ Root Folders │ Indexers │ System │ │╭ Tasks ───────────────────────────────────────────────────────────────────────╮╭ Queued Events ──────────────────────────────────────────────────────────────╮│ ││Name Interval Last Execution Last Duration Next Execution ││Trigger Status Name Queued Started Duration ││ ││Backup 1 hour now 00:00:17 59 minutes ││manual completed Refresh Monitored 4 minutes ago 4 minutes a 00:03:03 ││ diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index 2939924..79cf00c 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -1,3 +1,4 @@ +use crate::ui::styles::default_style; use std::ops::Sub; #[cfg(test)] @@ -5,7 +6,6 @@ use crate::ui::ui_test_utils::test_utils::Utc; #[cfg(not(test))] use chrono::Utc; use ratatui::layout::Layout; -use ratatui::style::Style; use ratatui::text::{Span, Text}; use ratatui::widgets::{Cell, Row}; use ratatui::{ @@ -178,7 +178,7 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) .block(block) - .highlight_style(Style::new().default()); + .highlight_style(default_style()); f.render_widget(logs_box, area); } diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.snap new file mode 100644 index 0000000..e7c2b01 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab.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: ││ ││ ⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ │ +│Disk 1: 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 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_popup.snap new file mode 100644 index 0000000..c90b1f0 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_error_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 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ │ +│Uptime: 0d 00:00:44 │ Key Alt Key Description │━━━━━━━━━━━━━━━━━││ ⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀ │ +│Storage: │=> a add │ ││ ⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ │ +│Disk 1: 100% ━━━━━━━━━━━━━━━━━━━━━━│ e edit │ ││ ⣾⡟⠀⢸⠃⠀⠀⠀⠈⠉⠉⠁⠀⢹⣷ │ +│Root Folders: │ m toggle monitoring │ ││ ⢿⣧⠀⠈⠀⠀⠀⠀⠀⠀⢀⡟⠀⣸⡿ │ +│/nfs: 204800.00 GB free │ o sort │ ││ ⠘⣿⣄⠀⠻⣿⣿⡇⠀⢀⠞⠀⣠⣿⠃ │ +│ │ del delete │ ││ ⠀⠘⢿⣦⣄⠀⠉⠁⠀⠀⣠⣴⡿⠃⠀ │ +│ │ s search │ ││ ⠀⠀⠀⠉⠻⠿⢿⡆⡾⠿⠟⠉⠀⠀⠀ │ +╰───────────────────────────────────│ f filter │─────────────────╯╰──────────────────╯ +╭ Artists ────────────────────────│ ctrl-r refresh │─────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ │ u update all │ │ +│───────────────────────────────────│ enter details │─────────────────────────────────────│ +│ Name ▼ Typ│ esc cancel filter │e Monitored Tags │ +│=> Alex Per│ ↑ k scroll up │0 GB 🏷 alex │ +│ │ ↓ j scroll down │ │ +│ │ ← h previous tab │ │ +│ │ → l next tab │ │ +│ │ pgUp ctrl-u page up │ │ +│ │ pgDown ctrl-d page down │ │ +│ │ tab next servarr │ │ +│ │ shift-tab previous servarr │ │ +│ │ q quit │ │ +│ │ ? show/hide keybindings │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap new file mode 100644 index 0000000..25b0aa2 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__lidarr_ui_renders_library_tab_with_error.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/ui_tests.rs +expression: output +--- +╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Radarr │ Sonarr │ Lidarr to open help│ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭ Error | to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│Some error │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ +│Lidarr Version: 1.2.3.4 ││Test download title ││ ⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ │ +│Uptime: 0d 00:00:44 ││50% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀ │ +│Storage: ││ ││ ⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ │ +│Disk 1: 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 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap index 8f537b7..9704700 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr to open help│ +│ Radarr │ Sonarr │ Lidarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Radarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │ @@ -16,7 +16,7 @@ expression: output │ ││ ││ │ ╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯ ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ 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 │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap index 72a40f2..15eca48 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr to open help│ +│ Radarr │ Sonarr │ Lidarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ │Radarr Version: 1.2.3.4 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap index 17602d3..f0e42bc 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap @@ -3,7 +3,7 @@ source: src/ui/ui_tests.rs expression: output --- ╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Radarr │ Sonarr to open help│ +│ Radarr │ Sonarr │ Lidarr to open help│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ Error | to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │Some error │ @@ -19,7 +19,7 @@ expression: output │ ││ ││ │ ╰───────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────╯ ╭ Movies ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Collections │ Downloads │ Blocklist │ Root Folders │ Indexers │ System │ +│ 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 │ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.snap new file mode 100644 index 0000000..daf2d86 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab.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: ││ ││ ⢰⣿⣿⣦⠐⠄⠉⠉⠉⠉⠠⠂⣰⣿⣿⡆ │ +│Disk 1: 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 🏷 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_popup.snap new file mode 100644 index 0000000..24ea1ef --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_error_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 ╭ Keybindings ──────────────────────────────────────────────────────────────────────────╮ ││ ⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ │ +│Uptime: 0d 00:00:44 │ Key Alt Key Description │━━━━━━━━━━━━━━━━━││ ⠀⣠⡀⠈⠻⣿⣿⣿⣿⣿⣿⠟⠁⢀⣀⠀ │ +│Storage: │=> a add │ ││ ⢰⣿⣿⣦⠐⠄⠉⠉⠉⠉⠠⠂⣰⣿⣿⡆ │ +│Disk 1: 100% ━━━━━━━━━━━━━━━━━━━━━━│ e edit │ ││ ⣿⣿⣿⣿⡆⠀⣴⣿⣿⣦⠀⢰⣿⣿⣿⣿ │ +│Root Folders: │ m toggle monitoring │ ││ ⣿⣿⣿⣿⡇⠀⠻⣿⣿⠟⠀⠸⣿⣿⣿⣿ │ +│/nfs: 204800.00 GB free │ o sort │ ││ ⠸⣿⣿⠟⠠⠂⠀⢀⡀⠀⠐⠄⠻⣿⣿⠇ │ +│ │ del delete │ ││ ⠀⠙⠁⢀⣴⣾⣿⣿⣿⣿⣷⣦⡀⠈⠋⠀ │ +│ │ s search │ ││ ⠀⠀⠀⠘⠻⠿⣿⣿⣿⣿⠿⠟⠋⠀⠀⠀ │ +╰───────────────────────────────────│ f filter │─────────────────╯╰──────────────────╯ +╭ Series ─────────────────────────│ ctrl-r refresh │─────────────────────────────────────╮ +│ Library │ Downloads │ Blocklist │ │ u update all │ │ +│───────────────────────────────────│ enter details │─────────────────────────────────────│ +│ Title ▼ │ esc cancel filter │ Monitored Tags │ +│=> Test │ ↑ k scroll up │ GB 🏷 │ +│ │ ↓ j scroll down │ │ +│ │ ← h previous tab │ │ +│ │ → l next tab │ │ +│ │ pgUp ctrl-u page up │ │ +│ │ pgDown ctrl-d page down │ │ +│ │ tab next servarr │ │ +│ │ shift-tab previous servarr │ │ +│ │ q quit │ │ +│ │ ? show/hide keybindings │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap new file mode 100644 index 0000000..730ecd4 --- /dev/null +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__sonarr_ui_renders_library_tab_with_error.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/ui_tests.rs +expression: output +--- +╭ Managarr - A Servarr management TUI ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Radarr │ Sonarr │ Lidarr to open help│ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭ Error | to close ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│Some error │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭ Stats ──────────────────────────────────────────────────────────────╮╭ Downloads ─────────────────────────────────────────────────────────╮╭──────────────────╮ +│Sonarr Version: 1.2.3.4 ││Test Download Title ││ ⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ │ +│Uptime: 0d 00:00:44 ││50% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━││ ⠀⣠⡀⠈⠻⣿⣿⣿⣿⣿⣿⠟⠁⢀⣀⠀ │ +│Storage: ││ ││ ⢰⣿⣿⣦⠐⠄⠉⠉⠉⠉⠠⠂⣰⣿⣿⡆ │ +│Disk 1: 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 🏷 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/sonarr_ui/blocklist/mod.rs b/src/ui/sonarr_ui/blocklist/mod.rs index 6eb7f07..ea31dc2 100644 --- a/src/ui/sonarr_ui/blocklist/mod.rs +++ b/src/ui/sonarr_ui/blocklist/mod.rs @@ -3,7 +3,7 @@ use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; use crate::models::sonarr_models::BlocklistItem; use crate::ui::DrawUi; -use crate::ui::styles::ManagarrStyle; +use crate::ui::styles::{ManagarrStyle, secondary_style}; use crate::ui::utils::layout_block_top_border; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; @@ -11,7 +11,7 @@ use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::style::{Style, Stylize}; +use ratatui::style::Stylize; use ratatui::text::{Line, Text}; use ratatui::widgets::{Cell, Row}; @@ -163,7 +163,7 @@ fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let message = Message::new(text) .title("Details") - .style(Style::new().secondary()) + .style(secondary_style()) .alignment(Alignment::Left); f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs index 05c00ef..b37bf8c 100644 --- a/src/ui/sonarr_ui/history/mod.rs +++ b/src/ui/sonarr_ui/history/mod.rs @@ -2,27 +2,19 @@ use crate::app::App; use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; use crate::models::servarr_models::Language; -use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; +use crate::models::sonarr_models::SonarrHistoryItem; use crate::ui::DrawUi; -use crate::ui::styles::ManagarrStyle; +use crate::ui::sonarr_ui::sonarr_ui_utils::create_history_event_details; +use crate::ui::styles::{ManagarrStyle, secondary_style}; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::style::Style; use ratatui::text::Text; use ratatui::widgets::{Cell, Row}; -use super::sonarr_ui_utils::{ - create_download_failed_history_event_details, - create_download_folder_imported_history_event_details, - create_episode_file_deleted_history_event_details, - create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, - create_no_data_history_event_details, -}; - #[cfg(test)] #[path = "history_ui_tests.rs"] mod history_ui_tests; @@ -131,28 +123,13 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { app.data.sonarr_data.history.current_selection().clone() }; - let line_vec = match current_selection.event_type { - SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), - SonarrHistoryEventType::DownloadFolderImported => { - create_download_folder_imported_history_event_details(current_selection) - } - SonarrHistoryEventType::DownloadFailed => { - create_download_failed_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileDeleted => { - create_episode_file_deleted_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileRenamed => { - create_episode_file_renamed_history_event_details(current_selection) - } - _ => create_no_data_history_event_details(current_selection), - }; + let line_vec = create_history_event_details(current_selection); let text = Text::from(line_vec); let message = Message::new(text) .title("Details") - .style(Style::new().secondary()) + .style(secondary_style()) .alignment(Alignment::Left); - f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); } diff --git a/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap b/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap index 77e2c98..23e8779 100644 --- a/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap +++ b/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__history_tab_HistoryItemDetails.snap @@ -18,12 +18,9 @@ expression: output - - - - ╭─────────────────────────────────── Details ───────────────────────────────────╮ │Source Title: Test source │ + │Event Type: grabbed │ │Indexer: │ │Release Group: │ │Series Match Type: │ @@ -31,4 +28,11 @@ expression: output │Download Client Name: │ │Age: 0 days │ │Published Date: 1970-01-01 00:00:00 UTC │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap b/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap index 71155ea..e3a9d6f 100644 --- a/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap +++ b/src/ui/sonarr_ui/history/snapshots/managarr__ui__sonarr_ui__history__history_ui_tests__tests__snapshot_tests__loading_history_tab_HistoryItemDetails.snap @@ -18,12 +18,9 @@ expression: output - - - - ╭─────────────────────────────────── Details ───────────────────────────────────╮ │Source Title: │ + │Event Type: unknown │ │ │ │No additional data available │ │ │ @@ -31,4 +28,11 @@ expression: output │ │ │ │ │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ ╰─────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs b/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs index c12cafe..b0f9267 100644 --- a/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs @@ -4,7 +4,6 @@ use crate::app::App; use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; use crate::render_selectable_input_box; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -151,10 +150,10 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .flex(Flex::Center) .areas(buttons_area); - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs b/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs index 5dcdce9..6a77576 100644 --- a/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs @@ -7,7 +7,6 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, }; use crate::render_selectable_input_box; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; use crate::ui::widgets::input_box::InputBox; @@ -103,10 +102,10 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: .flex(Flex::Center) .areas(buttons_area); - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/sonarr_ui/indexers/mod.rs b/src/ui/sonarr_ui/indexers/mod.rs index 86b08b0..6dcda09 100644 --- a/src/ui/sonarr_ui/indexers/mod.rs +++ b/src/ui/sonarr_ui/indexers/mod.rs @@ -1,6 +1,6 @@ +use crate::ui::styles::success_style; use ratatui::Frame; use ratatui::layout::{Constraint, Rect}; -use ratatui::style::{Style, Stylize}; use ratatui::text::Text; use ratatui::widgets::{Cell, Row}; @@ -73,7 +73,7 @@ impl DrawUi for IndexersUi { } else { let message = Message::new("Indexer test succeeded!") .title("Success") - .style(Style::new().success().bold()); + .style(success_style().bold()); Popup::new(message).size(Size::Message) } }; diff --git a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs index 8b0bf20..bc02ed8 100644 --- a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs @@ -33,12 +33,14 @@ impl DrawUi for TestAllIndexersUi { fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let is_loading = app.is_loading || app.data.sonarr_data.indexer_test_all_results.is_none(); - let current_selection = - if let Some(test_all_results) = app.data.sonarr_data.indexer_test_all_results.as_ref() { - test_all_results.current_selection().clone() - } else { - IndexerTestResultModalItem::default() - }; + let current_selection = if let Some(test_all_results) = + app.data.sonarr_data.indexer_test_all_results.as_ref() + && !test_all_results.is_empty() + { + test_all_results.current_selection().clone() + } else { + IndexerTestResultModalItem::default() + }; f.render_widget(title_block("Test All Indexers"), area); let test_results_row_mapping = |result: &IndexerTestResultModalItem| { result.validation_failures.scroll_left_or_reset( diff --git a/src/ui/sonarr_ui/library/add_series_ui.rs b/src/ui/sonarr_ui/library/add_series_ui.rs index 6d09c02..942cb8e 100644 --- a/src/ui/sonarr_ui/library/add_series_ui.rs +++ b/src/ui/sonarr_ui/library/add_series_ui.rs @@ -64,12 +64,14 @@ impl DrawUi for AddSeriesUi { fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let is_loading = app.is_loading || app.data.sonarr_data.add_searched_series.is_none(); - let current_selection = - if let Some(add_searched_series) = app.data.sonarr_data.add_searched_series.as_ref() { - add_searched_series.current_selection().clone() - } else { - AddSeriesSearchResult::default() - }; + let current_selection = if let Some(add_searched_series) = + app.data.sonarr_data.add_searched_series.as_ref() + && !add_searched_series.is_empty() + { + add_searched_series.current_selection().clone() + } else { + AddSeriesSearchResult::default() + }; let [search_box_area, results_area] = Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) @@ -142,14 +144,14 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .block(title_block_centered("Add Series")); search_box.show_cursor(f, search_box_area); - f.render_widget(layout_block().default(), results_area); + f.render_widget(layout_block().default_color(), results_area); f.render_widget(search_box, search_box_area); } ActiveSonarrBlock::AddSeriesEmptySearchResults => { let error_message = Message::new("No series found matching your query!"); let error_message_popup = Popup::new(error_message).size(Size::Message); - f.render_widget(layout_block().default(), results_area); + f.render_widget(layout_block().default_color(), results_area); f.render_widget(error_message_popup, f.area()); } ActiveSonarrBlock::AddSeriesSearchResults @@ -166,7 +168,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { search_results_row_mapping, ) .loading(is_loading) - .block(layout_block().default()) + .block(layout_block().default_color()) .headers([ "✔", "Title", "Year", "Network", "Seasons", "Rating", "Genres", ]) @@ -312,27 +314,27 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let use_season_folder_checkbox = Checkbox::new("Season Folder") .checked(*use_season_folder) .highlighted(selected_block == ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder); - let root_folder_drop_down_button = Button::new() + let root_folder_drop_down_button = Button::default() .title(&selected_root_folder.path) .label("Root Folder") .icon("▼") .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectRootFolder); - let monitor_drop_down_button = Button::new() + let monitor_drop_down_button = Button::default() .title(selected_monitor.to_display_str()) .label("Monitor") .icon("▼") .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectMonitor); - let series_type_drop_down_button = Button::new() + let series_type_drop_down_button = Button::default() .title(selected_series_type.to_display_str()) .label("Series Type") .icon("▼") .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectSeriesType); - let quality_profile_drop_down_button = Button::new() + let quality_profile_drop_down_button = Button::default() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectQualityProfile); - let language_profile_drop_down_button = Button::new() + let language_profile_drop_down_button = Button::default() .title(selected_language_profile) .label("Language Profile") .icon("▼") @@ -354,10 +356,10 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { render_selectable_input_box!(tags_input_box, f, tags_area); } - let add_button = Button::new() + let add_button = Button::default() .title("Add") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/sonarr_ui/library/edit_series_ui.rs b/src/ui/sonarr_ui/library/edit_series_ui.rs index 40c7f45..4e08a0a 100644 --- a/src/ui/sonarr_ui/library/edit_series_ui.rs +++ b/src/ui/sonarr_ui/library/edit_series_ui.rs @@ -13,7 +13,6 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ }; use crate::render_selectable_input_box; -use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -145,17 +144,17 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar let season_folder_checkbox = Checkbox::new("Season Folder") .checked(use_season_folders.unwrap_or_default()) .highlighted(selected_block == ActiveSonarrBlock::EditSeriesToggleSeasonFolder); - let series_type_drop_down_button = Button::new() + let series_type_drop_down_button = Button::default() .title(selected_series_type.to_display_str()) .label("Series Type") .icon("▼") .selected(selected_block == ActiveSonarrBlock::EditSeriesSelectSeriesType); - let quality_profile_drop_down_button = Button::new() + let quality_profile_drop_down_button = Button::default() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") .selected(selected_block == ActiveSonarrBlock::EditSeriesSelectQualityProfile); - let language_profile_drop_down_button = Button::new() + let language_profile_drop_down_button = Button::default() .title(selected_language_profile) .label("Language Profile") .icon("▼") @@ -183,10 +182,10 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar render_selectable_input_box!(tags_input_box, f, tags_area); } - let save_button = Button::new() + let save_button = Button::default() .title("Save") .selected(yes_no_value && highlight_yes_no); - let cancel_button = Button::new() + let cancel_button = Button::default() .title("Cancel") .selected(!yes_no_value && highlight_yes_no); diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs index f5af16a..72ca7e4 100644 --- a/src/ui/sonarr_ui/library/episode_details_ui.rs +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -3,16 +3,14 @@ use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS}; use crate::models::servarr_models::Language; use crate::models::sonarr_models::{ - DownloadRecord, DownloadStatus, Episode, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, -}; -use crate::ui::sonarr_ui::sonarr_ui_utils::{ - create_download_failed_history_event_details, - create_download_folder_imported_history_event_details, - create_episode_file_deleted_history_event_details, - create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, - create_no_data_history_event_details, + DownloadRecord, DownloadStatus, Episode, SonarrHistoryItem, SonarrRelease, }; +use crate::ui::sonarr_ui::sonarr_ui_utils::create_history_event_details; use crate::ui::styles::ManagarrStyle; +use crate::ui::styles::{ + awaiting_import_style, downloaded_style, downloading_style, missing_style, secondary_style, + unmonitored_missing_style, unmonitored_style, unreleased_style, +}; use crate::ui::utils::{ borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_bottom_border, layout_block_top_border, @@ -97,7 +95,7 @@ impl DrawUi for EpisodeDetailsUi { draw_manual_episode_search_confirm_prompt(f, app); } ActiveSonarrBlock::EpisodeHistoryDetails => { - draw_history_item_details_popup(f, app, popup_area); + draw_history_item_details_popup(f, app); } _ => (), } @@ -349,7 +347,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) } } -fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let current_selection = if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { @@ -368,30 +366,15 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: R SonarrHistoryItem::default() }; - let line_vec = match current_selection.event_type { - SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), - SonarrHistoryEventType::DownloadFolderImported => { - create_download_folder_imported_history_event_details(current_selection) - } - SonarrHistoryEventType::DownloadFailed => { - create_download_failed_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileDeleted => { - create_episode_file_deleted_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileRenamed => { - create_episode_file_renamed_history_event_details(current_selection) - } - _ => create_no_data_history_event_details(current_selection), - }; + let line_vec = create_history_event_details(current_selection); let text = Text::from(line_vec); let message = Message::new(text) .title("Details") - .style(Style::new().secondary()) + .style(secondary_style()) .alignment(Alignment::Left); - f.render_widget(Popup::new(message).size(Size::NarrowMessage), area); + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); } fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -602,29 +585,29 @@ fn style_from_status(download: Option<&DownloadRecord>, episode: &Episode) -> St if !episode.has_file { if let Some(download) = download { if download.status == DownloadStatus::Downloading { - return Style::new().downloading(); + return downloading_style(); } if download.status == DownloadStatus::Completed { - return Style::new().awaiting_import(); + return awaiting_import_style(); } } if !episode.monitored { - return Style::new().unmonitored_missing(); + return unmonitored_missing_style(); } if let Some(air_date) = episode.air_date_utc.as_ref() && air_date > &Utc::now() { - return Style::new().unreleased(); + return unreleased_style(); } - return Style::new().missing(); + return missing_style(); } if !episode.monitored { - Style::new().unmonitored() + unmonitored_style() } else { - Style::new().downloaded() + downloaded_style() } } diff --git a/src/ui/sonarr_ui/library/library_ui_tests.rs b/src/ui/sonarr_ui/library/library_ui_tests.rs index 7d65fdc..0f73c61 100644 --- a/src/ui/sonarr_ui/library/library_ui_tests.rs +++ b/src/ui/sonarr_ui/library/library_ui_tests.rs @@ -382,5 +382,18 @@ mod tests { insta::assert_snapshot!(output); } + + #[test] + fn test_library_ui_renders_update_all_series_prompt() { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LibraryUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } } } diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs index b676c6f..9eb28ca 100644 --- a/src/ui/sonarr_ui/library/season_details_ui.rs +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -2,17 +2,12 @@ use crate::app::App; use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS}; use crate::models::sonarr_models::{ - DownloadRecord, DownloadStatus, Episode, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, + DownloadRecord, DownloadStatus, Episode, SonarrHistoryItem, SonarrRelease, }; use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi; -use crate::ui::sonarr_ui::sonarr_ui_utils::{ - create_download_failed_history_event_details, - create_download_folder_imported_history_event_details, - create_episode_file_deleted_history_event_details, - create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, - create_no_data_history_event_details, -}; +use crate::ui::sonarr_ui::sonarr_ui_utils::create_history_event_details; use crate::ui::styles::ManagarrStyle; +use crate::ui::styles::secondary_style; use crate::ui::utils::{ borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_top_border, }; @@ -26,7 +21,7 @@ use crate::utils::convert_to_gb; use chrono::Utc; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Rect}; -use ratatui::prelude::{Line, Style, Stylize, Text}; +use ratatui::prelude::{Line, Stylize, Text}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use serde_json::Number; @@ -122,7 +117,7 @@ impl DrawUi for SeasonDetailsUi { draw_manual_season_search_confirm_prompt(f, app); } ActiveSonarrBlock::SeasonHistoryDetails => { - draw_history_item_details_popup(f, app, popup_area); + draw_history_item_details_popup(f, app); } _ => (), } @@ -532,7 +527,7 @@ fn draw_manual_season_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_> } } -fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let current_selection = if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { if season_details_modal.season_history.is_empty() { @@ -547,30 +542,15 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: R SonarrHistoryItem::default() }; - let line_vec = match current_selection.event_type { - SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), - SonarrHistoryEventType::DownloadFolderImported => { - create_download_folder_imported_history_event_details(current_selection) - } - SonarrHistoryEventType::DownloadFailed => { - create_download_failed_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileDeleted => { - create_episode_file_deleted_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileRenamed => { - create_episode_file_renamed_history_event_details(current_selection) - } - _ => create_no_data_history_event_details(current_selection), - }; + let line_vec = create_history_event_details(current_selection); let text = Text::from(line_vec); let message = Message::new(text) .title("Details") - .style(Style::new().secondary()) + .style(secondary_style()) .alignment(Alignment::Left); - f.render_widget(Popup::new(message).size(Size::NarrowMessage), area); + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); } fn decorate_with_row_style<'a>( diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs index 92364ae..37dce9b 100644 --- a/src/ui/sonarr_ui/library/series_details_ui.rs +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -1,8 +1,9 @@ +use crate::ui::styles::secondary_style; use chrono::Utc; use deunicode::deunicode; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; -use ratatui::style::{Style, Stylize}; +use ratatui::style::Stylize; use ratatui::text::{Line, Text}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use regex::Regex; @@ -10,18 +11,10 @@ use regex::Regex; use crate::app::App; use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS}; -use crate::models::sonarr_models::{ - Season, SeasonStatistics, SonarrHistoryEventType, SonarrHistoryItem, -}; +use crate::models::sonarr_models::{Season, SeasonStatistics, SonarrHistoryItem}; use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi; use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi; -use crate::ui::sonarr_ui::sonarr_ui_utils::{ - create_download_failed_history_event_details, - create_download_folder_imported_history_event_details, - create_episode_file_deleted_history_event_details, - create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, - create_no_data_history_event_details, -}; +use crate::ui::sonarr_ui::sonarr_ui_utils::create_history_event_details; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block_top_border, title_block, @@ -103,7 +96,7 @@ impl DrawUi for SeriesDetailsUi { ); } ActiveSonarrBlock::SeriesHistoryDetails => { - draw_history_item_details_popup(f, app, popup_area); + draw_history_item_details_popup(f, app); } _ => (), }; @@ -157,54 +150,60 @@ fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { "Title: ".primary().bold(), current_selection.title.text.clone().primary().bold(), ]), - Line::from(vec!["Overview: ".primary().bold(), overview.default()]), + Line::from(vec![ + "Overview: ".primary().bold(), + overview.default_color(), + ]), Line::from(vec![ "Network: ".primary().bold(), current_selection .network .clone() .unwrap_or_default() - .default(), + .default_color(), ]), Line::from(vec![ "Status: ".primary().bold(), - current_selection.status.to_display_str().default(), + current_selection.status.to_display_str().default_color(), ]), Line::from(vec![ "Genres: ".primary().bold(), - current_selection.genres.join(", ").default(), + current_selection.genres.join(", ").default_color(), ]), Line::from(vec![ "Rating: ".primary().bold(), - format!("{}%", (current_selection.ratings.value * 10.0) as i32).default(), + format!("{}%", (current_selection.ratings.value * 10.0) as i32).default_color(), ]), Line::from(vec![ "Year: ".primary().bold(), - current_selection.year.to_string().default(), + current_selection.year.to_string().default_color(), ]), Line::from(vec![ "Runtime: ".primary().bold(), - format!("{} minutes", current_selection.runtime).default(), + format!("{} minutes", current_selection.runtime).default_color(), ]), Line::from(vec![ "Path: ".primary().bold(), - current_selection.path.clone().default(), + current_selection.path.clone().default_color(), ]), Line::from(vec![ "Quality Profile: ".primary().bold(), - quality_profile.default(), + quality_profile.default_color(), ]), Line::from(vec![ "Language Profile: ".primary().bold(), - language_profile.default(), + language_profile.default_color(), + ]), + Line::from(vec![ + "Monitored: ".primary().bold(), + monitored.default_color(), ]), - Line::from(vec!["Monitored: ".primary().bold(), monitored.default()]), ]; if let Some(stats) = current_selection.statistics.as_ref() { let size = convert_to_gb(stats.size_on_disk); series_description.extend(vec![Line::from(vec![ "Size on Disk: ".primary().bold(), - format!("{size:.2} GB").default(), + format!("{size:.2} GB").default_color(), ])]); } @@ -389,7 +388,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } } -fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let current_selection = if let Some(series_history_items) = app.data.sonarr_data.series_history.as_ref() { if series_history_items.is_empty() { @@ -401,28 +400,13 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: R SonarrHistoryItem::default() }; - let line_vec = match current_selection.event_type { - SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), - SonarrHistoryEventType::DownloadFolderImported => { - create_download_folder_imported_history_event_details(current_selection) - } - SonarrHistoryEventType::DownloadFailed => { - create_download_failed_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileDeleted => { - create_episode_file_deleted_history_event_details(current_selection) - } - SonarrHistoryEventType::EpisodeFileRenamed => { - create_episode_file_renamed_history_event_details(current_selection) - } - _ => create_no_data_history_event_details(current_selection), - }; + let line_vec = create_history_event_details(current_selection); let text = Text::from(line_vec); let message = Message::new(text) .title("Details") - .style(Style::new().secondary()) + .style(secondary_style()) .alignment(Alignment::Left); - f.render_widget(Popup::new(message).size(Size::NarrowMessage), area); + f.render_widget(Popup::new(message).size(Size::NarrowLongMessage), f.area()); } diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__empty_episode_details_EpisodeHistoryDetails_1.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__empty_episode_details_EpisodeHistoryDetails_1.snap index e8150f4..50ca04c 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__empty_episode_details_EpisodeHistoryDetails_1.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__empty_episode_details_EpisodeHistoryDetails_1.snap @@ -18,24 +18,24 @@ expression: output │ │ │ │ │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ ╭──────────────────────── Details ─────────────────────────╮ │ - │ │Source Title: │ │ - │ │ │ │ - │ │No additional data available │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ ╰────────────────────────────────────────────────────────────╯ │ - │ │ - │ │ - │ │ - │ │ - │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: │ │ + │ │Event Type: unknown │ │ + │ │ │ │ + │ │No additional data available │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ │ │ │ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__episode_details_EpisodeHistoryDetails_1.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__episode_details_EpisodeHistoryDetails_1.snap index 0ae4533..27e655d 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__episode_details_EpisodeHistoryDetails_1.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__episode_details_ui__episode_details_ui_tests__tests__snapshot_tests__episode_details_EpisodeHistoryDetails_1.snap @@ -18,24 +18,24 @@ expression: output │ │ │ │ │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ ╭──────────────────────── Details ─────────────────────────╮ │ - │ │Source Title: Test source │ │ - │ │Indexer: │ │ - │ │Release Group: │ │ - │ │Series Match Type: │ │ - │ │NZB Info URL: │ │ - │ │Download Client Name: │ │ - │ ╰────────────────────────────────────────────────────────────╯ │ - │ │ - │ │ - │ │ - │ │ - │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: Test source │ │ + │ │Event Type: grabbed │ │ + │ │Indexer: │ │ + │ │Release Group: │ │ + │ │Series Match Type: │ │ + │ │NZB Info URL: │ │ + │ │Download Client Name: │ │ + │ │Age: 0 days │ │ + │ │Published Date: 1970-01-01 00:00:00 UTC │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ │ │ │ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_update_all_series_prompt.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_update_all_series_prompt.snap new file mode 100644 index 0000000..b43cea3 --- /dev/null +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__library_ui_tests__tests__snapshot_tests__library_ui_renders_update_all_series_prompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/sonarr_ui/library/library_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 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 🏷 + + + + + + + + + + + + + + ╭─────────────────── Update All Series ───────────────────╮ + │ Do you want to update info and scan your disks for all of │ + │ your series? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__empty_season_details_SeasonHistoryDetails_1.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__empty_season_details_SeasonHistoryDetails_1.snap index b0bd2ac..3cccf44 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__empty_season_details_SeasonHistoryDetails_1.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__empty_season_details_SeasonHistoryDetails_1.snap @@ -18,24 +18,24 @@ expression: output │ │ │ │ │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ ╭──────────────────────────── Details ────────────────────────────╮ │ - │ │Source Title: │ │ - │ │ │ │ - │ │No additional data available │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ ╰───────────────────────────────────────────────────────────────────╯ │ - │ │ - │ │ - │ │ - │ │ - │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: │ │ + │ │Event Type: unknown │ │ + │ │ │ │ + │ │No additional data available │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ │ │ │ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__loading_season_details_SeasonHistoryDetails_1.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__loading_season_details_SeasonHistoryDetails_1.snap index 7c519e8..6b7fb80 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__loading_season_details_SeasonHistoryDetails_1.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__loading_season_details_SeasonHistoryDetails_1.snap @@ -18,24 +18,24 @@ expression: output │ │ │ │ │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ ╭──────────────────────────── Details ────────────────────────────╮ │ - │ │Source Title: │ │ - │ │ │ │ - │ │No additional data available │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ ╰───────────────────────────────────────────────────────────────────╯ │ - │ │ - │ │ - │ │ - │ │ - │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: │ │ + │ │Event Type: unknown │ │ + │ │ │ │ + │ │No additional data available │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ │ │ │ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__season_details_renders_SeasonHistoryDetails_1.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__season_details_renders_SeasonHistoryDetails_1.snap index 6fe8412..1de8caf 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__season_details_renders_SeasonHistoryDetails_1.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__season_details_ui__season_details_ui_tests__tests__snapshot_tests__season_details_renders_SeasonHistoryDetails_1.snap @@ -18,24 +18,24 @@ expression: output │ │ │ │ │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ ╭──────────────────────────── Details ────────────────────────────╮ │ - │ │Source Title: Test source │ │ - │ │Indexer: │ │ - │ │Release Group: │ │ - │ │Series Match Type: │ │ - │ │NZB Info URL: │ │ - │ │Download Client Name: │ │ - │ ╰───────────────────────────────────────────────────────────────────╯ │ - │ │ - │ │ - │ │ - │ │ - │ │ + │ ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: Test source │ │ + │ │Event Type: grabbed │ │ + │ │Indexer: │ │ + │ │Release Group: │ │ + │ │Series Match Type: │ │ + │ │NZB Info URL: │ │ + │ │Download Client Name: │ │ + │ │Age: 0 days │ │ + │ │Published Date: 1970-01-01 00:00:00 UTC │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ╰─────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ │ │ │ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__empty_series_details_SeriesHistoryDetails.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__empty_series_details_SeriesHistoryDetails.snap index f70e2fe..12609fd 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__empty_series_details_SeriesHistoryDetails.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__empty_series_details_SeriesHistoryDetails.snap @@ -18,24 +18,24 @@ expression: output │Quality Profile: Bluray-1080p │ │Language Profile: English │ │Monitored: Yes │ - │Size on Disk: 59.51 GB │ - │ │ - │ │ - │ │ - │╭ Series Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ - ││ Seasons │ History ╭─────────────────────────────── Details ───────────────────────────────╮ ││ - ││───────────────────────────────────│Source Title: │───────────────────────────────────││ - ││ │ │ ││ - ││ │No additional data available │ ││ - ││ │ │ ││ - ││ │ │ ││ - ││ │ │ ││ - ││ │ │ ││ - ││ ╰─────────────────────────────────────────────────────────────────────────╯ ││ - ││ ││ - ││ ││ - ││ ││ - ││ ││ + │Size on Disk: 59.51 GB ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: │ │ + │ │Event Type: unknown │ │ + │ │ │ │ + │╭ Series Details ─────────────│No additional data available │───────────────────────────────╮│ + ││ Seasons │ History │ │ ││ + ││───────────────────────────────│ │───────────────────────────────││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────╯ ││ ││ ││ ││ ││ ││ ││ diff --git a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__series_details_ui_SeriesHistoryDetails_1.snap b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__series_details_ui_SeriesHistoryDetails_1.snap index 756cf8f..851a727 100644 --- a/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__series_details_ui_SeriesHistoryDetails_1.snap +++ b/src/ui/sonarr_ui/library/snapshots/managarr__ui__sonarr_ui__library__series_details_ui__series_details_ui_tests__tests__snapshot_tests__series_details_ui_SeriesHistoryDetails_1.snap @@ -18,24 +18,24 @@ expression: output │Quality Profile: Bluray-1080p │ │Language Profile: English │ │Monitored: Yes │ - │Size on Disk: 59.51 GB │ - │ │ - │ │ - │ │ - │╭ Series Details ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│ - ││ Seasons │ History ╭─────────────────────────────── Details ───────────────────────────────╮ ││ - ││───────────────────────────────────│Source Title: Test source │───────────────────────────────────││ - ││ Source Title ▼ │Indexer: │ Date ││ - ││=> Test source │Release Group: │ 2024-02-10 07:28:45 UTC ││ - ││ │Series Match Type: │ ││ - ││ │NZB Info URL: │ ││ - ││ │Download Client Name: │ ││ - ││ │Age: 0 days │ ││ - ││ ╰─────────────────────────────────────────────────────────────────────────╯ ││ - ││ ││ - ││ ││ - ││ ││ - ││ ││ + │Size on Disk: 59.51 GB ╭─────────────────────────────────── Details ───────────────────────────────────╮ │ + │ │Source Title: Test source │ │ + │ │Event Type: grabbed │ │ + │ │Indexer: │ │ + │╭ Series Details ─────────────│Release Group: │───────────────────────────────╮│ + ││ Seasons │ History │Series Match Type: │ ││ + ││───────────────────────────────│NZB Info URL: │───────────────────────────────││ + ││ Source Title ▼ │Download Client Name: │ Date ││ + ││=> Test source │Age: 0 days │ 2024-02-10 07:28:45 UTC ││ + ││ │Published Date: 1970-01-01 00:00:00 UTC │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ │ │ ││ + ││ ╰─────────────────────────────────────────────────────────────────────────────────╯ ││ ││ ││ ││ ││ ││ ││ diff --git a/src/ui/sonarr_ui/mod.rs b/src/ui/sonarr_ui/mod.rs index bbe1979..5469e8a 100644 --- a/src/ui/sonarr_ui/mod.rs +++ b/src/ui/sonarr_ui/mod.rs @@ -173,7 +173,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { let space: f64 = convert_to_gb(*free_space); let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) .block(borderless_block()) - .default(); + .default_color(); f.render_widget( root_folder_space, @@ -224,7 +224,7 @@ fn draw_sonarr_logo(f: &mut Frame<'_>, area: Rect) { let logo_text = Text::from(SONARR_LOGO); let logo = Paragraph::new(logo_text) .light_cyan() - .block(layout_block().default()) + .block(layout_block().default_color()) .centered(); f.render_widget(logo, area); } diff --git a/src/ui/sonarr_ui/sonarr_ui_utils.rs b/src/ui/sonarr_ui/sonarr_ui_utils.rs index 8fff51a..b1eeb35 100644 --- a/src/ui/sonarr_ui/sonarr_ui_utils.rs +++ b/src/ui/sonarr_ui/sonarr_ui_utils.rs @@ -1,18 +1,17 @@ -use ratatui::style::Stylize; use ratatui::text::Line; -use crate::models::sonarr_models::{SonarrHistoryData, SonarrHistoryItem}; -use crate::ui::styles::ManagarrStyle; +use crate::models::sonarr_models::{SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem}; #[cfg(test)] #[path = "sonarr_ui_utils_tests.rs"] mod sonarr_ui_utils_tests; -pub(super) fn create_grabbed_history_event_details( - history_item: SonarrHistoryItem, -) -> Vec> { +pub(super) fn create_history_event_details(history_item: SonarrHistoryItem) -> Vec> { let SonarrHistoryItem { - source_title, data, .. + source_title, + data, + event_type, + .. } = history_item; let SonarrHistoryData { indexer, @@ -22,120 +21,10 @@ pub(super) fn create_grabbed_history_event_details( download_client_name, age, published_date, - .. - } = data; - - vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Indexer: ".bold().secondary(), - indexer.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Release Group: ".bold().secondary(), - release_group.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Series Match Type: ".bold().secondary(), - series_match_type.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "NZB Info URL: ".bold().secondary(), - nzb_info_url.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Download Client Name: ".bold().secondary(), - download_client_name.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Age: ".bold().secondary(), - format!("{} days", age.unwrap_or("0".to_owned())).secondary(), - ]), - Line::from(vec![ - "Published Date: ".bold().secondary(), - published_date.unwrap_or_default().to_string().secondary(), - ]), - ] -} - -pub(super) fn create_download_folder_imported_history_event_details( - history_item: SonarrHistoryItem, -) -> Vec> { - let SonarrHistoryItem { - source_title, data, .. - } = history_item; - let SonarrHistoryData { dropped_path, imported_path, - .. - } = data; - - vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Dropped Path: ".bold().secondary(), - dropped_path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Imported Path: ".bold().secondary(), - imported_path.unwrap_or_default().secondary(), - ]), - ] -} - -pub(super) fn create_download_failed_history_event_details( - history_item: SonarrHistoryItem, -) -> Vec> { - let SonarrHistoryItem { - source_title, data, .. - } = history_item; - let SonarrHistoryData { message, .. } = data; - - vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Message: ".bold().secondary(), - message.unwrap_or_default().secondary(), - ]), - ] -} - -pub(super) fn create_episode_file_deleted_history_event_details( - history_item: SonarrHistoryItem, -) -> Vec> { - let SonarrHistoryItem { - source_title, data, .. - } = history_item; - let SonarrHistoryData { reason, .. } = data; - - vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Reason: ".bold().secondary(), - reason.unwrap_or_default().secondary(), - ]), - ] -} - -pub(super) fn create_episode_file_renamed_history_event_details( - history_item: SonarrHistoryItem, -) -> Vec> { - let SonarrHistoryItem { - source_title, data, .. - } = history_item; - let SonarrHistoryData { + message, + reason, source_path, source_relative_path, path, @@ -143,41 +32,87 @@ pub(super) fn create_episode_file_renamed_history_event_details( .. } = data; - vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Source Path: ".bold().secondary(), - source_path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Source Relative Path: ".bold().secondary(), - source_relative_path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Destination Path: ".bold().secondary(), - path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Destination Relative Path: ".bold().secondary(), - relative_path.unwrap_or_default().secondary(), - ]), - ] -} + let mut lines = vec![ + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {}", event_type)), + ]; -pub(super) fn create_no_data_history_event_details( - history_item: SonarrHistoryItem, -) -> Vec> { - let SonarrHistoryItem { source_title, .. } = history_item; + match event_type { + SonarrHistoryEventType::Grabbed => { + lines.push(Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Series Match Type: {}", + series_match_type.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Download Client Name: {}", + download_client_name.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Age: {}", + format!("{} days", age.unwrap_or("0".to_owned())).trim_start(), + ))); + lines.push(Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default().to_string().trim_start(), + ))); + } + SonarrHistoryEventType::DownloadFolderImported => { + lines.push(Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start(), + ))); + } + SonarrHistoryEventType::DownloadFailed => { + lines.push(Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start(), + ))); + } + SonarrHistoryEventType::EpisodeFileDeleted => { + lines.push(Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start(), + ))); + } + SonarrHistoryEventType::EpisodeFileRenamed => { + lines.push(Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Source Relative Path: {}", + source_relative_path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Destination Path: {}", + path.unwrap_or_default().trim_start(), + ))); + lines.push(Line::from(format!( + "Destination Relative Path: {}", + relative_path.unwrap_or_default().trim_start(), + ))); + } + _ => { + lines.push(Line::from(String::new())); + lines.push(Line::from("No additional data available")); + } + } - vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![String::new().secondary()]), - Line::from(vec!["No additional data available".bold().secondary()]), - ] + lines } diff --git a/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs b/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs index a0255c2..ef308e2 100644 --- a/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs +++ b/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs @@ -1,28 +1,21 @@ #[cfg(test)] mod tests { use chrono::Utc; - use ratatui::{style::Stylize, text::Line}; + use ratatui::text::Line; - use crate::{ - models::sonarr_models::{SonarrHistoryData, SonarrHistoryItem}, - ui::{ - sonarr_ui::sonarr_ui_utils::{ - create_download_failed_history_event_details, - create_download_folder_imported_history_event_details, - create_episode_file_deleted_history_event_details, - create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, - create_no_data_history_event_details, - }, - styles::ManagarrStyle, - }, - }; + use crate::models::sonarr_models::SonarrHistoryEventType; + use crate::models::sonarr_models::{SonarrHistoryData, SonarrHistoryItem}; + use crate::ui::sonarr_ui::sonarr_ui_utils::create_history_event_details; use pretty_assertions::assert_eq; #[test] fn test_create_grabbed_history_event_details() { - let history_item = sonarr_history_item(); + let history_item = sonarr_history_item(SonarrHistoryEventType::Grabbed); let SonarrHistoryItem { - source_title, data, .. + source_title, + data, + event_type, + .. } = history_item.clone(); let SonarrHistoryData { indexer, @@ -35,50 +28,51 @@ mod tests { .. } = data; let expected_vec = vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Indexer: ".bold().secondary(), - indexer.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Release Group: ".bold().secondary(), - release_group.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Series Match Type: ".bold().secondary(), - series_match_type.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "NZB Info URL: ".bold().secondary(), - nzb_info_url.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Download Client Name: ".bold().secondary(), - download_client_name.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Age: ".bold().secondary(), - format!("{} days", age.unwrap_or("0".to_owned())).secondary(), - ]), - Line::from(vec![ - "Published Date: ".bold().secondary(), - published_date.unwrap_or_default().to_string().secondary(), - ]), + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Indexer: {}", + indexer.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Release Group: {}", + release_group.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Series Match Type: {}", + series_match_type.unwrap_or_default().trim_start() + )), + Line::from(format!( + "NZB Info URL: {}", + nzb_info_url.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Download Client Name: {}", + download_client_name.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Age: {}", + format!("{} days", age.unwrap_or("0".to_owned())).trim_start() + )), + Line::from(format!( + "Published Date: {}", + published_date.unwrap_or_default().to_string().trim_start() + )), ]; - let history_details_vec = create_grabbed_history_event_details(history_item); + let history_details_vec = create_history_event_details(history_item); assert_eq!(expected_vec, history_details_vec); } #[test] fn test_create_download_folder_imported_history_event_details() { - let history_item = sonarr_history_item(); + let history_item = sonarr_history_item(SonarrHistoryEventType::DownloadFolderImported); let SonarrHistoryItem { - source_title, data, .. + source_title, + data, + event_type, + .. } = history_item.clone(); let SonarrHistoryData { dropped_path, @@ -86,76 +80,79 @@ mod tests { .. } = data; let expected_vec = vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Dropped Path: ".bold().secondary(), - dropped_path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Imported Path: ".bold().secondary(), - imported_path.unwrap_or_default().secondary(), - ]), + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Dropped Path: {}", + dropped_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Imported Path: {}", + imported_path.unwrap_or_default().trim_start() + )), ]; - let history_details_vec = create_download_folder_imported_history_event_details(history_item); + let history_details_vec = create_history_event_details(history_item); assert_eq!(expected_vec, history_details_vec); } #[test] fn test_create_download_failed_history_event_details() { - let history_item = sonarr_history_item(); + let history_item = sonarr_history_item(SonarrHistoryEventType::DownloadFailed); let SonarrHistoryItem { - source_title, data, .. + source_title, + data, + event_type, + .. } = history_item.clone(); let SonarrHistoryData { message, .. } = data; let expected_vec = vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Message: ".bold().secondary(), - message.unwrap_or_default().secondary(), - ]), + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Message: {}", + message.unwrap_or_default().trim_start() + )), ]; - let history_details_vec = create_download_failed_history_event_details(history_item); + let history_details_vec = create_history_event_details(history_item); assert_eq!(expected_vec, history_details_vec); } #[test] fn test_create_episode_file_deleted_history_event_details() { - let history_item = sonarr_history_item(); + let history_item = sonarr_history_item(SonarrHistoryEventType::EpisodeFileDeleted); let SonarrHistoryItem { - source_title, data, .. + source_title, + data, + event_type, + .. } = history_item.clone(); let SonarrHistoryData { reason, .. } = data; let expected_vec = vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Reason: ".bold().secondary(), - reason.unwrap_or_default().secondary(), - ]), + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Reason: {}", + reason.unwrap_or_default().trim_start() + )), ]; - let history_details_vec = create_episode_file_deleted_history_event_details(history_item); + let history_details_vec = create_history_event_details(history_item); assert_eq!(expected_vec, history_details_vec); } #[test] fn test_create_episode_file_renamed_history_event_details() { - let history_item = sonarr_history_item(); + let history_item = sonarr_history_item(SonarrHistoryEventType::EpisodeFileRenamed); let SonarrHistoryItem { - source_title, data, .. + source_title, + data, + event_type, + .. } = history_item.clone(); let SonarrHistoryData { source_path, @@ -165,54 +162,55 @@ mod tests { .. } = data; let expected_vec = vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![ - "Source Path: ".bold().secondary(), - source_path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Source Relative Path: ".bold().secondary(), - source_relative_path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Destination Path: ".bold().secondary(), - path.unwrap_or_default().secondary(), - ]), - Line::from(vec![ - "Destination Relative Path: ".bold().secondary(), - relative_path.unwrap_or_default().secondary(), - ]), + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(format!( + "Source Path: {}", + source_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Source Relative Path: {}", + source_relative_path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Destination Path: {}", + path.unwrap_or_default().trim_start() + )), + Line::from(format!( + "Destination Relative Path: {}", + relative_path.unwrap_or_default().trim_start() + )), ]; - let history_details_vec = create_episode_file_renamed_history_event_details(history_item); + let history_details_vec = create_history_event_details(history_item); assert_eq!(expected_vec, history_details_vec); } #[test] fn test_create_no_data_history_event_details() { - let history_item = sonarr_history_item(); - let SonarrHistoryItem { source_title, .. } = history_item.clone(); + let history_item = sonarr_history_item(SonarrHistoryEventType::Unknown); + let SonarrHistoryItem { + source_title, + event_type, + .. + } = history_item.clone(); let expected_vec = vec![ - Line::from(vec![ - "Source Title: ".bold().secondary(), - source_title.text.secondary(), - ]), - Line::from(vec![String::new().secondary()]), - Line::from(vec!["No additional data available".bold().secondary()]), + Line::from(format!("Source Title: {}", source_title.text.trim_start())), + Line::from(format!("Event Type: {event_type}")), + Line::from(String::new()), + Line::from("No additional data available"), ]; - let history_details_vec = create_no_data_history_event_details(history_item); + let history_details_vec = create_history_event_details(history_item); assert_eq!(expected_vec, history_details_vec); } - fn sonarr_history_item() -> SonarrHistoryItem { + fn sonarr_history_item(event_type: SonarrHistoryEventType) -> SonarrHistoryItem { SonarrHistoryItem { - source_title: "test.source.title".into(), + source_title: "\ntest.source.title".into(), + event_type, data: sonarr_history_data(), ..SonarrHistoryItem::default() } @@ -220,21 +218,21 @@ mod tests { fn sonarr_history_data() -> SonarrHistoryData { SonarrHistoryData { - dropped_path: Some("/dropped/test".into()), - imported_path: Some("/imported/test".into()), - indexer: Some("Test Indexer".into()), - release_group: Some("test release group".into()), - series_match_type: Some("test match type".into()), - nzb_info_url: Some("test url".into()), - download_client_name: Some("test download client".into()), - age: Some("1".into()), + dropped_path: Some("\n/dropped/test".into()), + imported_path: Some("\n/imported/test".into()), + indexer: Some("\nTest Indexer".into()), + release_group: Some("\ntest release group".into()), + series_match_type: Some("\ntest match type".into()), + nzb_info_url: Some("\ntest url".into()), + download_client_name: Some("\ntest download client".into()), + age: Some("\n1".into()), published_date: Some(Utc::now()), - message: Some("test message".into()), - reason: Some("test reason".into()), - source_path: Some("/source/path".into()), - source_relative_path: Some("/relative/source/path".into()), - path: Some("/path".into()), - relative_path: Some("/relative/path".into()), + message: Some("\ntest message".into()), + reason: Some("\ntest reason".into()), + source_path: Some("\n/source/path".into()), + source_relative_path: Some("\n/relative/source/path".into()), + path: Some("\n/path".into()), + relative_path: Some("\n/relative/path".into()), } } } diff --git a/src/ui/sonarr_ui/system/mod.rs b/src/ui/sonarr_ui/system/mod.rs index 96d06ee..e983762 100644 --- a/src/ui/sonarr_ui/system/mod.rs +++ b/src/ui/sonarr_ui/system/mod.rs @@ -1,3 +1,4 @@ +use crate::ui::styles::default_style; use std::ops::Sub; #[cfg(test)] @@ -5,7 +6,6 @@ use crate::ui::ui_test_utils::test_utils::Utc; #[cfg(not(test))] use chrono::Utc; use ratatui::layout::Layout; -use ratatui::style::Style; use ratatui::text::{Span, Text}; use ratatui::widgets::{Cell, Row}; use ratatui::{ @@ -171,7 +171,7 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) .block(block) - .highlight_style(Style::new().default()); + .highlight_style(default_style()); f.render_widget(logs_box, area); } diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 306f794..6d0f202 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -1,253 +1,303 @@ use crate::ui::THEME; -use ratatui::style::{Styled, Stylize}; +use ratatui::style::{Style, Styled}; #[cfg(test)] #[path = "styles_tests.rs"] mod styles_tests; -pub trait ManagarrStyle<'a, T>: Stylize<'a, T> -where - T: Default, -{ - #[allow(clippy::new_ret_no_self)] - fn new() -> T; - fn awaiting_import(self) -> T; - fn indeterminate(self) -> T; - fn default(self) -> T; - fn downloaded(self) -> T; - fn downloading(self) -> T; - fn failure(self) -> T; - fn help(self) -> T; - fn highlight(self) -> T; - fn missing(self) -> T; - fn primary(self) -> T; - fn secondary(self) -> T; - fn success(self) -> T; - fn system_function(self) -> T; - fn unmonitored(self) -> T; - fn unmonitored_missing(self) -> T; - fn unreleased(self) -> T; - fn warning(self) -> T; +pub fn awaiting_import_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .awaiting_import + .expect("awaiting_import style must be defined in theme") + .color + .expect("awaiting_import color must be defined"), + ) + }) } -impl ManagarrStyle<'_, T> for U -where - U: Styled, - T: Default, -{ - fn new() -> T { - T::default() +pub fn indeterminate_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .indeterminate + .expect("indeterminate style must be defined in theme") + .color + .expect("indeterminate color must be defined"), + ) + }) +} + +pub fn default_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .default + .expect("default style must be defined in theme") + .color + .expect("default color must be defined"), + ) + }) +} + +pub fn downloaded_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .downloaded + .expect("downloaded style must be defined in theme") + .color + .expect("downloaded color must be defined"), + ) + }) +} + +pub fn downloading_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .downloading + .expect("downloading style must be defined in theme") + .color + .expect("downloading color must be defined"), + ) + }) +} + +pub fn failure_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .failure + .expect("failure style must be defined in theme") + .color + .expect("failure color must be defined"), + ) + }) +} + +pub fn help_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .help + .expect("help style must be defined in theme") + .color + .expect("help color must be defined"), + ) + }) +} + +pub fn highlight_style() -> Style { + Style::new().reversed() +} + +pub fn missing_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .missing + .expect("missing style must be defined in theme") + .color + .expect("missing color must be defined"), + ) + }) +} + +pub fn primary_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .primary + .expect("primary style must be defined in theme") + .color + .expect("primary color must be defined"), + ) + }) +} + +pub fn secondary_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .secondary + .expect("secondary style must be defined in theme") + .color + .expect("secondary color must be defined"), + ) + }) +} + +pub fn success_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .success + .expect("success style must be defined in theme") + .color + .expect("success color must be defined"), + ) + }) +} + +pub fn system_function_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .system_function + .expect("system_function style must be defined in theme") + .color + .expect("system_function color must be defined"), + ) + }) +} + +pub fn unmonitored_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .unmonitored + .expect("unmonitored style must be defined in theme") + .color + .expect("unmonitored color must be defined"), + ) + }) +} + +pub fn unmonitored_missing_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .unmonitored_missing + .expect("unmonitored_missing style must be defined in theme") + .color + .expect("unmonitored_missing color must be defined"), + ) + }) +} + +pub fn unreleased_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .unreleased + .expect("unreleased style must be defined in theme") + .color + .expect("unreleased color must be defined"), + ) + }) +} + +pub fn warning_style() -> Style { + THEME.with(|theme| { + Style::new().fg( + theme + .get() + .warning + .expect("warning style must be defined in theme") + .color + .expect("warning color must be defined"), + ) + }) +} + +pub trait ManagarrStyle: Styled { + fn awaiting_import(self) -> Self::Item; + fn indeterminate(self) -> Self::Item; + fn default_color(self) -> Self::Item; + fn downloaded(self) -> Self::Item; + fn downloading(self) -> Self::Item; + fn failure(self) -> Self::Item; + fn help(self) -> Self::Item; + fn missing(self) -> Self::Item; + fn primary(self) -> Self::Item; + fn secondary(self) -> Self::Item; + fn success(self) -> Self::Item; + fn system_function(self) -> Self::Item; + fn unmonitored(self) -> Self::Item; + fn unmonitored_missing(self) -> Self::Item; + fn unreleased(self) -> Self::Item; + fn warning(self) -> Self::Item; +} + +impl ManagarrStyle for T { + fn awaiting_import(self) -> Self::Item { + self.set_style(awaiting_import_style()) } - fn awaiting_import(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .awaiting_import - .expect("awaiting_import style must be defined in theme") - .color - .expect("awaiting_import color must be defined"), - ) - }) + fn indeterminate(self) -> Self::Item { + self.set_style(indeterminate_style()) } - fn indeterminate(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .indeterminate - .expect("indeterminate style must be defined in theme") - .color - .expect("indeterminate color must be defined"), - ) - }) + fn default_color(self) -> Self::Item { + self.set_style(default_style()) } - fn default(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .default - .expect("default style must be defined in theme") - .color - .expect("default color must be defined"), - ) - }) + fn downloaded(self) -> Self::Item { + self.set_style(downloaded_style()) } - fn downloaded(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .downloaded - .expect("downloaded style must be defined in theme") - .color - .expect("downloaded color must be defined"), - ) - }) + fn downloading(self) -> Self::Item { + self.set_style(downloading_style()) } - fn downloading(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .downloading - .expect("downloading style must be defined in theme") - .color - .expect("downloading color must be defined"), - ) - }) + fn failure(self) -> Self::Item { + self.set_style(failure_style()) } - fn failure(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .failure - .expect("failure style must be defined in theme") - .color - .expect("failure color must be defined"), - ) - }) + fn help(self) -> Self::Item { + self.set_style(help_style()) } - fn help(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .help - .expect("help style must be defined in theme") - .color - .expect("help color must be defined"), - ) - }) + fn missing(self) -> Self::Item { + self.set_style(missing_style()) } - fn highlight(self) -> T { - self.reversed() + fn primary(self) -> Self::Item { + self.set_style(primary_style()) } - fn missing(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .missing - .expect("missing style must be defined in theme") - .color - .expect("missing color must be defined"), - ) - }) + fn secondary(self) -> Self::Item { + self.set_style(secondary_style()) } - fn primary(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .primary - .expect("primary style must be defined in theme") - .color - .expect("primary color must be defined"), - ) - }) + fn success(self) -> Self::Item { + self.set_style(success_style()) } - fn secondary(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .secondary - .expect("secondary style must be defined in theme") - .color - .expect("secondary color must be defined"), - ) - }) + fn system_function(self) -> Self::Item { + self.set_style(system_function_style()) } - fn success(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .success - .expect("success style must be defined in theme") - .color - .expect("success color must be defined"), - ) - }) + fn unmonitored(self) -> Self::Item { + self.set_style(unmonitored_style()) } - fn system_function(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .system_function - .expect("system_function style must be defined in theme") - .color - .expect("system_function color must be defined"), - ) - }) + fn unmonitored_missing(self) -> Self::Item { + self.set_style(unmonitored_missing_style()) } - fn unmonitored(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .unmonitored - .expect("unmonitored style must be defined in theme") - .color - .expect("unmonitored color must be defined"), - ) - }) + fn unreleased(self) -> Self::Item { + self.set_style(unreleased_style()) } - fn unmonitored_missing(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .unmonitored_missing - .expect("unmonitored_missing style must be defined in theme") - .color - .expect("unmonitored_missing color must be defined"), - ) - }) - } - - fn unreleased(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .unreleased - .expect("unreleased style must be defined in theme") - .color - .expect("unreleased color must be defined"), - ) - }) - } - - fn warning(self) -> T { - THEME.with(|theme| { - self.fg( - theme - .get() - .warning - .expect("warning style must be defined in theme") - .color - .expect("warning color must be defined"), - ) - }) + fn warning(self) -> Self::Item { + self.set_style(warning_style()) } } diff --git a/src/ui/styles_tests.rs b/src/ui/styles_tests.rs index 2ec748f..ee61ee4 100644 --- a/src/ui/styles_tests.rs +++ b/src/ui/styles_tests.rs @@ -1,19 +1,19 @@ #[cfg(test)] mod test { - use crate::ui::styles::ManagarrStyle; + use crate::ui::styles::{ + awaiting_import_style, default_style, downloaded_style, downloading_style, failure_style, + help_style, highlight_style, indeterminate_style, missing_style, primary_style, + secondary_style, success_style, system_function_style, unmonitored_missing_style, + unmonitored_style, unreleased_style, warning_style, + }; use pretty_assertions::assert_eq; use ratatui::prelude::Modifier; - use ratatui::style::{Color, Style, Stylize}; - - #[test] - fn test_new() { - assert_eq!(Style::new(),