Compare commits

..

311 Commits

Author SHA1 Message Date
0048d71b74 feat: Support alternative keymappings for all keys, featuring hjkl movements 2025-03-17 22:02:15 -06:00
c633347ecc Merge remote-tracking branch 'refs/remotes/origin/develop' 2025-03-17 20:49:52 -06:00
Alex Clarke
ecd6a0ec32 Merge pull request #37 from Dark-Alex-17/custom-themes
Support Themes
2025-03-17 14:23:18 -06:00
30507d9d01 docs: Updated the README to include the new flags 2025-03-17 13:26:46 -06:00
6245a794d5 docs: Update all screenshots to not have any auto-generated usernames in the tags columns 2025-03-10 16:22:24 -06:00
5c822e4890 Merge branch 'develop' 2025-03-10 16:13:48 -06:00
cab06fe43f fix: Marked the Season.statistics field as Option so that a panic does not happen for outdated Sonarr data. This resolves #35 2025-03-10 16:13:04 -06:00
b4ff5f3351 feat: Added the Eldritch theme and updated documentation 2025-03-10 15:49:40 -06:00
0834802481 fix: When adding a film from the Collection Details modal, the render order was wrong: Radarr Library -> Collection Table -> Add Movie Prompt (missing the Collection details prompt too). Correct order is: Collection Table -> Collection Details Modal -> Add Movie Modal 2025-03-10 15:08:02 -06:00
3afd74dcbf fix: Fixed a bug that was rendering encompassing blocks after other widgets were rendered, thus overwriting the custom styles on each previously rendered widget 2025-03-10 15:01:58 -06:00
b1a0bdfbb6 Merge branch 'develop' 2025-03-07 12:02:47 -07:00
6d38bc5e1d Merge branch 'main' 2025-03-07 12:02:19 -07:00
5ba1ba15c9 ci: Update to the most recent Rust version 2025-03-07 11:55:32 -07:00
db05d2abfb Merge branch 'develop' into custom-themes 2025-03-07 10:37:48 -07:00
1840c4e39a Merge branch 'main' into develop
# Conflicts:
#	proc_macros/enum_display_style_derive/src/lib.rs
2025-03-07 10:37:23 -07:00
c5a3f424d6 refactor: Reformatted code to make the format checks pass 2025-03-07 10:36:40 -07:00
04aa6b81b5 Merge branch 'develop' into custom-themes 2025-03-07 10:35:07 -07:00
5ff3b9b996 Merge branch 'main' into develop 2025-03-07 10:34:16 -07:00
228e4a61a4 fix: Updated ring dependency to mitigate CWE-770 2025-03-07 10:33:57 -07:00
df38ea5413 feat: Write built in themes to the themes file on first run so users can define custom themes 2025-03-06 17:44:52 -07:00
709f6ca6ca test: Added integration tests for the ValidateTheme macro 2025-03-06 16:00:50 -07:00
b012fc29e4 Merge branch 'develop' into custom-themes
# Conflicts:
#	Cargo.toml
2025-03-06 15:35:05 -07:00
bdad723aef refactor: Formatted files using rustfmt 2025-03-06 15:32:59 -07:00
f97d46cec3 refactor: Created a derive macro for defining the display style of Enum models and removed the use of the EnumDisplayStyle trait 2025-03-06 15:29:30 -07:00
7381eaef57 refactor: Expanded the serde_enum_from macro to further reduce code duplication 2025-03-05 15:09:51 -07:00
72c922b311 feat: Created a theme validation macro to verify theme configurations before allowing the TUI to start 2025-03-05 14:37:34 -07:00
Alex Clarke
fd14a8152c fix: change the name of the theme configuration file to 'themes' 2025-03-04 18:29:21 -07:00
5cb60c317d feat: Initial support for custom user-defined themes 2025-03-04 18:09:09 -07:00
847de75713 fix: Modified the Sonarr DownloadRecord so that the episode_id is optional to prevent crashes for weird downloads 2025-03-01 14:50:20 -07:00
58723cf3e8 ci: Ensure the docker release is fully up-to-date 2025-02-28 21:45:05 -07:00
c613168bfb docs: Updated the CHANGELOG accordingly 2025-02-28 21:26:13 -07:00
github-actions[bot]
6f83de77f2 chore: Bump the version in Cargo.lock 2025-03-01 03:30:05 +00:00
github-actions[bot]
3f6ef3beb4 bump: version 0.5.0 → 0.5.1 [skip ci] 2025-03-01 03:30:04 +00:00
14e50c1465 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-02-28 20:29:29 -07:00
0aa9fdca14 ci: Overwrite previous artifact uploads for proper releases 2025-02-28 20:29:15 -07:00
github-actions[bot]
dc50820abc chore: Bump the version in Cargo.lock 2025-03-01 03:08:01 +00:00
github-actions[bot]
81afce78ad bump: version 0.4.2 → 0.5.0 [skip ci] 2025-03-01 03:07:57 +00:00
8a0b912601 ci: Updated the release flow to use the newer upload/download artifact actions 2025-02-28 20:06:41 -07:00
85105a953e docs: Pre-Release update of versions and added link to the Matrix Space. 2025-02-28 16:45:16 -07:00
40f3452d08 ci: Removed the minimal-versions check 2025-02-27 20:51:50 -07:00
a287a5c903 docs: Updated the README to also include details on the new CLI flags 2025-02-27 20:51:00 -07:00
f30e5270d8 refactor: Updated dependencies 2025-02-27 20:45:32 -07:00
104bcd7bb2 refactor: Addressed Cargo fmt complaints 2025-02-27 20:42:32 -07:00
fd6fcfc98f feat: CLI Support for multiple Servarr instances 2025-02-27 20:37:03 -07:00
f87e02cd7c test: Added in unit tests for TUI support for multiple custom named Servarrs 2025-02-27 19:30:17 -07:00
9b63b10118 feat: Support for multiple servarr definitions - no tests [skip ci] 2025-02-27 18:00:28 -07:00
111485e7c4 feat: Support for loading Servarr API tokens from a file 2025-02-27 16:53:29 -07:00
Alex Clarke
0167753cfe Merge pull request #30 from tangowithfoxtrot/var-interpolation
feat: environment variable interpolation in the managarr config file
2025-02-19 17:52:23 -07:00
Alex Clarke
73131cc518 Merge branch 'main' into var-interpolation 2025-02-19 17:40:16 -07:00
25576757bb ci: Updated codecov config to consider patches as well to hopefully fix PR issues [skip ci] 2025-02-19 17:40:03 -07:00
105c8f3a82 test: Hopefully the final environment variable name fix to correct all race conditions with parallel tests 2025-02-19 17:27:56 -07:00
5164d81492 test: Fix a potential race condition happening with parallel tests 2025-02-19 15:59:31 -07:00
319e5f1ac2 test: Added remaining unit tests for the deserialize_optional_env_var deserialization functions 2025-02-19 15:44:55 -07:00
Alex Clarke
b24c2fbeb1 Merge branch 'main' into var-interpolation 2025-02-19 15:14:41 -07:00
bc5053c39c fix: Updated openssl to 0.10.70 to mitigate CVE-2025-24898 2025-02-03 16:06:47 -07:00
tangowithfoxtrot
f06a031c93 Merge branch 'main' into var-interpolation 2025-01-26 14:40:14 -08:00
tangowithfoxtrot
8d450dea5a Merge branch 'main' into var-interpolation 2025-01-26 14:36:45 -08:00
c4ace8c53f feat: Tweaked the implementation for environment variables in the config a bit 2025-01-26 14:59:09 -07:00
78f104f558 refactor: Added a debug line for logging to output the config used when starting Managarr 2025-01-26 14:56:37 -07:00
e8a6f740b9 refactor: Updated the 2018 idiom lint to the 2021_compatibility lint 2025-01-26 14:47:40 -07:00
tangowithfoxtrot
6f3c6ec840 feat: var interpolation 2025-01-26 09:28:47 -08:00
Alex Clarke
47a3ef1d8b Merge pull request #29 from Dark-Alex-17/license-update
Updated license and attribution requirements
2025-01-21 15:47:00 -07:00
Alex Clarke
367e9bf33b Added attribution guidelines to the CONTRIBUTING
Added attribution guidelines to the CONTRIBUTING file so the license is easier to understand
2025-01-21 15:10:01 -07:00
Alex Clarke
f122b02424 Do not require attributions for forks
Update the LICENSE to not require an attribution for forks that merge back into the main/aren't distributed as separate projects
2025-01-21 14:58:35 -07:00
Alex Clarke
3a09c17f0a Update LICENSE
Made the attribution wording more flexible and less legal in nature.
2025-01-21 14:53:30 -07:00
Alex Clarke
6773abb04e Update LICENSE due to scammer abuse
Updated the LICENSE to incorporate an attribution clause, and prohibit commercial use. 

This was done in response to a recently discovered abuse of some scammers using both my GitHub and this project in job applications to scam other companies and job applicants, offering to "adapt the project to the needs of their internal tools". 

Previously, this would have been fine. However, with scams spiking recently and myself struggling to find a job, this kind of abuse is, regrettably, something I must limit.
2025-01-21 13:14:15 -07:00
b757d66d7a fix: Addressed rustfmt complaints 2025-01-18 15:33:56 -07:00
81cb7a750c refactor: Removed unnecessary clones in the networking module to speed up network request handling 2025-01-18 15:23:03 -07:00
3be59108a9 refactor: Corrected some clone instead of copy behaviors in the command line handlers 2025-01-18 14:54:25 -07:00
fac9c45aee refactor: Removed unnecessary clone from stateful table 2025-01-18 14:24:23 -07:00
184bd2b510 refactor: Removed unnecessary clone call from extract_and_add_tag_ids_vec method 2025-01-18 14:15:52 -07:00
fda69178b9 refactor: Reduced the number of clones necessary when building modal structs 2025-01-18 13:56:18 -07:00
652bbcd5d4 refactor: Refactored a handful of Option calls to use take instead 2025-01-18 13:00:21 -07:00
fd35106df8 refactor: Renamed KeyEventHandler::with to KeyEventHandler::new to keep with Rust best practices and conventions 2025-01-18 12:43:25 -07:00
Alex Clarke
5ead5bc3d6 docs: removed the Unraid section of the README now that the issue has been corrected and fixed. 2024-12-31 16:55:39 -06:00
3ce0003315 docs: Added installation instructions for Nix and a note for Unraid users until the template is corrected by the maintainer 2024-12-30 11:25:15 -07:00
Alex Clarke
ee94059a15 fix: Corrected typo in the managarr.nuspec.template 2024-12-21 21:26:38 -07:00
844742053d ci: Finalized corrected release workflow [skip ci] 2024-12-21 16:52:29 -07:00
github-actions[bot]
7ed9cfa018 chore: Bump the version in Cargo.lock 2024-12-21 23:48:07 +00:00
github-actions[bot]
71791afca0 bump: version 0.4.1 → 0.4.2 [skip ci] 2024-12-21 23:48:04 +00:00
a52eddfdac ci: Reverted failed release [skip ci] 2024-12-21 16:47:31 -07:00
0474978ac0 ci: Fixed final typo I hope [skip ci] 2024-12-21 16:46:16 -07:00
4b94a0ce2a ci: Fixed typo in release flow [skip ci] 2024-12-21 16:44:48 -07:00
0d1eac7610 ci: Final test of corrected release flow for GitHub [skip ci] 2024-12-21 16:43:04 -07:00
b129e5d5a4 Merge branch 'main' of github.com:Dark-Alex-17/managarr 2024-12-21 14:59:58 -07:00
9f5c22890d ci: Configure release workflow to only release docker now [skip ci] 2024-12-21 14:58:54 -07:00
github-actions[bot]
74f4a19003 chore: Bump the version in Cargo.lock 2024-12-21 21:57:10 +00:00
github-actions[bot]
717d9872dc bump: version 0.4.1 → 0.4.2 [skip ci] 2024-12-21 21:57:08 +00:00
439270fe2d ci: Revert failed release and add fix to (hopefully) finally fix the GitHub release [skip ci] 2024-12-21 14:37:18 -07:00
github-actions[bot]
601fd55435 chore: Bump the version in Cargo.lock 2024-12-21 21:34:35 +00:00
github-actions[bot]
341c5254f1 bump: version 0.4.1 → 0.4.2 [skip ci] 2024-12-21 21:34:33 +00:00
cb2701ada4 ci: Fixed a typo in the Managarr docker release. Still in release testing mode [skip ci] 2024-12-21 14:04:54 -07:00
28fcccce98 Reverted failed release once again... 2024-12-21 13:51:01 -07:00
2ba3e56772 docs: Removed unnecessary revert commit mention in the CHANGELOG 2024-12-19 21:51:02 -07:00
github-actions[bot]
f3fa3401f1 chore: Bump the version in Cargo.lock 2024-12-20 04:19:08 +00:00
github-actions[bot]
820f339982 bump: version 0.4.1 → 0.4.2 [skip ci] 2024-12-20 04:19:04 +00:00
afd9dd34a7 fix: Revert failed release [skip ci] 2024-12-19 20:37:32 -07:00
4a4e5d2cf4 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-12-19 20:34:54 -07:00
029b00532e ci: Fix a typo in the download-artifacts version [skip ci] 2024-12-19 20:34:41 -07:00
github-actions[bot]
0b052684c2 chore: Bump the version in Cargo.lock 2024-12-20 03:33:06 +00:00
github-actions[bot]
3dda80b50f bump: version 0.4.1 → 0.4.2 [skip ci] 2024-12-20 03:33:03 +00:00
9dede1e45d ci: Removed the now defunct Scoop testing workflow [skip ci] 2024-12-19 20:13:04 -07:00
6dc64c6edf docs: Updated README with Mac and Linux installation steps with Homebrew [skip ci] 2024-12-19 19:56:27 -07:00
c21748df1e ci: Deleted the test homebrew release workflow and updated the main release workflow to have the homebrew release [skip ci] 2024-12-19 19:52:38 -07:00
27d024cca2 ci: Set username and email globally [skip ci] 2024-12-19 19:48:19 -07:00
b0dd2575d9 ci: Typo in clone URL [skip ci] 2024-12-19 19:47:30 -07:00
7a004c2cfc ci: Test using the token directly in the clone [skip ci] 2024-12-19 19:46:32 -07:00
53ab164379 ci: Changed username [skip ci] 2024-12-19 19:45:00 -07:00
bf4fb3a55d ci: Fixed a typo in the homebrew test [skip ci] 2024-12-19 19:43:39 -07:00
2235f1d0d9 ci: Created a token for all Managarr repos [skip ci] 2024-12-19 19:42:13 -07:00
17dcb2b10b ci: Removed the use of the deploy key to force git to use SSH [skip ci] 2024-12-19 19:26:13 -07:00
4c7c62eb0b ci: Changed to cloning the homebrew repo with SSH [skip ci] 2024-12-19 19:25:09 -07:00
81dcf4b003 ci: Updated the username and email to be that of my personal user [skip ci] 2024-12-19 19:22:45 -07:00
3fffdd20a8 ci: Set username and email globally [skip ci] 2024-12-19 19:20:37 -07:00
078d75473b ci: Update the test homebrew release [skip ci] 2024-12-19 19:18:35 -07:00
8b7ff58c7d ci: Created a test homebrew release workflow [skip ci] 2024-12-19 19:15:59 -07:00
ff67eebcea ci: Removed scoop workflow [skip ci] 2024-12-19 18:25:05 -07:00
github-actions[bot]
e585eb46ea chore: Update Scoop bucket for version 0.4.1 [skip ci] 2024-12-20 01:10:06 +00:00
01507f88da ci: Corrected the use of the SSH key for pushing to the repo [skip ci] 2024-12-19 18:08:43 -07:00
6344b6dba2 ci: Fixing the push step [skip ci] 2024-12-19 18:04:07 -07:00
d3042cf724 ci: Corrected the packager script path [skip ci] 2024-12-19 17:58:00 -07:00
fd4b9a4f15 ci: Updated a typo in the scoop test workflow config [skip ci] 2024-12-19 17:55:58 -07:00
2879a2aa67 ci: Corrected the scoop workflow to publish changes to repo first, and then to publish them to Scoop [skip ci] 2024-12-19 17:53:16 -07:00
5cd8cb66f1 ci: Fix erroring out when removing the scoop bucket if files don't exist [skip ci] 2024-12-19 17:40:11 -07:00
47c206ca1d ci: Created the test scoop deployment workflow [skip ci] 2024-12-19 17:34:47 -07:00
8f22b64a0a ci: Moved the package.py script into the main deployment directory for reuse, and also updated the Chocolatey workflow to push the generated Chocolatey files to the repo for posterity [skip ci] 2024-12-19 17:23:27 -07:00
7941b3d16c docs: Updated the README to include steps on how to install Managarr in Windows using Chocolatey [skip ci] 2024-12-19 16:55:06 -07:00
e9ded4bde4 ci: Delete the test chocolatey deploy and add the official chocolatey deploy to the release workflow [skip ci] 2024-12-19 16:50:58 -07:00
7b02472f67 ci: Fix the test workflow to use the first release that actually has a sha256 for Windows attached to it [skip ci] 2024-12-19 16:46:16 -07:00
a370d67121 ci: Attempting to explicitly set the output encoding to UTF-8 for Windows [skip ci] 2024-12-19 16:41:56 -07:00
4f6e64083a ci: Explicitly set the encoding as UTF-8 in the python script since that is not the windows default encoding [skip ci] 2024-12-19 16:36:56 -07:00
276a672df4 ci: Try refreshing the environment after installing managarr choco from local [skip ci] 2024-12-19 16:31:44 -07:00
e55a157977 ci: Fixed a typo in the chocolatey deployment PowerShell template [skip ci] 2024-12-19 16:26:28 -07:00
e1e91d1add ci: Testing fixes in the test chocolatey deployment workflow [skip ci] 2024-12-19 16:25:04 -07:00
8102b5933f ci: Mock the artifact creation for the test workflow [skip ci] 2024-12-19 16:19:43 -07:00
bc60128679 ci: Fixed a typo in the test chocolatey deployment workflow [skip ci] 2024-12-19 16:11:41 -07:00
fc3a7ab789 ci: Created test GHA workflow to rest releasing Managarr to Chocolatey [skip ci] 2024-12-19 16:10:38 -07:00
93d3e6cec7 docs: Fixed working in the README about the new managarr-demo.alexjclarke.com site 2024-12-18 18:22:42 -07:00
95779f1ac2 docs: Updated the README to point to the new managarr-demo.alexjclarke.com site for testing out the TUI as an alternative to running the command 2024-12-18 18:20:36 -07:00
7bc57d7696 ci: Remove the test multi-platform docker test job [skip ci] 2024-12-18 18:12:38 -07:00
7e7b75f378 ci: Correct another typo in the test multi-platform job [skip ci] 2024-12-18 17:57:31 -07:00
a958350b2d ci: Fix typo in multi-platform release test [skip ci] 2024-12-18 17:56:41 -07:00
c502b3d08f ci: Attempting multi-platform builds for docker [skip ci] 2024-12-18 17:54:37 -07:00
Alex Clarke
e602b66188 Merge pull request #27 from Dark-Alex-17/race-condition-refactor
Race condition refactor
2024-12-18 17:38:13 -07:00
12fba15bcf style: Clean up all remaining unused test helper functions 2024-12-18 01:44:27 -07:00
7e36ad4e8a fix(sonarr): Pass the series ID alongside all UpdateAndScan events when publishing to the networking channel 2024-12-18 01:40:47 -07:00
33249f509f fix(sonarr): pass the series ID alongside all TriggerAutomaticSeriesSearch events when publishing to the networking channel 2024-12-18 01:38:05 -07:00
ed645dd0d5 fix(sonarr): Pass the series ID and season number alongside all TriggerAutomaticSeasonSearch events when publishing to the networking channel 2024-12-18 01:34:45 -07:00
b12c635c27 fix(sonarr): Pass the episode ID alongside all TriggerAutomaticEpisodeSearch events when publishing to the networking channel 2024-12-18 01:29:30 -07:00
c16ecfb188 fix(sonarr): Pass the episode ID alongside all ToggleEpisodeMonitoring events when publishing to the networking channel 2024-12-18 01:22:28 -07:00
18a8b81631 fix(sonarr): Pass the series ID and season number alongside all toggle season monitoring events when publishing to the networking channel 2024-12-18 01:12:32 -07:00
1d404d4d2c fix(sonarr): Pass the indexer ID directly alongside all TestIndexer events when publishing to the networking channel 2024-12-18 01:01:01 -07:00
42479ced21 fix(sonarr): Provide the task name directly alongside all StartTask events when publishing to the networking channel 2024-12-18 00:56:21 -07:00
1193b8c848 fix(sonarr): Pass the search query directly to the networking channel when searching for a new series 2024-12-18 00:49:36 -07:00
ec8d748991 fix(sonarr): Pass the series ID alongside all GetSeriesHistory events when publishing to the networking channel 2024-12-18 00:39:50 -07:00
bafaf7ca7a fix(sonarr): Pass the series ID alongside all GetSeriesDetails events when publishing to the networking channel 2024-12-18 00:37:06 -07:00
f7315a3bec fix(sonarr): Pass series ID and season number alongside all ManualSeasonSearch events when publishing to the networking channel 2024-12-18 00:32:36 -07:00
f655ca989d fix(sonarr): Provide the series ID and season number alongside all GetSeasonHistory events when publishing to the networking channel 2024-12-18 00:22:24 -07:00
fcb87a6779 fix(sonarr): Pass the episode ID alongside all ManualEpisodeSearch events when publishing to the networking channel 2024-12-18 00:12:18 -07:00
924f8d5eff fix(sonarr): Pass events alongside all GetLogs events when publishing to the networking channel 2024-12-18 00:07:59 -07:00
64ecc38073 fix(sonarr): Pass the episode ID alongside all GetEpisodeHistory events when publishing to the networking channel 2024-12-18 00:05:22 -07:00
5f94dbcabe fix(sonarr): Pass series ID alongside all GetEpisodeFiles events when publishing to the networking channel 2024-12-17 23:59:49 -07:00
2ecc591966 fix(sonarr): Pass series ID alognside all GetEpisodes events when publishing to the networking channel 2024-12-17 23:57:13 -07:00
30ba1f3317 fix(sonarr): Pass the episode ID alongside all GetEpisodeDetails events when publishing to the networking channel 2024-12-17 23:52:18 -07:00
4fdf9b3df1 fix(sonarr): Pass history events alongside all GetHistory events when publishing to the networking channel 2024-12-17 23:40:23 -07:00
22fe1a8f73 fix(sonarr): Construct and pass edit series parameters alongside all EditSeries events when publishing to the networking channel 2024-12-17 23:37:18 -07:00
38c0ad29dd fix(sonarr): Construct and pass edit indexer parameters alongside all EditIndexer events when publishing to the networking channel 2024-12-17 23:22:56 -07:00
89d106c03e fix(sonarr): Construct and pass edit all indexer settings alongside all EditAllIndexerSettings events when publishing to the networking channel 2024-12-17 23:05:29 -07:00
3e36bcf307 fix(sonarr): Construct and pass delete series params alongside all DeleteSeries events when publishing to the networking channel 2024-12-17 22:56:14 -07:00
acf983c07c fix(sonarr): Corrected a bug that would cause a crash if a user spams the ESC key while searching for a new series and the search results are still loading 2024-12-17 22:45:34 -07:00
fedb78fb88 fix(sonarr): Pass the root folder ID alongside all DeleteRootFolder events when publishing to the networking channel 2024-12-17 22:42:37 -07:00
db64a0968b fix(sonarr): Pass the indexer ID alongside all DeleteIndexer events when publishing to the networking channel 2024-12-17 22:37:50 -07:00
aece20af47 fix(sonarr): Pass the episode file ID alongside all DeleteEpisodeFile events when publishing to the networking channel 2024-12-17 22:33:10 -07:00
6c5a73f78f fix(sonarr): Pass the download ID alongside all DeleteDownload events published to the networking channel 2024-12-17 22:27:08 -07:00
906e093152 fix(sonarr): Pass the blocklist item ID alongside the DeleteBlocklistItem event when publishing to the networking channel 2024-12-17 22:22:32 -07:00
478b4ae3c0 fix(sonarr): Construct and pass the add series body alongside AddSeries events when publishing to the networking channel 2024-12-17 22:16:43 -07:00
23971cbb76 fix(sonarr): Construct and pass the AddRootFolderBody alongside all AddRootFolder events when publishing to the networking channel 2024-12-17 21:48:52 -07:00
43410fac60 fix(radarr): Pass the movie ID alongside all UpdateAndScan events published to the networking channel 2024-12-17 21:34:14 -07:00
cb8035a2ce fix(radarr): Provide the movie ID alongside all TriggerAutomaticMovieSearch events when publishing to the networking channel 2024-12-17 21:26:34 -07:00
8d071c7674 fix(radarr): Pass in the indexer id with all TestIndexer events when publishing to the networking channel 2024-12-17 21:21:23 -07:00
965c488468 fix(radarr): Pass in the task name alongside the StartTask event when publishing to the networking channel 2024-12-17 21:13:47 -07:00
ede7f64c4b fix(radarr): Pass in the search query for the SearchNewMovie event when publishing to the networking channel 2024-12-17 21:06:07 -07:00
ba38dcdc15 fix(radarr): Pass in the movie ID alongside the GetReleases event when publishing to the networking channel 2024-12-17 20:52:31 -07:00
92d9222b05 fix(radarr): Pass in the movie ID alongside the GetMovieHistory event when publishing to the networking channel 2024-12-17 20:50:00 -07:00
4c396c3442 fix(radarr): Pass the movie ID in alongside the GetMovieDetaisl event when publishing to the networking channel 2024-12-17 20:47:29 -07:00
e1d5139e36 fix(radarr): Provide the movie id alongside the GetMovieCredits event when publishing to the networking channel 2024-12-17 20:42:52 -07:00
1ad35652f8 fix(radarr): Pass the number of log events to fetch in with the GetLogs event when publishing to the networking channel 2024-12-17 20:33:39 -07:00
9a9b13d604 fix(radarr): Construct and pass the edit movie parameters alongside the EditMovie event when publishing to the networking channel 2024-12-17 17:50:07 -07:00
77b8b61079 fix(radarr): Construct and pass params when publishing the EditIndexer event to the networking channel 2024-12-17 17:29:21 -07:00
bdf48d1bf4 fix(radarr): Construct and pass edit collection parameters alongside the EditCollection event when publishing to the networking channel 2024-12-17 16:32:35 -07:00
f8792ea012 fix(radarr): Build and pass the edit indexer settings body with the EditAllIndexerSettings event when publishing to the networking channel 2024-12-17 16:10:11 -07:00
4afde8b750 fix(radarr): Send the parameters alongside the DownloadRelease event when publishing to the networking channel 2024-12-17 15:56:58 -07:00
f5614995c7 fix(radarr): Pass the root folder ID in with the DeleteRootFolder event when publishing to the networking channel 2024-12-17 15:41:28 -07:00
9ea6dbec20 fix: Pass the delete movie params in with the DeleteMovie event when publishing to the networking channel 2024-12-17 15:35:29 -07:00
d73dfb9fc7 fix: Pass the indexer ID in with the DeleteIndexer event when sending to the networking channel 2024-12-17 15:21:34 -07:00
a7da73300c fix: Pass the download ID directly in the DeleteDownload event when publishing into the networking channel 2024-12-17 15:14:17 -07:00
a308b8fe95 fix: Blocklist Item ID passed in the DeleteBlocklistItem event when sent to the networking channel 2024-12-17 15:03:06 -07:00
1d1e42aeb1 fix: AddRootFolderBody now constructed prior to AddRootFolder event being sent down the network channel 2024-12-17 14:53:40 -07:00
1f81061152 Merge remote-tracking branch 'origin/main' into race-condition-refactor 2024-12-17 14:37:46 -07:00
368d5d3db7 fix: Cancel all requests when switching Servarr tabs to both improve performance and fix issue #15 2024-12-17 14:36:49 -07:00
0612a02d68 fix(add_movie_handler_tests): Added in a forgotten test for the build_add_movie_body function 2024-12-17 14:19:12 -07:00
a0d470087b Merge remote-tracking branch 'origin/main' into race-condition-refactor 2024-12-17 14:11:12 -07:00
3ecaf04eb4 fix: Missing tagged version of docker builds in release flow 2024-12-17 12:40:13 -07:00
df0811985d Fixed a bug in the release flow that published the docker image before the version bump 2024-12-17 12:15:36 -07:00
057ff0fef1 Fixed a bug in the release pipeline that created a conflict between the tag and the actual code 2024-12-16 21:09:24 -07:00
14c46f88ab fix: AddMovie Radarr event is now populated in the dispatch thread before being sent to the network thread 2024-12-16 15:31:26 -07:00
e38e430c77 fix: dynamically load servarrs in UI based on what configs are provided 2024-12-16 14:16:01 -07:00
Alex Clarke
93cd235aef docs: Update README.md to reference the Wekan board and not mention tracking the Beta release since it's live now 2024-12-14 02:26:38 -07:00
github-actions[bot]
df9bba32cb chore: Bump the version in Cargo.lock 2024-12-14 08:07:55 +00:00
github-actions[bot]
28a8f9b2fa bump: version 0.4.0 → 0.4.1 [skip ci] 2024-12-14 08:07:52 +00:00
e49b366d77 docs: Removed the docker version from the README [skip ci] 2024-12-14 00:46:35 -07:00
9a0963ca2c Merge remote-tracking branch 'origin/main' 2024-12-14 00:41:45 -07:00
17737a06a4 ci: Add the tar.gz files to the artifacts [skip ci] 2024-12-14 00:41:26 -07:00
github-actions[bot]
464779cc17 chore: Bump the version in Cargo.lock 2024-12-14 07:35:49 +00:00
github-actions[bot]
f25c0889a3 bump: version 0.3.7 → 0.4.0 [skip ci] 2024-12-14 07:35:46 +00:00
90170cb3d5 ci: Fixed a typo in the github-release job [skip ci] 2024-12-14 00:14:52 -07:00
4dcb141f3a ci: Fixed a typo in the docker release [skip ci] 2024-12-13 23:41:01 -07:00
133721917f ci: Attempting a different artifact job version to see if it corrects the error [skip ci] 2024-12-13 23:19:07 -07:00
766e23d265 ci: Use the same version of upload/download-artifact action [skip ci] 2024-12-13 23:06:36 -07:00
77d8e84e14 ci: Correct the artifact paths for the release [skip ci] 2024-12-13 22:42:42 -07:00
00c1cca412 ci: Fix the artifacts directory creation for Windows binaries [skip ci] 2024-12-13 22:24:03 -07:00
4968833d05 ci: Attempting a different way of creating the artifacts directory [skip ci] 2024-12-13 22:05:22 -07:00
d172fa17f6 ci: Attempting to fix the artifacts directory [skip ci] 2024-12-13 21:58:30 -07:00
3c99b38db7 ci: Support for arm64 docker builds 2024-12-13 21:40:53 -07:00
Alex Clarke
1128937cac Merge pull request #21 from Dark-Alex-17/sonarr-tui-support
Sonarr TUI support
2024-12-13 21:09:53 -07:00
6fc1228173 docs: Updated the README to include OS-specific steps for running Managarr on different platforms 2024-12-13 21:04:10 -07:00
b48a2efb7d fix(blocklist_handler): Fixed a breaking change between Sonarr v3 and v4 2024-12-13 20:48:10 -07:00
412cb2408e fix(style): Addressed linter complaints on formatting 2024-12-13 19:48:22 -07:00
682bc91855 fix: Implemented a handful of fixes that are breaking changes between Sonarr v3 and v4 2024-12-13 19:44:10 -07:00
f03120e5a1 ci: Updated CI to cross compile for a handful of additional architectures to increase availability 2024-12-13 17:58:59 -07:00
8dd63b30e8 feat(docs): Updated the README with new screeshots for the Sonarr release 2024-12-13 16:28:42 -07:00
54006c378f feat(handler): Support for toggling the monitoring status of a specified episode in the Sonarr UI 2024-12-13 16:18:02 -07:00
9269b66aa8 feat(handlers): Support for toggling the monitoring status of a season in the Sonarr UI 2024-12-13 16:10:06 -07:00
cfac433861 feat(keybindings): Added a new keybinding for toggling the monitoring of a highlighted table item 2024-12-13 14:53:39 -07:00
a28f8c3dd2 feat(cli): Support for toggling monitoring on a specific episode in Sonarr 2024-12-13 14:49:00 -07:00
4001dee1bd refactor(network): Changed the toggle episode monitoring handler to simply return empty since the response is always empty from Sonarr 2024-12-13 14:45:06 -07:00
d1ffd0d77f feat(network): Support for toggling the monitoring status of an episode in Sonarr 2024-12-13 14:40:11 -07:00
91ad50350d feat(cli): Support for toggling monitoring for a specific season in Sonarr 2024-12-13 14:09:11 -07:00
a88d43807e feat(network): Support for toggling monitoring/unmonitoring a season 2024-12-13 13:59:02 -07:00
98619664cf refactor(ui): Tweaked some of the color schemes in the series table 2024-12-13 13:10:57 -07:00
39f8ad2106 refactor: Fixed a couple of typos in some test function names 2024-12-13 11:51:23 -07:00
82ce38d7b5 feat(handlers): Support for the episode details popup 2024-12-12 18:52:27 -07:00
12eb453fc7 feat(ui): Support for the episode details UI 2024-12-12 16:25:02 -07:00
a84324d3bc feat(handler): Full handler support for the Season details UI in Sonarr 2024-12-11 23:18:37 -07:00
ed2211586e refactor(handlers): Refactored the handlers to all use the handle_table_events macro when appropriate and created tests for the macro so tests don't have to be duplicated across each handler 2024-12-11 17:03:52 -07:00
c09950d0af refactor(ui): Simplified the popup delegation so all future UI is easier to implement 2024-12-11 15:08:52 -07:00
e9a30382a3 feat(ui): Sonarr support for viewing season details 2024-12-10 18:23:09 -07:00
7bf3311102 feat(cli): Sonarr support for fetching a list of all episode files for a given series ID 2024-12-10 16:32:35 -07:00
cbad40245f feat(app): Dispatch support for Season Details to fetch both the current downloads as well as the episode files to match qualities to them 2024-12-10 16:23:30 -07:00
75c4fcbb9e feat(network): Support for fetching all episode files for a given series 2024-12-10 16:22:02 -07:00
f3b7f155b7 feat(app): Model and modal support for the season and episode details popups 2024-12-09 15:15:09 -07:00
6427a80bd1 feat(cli): Sonarr support for fetching season history events 2024-12-09 14:30:07 -07:00
5b65e87225 feat(network): Sonarr support for fetching season history 2024-12-09 14:15:47 -07:00
1b8b19fde5 refactor(indexers_handler): Use the new handle_table_events macro 2024-12-08 14:42:18 -07:00
03d7aed258 refactor(root_folders_handler): Use the new handle_table_events macro 2024-12-08 14:38:26 -07:00
23d149093f refactor(blocklist_handler): Use the new handle_table_events macro 2024-12-08 14:34:47 -07:00
27f12716d9 refactor(downloads_handler): Use the new handle_table_events macro 2024-12-08 14:28:12 -07:00
048877bbb6 refactor(collection_details_handler): use the new handle_table_events macro 2024-12-08 14:22:59 -07:00
87a652d911 refactor(collections_handler): Use the new handle_table_events macro 2024-12-08 14:14:24 -07:00
d6863dc1fd refactor(movie_details_handler): Use the new handle_table_events macro 2024-12-08 14:04:34 -07:00
f1d934b0a6 refactor(library_handler): Radarr use the new handle_table_events macro 2024-12-08 13:43:01 -07:00
5850f7a621 refactor(indexers_handler): Use the new handle_table_events macro 2024-12-08 13:26:59 -07:00
dd23e84ccf refactor(indexers_handler): Use the new handle_table_events macro 2024-12-08 13:24:18 -07:00
b060518778 refactor(root_folder_handler): Use the new handle_table_events macro 2024-12-08 13:15:59 -07:00
de95f13feb fix(handler_tests): Fixed all delegation tests to have initial conditions set properly 2024-12-08 13:10:17 -07:00
0205f13e53 refactor(history_handler): Use the new handle_table_event macro 2024-12-08 13:08:43 -07:00
b4de97dfe2 refactor(blocklist_handler): Use the new handle_table_events macro 2024-12-08 12:39:07 -07:00
35bc6cf31c refactor(downloads_handler): Use the new handle_table_events macro 2024-12-08 12:35:12 -07:00
c58e8b1a00 refactor(series_details_handler): Use the new handle_table_events macro 2024-12-08 12:29:59 -07:00
accdf99503 fix(ui): Fixed a bug that requires a minimum height for all popups so all error messages and other simple popups appear 2024-12-07 19:36:43 -07:00
47b609369b refactor(handler): Created a macro to handle all table key events to reduce code duplication and make future implementations faster; Only refactored the Sonarr library to use it thus far 2024-12-07 19:20:13 -07:00
23b1ca4371 feat(ui): Sonarr support for the series details popup 2024-12-06 20:30:26 -07:00
73d666d1f5 feat(ui): Sonarr support for editing a series from within the series details popup 2024-12-05 19:11:54 -07:00
b27c13cf74 fix(handler): Fixed a bug in the history handler that wouldn't reset the filter or search if a user hit 'esc' on the History tab 2024-12-05 19:08:11 -07:00
bd1a4f0939 feat(ui): Sonarr Series details UI is now available 2024-12-05 19:07:45 -07:00
5abed23cf2 refactor(ui): all table search and filter functionality is now available directly through the ManagarrTable widget to make life easier moving forward 2024-12-05 19:07:03 -07:00
9d0948e124 refactor(keys): Created a auto search key instead of reusing the existing search key to make things easier 2024-12-05 12:29:09 -07:00
678bc77a23 fix(ui): Fix the System Details Tasks popup to be navigable in both Sonarr and Radarr 2024-12-05 11:45:46 -07:00
00cdeee5c6 feat(ui): Full Sonarr system tab support
Signed-off-by: Alex Clarke <alex.j.tusa@gmail.com>
2024-12-04 17:41:30 -07:00
2d251554ad feat(handler): System handler support for Sonarr 2024-12-04 17:04:36 -07:00
1b5d70ae2d perf: Improved performance by optimizing API calls to only refresh when the tick prompts a refresh. All UI is now significantly faster 2024-12-04 16:46:06 -07:00
2d2901f6dc feat(ui): Full Sonarr support for the indexer tab 2024-12-04 16:39:37 -07:00
a0b27ec105 feat(ui): Support for modifying the indexer priority in Radarr 2024-12-03 18:12:23 -07:00
093ef136e7 feat(handler): Full indexer tab handler support 2024-12-03 17:46:37 -07:00
8660de530d feat(ui): Root folder tab support 2024-12-03 16:24:23 -07:00
bda6f253e0 feat(handlers): Support for root folder actions 2024-12-03 16:18:39 -07:00
4eb974567f feat(ui): History tab support 2024-12-02 18:47:50 -07:00
4f5bad5874 feat(handler): History tab support 2024-12-02 18:03:59 -07:00
1c6e798632 feat(ui): Blocklist UI support 2024-12-02 16:54:27 -07:00
3186fb42e7 feat(handler): Wired in the blocklist handler to the main handlers 2024-12-02 16:39:40 -07:00
4b7185fbb0 feat(handler): Blocklist handler support 2024-12-02 16:37:46 -07:00
f0d8555a8a feat(ui): Downloads tab support 2024-12-02 15:57:48 -07:00
f338dfcb12 feat(handler): Download tab support 2024-12-02 15:40:11 -07:00
188d781b0d feat(ui): Edit series support 2024-12-02 15:31:12 -07:00
adb1f07fd0 feat(handler): Edit series support 2024-12-02 14:58:51 -07:00
82e51be096 feat(ui): Add series support Sonarr 2024-12-02 13:53:28 -07:00
d7f6d12f59 feat(handler): Add series support for Sonarr 2024-12-02 12:43:17 -07:00
0db57fbff1 feat(ui): Delete a series 2024-12-02 11:45:13 -07:00
b1bdc19afb feat(handler): Support for deleting a series in Sonarr 2024-12-02 11:30:34 -07:00
b75a95a708 feat(ui): Support for the Series table 2024-12-01 14:08:06 -07:00
c3fb5dcd5f feat(handlers): Sonarr key support for the Series table 2024-12-01 13:48:48 -07:00
21911f93d1 feat(models): Added the necessary contextual help and tabs for the Sonarr UI 2024-12-01 12:05:20 -07:00
f7c96d81e9 refactor(BlockSelectionState): Refactored so selection of blocks in 2x2 grids is more intuitive and added left() and right() methods to aid this effort. 2024-11-30 12:22:46 -07:00
9b2040059d fix(ui): Fixed a potential rare bug in the UI where the application would panic if the height of the downloads window is 0. 2024-11-29 16:31:51 -07:00
08f190fc6e feat(ui): Initial UI support for switching to Sonarr tabs 2024-11-29 15:58:19 -07:00
4d1b0fe301 docs(context): Updated the Servarr context clues to say how to switch Servarr tabs via TAB and SHIFT+TAB 2024-11-27 17:14:40 -07:00
f139db07d9 feat(app): Dispatch support for all relevant Sonarr blocks 2024-11-27 17:06:20 -07:00
Alex Clarke
73a4129000 Update environment variables table so it appears better in Crates.io [skip ci] 2024-11-25 20:25:04 -07:00
1ddf797e28 ci: Updated the release so all GitHub release names are correctly simply the version: v0.1.2 [skip ci] 2024-11-25 20:11:15 -07:00
github-actions[bot]
18280f0478 chore: Bump the version in Cargo.lock 2024-11-26 03:04:19 +00:00
github-actions[bot]
4348705a0a bump: version 0.3.6 → 0.3.7 [skip ci] 2024-11-26 03:04:17 +00:00
306 changed files with 46841 additions and 18099 deletions
+3
View File
@@ -4,6 +4,9 @@ set -e
echo "Running pre-push hook:" echo "Running pre-push hook:"
echo "Executing: cargo fmt"
cargo fmt
echo "Executing: make lint" echo "Executing: make lint"
make lint make lint
+3
View File
@@ -4,6 +4,9 @@ set -e
echo "Running pre-push hook:" echo "Running pre-push hook:"
echo "Executing: cargo fmt --check"
cargo fmt --check
echo "Executing: make lint" echo "Executing: make lint"
make lint make lint
+15 -6
View File
@@ -11,8 +11,6 @@ name: Check
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
@@ -24,14 +22,18 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt components: rustfmt
- name: Run cargo fmt - name: Run cargo fmt
run: cargo fmt -- --check run: cargo fmt -- --check
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
clippy: clippy:
name: ${{ matrix.toolchain }} / clippy name: ${{ matrix.toolchain }} / clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -45,12 +47,15 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
- name: Run clippy action - name: Run clippy action
uses: clechasseur/rs-clippy-check@v3 uses: clechasseur/rs-clippy-check@v3
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
doc: doc:
@@ -61,21 +66,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust nightly - name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
- name: Run cargo doc - name: Run cargo doc
run: cargo doc --no-deps --all-features run: cargo doc --no-deps --all-features
env: env:
RUSTDOCFLAGS: --cfg docsrs RUSTDOCFLAGS: --cfg docsrs
msrv: msrv:
# check that we can build using the minimal rust version that is specified by this crate # check that we can build using the minimal rust version that is specified by this crate
name: 1.82.0 / check name: 1.85.0 / check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install 1.82.0
- name: Install 1.85.0
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.82.0 toolchain: 1.85.0
- name: cargo +1.82.0 check
- name: cargo +1.85.0 check
run: cargo check run: cargo check
+388 -17
View File
@@ -18,7 +18,8 @@ on:
- major - major
jobs: jobs:
bump: bump-version:
name: bump-version
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Configure SSH for Git - name: Configure SSH for Git
@@ -70,9 +71,11 @@ jobs:
- name: Get the new version tag - name: Get the new version tag
id: version id: version
run: | run: |
mkdir -p artifacts
NEW_TAG=$(cz version --project) NEW_TAG=$(cz version --project)
echo "New version: $NEW_TAG" echo "New version: $NEW_TAG"
echo "version=$NEW_TAG" >> $GITHUB_ENV echo "version=$NEW_TAG" >> $GITHUB_ENV
echo "$NEW_TAG" > artifacts/release-version
- name: Get the previous version tag - name: Get the previous version tag
id: prev_version id: prev_version
@@ -85,19 +88,8 @@ jobs:
id: changelog id: changelog
run: | run: |
changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }}) changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }})
echo "$changelog" > changelog.md echo "$changelog" > artifacts/changelog.md
echo "changelog_body=$(cat changelog.md)" >> $GITHUB_ENV echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
- name: Create a GitHub Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ env.version }}
name: "Release v${{ env.version }}"
body: ${{ env.changelog_body }}
draft: false
prerelease: false
- name: Push changes - name: Push changes
env: env:
@@ -105,9 +97,377 @@ jobs:
run: | run: |
git push origin --follow-tags git push origin --follow-tags
release-crate: - name: Upload artifacts
needs: bump uses: actions/upload-artifact@v4
name: Release Crate with:
path: artifacts
build-release-artifacts:
name: build-release
needs: [bump-version]
runs-on: ${{ matrix.job.os }}
env:
RUST_BACKTRACE: 1
strategy:
fail-fast: true
matrix:
# prettier-ignore
job:
- { name: "macOS-arm64", os: "macOS-latest", target: "aarch64-apple-darwin", artifact_suffix: "macos-arm64", use-cross: true }
- { name: "macOS-amd64", os: "macOS-latest", target: "x86_64-apple-darwin", artifact_suffix: "macos" }
- { name: "windows-amd64", os: "windows-latest", target: "x86_64-pc-windows-msvc", artifact_suffix: "windows" }
- { name: "windows-aarch64", os: "windows-latest", target: "aarch64-pc-windows-msvc", artifact_suffix: "windows-aarch64", use-cross: true }
- { name: "linux-gnu", os: "ubuntu-latest", target: "x86_64-unknown-linux-gnu", artifact_suffix: "linux" }
- { name: "linux-musl", os: "ubuntu-latest", target: "x86_64-unknown-linux-musl", artifact_suffix: "linux-musl", use-cross: true, }
- { name: "linux-aarch64-gnu", os: "ubuntu-latest", target: "aarch64-unknown-linux-gnu", artifact_suffix: "aarch64-gnu", use-cross: true, test-bin: "--bin managarr" }
- { name: "linux-aarch64-musl", os: "ubuntu-latest", target: "aarch64-unknown-linux-musl", artifact_suffix: "aarch64-musl", use-cross: true, test-bin: "--bin managarr" }
- { name: "linux-arm-gnu", os: "ubuntu-latest", target: "arm-unknown-linux-gnueabi", artifact_suffix: "armv6-gnu", use-cross: true, test-bin: "--bin managarr" }
- { name: "linux-arm-musl", os: "ubuntu-latest", target: "arm-unknown-linux-musleabihf", artifact_suffix: "armv6-musl", use-cross: true, test-bin: "--bin managarr" }
- { name: "linux-armv7-gnu", os: "ubuntu-latest", target: "armv7-unknown-linux-gnueabihf", artifact_suffix: "armv7-gnu", use-cross: true, test-bin: "--bin managarr" }
- { name: "linux-armv7-musl", os: "ubuntu-latest", target: "armv7-unknown-linux-musleabihf", artifact_suffix: "armv7-musl", use-cross: true, test-bin: "--bin managarr" }
rust: [stable]
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Ensure repository is up-to-date
run: |
git fetch --all
git pull
- uses: actions/cache@v3
name: Cache Cargo registry
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- uses: actions/cache@v3
if: startsWith(matrix.job.name, 'linux-')
with:
path: ~/.cargo/bin
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
- uses: dtolnay/rust-toolchain@stable
name: Set Rust toolchain
with:
targets: ${{ matrix.job.target }}
- uses: taiki-e/setup-cross-toolchain-action@v1
with:
# NB: sets CARGO_BUILD_TARGET evar - do not need --target flag in build
target: ${{ matrix.job.target }}
- uses: taiki-e/install-action@cross
if: ${{ matrix.job.use-cross }}
- name: Installing needed Ubuntu dependencies
if: matrix.job.os == 'ubuntu-latest'
shell: bash
run: |
sudo apt-get -y update
case ${{ matrix.job.target }} in
arm*-linux-*) sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
aarch64-*-linux-*) sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac
- name: Build
run: cargo build --release --verbose --target=${{ matrix.job.target }} --locked
- name: Verify file
shell: bash
run: |
file target/${{ matrix.job.target }}/release/managarr
- name: Test
if: matrix.job.target != 'aarch64-apple-darwin' && matrix.job.target != 'aarch64-pc-windows-msvc'
run: cargo test --release --verbose --target=${{ matrix.job.target }} ${{ matrix.job.test-bin }}
- name: Packaging final binary (Windows)
if: matrix.job.os == 'windows-latest'
shell: bash
run: |
cd target/${{ matrix.job.target }}/release
BINARY_NAME=managarr.exe
if [ "${{ matrix.job.target }}" != "aarch64-pc-windows-msvc" ]; then
# strip the binary
strip $BINARY_NAME
fi
RELEASE_NAME=managarr-${{ matrix.job.artifact_suffix }}
mkdir -p artifacts
tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME
# create sha checksum files
certutil -hashfile $RELEASE_NAME.tar.gz sha256 | grep -E [A-Fa-f0-9]{64} > $RELEASE_NAME.sha256
echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV
- name: Packaging final binary (macOS and Linux)
if: matrix.job.os != 'windows-latest'
shell: bash
run: |
# set the right strip executable
STRIP="strip";
case ${{ matrix.job.target }} in
arm*-linux-*) STRIP="arm-linux-gnueabihf-strip" ;;
aarch64-*-linux-*) STRIP="aarch64-linux-gnu-strip" ;;
esac;
cd target/${{ matrix.job.target }}/release
BINARY_NAME=managarr
# strip the binary
"$STRIP" "$BINARY_NAME"
RELEASE_NAME=managarr-${{ matrix.job.artifact_suffix }}
tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME
# create sha checksum files
shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.sha256
echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV
- name: Add artifacts
run: |
mkdir -p artifacts
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.tar.gz artifacts/
cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ env.RELEASE_NAME }}
path: artifacts
overwrite: true
publish-github-release:
name: publish-github-release
needs: [build-release-artifacts]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Ensure repository is up-to-date
run: |
git fetch --all
git pull
- name: Set environment variables
run: |
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
changelog_body="$(cat ./artifacts/changelog.md)"
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
- name: Create a GitHub Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
artifacts/managarr-macos-arm64.tar.gz
artifacts/managarr-macos-arm64.sha256
artifacts/managarr-macos.tar.gz
artifacts/managarr-macos.sha256
artifacts/managarr-windows.tar.gz
artifacts/managarr-windows.sha256
artifacts/managarr-windows-aarch64.tar.gz
artifacts/managarr-windows-aarch64.sha256
artifacts/managarr-linux.tar.gz
artifacts/managarr-linux.sha256
artifacts/managarr-linux-musl.tar.gz
artifacts/managarr-linux-musl.sha256
artifacts/managarr-aarch64-gnu.tar.gz
artifacts/managarr-aarch64-gnu.sha256
artifacts/managarr-aarch64-musl.tar.gz
artifacts/managarr-aarch64-musl.sha256
artifacts/managarr-armv6-gnu.tar.gz
artifacts/managarr-armv6-gnu.sha256
artifacts/managarr-armv6-musl.tar.gz
artifacts/managarr-armv6-musl.sha256
artifacts/managarr-armv7-gnu.tar.gz
artifacts/managarr-armv7-gnu.sha256
artifacts/managarr-armv7-musl.tar.gz
artifacts/managarr-armv7-musl.sha256
tag_name: v${{ env.RELEASE_VERSION }}
name: "v${{ env.RELEASE_VERSION }}"
body: ${{ env.changelog_body }}
draft: false
prerelease: false
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
path: artifacts
overwrite: true
publish-chocolatey-package:
needs: [publish-github-release]
name: Publish Chocolatey Package
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get release artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Set release assets and version
shell: pwsh
run: |
# Read the first column from the SHA256 file
$windows_sha = Get-Content ./artifacts/managarr-windows.sha256 | ForEach-Object { $_.Split(' ')[0] }
Add-Content -Path $env:GITHUB_ENV -Value "WINDOWS_SHA=$windows_sha"
# Read the release version from the release-version file
$release_version = Get-Content ./artifacts/release-version
Add-Content -Path $env:GITHUB_ENV -Value "RELEASE_VERSION=$release_version"
- name: Validate release environment variables
run: |
echo "Release SHA windows: ${{ env.WINDOWS_SHA }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Package and Publish package to Chocolatey
run: |
mkdir ./deployment/chocolatey/tools
# Run packaging script
python "./deployment/chocolatey/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/chocolatey/managarr.nuspec.template" "./deployment/chocolatey/managarr.nuspec" ${{ env.WINDOWS_SHA }}
python "./deployment/chocolatey/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/chocolatey/chocolateyinstall.ps1.template" "./deployment/chocolatey/tools/chocolateyinstall.ps1" ${{ env.WINDOWS_SHA }}
# Publish to Chocolatey
cd ./deployment/chocolatey
choco pack
echo y | choco install managarr -dv -s .
$version = managarr --version
$version = $version -replace " ", "."
choco push $version.nupkg -s https://push.chocolatey.org/ --api-key ${{ secrets.CHOCOLATEY_API_KEY }};
publish-homebrew-formula:
needs: [publish-github-release]
name: Update Homebrew formulas
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get release artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Set release assets and version
shell: bash
run: |
# Set environment variables
macos_sha="$(cat ./artifacts/managarr-macos.sha256 | awk '{print $1}')"
echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV
macos_sha_arm="$(cat ./artifacts/managarr-macos-arm64.sha256 | awk '{print $1}')"
echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV
linux_sha="$(cat ./artifacts/managarr-linux-musl.sha256 | awk '{print $1}')"
echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release SHA macos: ${{ env.MACOS_SHA }}"
echo "Release SHA macos-arm: ${{ env.MACOS_SHA_ARM }}"
echo "Release SHA linux musl: ${{ env.LINUX_SHA }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Execute Homebrew packaging script
run: |
# run packaging script
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/managarr.rb.template" "./managarr.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
- name: Push changes to Homebrew tap
env:
TOKEN: ${{ secrets.MANAGARR_GITHUB_TOKEN }}
run: |
# push to Git
git config --global user.name "Dark-Alex-17"
git config --global user.email "alex.j.tusa@gmail.com"
git clone https://Dark-Alex-17:${{ secrets.MANAGARR_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-managarr.git
rm homebrew-managarr/Formula/managarr.rb
cp managarr.rb homebrew-managarr/Formula
cd homebrew-managarr
git add .
git diff-index --quiet HEAD || git commit -am "Update formula for Managarr release ${{ env.RELEASE_VERSION }}"
git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-managarr.git
publish-docker-image:
needs: [publish-github-release]
name: Publishing Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Get release artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Ensure repository is up-to-date
run: |
git fetch --all
git pull
- name: Set version variable
run: |
version="$(cat artifacts/release-version)"
echo "version=$version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release version: ${{ env.version }}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: darkalex17/managarr:latest, darkalex17/managarr:${{ env.version }}
publish-crate:
needs: publish-github-release
name: Publish Crate
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if actor is repository owner - name: Check if actor is repository owner
@@ -126,6 +486,17 @@ jobs:
git fetch --all git fetch --all
git pull git pull
- uses: actions/cache@v3
name: Cache Cargo registry
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- uses: actions/cache@v3
with:
path: ~/.cargo/bin
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
+62 -39
View File
@@ -34,55 +34,66 @@ jobs:
toolchain: [stable, beta] toolchain: [stable, beta]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install ${{ matrix.toolchain }} - name: Install ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
# enable this ci template to run regardless of whether the lockfile is checked in or not # enable this ci template to run regardless of whether the lockfile is checked in or not
- name: cargo generate-lockfile - name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == '' if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile run: cargo generate-lockfile
- name: cargo test --locked - name: cargo test --locked
run: cargo test --locked --all-features --all-targets run: cargo test --locked --all-features --all-targets
minimal-versions:
# This action chooses the oldest version of the dependencies permitted by Cargo.toml to ensure # minimal-versions:
# that this crate is compatible with the minimal version that this crate and its dependencies # # This action chooses the oldest version of the dependencies permitted by Cargo.toml to ensure
# require. This will pickup issues where this create relies on functionality that was introduced # # that this crate is compatible with the minimal version that this crate and its dependencies
# later than the actual version specified (e.g., when we choose just a major version, but a # # require. This will pickup issues where this create relies on functionality that was introduced
# method was added after this version). # # later than the actual version specified (e.g., when we choose just a major version, but a
# # # method was added after this version).
# This particular check can be difficult to get to succeed as often transitive dependencies may # #
# be incorrectly specified (e.g., a dependency specifies 1.0 but really requires 1.1.5). There # # This particular check can be difficult to get to succeed as often transitive dependencies may
# is an alternative flag available -Zdirect-minimal-versions that uses the minimal versions for # # be incorrectly specified (e.g., a dependency specifies 1.0 but really requires 1.1.5). There
# direct dependencies of this crate, while selecting the maximal versions for the transitive # # is an alternative flag available -Zdirect-minimal-versions that uses the minimal versions for
# dependencies. Alternatively, you can add a line in your Cargo.toml to artificially increase # # direct dependencies of this crate, while selecting the maximal versions for the transitive
# the minimal dependency, which you do with e.g.: # # dependencies. Alternatively, you can add a line in your Cargo.toml to artificially increase
# ```toml # # the minimal dependency, which you do with e.g.:
# # for minimal-versions # # ```toml
# [target.'cfg(any())'.dependencies] # # # for minimal-versions
# openssl = { version = "0.10.55", optional = true } # needed to allow foo to build with -Zminimal-versions # # [target.'cfg(any())'.dependencies]
# ``` # # openssl = { version = "0.10.55", optional = true } # needed to allow foo to build with -Zminimal-versions
# The optional = true is necessary in case that dependency isn't otherwise transitively required # # ```
# by your library, and the target bit is so that this dependency edge never actually affects # # The optional = true is necessary in case that dependency isn't otherwise transitively required
# Cargo build order. See also # # by your library, and the target bit is so that this dependency edge never actually affects
# https://github.com/jonhoo/fantoccini/blob/fde336472b712bc7ebf5b4e772023a7ba71b2262/Cargo.toml#L47-L49. # # Cargo build order. See also
# This action is run on ubuntu with the stable toolchain, as it is not expected to fail # # https://github.com/jonhoo/fantoccini/blob/fde336472b712bc7ebf5b4e772023a7ba71b2262/Cargo.toml#L47-L49.
runs-on: ubuntu-latest # # This action is run on ubuntu with the stable toolchain, as it is not expected to fail
name: ubuntu / stable / minimal-versions # runs-on: ubuntu-latest
steps: # name: ubuntu / stable / minimal-versions
- uses: actions/checkout@v4 # steps:
- name: Install Rust stable # - uses: actions/checkout@v4
uses: dtolnay/rust-toolchain@stable
- name: Install nightly for -Zdirect-minimal-versions # - name: Install Rust stable
uses: dtolnay/rust-toolchain@nightly # uses: dtolnay/rust-toolchain@stable
- name: rustup default stable
run: rustup default stable # - name: Install nightly for -Zdirect-minimal-versions
- name: cargo update -Zdirect-minimal-versions # uses: dtolnay/rust-toolchain@nightly
run: cargo +nightly update -Zdirect-minimal-versions
- name: cargo test # - name: rustup default stable
run: cargo test --locked --all-features --all-targets # run: rustup default stable
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 # - name: cargo update -Zdirect-minimal-versions
# run: cargo +nightly update -Zdirect-minimal-versions
# - name: cargo test
# run: cargo test --locked --all-features --all-targets
# - name: Cache Cargo dependencies
# uses: Swatinem/rust-cache@v2
os-check: os-check:
# run cargo test on mac and windows # run cargo test on mac and windows
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -100,15 +111,20 @@ jobs:
# if: runner.os == 'Windows' # if: runner.os == 'Windows'
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: cargo generate-lockfile - name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == '' if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile run: cargo generate-lockfile
- name: cargo test - name: cargo test
run: cargo test --locked --all-features --all-targets run: cargo test --locked --all-features --all-targets
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
coverage: coverage:
# use llvm-cov to build and collect coverage and outputs in a format that # use llvm-cov to build and collect coverage and outputs in a format that
# is compatible with codecov.io # is compatible with codecov.io
@@ -136,21 +152,28 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: llvm-tools-preview components: llvm-tools-preview
- name: cargo install cargo-llvm-cov - name: cargo install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov
- name: cargo generate-lockfile - name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == '' if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile run: cargo generate-lockfile
- name: cargo llvm-cov - name: cargo llvm-cov
run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info
- name: Record Rust version - name: Record Rust version
run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV" run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV"
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Upload to codecov.io - name: Upload to codecov.io
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:
+193
View File
@@ -5,6 +5,199 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v0.5.1 (2025-03-01)
### Feat
- CLI Support for multiple Servarr instances
- Support for multiple servarr definitions - no tests [skip ci]
- Support for loading Servarr API tokens from a file
- Tweaked the implementation for environment variables in the config a bit
- var interpolation
### Fix
- Updated openssl to 0.10.70 to mitigate CVE-2025-24898
- Addressed rustfmt complaints
- Corrected typo in the managarr.nuspec.template
### Refactor
- Updated dependencies
- Addressed Cargo fmt complaints
- Added a debug line for logging to output the config used when starting Managarr
- Updated the 2018 idiom lint to the 2021_compatibility lint
- Removed unnecessary clones in the networking module to speed up network request handling
- Corrected some clone instead of copy behaviors in the command line handlers
- Removed unnecessary clone from stateful table
- Removed unnecessary clone call from extract_and_add_tag_ids_vec method
- Reduced the number of clones necessary when building modal structs
- Refactored a handful of Option calls to use take instead
- Renamed KeyEventHandler::with to KeyEventHandler::new to keep with Rust best practices and conventions
## v0.4.2 (2024-12-21)
### Fix
- Revert failed release [skip ci]
- **sonarr**: Pass the series ID alongside all UpdateAndScan events when publishing to the networking channel
- **sonarr**: pass the series ID alongside all TriggerAutomaticSeriesSearch events when publishing to the networking channel
- **sonarr**: Pass the series ID and season number alongside all TriggerAutomaticSeasonSearch events when publishing to the networking channel
- **sonarr**: Pass the episode ID alongside all TriggerAutomaticEpisodeSearch events when publishing to the networking channel
- **sonarr**: Pass the episode ID alongside all ToggleEpisodeMonitoring events when publishing to the networking channel
- **sonarr**: Pass the series ID and season number alongside all toggle season monitoring events when publishing to the networking channel
- **sonarr**: Pass the indexer ID directly alongside all TestIndexer events when publishing to the networking channel
- **sonarr**: Provide the task name directly alongside all StartTask events when publishing to the networking channel
- **sonarr**: Pass the search query directly to the networking channel when searching for a new series
- **sonarr**: Pass the series ID alongside all GetSeriesHistory events when publishing to the networking channel
- **sonarr**: Pass the series ID alongside all GetSeriesDetails events when publishing to the networking channel
- **sonarr**: Pass series ID and season number alongside all ManualSeasonSearch events when publishing to the networking channel
- **sonarr**: Provide the series ID and season number alongside all GetSeasonHistory events when publishing to the networking channel
- **sonarr**: Pass the episode ID alongside all ManualEpisodeSearch events when publishing to the networking channel
- **sonarr**: Pass events alongside all GetLogs events when publishing to the networking channel
- **sonarr**: Pass the episode ID alongside all GetEpisodeHistory events when publishing to the networking channel
- **sonarr**: Pass series ID alongside all GetEpisodeFiles events when publishing to the networking channel
- **sonarr**: Pass series ID alognside all GetEpisodes events when publishing to the networking channel
- **sonarr**: Pass the episode ID alongside all GetEpisodeDetails events when publishing to the networking channel
- **sonarr**: Pass history events alongside all GetHistory events when publishing to the networking channel
- **sonarr**: Construct and pass edit series parameters alongside all EditSeries events when publishing to the networking channel
- **sonarr**: Construct and pass edit indexer parameters alongside all EditIndexer events when publishing to the networking channel
- **sonarr**: Construct and pass edit all indexer settings alongside all EditAllIndexerSettings events when publishing to the networking channel
- **sonarr**: Construct and pass delete series params alongside all DeleteSeries events when publishing to the networking channel
- **sonarr**: Corrected a bug that would cause a crash if a user spams the ESC key while searching for a new series and the search results are still loading
- **sonarr**: Pass the root folder ID alongside all DeleteRootFolder events when publishing to the networking channel
- **sonarr**: Pass the indexer ID alongside all DeleteIndexer events when publishing to the networking channel
- **sonarr**: Pass the episode file ID alongside all DeleteEpisodeFile events when publishing to the networking channel
- **sonarr**: Pass the download ID alongside all DeleteDownload events published to the networking channel
- **sonarr**: Pass the blocklist item ID alongside the DeleteBlocklistItem event when publishing to the networking channel
- **sonarr**: Construct and pass the add series body alongside AddSeries events when publishing to the networking channel
- **sonarr**: Construct and pass the AddRootFolderBody alongside all AddRootFolder events when publishing to the networking channel
- **radarr**: Pass the movie ID alongside all UpdateAndScan events published to the networking channel
- **radarr**: Provide the movie ID alongside all TriggerAutomaticMovieSearch events when publishing to the networking channel
- **radarr**: Pass in the indexer id with all TestIndexer events when publishing to the networking channel
- **radarr**: Pass in the task name alongside the StartTask event when publishing to the networking channel
- **radarr**: Pass in the search query for the SearchNewMovie event when publishing to the networking channel
- **radarr**: Pass in the movie ID alongside the GetReleases event when publishing to the networking channel
- **radarr**: Pass in the movie ID alongside the GetMovieHistory event when publishing to the networking channel
- **radarr**: Pass the movie ID in alongside the GetMovieDetaisl event when publishing to the networking channel
- **radarr**: Provide the movie id alongside the GetMovieCredits event when publishing to the networking channel
- **radarr**: Pass the number of log events to fetch in with the GetLogs event when publishing to the networking channel
- **radarr**: Construct and pass the edit movie parameters alongside the EditMovie event when publishing to the networking channel
- **radarr**: Construct and pass params when publishing the EditIndexer event to the networking channel
- **radarr**: Construct and pass edit collection parameters alongside the EditCollection event when publishing to the networking channel
- **radarr**: Build and pass the edit indexer settings body with the EditAllIndexerSettings event when publishing to the networking channel
- **radarr**: Send the parameters alongside the DownloadRelease event when publishing to the networking channel
- **radarr**: Pass the root folder ID in with the DeleteRootFolder event when publishing to the networking channel
- Pass the delete movie params in with the DeleteMovie event when publishing to the networking channel
- Pass the indexer ID in with the DeleteIndexer event when sending to the networking channel
- Pass the download ID directly in the DeleteDownload event when publishing into the networking channel
- Blocklist Item ID passed in the DeleteBlocklistItem event when sent to the networking channel
- AddRootFolderBody now constructed prior to AddRootFolder event being sent down the network channel
- Cancel all requests when switching Servarr tabs to both improve performance and fix issue #15
- **add_movie_handler_tests**: Added in a forgotten test for the build_add_movie_body function
- Missing tagged version of docker builds in release flow
- AddMovie Radarr event is now populated in the dispatch thread before being sent to the network thread
- dynamically load servarrs in UI based on what configs are provided
## v0.4.1 (2024-12-14)
### Feat
- **docs**: Updated the README with new screeshots for the Sonarr release
- **handler**: Support for toggling the monitoring status of a specified episode in the Sonarr UI
- **handlers**: Support for toggling the monitoring status of a season in the Sonarr UI
- **keybindings**: Added a new keybinding for toggling the monitoring of a highlighted table item
- **cli**: Support for toggling monitoring on a specific episode in Sonarr
- **network**: Support for toggling the monitoring status of an episode in Sonarr
- **cli**: Support for toggling monitoring for a specific season in Sonarr
- **network**: Support for toggling monitoring/unmonitoring a season
- **handlers**: Support for the episode details popup
- **ui**: Support for the episode details UI
- **handler**: Full handler support for the Season details UI in Sonarr
- **ui**: Sonarr support for viewing season details
- **cli**: Sonarr support for fetching a list of all episode files for a given series ID
- **app**: Dispatch support for Season Details to fetch both the current downloads as well as the episode files to match qualities to them
- **network**: Support for fetching all episode files for a given series
- **app**: Model and modal support for the season and episode details popups
- **cli**: Sonarr support for fetching season history events
- **network**: Sonarr support for fetching season history
- **ui**: Sonarr support for the series details popup
- **ui**: Sonarr support for editing a series from within the series details popup
- **ui**: Sonarr Series details UI is now available
- **ui**: Full Sonarr system tab support
- **handler**: System handler support for Sonarr
- **ui**: Full Sonarr support for the indexer tab
- **ui**: Support for modifying the indexer priority in Radarr
- **handler**: Full indexer tab handler support
- **ui**: Root folder tab support
- **handlers**: Support for root folder actions
- **ui**: History tab support
- **handler**: History tab support
- **ui**: Blocklist UI support
- **handler**: Wired in the blocklist handler to the main handlers
- **handler**: Blocklist handler support
- **ui**: Downloads tab support
- **handler**: Download tab support
- **ui**: Edit series support
- **handler**: Edit series support
- **ui**: Add series support Sonarr
- **handler**: Add series support for Sonarr
- **ui**: Delete a series
- **handler**: Support for deleting a series in Sonarr
- **ui**: Support for the Series table
- **handlers**: Sonarr key support for the Series table
- **models**: Added the necessary contextual help and tabs for the Sonarr UI
- **ui**: Initial UI support for switching to Sonarr tabs
- **app**: Dispatch support for all relevant Sonarr blocks
### Fix
- **blocklist_handler**: Fixed a breaking change between Sonarr v3 and v4
- **style**: Addressed linter complaints on formatting
- Implemented a handful of fixes that are breaking changes between Sonarr v3 and v4
- **handler_tests**: Fixed all delegation tests to have initial conditions set properly
- **ui**: Fixed a bug that requires a minimum height for all popups so all error messages and other simple popups appear
- **handler**: Fixed a bug in the history handler that wouldn't reset the filter or search if a user hit 'esc' on the History tab
- **ui**: Fix the System Details Tasks popup to be navigable in both Sonarr and Radarr
- **ui**: Fixed a potential rare bug in the UI where the application would panic if the height of the downloads window is 0.
### Refactor
- **network**: Changed the toggle episode monitoring handler to simply return empty since the response is always empty from Sonarr
- **ui**: Tweaked some of the color schemes in the series table
- Fixed a couple of typos in some test function names
- **handlers**: Refactored the handlers to all use the handle_table_events macro when appropriate and created tests for the macro so tests don't have to be duplicated across each handler
- **ui**: Simplified the popup delegation so all future UI is easier to implement
- **indexers_handler**: Use the new handle_table_events macro
- **root_folders_handler**: Use the new handle_table_events macro
- **blocklist_handler**: Use the new handle_table_events macro
- **downloads_handler**: Use the new handle_table_events macro
- **collection_details_handler**: use the new handle_table_events macro
- **collections_handler**: Use the new handle_table_events macro
- **movie_details_handler**: Use the new handle_table_events macro
- **library_handler**: Radarr use the new handle_table_events macro
- **indexers_handler**: Use the new handle_table_events macro
- **indexers_handler**: Use the new handle_table_events macro
- **root_folder_handler**: Use the new handle_table_events macro
- **history_handler**: Use the new handle_table_event macro
- **blocklist_handler**: Use the new handle_table_events macro
- **downloads_handler**: Use the new handle_table_events macro
- **series_details_handler**: Use the new handle_table_events macro
- **handler**: Created a macro to handle all table key events to reduce code duplication and make future implementations faster; Only refactored the Sonarr library to use it thus far
- **ui**: all table search and filter functionality is now available directly through the ManagarrTable widget to make life easier moving forward
- **keys**: Created a auto search key instead of reusing the existing search key to make things easier
- **BlockSelectionState**: Refactored so selection of blocks in 2x2 grids is more intuitive and added left() and right() methods to aid this effort.
### Perf
- Improved performance by optimizing API calls to only refresh when the tick prompts a refresh. All UI is now significantly faster
## v0.3.7 (2024-11-26)
### Fix
- **ci**: Forgot to also pull in the most recent changes [skip ci]
## v0.3.6 (2024-11-26) ## v0.3.6 (2024-11-26)
### Fix ### Fix
+17
View File
@@ -1,6 +1,23 @@
# Contributing # Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.** Contributors are very welcome! **No contribution is too small and all contributions are valued.**
## License and Attribution
#### _If you plan on contributing to the base project, no attribution is needed!_ Feel free to proceed to the [next steps](CONTRIBUTING.md#Rust).
Otherwise, below are key points to understand from the [Managarr License, Version 1.0](LICENSE):
1. **Non-Commercial Use**:
- Managarr is licensed solely for non-commercial purposes. Any commercial use of Managarr (e.g., selling or offering as a paid service) requires separate permission.
2. **Attribution when Forking and Redistributing Without Contributing back to Main Project**:
- **If you fork the project and distribute it separately** (e.g., publish or _publicly_ host it independently from the original project), you are required to provide attribution.
- You may credit the original author by using any of the following phrasing:
- "Thanks to Alexander J. Clarke (Dark-Alex-17) for creating the original Managarr project!"
- "Forked from the Managarr project, created by Alexander J. Clarke (Dark-Alex-17)"
- "This software is based on the original Managarr project by Alexander J. Clarke (Dark-Alex-17)"
- "Inspired by Alexander J. Clarke (Dark-Alex-17)'s Managarr project"
- If changes are made to the base Managarr project, please note those modifications and provide the new attribution accordingly.
## Rust ## Rust
You'll need to have the stable Rust toolchain installed in order to develop Managarr. You'll need to have the stable Rust toolchain installed in order to develop Managarr.
Generated
+528 -282
View File
File diff suppressed because it is too large Load Diff
+26 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.3.6" version = "0.5.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs" description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"] keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
@@ -10,9 +10,12 @@ homepage = "https://github.com/Dark-Alex-17/managarr"
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.82.0" rust-version = "1.85.0"
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"] exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[workspace]
members = ["proc_macros/enum_display_style_derive", "proc_macros/validate_theme_derive"]
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
backtrace = "0.3.74" backtrace = "0.3.74"
@@ -36,24 +39,40 @@ strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4" strum_macros = "0.26.4"
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.36.0", features = ["full"] }
tokio-util = "0.7.8" tokio-util = "0.7.8"
ratatui = { version = "0.29.0", features = ["all-widgets"] } ratatui = { version = "0.29.0", features = [
"all-widgets",
"unstable-widget-ref",
] }
urlencoding = "2.1.2" urlencoding = "2.1.2"
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] } clap = { version = "4.5.20", features = [
"derive",
"cargo",
"env",
"wrap_help",
] }
clap_complete = "4.5.33" clap_complete = "4.5.33"
itertools = "0.13.0" itertools = "0.14.0"
ctrlc = "3.4.5" ctrlc = "3.4.5"
colored = "2.1.0" colored = "3.0.0"
async-trait = "0.1.83" async-trait = "0.1.83"
dirs-next = "2.0.0" dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0" managarr-tree-widget = "0.24.0"
indicatif = "0.17.9" indicatif = "0.17.9"
derive_setters = "0.1.6"
deunicode = "1.6.0"
paste = "1.0.15"
openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0"
validate_theme_derive = { path = "proc_macros/validate_theme_derive" }
enum_display_style_derive = { path = "proc_macros/enum_display_style_derive" }
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.0.16"
mockall = "0.13.0" mockall = "0.13.0"
mockito = "1.0.0" mockito = "1.0.0"
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
rstest = "0.23.0" rstest = "0.25.0"
serial_test = "3.2.0"
[dev-dependencies.cargo-husky] [dev-dependencies.cargo-husky]
version = "1" version = "1"
+5 -5
View File
@@ -1,4 +1,4 @@
FROM clux/muslrust:stable AS builder FROM rust:1.85 AS builder
WORKDIR /usr/src WORKDIR /usr/src
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer # Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
@@ -6,17 +6,17 @@ RUN USER=root cargo new --bin managarr-temp
WORKDIR /usr/src/managarr-temp WORKDIR /usr/src/managarr-temp
COPY Cargo.* . COPY Cargo.* .
RUN cargo build --release --target x86_64-unknown-linux-musl RUN cargo build --release
# remove src from empty project # remove src from empty project
RUN rm -r src RUN rm -r src
COPY src ./src COPY src ./src
# remove previous deps # remove previous deps
RUN rm ./target/x86_64-unknown-linux-musl/release/deps/managarr* RUN rm ./target/release/deps/managarr*
RUN --mount=type=cache,target=/volume/target \ RUN --mount=type=cache,target=/volume/target \
--mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/root/.cargo/registry \
cargo build --release --target x86_64-unknown-linux-musl --bin managarr cargo build --release --bin managarr
RUN mv target/x86_64-unknown-linux-musl/release/managarr . RUN mv target/release/managarr .
FROM debian:stable-slim FROM debian:stable-slim
+46 -9
View File
@@ -1,16 +1,53 @@
MIT License Managarr License
Version 1.0, 2025
Copyright (c) 2023 Alexander J. Clarke Copyright (c) 2025 Alexander J. Clarke (Dark-Alex-17)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to use,
in the Software without restriction, including without limitation the rights copy, modify, merge, publish, and distribute the Software solely for
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell non-commercial purposes, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all 1. Attribution:
copies or substantial portions of the Software. - The above copyright notice, this permission notice, and a prominent notice stating
that the Software is part of the "Managarr" project shall be included in all copies or
substantial portions of the Software **when the Software is forked and redistributed separately** from the original project.
- If you fork the software and **distribute it separately** without merging it back into the original base project (the Managarr repository), you must provide attribution to the original author.
You may use any of the following forms of attribution:
- "Thanks to Alexander J. Clarke (Dark-Alex-17) for creating the original Managarr project!"
- "Forked from the Managarr project, created by Alexander J. Clarke (Dark-Alex-17)"
- "This software is based on the original Managarr project by Alexander J. Clarke (Dark-Alex-17)"
- "Inspired by Alexander J. Clarke (Dark-Alex-17)'s Managarr project"
- If you modify the software, the attribution must also note that changes were made and describe those modifications, if feasible.
2. Non-Commercial Use Only:
The use of this Software for commercial purposes, including but not limited
to sale, licensing, or use in any product or service for monetary
compensation, is strictly prohibited without prior written permission from
Alexander J. Clarke (Dark-Alex-17).
For avoidance of doubt:
- **Allowed:** Private use, educational purposes, research, or any usage
that does not directly generate revenue.
- **Prohibited:** Selling, sublicensing, or incorporating the Software into
commercial products or services.
3. Modifications and Derivatives:
- Any modifications or derivative works based on this Software must clearly
document all changes made and prominently credit the original "Managarr"
project as described in section 1.
- Derivative works must retain this license, the original copyright notice,
and all terms and conditions described herein. This applies to the entire
derivative work, even if combined with other software.
4. Warranty Disclaimer:
This Software is provided "as is," without warranty of any kind, express
or implied, including but not limited to the warranties of merchantability,
fitness for a particular purpose, and noninfringement. In no event shall the
authors or copyright holders be liable for any claim, damages, or other
liability, whether in an action of contract, tort, or otherwise, arising
from, out of, or in connection with the Software or the use or other
dealings in the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+1 -1
View File
@@ -8,7 +8,7 @@ default: run
.PHONY: test test-cov build run lint lint-fix fmt analyze sonar release delete-tag .PHONY: test test-cov build run lint lint-fix fmt analyze sonar release delete-tag
test: test:
@cargo test @cargo test --all
## Run all tests with coverage - `cargo install cargo-tarpaulin` ## Run all tests with coverage - `cargo install cargo-tarpaulin`
test-cov: test-cov:
+216 -63
View File
@@ -1,17 +1,21 @@
# managarr - A TUI and CLI to manage your Servarrs # managarr - A TUI and CLI to manage your Servarrs
![check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg) ![Check](https://github.com/Dark-Alex-17/managarr/actions/workflows/check.yml/badge.svg)
![test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg) ![Test](https://github.com/Dark-Alex-17/managarr/actions/workflows/test.yml/badge.svg)
![License](https://img.shields.io/badge/license-MIT-blueviolet.svg) ![License](https://img.shields.io/badge/license-MIT-blueviolet.svg)
![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code) ![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code)
[![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr) [![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr)
![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff) ![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff)
[![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr) [![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr)
![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads) ![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads)
[![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/managarr/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/managarr/releases)
![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! Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust!
![library](screenshots/library.png) ![library](screenshots/sonarr/sonarr_library.png)
## What Servarrs are supported? ## What Servarrs are supported?
@@ -32,7 +36,10 @@ Simply run the following command to start a demo:
curl https://raw.githubusercontent.com/Dark-Alex-17/managarr-demo/main/managarr-demo.sh > /tmp/managarr-demo.sh && bash /tmp/managarr-demo.sh curl https://raw.githubusercontent.com/Dark-Alex-17/managarr-demo/main/managarr-demo.sh > /tmp/managarr-demo.sh && bash /tmp/managarr-demo.sh
``` ```
Alternatively, you can try out the demo container without downloading anything by visiting the [Managarr Demo site](https://managarr-demo.alexjclarke.com).
## Installation ## Installation
### Cargo ### Cargo
If you have Cargo installed, then you can install Managarr from Crates.io: If you have Cargo installed, then you can install Managarr from Crates.io:
@@ -46,13 +53,84 @@ cargo install --locked managarr
### Docker ### Docker
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example: Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
```shell ```shell
docker run --rm -it -v ~/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr docker run --rm -it -v /home/aclarke/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr:latest
``` ```
You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command. You can also clone this repo and run `make docker` to build a docker image locally and run it using the above command.
Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start. Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start.
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
### Homebrew (Mac and Linux)
To install Managarr from Homebrew, install the Managarr tap and then you'll be able to install Managarr:
```shell
brew tap Dark-Alex-17/managarr
brew install managarr
# If you need to be more specific, use the following:
brew install Dark-Alex-17/managarr/managarr
```
To upgrade to a newer version of Managarr:
```shell
brew upgrade managarr
```
### Nix (Externally Maintained)
To install Managarr on NixOS, you can use the following command:
```shell
nix-env --install managarr
# Alternatively, for non-NixOS users, you can spawn a temporary shell with Managarr available like so:
nix-shell -p managarr
```
### Chocolatey (Windows)
The Managarr Chocolatey package is located [here](https://community.chocolatey.org/packages/managarr). Please note that validation
of Chocolatey packages take quite some time, and thus the package may not be available immediately after a new release.
```powershell
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
```
To upgrade to the latest and greatest version of Managarr:
```powershell
choco upgrade managarr
# To upgrade to a specific version:
choco upgrade managarr --version=0.5.0
```
### Manual
Binaries are available on the [releases](https://github.com/Dark-Alex-17/managarr/releases) page for the following platforms:
| Platform | Architecture(s) |
|----------------|----------------------------|
| macOS | x86_64, arm64 |
| Linux GNU/MUSL | x86_64,armv6,armv7,aarch64 |
| Windows | x86_64,aarch64 |
#### Windows Instructions
To use a binary from the releases page on Windows, do the following:
1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
2. Use 7-Zip or TarTool to unpack the Tar file.
3. Run the executable `managarr.exe`!
#### Linux/MacOS Instructions
To use a binary from the releases page on Linux/MacOS, do the following:
1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
2. `cd` to the directory where you downloaded the binary.
3. Extract the binary with `tar -C /usr/local/bin -xzf managarr-<arch>.tar.gz` (NB: This may require `sudo`)
4. Now you can run `managarr`!
## Features ## Features
Key: Key:
@@ -88,22 +166,21 @@ Key:
| TUI | CLI | Feature | | TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------| |-----|-----|--------------------------------------------------------------------------------------------------------------------|
| 🕒 | ✅ | View your library, downloads, blocklist, episodes | | | ✅ | View your library, downloads, blocklist, episodes |
| 🕒 | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits | | | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| 🕒 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | | 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| 🕒 | ✅ | Search your library | | | ✅ | Search your library |
| 🕒 | ✅ | Add series to your library | | | ✅ | Add series to your library |
| 🕒 | ✅ | Delete series, downloads, indexers, root folders, and episode files | | | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| 🕒 | ✅ | Mark history events as failed | | | ✅ | Trigger automatic searches for series, seasons, or episodes |
| 🕒 | ✅ | Trigger automatic searches for series, seasons, or episodes | | | ✅ | Trigger refresh and disk scan for series and downloads |
| 🕒 | ✅ | Trigger refresh and disk scan for series and downloads | | | ✅ | Manually search for series, seasons, or episodes |
| 🕒 | ✅ | Manually search for series, seasons, or episodes | | | ✅ | Edit your series and indexers |
| 🕒 | ✅ | Edit your series and indexers | | | ✅ | Manage your tags |
| 🕒 | ✅ | Manage your tags | | | ✅ | Manage your root folders |
| 🕒 | ✅ | Manage your root folders | | | ✅ | Manage your blocklist |
| 🕒 | ✅ | Manage your blocklist | | | ✅ | View and browse logs, tasks, events queues, and updates |
| 🕒 | ✅ | View and browse logs, tasks, events queues, and updates | | | ✅ | Manually trigger scheduled tasks |
| 🕒 | ✅ | Manually trigger scheduled tasks |
### Readarr ### Readarr
@@ -129,6 +206,16 @@ Key:
- [ ] Support for Tautulli - [ ] Support for Tautulli
### Themes
Managarr ships with a few themes out of the box. Here's a few examples:
![default](themes/default/manual_episode_search.png)
![dracula](themes/dracula/manual_episode_search.png)
![watermelon-dark](themes/watermelon-dark/manual_episode_search.png)
You can also create your own custom themes as well. To learn more about what themes are built-in to Managarr and how
to create your own custom themes, check out the [Themes README](themes/README.md).
### The Managarr CLI ### The Managarr CLI
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs. Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
@@ -141,7 +228,7 @@ To see all available commands, simply run `managarr --help`:
```shell ```shell
$ managarr --help $ managarr --help
managarr 0.3.0 managarr 0.5.1
Alex Clarke <alex.j.tusa@gmail.com> Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs A TUI and CLI to manage your Servarrs
@@ -156,10 +243,15 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
--config <CONFIG> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] --config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
-h, --help Print help --themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
-V, --version Print version --theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
This is useful when you have multiple instances of the same Servarr defined in your config file.
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
``` ```
All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run: All subcommands also have detailed help menus to show you how to use them. For example, to see all available commands for Sonarr, you would run:
@@ -186,6 +278,8 @@ Commands:
start-task Start the specified Sonarr task start-task Start the specified Sonarr task
test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}' test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'
test-all-indexers Test all Sonarr indexers 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
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
@@ -228,44 +322,96 @@ where you may have more than one instance of a given Servarr running. Thus, you
config file using the `--config` flag: config file using the `--config` flag:
```shell ```shell
managarr --config /path/to/config.yml managarr --config-file /path/to/config.yml
``` ```
### Example Configuration: ### Example Configuration:
```yaml ```yaml
theme: default
radarr: radarr:
host: 192.168.0.78 - host: 192.168.0.78
port: 7878 port: 7878
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
sonarr: sonarr:
uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' - uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port'
api_token: someApiToken1234567890 api_token: someApiToken1234567890
- name: Anime Sonarr # An example of a custom name for a secondary Sonarr instance
host: 192.168.0.89
port: 8989
api_token: someApiToken1234567890
readarr: readarr:
host: 192.168.0.87 - host: 192.168.0.87
port: 8787 port: 8787
api_token: someApiToken1234567890 api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file
lidarr: lidarr:
host: 192.168.0.86 - host: 192.168.0.86
port: 8686 port: 8686
api_token: someApiToken1234567890 api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables
whisparr: whisparr:
host: 192.168.0.69 - host: 192.168.0.69
port: 6969 port: 6969
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/whisparr.crt ssl_cert_path: /path/to/whisparr.crt
bazarr: bazarr:
host: 192.168.0.67 - host: 192.168.0.67
port: 6767 port: 6767
api_token: someApiToken1234567890 api_token: someApiToken1234567890
prowlarr: prowlarr:
host: 192.168.0.96 - host: 192.168.0.96
port: 9696 port: 9696
api_token: someApiToken1234567890 api_token: someApiToken1234567890
tautulli: tautulli:
host: 192.168.0.81 - host: 192.168.0.81
port: 8181 port: 8181
api_token: someApiToken1234567890 api_token: someApiToken1234567890
```
### Example Multi-Instance Configuration:
```yaml
theme: default
radarr:
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
port: 7878
api_token: someApiToken1234567890
ssl_cert_path: /path/to/radarr.crt
- name: International Movies
host: 192.168.0.79
port: 7878
api_token: someApiToken1234567890
sonarr:
- name: Anime
weight: 1 # This instance will be the first tab in the TUI
uri: http://htpc.local/sonarr
api_token: someApiToken1234567890
- name: TV Shows
weight: 2 # This instance will be the second tab in the TUI
host: 192.168.0.89
port: 8989
api_token: someApiToken1234567890
```
In this configuration, you can see that we have multiple instances of Radarr and Sonarr configured. The `weight` key is
used to specify the order in which the tabs will appear in the TUI. The lower the weight, the further to the left the
tab will appear. If no weight is specified, then tabs will be ordered in the order they appear in the configuration
file.
When no `name` is specified for a Servarr instance, the name will default to the name of the Servarr with a number
appended to it. For example, if you have two Radarr instances and neither has a name, they will be named `Radarr 1` and
`Radarr 2`, respectively.
In this example configuration, the tabs in the TUI would appear as follows:
`Anime | TV Shows | Radarr 1 | International Movies`
### Specify Which Servarr Instance to Use in the CLI
If you have multiple instances of the same Servarr running, you can specify which instance you want to use by using the `--servarr-name` flag:
```shell
managarr --servarr-name "International Movies"
``` ```
## Environment Variables ## Environment Variables
@@ -275,21 +421,28 @@ Managarr supports using environment variables on startup so you don't have to al
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------| |-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` | | `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` | | `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
## Track My Progress for the Beta release (With Sonarr Support!) ## Track What I'm Currently Working On
Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr) To see what feature(s) I'm currently working on, check out my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr).
with all items tagged `Beta`.
## Screenshots ## Screenshots
![library](screenshots/library.png) ### Radarr
![manual_search](screenshots/manual_search.png) ![radarr_library](screenshots/radarr/radarr_library.png)
![logs](screenshots/logs.png) ![manual_search](screenshots/radarr/manual_search.png)
![new_movie_search](screenshots/new_movie_search.png) ![new_movie_search](screenshots/radarr/new_movie_search.png)
![add_new_movie](screenshots/add_new_movie.png) ![add_new_movie](screenshots/radarr/add_new_movie.png)
![collection_details](screenshots/collection_details.png) ![collection_details](screenshots/radarr/collection_details.png)
![indexers](screenshots/indexers.png)
### Sonarr
![sonarr_library](screenshots/sonarr/sonarr_library.png)
![series_details](screenshots/sonarr/series_details.png)
![season_details](screenshots/sonarr/season_details.png)
![manual_episode_search](screenshots/sonarr/manual_episode_search.png)
### General
![logs](screenshots/radarr/logs.png)
![indexers](screenshots/radarr/indexers.png)
## Dependencies ## Dependencies
* [ratatui](https://github.com/tui-rs-revival/ratatui) * [ratatui](https://github.com/tui-rs-revival/ratatui)
@@ -301,7 +454,7 @@ with all items tagged `Beta`.
## Servarr Requirements ## Servarr Requirements
* [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/) * [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/)
* [Sonarr >= v3](https://sonarr.tv/docs/api/) * [Sonarr >= v4](https://sonarr.tv/docs/api/)
* [Readarr v1](https://readarr.com/docs/api/) * [Readarr v1](https://readarr.com/docs/api/)
* [Lidarr v1](https://lidarr.audio/docs/api/) * [Lidarr v1](https://lidarr.audio/docs/api/)
* [Whisparr >= v3](https://whisparr.com/docs/api/) * [Whisparr >= v3](https://whisparr.com/docs/api/)
+9
View File
@@ -1,5 +1,14 @@
coverage: coverage:
range: "80..100" range: "80..100"
status:
project:
default:
threshold: 0
target: 80%
patch:
default:
threshold: 0
target: 80%
ignore: ignore:
- "**/*_tests.rs" - "**/*_tests.rs"
@@ -0,0 +1,20 @@
$ErrorActionPreference = 'Stop';
$PackageName = 'managarr'
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$url64 = 'https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-windows.tar.gz'
$checksum64 = '$hash_64'
$packageArgs = @{
packageName = $packageName
softwareName = $packageName
unzipLocation = $toolsDir
fileType = 'exe'
url = $url64
checksum = $checksum64
checksumType = 'sha256'
}
Install-ChocolateyZipPackage @packageArgs
$File = Get-ChildItem -File -Path $env:ChocolateyInstall\lib\$packageName\tools\ -Filter *.tar
Get-ChocolateyUnzip -fileFullPath $File.FullName -destination $env:ChocolateyInstall\lib\$packageName\tools\
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Read this before creating packages: https://chocolatey.org/docs/create-packages -->
<!-- It is especially important to read the above link to understand additional requirements when publishing packages to the community feed aka dot org (https://chocolatey.org/packages). -->
<!-- Test your packages in a test environment: https://github.com/chocolatey/chocolatey-test-environment -->
<!--
This is a nuspec. It mostly adheres to https://docs.nuget.org/create/Nuspec-Reference. Chocolatey uses a special version of NuGet.Core that allows us to do more than was initially possible. As such there are certain things to be aware of:
* the package xmlns schema url may cause issues with nuget.exe
* Any of the following elements can ONLY be used by choco tools - projectSourceUrl, docsUrl, mailingListUrl, bugTrackerUrl, packageSourceUrl, provides, conflicts, replaces
* nuget.exe can still install packages with those elements but they are ignored. Any authoring tools or commands will error on those elements
-->
<!-- You can embed software files directly into packages, as long as you are not bound by distribution rights. -->
<!-- * If you are an organization making private packages, you probably have no issues here -->
<!-- * If you are releasing to the community feed, you need to consider distribution rights. -->
<!-- Do not remove this test for UTF-8: if “Ω” doesnt appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<!-- == PACKAGE SPECIFIC SECTION == -->
<id>managarr</id>
<version>$version</version>
<!-- == SOFTWARE SPECIFIC SECTION == -->
<!-- This section is about the software itself -->
<title>Managarr</title>
<authors>Alex Clarke</authors>
<projectUrl>https://github.com/Dark-Alex-17/managarr</projectUrl>
<licenseUrl>https://github.com/Dark-Alex-17/managarr/blob/main/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/Dark-Alex-17/managarr</projectSourceUrl>
<docsUrl>https://github.com/Dark-Alex-17/managarr/blob/main/README.md</docsUrl>
<bugTrackerUrl>https://github.com/Dark-Alex-17/managarr/issues</bugTrackerUrl>
<tags>cli cross-platform terminal servarr tui sonarr radarr rust</tags>
<summary>A TUI and CLI for managing *arr servers.</summary>
<description>
A TUI and CLI for managing *arr servers. Built with love in Rust!
**Usage**
To use, run `managarr` in a terminal.
For more [documentation and usage](https://github.com/Dark-Alex-17/managarr/blob/main/README.md), see the [official repo](https://github.com/Dark-Alex-17/managarr).
</description>
<releaseNotes>https://github.com/Dark-Alex-17/managarr/releases/tag/v$version/</releaseNotes>
</metadata>
<files>
<!-- this section controls what actually gets packaged into the Chocolatey package -->
<file src="tools\**" target="tools" />
<!--Building from Linux? You may need this instead: <file src="tools/**" target="tools" />-->
</files>
</package>
+28
View File
@@ -0,0 +1,28 @@
import hashlib
import sys
from string import Template
sys.stdout.reconfigure(encoding='utf-8')
args = sys.argv
version = args[1].replace("v", "")
template_file_path = args[2]
generated_file_path = args[3]
# Deployment files
hash_64 = args[4].strip()
print("Generating formula")
print(" VERSION: %s" % version)
print(" TEMPLATE PATH: %s" % template_file_path)
print(" SAVING AT: %s" % generated_file_path)
print(" HASH: %s" % hash_64)
with open(template_file_path, "r", encoding="utf-8") as template_file:
template = Template(template_file.read())
substitute = template.safe_substitute(version=version, hash_64=hash_64)
print("\n================== Generated package file ==================\n")
print(substitute)
print("\n============================================================\n")
with open(generated_file_path, "w", encoding="utf-8") as generated_file:
generated_file.write(substitute)
+24
View File
@@ -0,0 +1,24 @@
# Documentation: https://docs.brew.sh/Formula-Cookbook
# https://rubydoc.brew.sh/Formula
class Managarr < Formula
desc "A fast and simple dashboard for Kubernetes written in Rust"
homepage "https://github.com/Dark-Alex-17/managarr"
if OS.mac? and Hardware::CPU.arm?
url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-macos-arm64.tar.gz"
sha256 "$hash_mac_arm"
elsif OS.mac? and Hardware::CPU.intel?
url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-macos.tar.gz"
sha256 "$hash_mac"
else
url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-linux-musl.tar.gz"
sha256 "$hash_linux"
end
version "$version"
license "MIT"
def install
bin.install "managarr"
ohai "You're done! Run with \"managarr\""
ohai "For runtime flags, see \"managarr --help\""
end
end
+31
View File
@@ -0,0 +1,31 @@
import hashlib
import sys
from string import Template
args = sys.argv
version = args[1]
template_file_path = args[2]
generated_file_path = args[3]
# Deployment files
hash_mac = args[4].strip()
hash_mac_arm = args[5].strip()
hash_linux = args[6].strip()
print("Generating formula")
print(" VERSION: %s" % version)
print(" TEMPLATE PATH: %s" % template_file_path)
print(" SAVING AT: %s" % generated_file_path)
print(" MAC HASH: %s" % hash_mac)
print(" MAC ARM HASH: %s" % hash_mac_arm)
print(" LINUX HASH: %s" % hash_linux)
with open(template_file_path, "r") as template_file:
template = Template(template_file.read())
substitute = template.safe_substitute(version=version, hash_mac=hash_mac, hash_mac_arm=hash_mac_arm, hash_linux=hash_linux)
print("\n================== Generated package file ==================\n")
print(substitute)
print("\n============================================================\n")
with open(generated_file_path, "w") as generated_file:
generated_file.write(substitute)
@@ -0,0 +1,12 @@
[package]
name = "enum_display_style_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
darling = "0.20.10"
@@ -0,0 +1,76 @@
mod macro_models;
use crate::macro_models::DisplayStyleArgs;
use darling::FromVariant;
use quote::quote;
use syn::{Data, DeriveInput, parse_macro_input};
/// Derive macro for generating a `to_display_str` method for an enum.
///
/// # Example
///
/// Using default values for the display style:
///
/// ```
/// use enum_display_style_derive::EnumDisplayStyle;
///
/// #[derive(EnumDisplayStyle)]
/// enum Weekend {
/// Saturday,
/// Sunday,
/// }
///
/// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday");
/// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday");
/// ```
///
/// Using custom values for the display style:
///
/// ```
/// use enum_display_style_derive::EnumDisplayStyle;
///
/// #[derive(EnumDisplayStyle)]
/// enum MonitorStatus {
/// #[display_style(name = "Monitor Transactions")]
/// Active,
/// #[display_style(name = "Don't Monitor Transactions")]
/// None,
/// }
///
/// assert_eq!(MonitorStatus::Active.to_display_str(), "Monitor Transactions");
/// assert_eq!(MonitorStatus::None.to_display_str(), "Don't Monitor Transactions");
/// ```
#[proc_macro_derive(EnumDisplayStyle, attributes(display_style))]
pub fn enum_display_style_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let enum_name = &input.ident;
let mut match_arms = Vec::new();
if let Data::Enum(data_enum) = &input.data {
let variants = &data_enum.variants;
for variant in variants {
let variant_ident = &variant.ident;
let variant_display_name = DisplayStyleArgs::from_variant(variant)
.unwrap()
.name
.unwrap_or_else(|| variant_ident.to_string());
match_arms.push(quote! {
#enum_name::#variant_ident => #variant_display_name,
});
}
}
quote! {
impl<'a> #enum_name {
pub fn to_display_str(self) -> &'a str {
match self {
#(#match_arms)*
}
}
}
}
.into()
}
@@ -0,0 +1,7 @@
use darling::FromVariant;
#[derive(Debug, FromVariant)]
#[darling(attributes(display_style))]
pub struct DisplayStyleArgs {
pub name: Option<String>,
}
@@ -0,0 +1,14 @@
[package]
name = "validate_theme_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
[dev-dependencies]
log = "0.4.17"
@@ -0,0 +1,106 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Derive macro for generating a `validate` method for a Theme struct.
/// The `validate` method ensures that all values with the `validate` attribute are not `None`.
/// Otherwise, an error message it output to both the log file and stdout and the program exits.
///
/// # Example
///
/// Valid themes pass through the program transitively without any messages being output.
///
/// ```
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// good: Some(Style { color: "Green".to_owned() }),
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since only `good` and `bad` have the `validate` attribute, the `validate` method will only check those fields.
/// theme.validate();
/// // Since both `good` and `bad` have values, the program will not exit and no message is output.
/// ```
///
/// Invalid themes will output an error message to both the log file and stdout and the program will exit.
///
/// ```should_panic
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since `good` has the `validate` attribute and since `good` is `None`, the `validate` method will output an error message and exit the program.
/// theme.validate();
/// ```
#[proc_macro_derive(ValidateTheme, attributes(validate))]
pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let mut validation_checks = Vec::new();
if let Data::Struct(data_struct) = &input.data {
if let Fields::Named(fields) = &data_struct.fields {
for field in &fields.named {
let field_name = &field.ident;
let has_validate_attr = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("validate"));
if has_validate_attr {
validation_checks.push(quote! {
if self.#field_name.is_none() {
log::error!("{} is missing a color value.", stringify!(#field_name));
eprintln!("{} is missing a color value.", stringify!(#field_name));
std::process::exit(1);
}
})
}
}
}
}
quote! {
impl #struct_name {
pub fn validate(&self) {
#(#validation_checks)*
}
}
}
.into()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

+310 -54
View File
@@ -1,46 +1,117 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::anyhow; use anyhow::anyhow;
use pretty_assertions::assert_eq; use pretty_assertions::{assert_eq, assert_str_eq};
use serial_test::serial;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::app::{App, AppConfig, Data, ServarrConfig, DEFAULT_ROUTE}; use crate::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute}; use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
use tokio_util::sync::CancellationToken;
#[test]
fn test_app_new() {
let radarr_config_1 = ServarrConfig {
name: Some("Radarr Test".to_owned()),
..ServarrConfig::default()
};
let radarr_config_2 = ServarrConfig {
weight: Some(3),
..ServarrConfig::default()
};
let sonarr_config_1 = ServarrConfig {
name: Some("Sonarr Test".to_owned()),
weight: Some(1),
..ServarrConfig::default()
};
let sonarr_config_2 = ServarrConfig::default();
let config = AppConfig {
theme: None,
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
};
let expected_tab_routes = vec![
TabRoute {
title: "Sonarr Test".to_owned(),
route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(sonarr_config_1),
},
TabRoute {
title: "Radarr 1".to_owned(),
route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(radarr_config_2),
},
TabRoute {
title: "Radarr Test".to_owned(),
route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(radarr_config_1),
},
TabRoute {
title: "Sonarr 1".to_owned(),
route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(sonarr_config_2),
},
];
let app = App::new(
mpsc::channel::<NetworkEvent>(500).0,
config,
CancellationToken::new(),
);
assert!(app.navigation_stack.is_empty());
assert_eq!(app.get_current_route(), ActiveSonarrBlock::default().into());
assert!(app.network_tx.is_some());
assert!(!app.cancellation_token.is_cancelled());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
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.tick_count, 0);
assert!(!app.is_loading);
assert!(!app.is_routing);
assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key);
assert!(!app.cli_mode);
}
#[test] #[test]
fn test_app_default() { fn test_app_default() {
let app = App::default(); let app = App::default();
assert_eq!(app.navigation_stack, vec![DEFAULT_ROUTE]); assert!(app.navigation_stack.is_empty());
assert!(app.network_tx.is_none()); assert!(app.network_tx.is_none());
assert!(!app.cancellation_token.is_cancelled()); assert!(!app.cancellation_token.is_cancelled());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.error, HorizontallyScrollableText::default());
assert_eq!(app.server_tabs.index, 0); assert_eq!(app.server_tabs.index, 0);
assert_eq!(
app.server_tabs.tabs,
vec![
TabRoute {
title: "Radarr",
route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
},
TabRoute {
title: "Sonarr",
route: ActiveSonarrBlock::Series.into(),
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
contextual_help: None,
},
]
);
assert_eq!(app.tick_until_poll, 400); assert_eq!(app.tick_until_poll, 400);
assert_eq!(app.ticks_until_scroll, 4); assert_eq!(app.ticks_until_scroll, 4);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
@@ -53,16 +124,14 @@ mod tests {
#[test] #[test]
fn test_navigation_stack_methods() { fn test_navigation_stack_methods() {
let mut app = App::default(); let mut app = App::test_default();
let default_route = app.server_tabs.tabs.first().unwrap().route;
assert_eq!(app.get_current_route(), &DEFAULT_ROUTE); assert_eq!(app.get_current_route(), default_route);
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert!(app.is_routing); assert!(app.is_routing);
app.is_routing = false; app.is_routing = false;
@@ -70,20 +139,20 @@ mod tests {
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::Collections.into() ActiveRadarrBlock::Collections.into()
); );
assert!(app.is_routing); assert!(app.is_routing);
app.is_routing = false; app.is_routing = false;
app.pop_navigation_stack(); app.pop_navigation_stack();
assert_eq!(app.get_current_route(), &DEFAULT_ROUTE); assert_eq!(app.get_current_route(), default_route);
assert!(app.is_routing); assert!(app.is_routing);
app.is_routing = false; app.is_routing = false;
app.pop_navigation_stack(); app.pop_navigation_stack();
assert_eq!(app.get_current_route(), &DEFAULT_ROUTE); assert_eq!(app.get_current_route(), default_route);
assert!(app.is_routing); assert!(app.is_routing);
} }
@@ -92,7 +161,7 @@ mod tests {
let mut app = App { let mut app = App {
is_loading: true, is_loading: true,
should_refresh: false, should_refresh: false,
..App::default() ..App::test_default()
}; };
app.cancellation_token.cancel(); app.cancellation_token.cancel();
@@ -110,7 +179,7 @@ mod tests {
fn test_reset_tick_count() { fn test_reset_tick_count() {
let mut app = App { let mut app = App {
tick_count: 2, tick_count: 2,
..App::default() ..App::test_default()
}; };
app.reset_tick_count(); app.reset_tick_count();
@@ -120,33 +189,38 @@ mod tests {
#[test] #[test]
fn test_reset() { fn test_reset() {
let radarr_data = RadarrData {
version: "test".into(),
..RadarrData::default()
};
let sonarr_data = SonarrData {
version: "test".into(),
..SonarrData::default()
};
let data = Data {
radarr_data,
sonarr_data,
};
let mut app = App { let mut app = App {
tick_count: 2, tick_count: 2,
error: "Test error".to_owned().into(), error: "Test error".to_owned().into(),
data: Data { is_first_render: false,
radarr_data: RadarrData { data,
version: "test".to_owned(), ..App::test_default()
..RadarrData::default()
},
sonarr_data: SonarrData {
version: "test".to_owned(),
..SonarrData::default()
},
},
..App::default()
}; };
app.reset(); app.reset();
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.is_first_render);
assert!(app.data.radarr_data.version.is_empty()); assert!(app.data.radarr_data.version.is_empty());
assert!(app.data.sonarr_data.version.is_empty()); assert!(app.data.sonarr_data.version.is_empty());
} }
#[test] #[test]
fn test_handle_error() { fn test_handle_error() {
let mut app = App::default(); let mut app = App::test_default();
let test_string = "Testing"; let test_string = "Testing";
app.handle_error(anyhow!(test_string)); app.handle_error(anyhow!(test_string));
@@ -165,7 +239,7 @@ mod tests {
let mut app = App { let mut app = App {
tick_until_poll: 2, tick_until_poll: 2,
network_tx: Some(sync_network_tx), network_tx: Some(sync_network_tx),
..App::default() ..App::test_default()
}; };
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
@@ -188,12 +262,13 @@ mod tests {
let mut app = App { let mut app = App {
tick_until_poll: 2, tick_until_poll: 2,
network_tx: Some(sync_network_tx), network_tx: Some(sync_network_tx),
..App::default() is_first_render: true,
..App::test_default()
}; };
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
app.on_tick(true).await; app.on_tick().await;
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
@@ -219,6 +294,14 @@ mod tests {
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into() RadarrEvent::GetStatus.into()
); );
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovies.into() RadarrEvent::GetMovies.into()
@@ -234,10 +317,10 @@ mod tests {
tick_until_poll: 2, tick_until_poll: 2,
tick_count: 2, tick_count: 2,
is_routing: true, is_routing: true,
..App::default() ..App::test_default()
}; };
app.on_tick(false).await; app.on_tick().await;
assert!(!app.is_routing); assert!(!app.is_routing);
} }
@@ -247,10 +330,10 @@ mod tests {
tick_until_poll: 2, tick_until_poll: 2,
tick_count: 2, tick_count: 2,
should_refresh: true, should_refresh: true,
..App::default() ..App::test_default()
}; };
app.on_tick(false).await; app.on_tick().await;
assert!(!app.should_refresh); assert!(!app.should_refresh);
} }
@@ -266,10 +349,183 @@ mod tests {
fn test_servarr_config_default() { fn test_servarr_config_default() {
let servarr_config = ServarrConfig::default(); let servarr_config = ServarrConfig::default();
assert_eq!(servarr_config.name, None);
assert_eq!(servarr_config.host, Some("localhost".to_string())); assert_eq!(servarr_config.host, Some("localhost".to_string()));
assert_eq!(servarr_config.port, None); assert_eq!(servarr_config.port, None);
assert_eq!(servarr_config.uri, None); assert_eq!(servarr_config.uri, None);
assert!(servarr_config.api_token.is_empty()); assert_eq!(servarr_config.weight, None);
assert_eq!(servarr_config.api_token, Some(String::new()));
assert_eq!(servarr_config.api_token_file, None);
assert_eq!(servarr_config.ssl_cert_path, None); assert_eq!(servarr_config.ssl_cert_path, None);
} }
#[test]
#[serial]
fn test_deserialize_optional_env_var_is_present() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION", "localhost") };
let yaml_data = r#"
host: ${TEST_VAR_DESERIALIZE_OPTION}
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.host, Some("localhost".to_string()));
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION") };
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_does_not_overwrite_non_env_value() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE", "localhost") };
let yaml_data = r#"
host: www.example.com
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.host, Some("www.example.com".to_string()));
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_NO_OVERWRITE") };
}
#[test]
fn test_deserialize_optional_env_var_empty() {
let yaml_data = r#"
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, None);
}
#[test]
#[serial]
fn test_deserialize_optional_u16_env_var_is_present() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_U16", "1") };
let yaml_data = r#"
port: ${TEST_VAR_DESERIALIZE_OPTION_U16}
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, Some(1));
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_U16") };
}
#[test]
#[serial]
fn test_deserialize_optional_u16_env_var_does_not_overwrite_non_env_value() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_U16_UNUSED", "1") };
let yaml_data = r#"
port: 1234
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, Some(1234));
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_U16_UNUSED") };
}
#[test]
fn test_deserialize_optional_u16_env_var_invalid_number() {
let yaml_data = r#"
port: "hi"
api_token: "test123"
"#;
let result: Result<ServarrConfig, _> = serde_yaml::from_str(yaml_data);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("invalid digit found in string"));
}
#[test]
fn test_deserialize_optional_u16_env_var_empty() {
let yaml_data = r#"
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.port, None);
}
#[test]
#[serial]
fn test_interpolate_env_vars() {
unsafe { std::env::set_var("TEST_VAR_INTERPOLATION", "testing") };
let var = interpolate_env_vars("${TEST_VAR_INTERPOLATION}");
assert_str_eq!(var, "testing");
unsafe { std::env::remove_var("TEST_VAR_INTERPOLATION") };
}
#[test]
fn test_interpolate_env_vars_defaults_to_original_string_if_not_in_yaml_interpolation_format() {
let var = interpolate_env_vars("TEST_VAR_INTERPOLATION_NON_YAML");
assert_str_eq!(var, "TEST_VAR_INTERPOLATION_NON_YAML");
}
#[test]
#[serial]
fn test_interpolate_env_vars_scrubs_all_unnecessary_characters() {
unsafe {
std::env::set_var(
"TEST_VAR_INTERPOLATION_UNNECESSARY_CHARACTERS",
r#"""
`"'https://dontdo:this@testing.com/query?test=%20query#results'"` {([\|$!])}
"""#,
)
};
let var = interpolate_env_vars("${TEST_VAR_INTERPOLATION_UNNECESSARY_CHARACTERS}");
assert_str_eq!(
var,
"https://dontdo:this@testing.com/query?test=%20query#results"
);
unsafe { std::env::remove_var("TEST_VAR_INTERPOLATION_UNNECESSARY_CHARACTERS") };
}
#[test]
fn test_interpolate_env_vars_scrubs_all_unnecessary_characters_from_non_environment_variable() {
let var = interpolate_env_vars("https://dontdo:this@testing.com/query?test=%20query#results");
assert_str_eq!(
var,
"https://dontdo:this@testing.com/query?test=%20query#results"
);
}
#[test]
fn test_servarr_config_redacted_debug() {
let name = "Servarr".to_owned();
let host = "localhost".to_owned();
let port = 1234;
let uri = "http://localhost:1234".to_owned();
let weight = 100;
let api_token = "thisisatest".to_owned();
let api_token_file = "/root/.config/api_token".to_owned();
let ssl_cert_path = "/some/path".to_owned();
let expected_str = format!("ServarrConfig {{ name: Some(\"{}\"), host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), weight: Some({}), api_token: Some(\"***********\"), api_token_file: Some(\"{}\"), ssl_cert_path: Some(\"{}\") }}",
name, host, port, uri, weight, api_token_file, ssl_cert_path);
let servarr_config = ServarrConfig {
name: Some(name),
host: Some(host),
port: Some(port),
uri: Some(uri),
weight: Some(weight),
api_token: Some(api_token),
api_token_file: Some(api_token_file),
ssl_cert_path: Some(ssl_cert_path),
};
assert_str_eq!(format!("{servarr_config:?}"), expected_str);
}
} }
+69 -2
View File
@@ -14,10 +14,77 @@ pub fn build_context_clue_string(context_clues: &[(KeyBinding, &str)]) -> String
.join(" | ") .join(" | ")
} }
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 2] = [ pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.tab, "change servarr"), (
DEFAULT_KEYBINDINGS.next_servarr,
DEFAULT_KEYBINDINGS.next_servarr.desc,
),
(
DEFAULT_KEYBINDINGS.previous_servarr,
DEFAULT_KEYBINDINGS.previous_servarr.desc,
),
(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc), (DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc),
]; ];
pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] = pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] =
[(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)]; [(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)];
pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.clear, "clear blocklist"),
];
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.confirm, "submit"),
(DEFAULT_KEYBINDINGS.esc, "cancel"),
];
pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 3] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.update, "update downloads"),
];
pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "edit indexer"),
(
DEFAULT_KEYBINDINGS.settings,
DEFAULT_KEYBINDINGS.settings.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.test, "test indexer"),
(DEFAULT_KEYBINDINGS.test_all, "test all indexers"),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.tasks, "open tasks"),
(DEFAULT_KEYBINDINGS.events, "open events"),
(DEFAULT_KEYBINDINGS.logs, "open logs"),
(DEFAULT_KEYBINDINGS.update, "open updates"),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
+168 -3
View File
@@ -2,7 +2,11 @@
mod test { mod test {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::context_clues::{BARE_POPUP_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES}; use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS}; use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS};
#[test] #[test]
@@ -24,8 +28,13 @@ mod test {
let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tab); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr);
assert_str_eq!(*description, "change servarr"); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.previous_servarr);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.previous_servarr.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
@@ -44,4 +53,160 @@ mod test {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(bare_popup_context_clues_iter.next(), None); assert_eq!(bare_popup_context_clues_iter.next(), None);
} }
#[test]
fn test_downloads_context_clues() {
let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter();
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "update downloads");
assert_eq!(downloads_context_clues_iter.next(), None);
}
#[test]
fn test_blocklist_context_clues() {
let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter();
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear);
assert_str_eq!(*description, "clear blocklist");
assert_eq!(blocklist_context_clues_iter.next(), None);
}
#[test]
fn test_confirmation_prompt_context_clues() {
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
assert_str_eq!(*description, "submit");
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel");
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
}
#[test]
fn test_root_folders_context_clues() {
let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter();
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc);
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(root_folders_context_clues_iter.next(), None);
}
#[test]
fn test_indexers_context_clues() {
let mut indexers_context_clues_iter = INDEXERS_CONTEXT_CLUES.iter();
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "edit indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.settings);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.settings.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test);
assert_str_eq!(*description, "test indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test_all);
assert_str_eq!(*description, "test all indexers");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(indexers_context_clues_iter.next(), None);
}
#[test]
fn test_system_context_clues() {
let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter();
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tasks);
assert_str_eq!(*description, "open tasks");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.events);
assert_str_eq!(*description, "open events");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.logs);
assert_str_eq!(*description, "open logs");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "open updates");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(system_context_clues_iter.next(), None);
}
} }
+82 -7
View File
@@ -15,8 +15,11 @@ generate_keybindings! {
left, left,
right, right,
backspace, backspace,
next_servarr,
previous_servarr,
clear, clear,
search, search,
auto_search,
settings, settings,
filter, filter,
sort, sort,
@@ -25,12 +28,12 @@ generate_keybindings! {
tasks, tasks,
test, test,
test_all, test_all,
toggle_monitoring,
refresh, refresh,
update, update,
events, events,
home, home,
end, end,
tab,
delete, delete,
submit, submit,
confirm, confirm,
@@ -41,116 +44,188 @@ generate_keybindings! {
#[derive(Clone, Copy, Eq, PartialEq, Debug)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct KeyBinding { pub struct KeyBinding {
pub key: Key, pub key: Key,
pub alt: Option<Key>,
pub desc: &'static str, pub desc: &'static str,
} }
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
add: KeyBinding { add: KeyBinding {
key: Key::Char('a'), key: Key::Char('a'),
alt: None,
desc: "add", desc: "add",
}, },
up: KeyBinding { up: KeyBinding {
key: Key::Up, key: Key::Up,
alt: Some(Key::Char('k')),
desc: "up", desc: "up",
}, },
down: KeyBinding { down: KeyBinding {
key: Key::Down, key: Key::Down,
alt: Some(Key::Char('j')),
desc: "down", desc: "down",
}, },
left: KeyBinding { left: KeyBinding {
key: Key::Left, key: Key::Left,
alt: Some(Key::Char('h')),
desc: "left", desc: "left",
}, },
right: KeyBinding { right: KeyBinding {
key: Key::Right, key: Key::Right,
alt: Some(Key::Char('l')),
desc: "right", desc: "right",
}, },
backspace: KeyBinding { backspace: KeyBinding {
key: Key::Backspace, key: Key::Backspace,
alt: None,
desc: "backspace", desc: "backspace",
}, },
next_servarr: KeyBinding {
key: Key::Tab,
alt: None,
desc: "next servarr",
},
previous_servarr: KeyBinding {
key: Key::BackTab,
alt: None,
desc: "previous servarr",
},
clear: KeyBinding { clear: KeyBinding {
key: Key::Char('c'), key: Key::Char('c'),
alt: None,
desc: "clear", desc: "clear",
}, },
auto_search: KeyBinding {
key: Key::Char('S'),
alt: None,
desc: "auto search",
},
search: KeyBinding { search: KeyBinding {
key: Key::Char('s'), key: Key::Char('s'),
alt: None,
desc: "search", desc: "search",
}, },
settings: KeyBinding { settings: KeyBinding {
key: Key::Char('s'), key: Key::Char('S'),
alt: None,
desc: "settings", desc: "settings",
}, },
filter: KeyBinding { filter: KeyBinding {
key: Key::Char('f'), key: Key::Char('f'),
alt: None,
desc: "filter", desc: "filter",
}, },
sort: KeyBinding { sort: KeyBinding {
key: Key::Char('o'), key: Key::Char('o'),
alt: None,
desc: "sort", desc: "sort",
}, },
edit: KeyBinding { edit: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
alt: None,
desc: "edit", desc: "edit",
}, },
events: KeyBinding { events: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
alt: None,
desc: "events", desc: "events",
}, },
logs: KeyBinding { logs: KeyBinding {
key: Key::Char('l'), key: Key::Char('L'),
alt: None,
desc: "logs", desc: "logs",
}, },
tasks: KeyBinding { tasks: KeyBinding {
key: Key::Char('t'), key: Key::Char('t'),
alt: None,
desc: "tasks", desc: "tasks",
}, },
test: KeyBinding { test: KeyBinding {
key: Key::Char('t'), key: Key::Char('t'),
alt: None,
desc: "test", desc: "test",
}, },
test_all: KeyBinding { test_all: KeyBinding {
key: Key::Char('T'), key: Key::Char('T'),
alt: None,
desc: "test all", desc: "test all",
}, },
toggle_monitoring: KeyBinding {
key: Key::Char('m'),
alt: None,
desc: "toggle monitoring",
},
refresh: KeyBinding { refresh: KeyBinding {
key: Key::Ctrl('r'), key: Key::Ctrl('r'),
alt: None,
desc: "refresh", desc: "refresh",
}, },
update: KeyBinding { update: KeyBinding {
key: Key::Char('u'), key: Key::Char('u'),
alt: None,
desc: "update", desc: "update",
}, },
home: KeyBinding { home: KeyBinding {
key: Key::Home, key: Key::Home,
alt: None,
desc: "home", desc: "home",
}, },
end: KeyBinding { end: KeyBinding {
key: Key::End, key: Key::End,
alt: None,
desc: "end", desc: "end",
}, },
tab: KeyBinding {
key: Key::Tab,
desc: "tab",
},
delete: KeyBinding { delete: KeyBinding {
key: Key::Delete, key: Key::Delete,
alt: None,
desc: "delete", desc: "delete",
}, },
submit: KeyBinding { submit: KeyBinding {
key: Key::Enter, key: Key::Enter,
alt: None,
desc: "submit", desc: "submit",
}, },
confirm: KeyBinding { confirm: KeyBinding {
key: Key::Ctrl('s'), key: Key::Ctrl('s'),
alt: None,
desc: "submit", desc: "submit",
}, },
quit: KeyBinding { quit: KeyBinding {
key: Key::Char('q'), key: Key::Char('q'),
alt: None,
desc: "quit", desc: "quit",
}, },
esc: KeyBinding { esc: KeyBinding {
key: Key::Esc, key: Key::Esc,
alt: None,
desc: "close", desc: "close",
}, },
}; };
#[macro_export]
macro_rules! matches_key {
($binding:ident, $key:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
($binding:ident, $key:expr, $ignore_alt_navigation:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| !$ignore_alt_navigation
&& ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
}
+6 -3
View File
@@ -13,22 +13,25 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")] #[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")] #[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")] #[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")]
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, "next servarr")]
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, "previous servarr")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")] #[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")]
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), "auto search")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")] #[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('s'), "settings")] #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), "settings")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), "sort")] #[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")] #[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), "events")] #[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('l'), "logs")] #[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")] #[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")] #[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")]
#[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")] #[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")]
#[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), "toggle monitoring")]
#[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")] #[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")]
#[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")] #[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")] #[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")] #[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")]
#[case(DEFAULT_KEYBINDINGS.tab, Key::Tab, "tab")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")] #[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")] #[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")] #[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
+248 -49
View File
@@ -1,11 +1,14 @@
use std::process; use anyhow::{anyhow, Error, Result};
use anyhow::anyhow;
use colored::Colorize; use colored::Colorize;
use itertools::Itertools;
use log::{debug, error}; use log::{debug, error};
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{fs, process};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use veil::Redact;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::cli::Command; use crate::cli::Command;
@@ -21,13 +24,13 @@ pub mod context_clues;
pub mod key_binding; pub mod key_binding;
mod key_binding_tests; mod key_binding_tests;
pub mod radarr; pub mod radarr;
pub mod sonarr;
const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies, None);
pub struct App<'a> { pub struct App<'a> {
navigation_stack: Vec<Route>, navigation_stack: Vec<Route>,
network_tx: Option<Sender<NetworkEvent>>, network_tx: Option<Sender<NetworkEvent>>,
cancellation_token: CancellationToken, pub cancellation_token: CancellationToken,
pub is_first_render: bool,
pub server_tabs: TabState, pub server_tabs: TabState,
pub error: HorizontallyScrollableText, pub error: HorizontallyScrollableText,
pub tick_until_poll: u64, pub tick_until_poll: u64,
@@ -38,20 +41,88 @@ pub struct App<'a> {
pub should_refresh: bool, pub should_refresh: bool,
pub should_ignore_quit_key: bool, pub should_ignore_quit_key: bool,
pub cli_mode: bool, pub cli_mode: bool,
pub config: AppConfig,
pub data: Data<'a>, pub data: Data<'a>,
} }
impl<'a> App<'a> { impl App<'_> {
pub fn new( pub fn new(
network_tx: Sender<NetworkEvent>, network_tx: Sender<NetworkEvent>,
config: AppConfig, config: AppConfig,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Self { ) -> Self {
let mut server_tabs = Vec::new();
let help = format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
);
if let Some(radarr_configs) = config.radarr {
let mut idx = 0;
for radarr_config in radarr_configs {
let name = if let Some(name) = radarr_config.name.clone() {
name
} else {
idx += 1;
format!("Radarr {}", idx)
};
server_tabs.push(TabRoute {
title: name,
route: ActiveRadarrBlock::Movies.into(),
help: help.clone(),
contextual_help: None,
config: Some(radarr_config),
});
}
}
if let Some(sonarr_configs) = config.sonarr {
let mut idx = 0;
for sonarr_config in sonarr_configs {
let name = if let Some(name) = sonarr_config.name.clone() {
name
} else {
idx += 1;
format!("Sonarr {}", idx)
};
server_tabs.push(TabRoute {
title: name,
route: ActiveSonarrBlock::Series.into(),
help: help.clone(),
contextual_help: None,
config: Some(sonarr_config),
});
}
}
let weight_sorted_tabs = server_tabs
.into_iter()
.sorted_by(|tab1, tab2| {
Ord::cmp(
tab1
.config
.as_ref()
.unwrap()
.weight
.as_ref()
.unwrap_or(&1000),
tab2
.config
.as_ref()
.unwrap()
.weight
.as_ref()
.unwrap_or(&1000),
)
})
.collect();
App { App {
network_tx: Some(network_tx), network_tx: Some(network_tx),
config,
cancellation_token, cancellation_token,
server_tabs: TabState::new(weight_sorted_tabs),
..App::default() ..App::default()
} }
} }
@@ -76,26 +147,26 @@ impl<'a> App<'a> {
self.tick_count = 0; self.tick_count = 0;
} }
// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then
#[allow(dead_code)] #[allow(dead_code)]
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.reset_tick_count(); self.reset_tick_count();
self.error = HorizontallyScrollableText::default(); self.error = HorizontallyScrollableText::default();
self.is_first_render = true;
self.data = Data::default(); self.data = Data::default();
} }
pub fn handle_error(&mut self, error: anyhow::Error) { pub fn handle_error(&mut self, error: Error) {
if self.error.text.is_empty() { if self.error.text.is_empty() {
self.error = error.to_string().into(); self.error = error.to_string().into();
} }
} }
pub async fn on_tick(&mut self, is_first_render: bool) { pub async fn on_tick(&mut self) {
if self.tick_count % self.tick_until_poll == 0 || self.is_routing || self.should_refresh { if self.tick_count % self.tick_until_poll == 0 || self.is_routing || self.should_refresh {
if let Route::Radarr(active_radarr_block, _) = self.get_current_route() { match self.get_current_route() {
self Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
.radarr_on_tick(*active_radarr_block, is_first_render) Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
.await; _ => (),
} }
self.is_routing = false; self.is_routing = false;
@@ -112,7 +183,7 @@ impl<'a> App<'a> {
pub fn pop_navigation_stack(&mut self) { pub fn pop_navigation_stack(&mut self) {
self.is_routing = true; self.is_routing = true;
if self.navigation_stack.len() > 1 { if !self.navigation_stack.is_empty() {
self.navigation_stack.pop(); self.navigation_stack.pop();
} }
} }
@@ -130,35 +201,23 @@ impl<'a> App<'a> {
self.push_navigation_stack(route); self.push_navigation_stack(route);
} }
pub fn get_current_route(&self) -> &Route { pub fn get_current_route(&self) -> Route {
self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE) *self
.navigation_stack
.last()
.unwrap_or(&self.server_tabs.tabs.first().unwrap().route)
} }
} }
impl<'a> Default for App<'a> { impl Default for App<'_> {
fn default() -> Self { fn default() -> Self {
App { App {
navigation_stack: vec![DEFAULT_ROUTE], navigation_stack: Vec::new(),
network_tx: None, network_tx: None,
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
error: HorizontallyScrollableText::default(), error: HorizontallyScrollableText::default(),
server_tabs: TabState::new(vec![ is_first_render: true,
TabRoute { server_tabs: TabState::new(Vec::new()),
title: "Radarr",
route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
},
TabRoute {
title: "Sonarr",
route: ActiveSonarrBlock::Series.into(),
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
contextual_help: None,
},
]),
tick_until_poll: 400, tick_until_poll: 400,
ticks_until_scroll: 4, ticks_until_scroll: 4,
tick_count: 0, tick_count: 0,
@@ -167,32 +226,70 @@ impl<'a> Default for App<'a> {
should_refresh: false, should_refresh: false,
should_ignore_quit_key: false, should_ignore_quit_key: false,
cli_mode: false, cli_mode: false,
config: AppConfig::default(),
data: Data::default(), data: Data::default(),
} }
} }
} }
#[cfg(test)]
impl App<'_> {
pub fn test_default() -> Self {
App {
server_tabs: TabState::new(vec![
TabRoute {
title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
]),
..App::default()
}
}
}
#[derive(Default)] #[derive(Default)]
pub struct Data<'a> { pub struct Data<'a> {
pub radarr_data: RadarrData<'a>, pub radarr_data: RadarrData<'a>,
pub sonarr_data: SonarrData, pub sonarr_data: SonarrData<'a>,
} }
#[derive(Debug, Deserialize, Serialize, Default, Clone)] #[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig { pub struct AppConfig {
pub radarr: Option<ServarrConfig>, pub theme: Option<String>,
pub sonarr: Option<ServarrConfig>, pub radarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<Vec<ServarrConfig>>,
} }
impl AppConfig { impl AppConfig {
pub fn validate(&self) { pub fn validate(&self) {
if let Some(radarr_config) = &self.radarr { if self.radarr.is_none() && self.sonarr.is_none() {
radarr_config.validate(); log_and_print_error(
"No Servarr configuration provided in the specified configuration file".to_owned(),
);
process::exit(1);
} }
if let Some(sonarr_config) = &self.sonarr { if let Some(radarr_configs) = &self.radarr {
sonarr_config.validate(); radarr_configs.iter().for_each(|config| config.validate());
}
if let Some(sonarr_configs) = &self.sonarr {
sonarr_configs.iter().for_each(|config| config.validate());
} }
} }
@@ -215,14 +312,40 @@ impl AppConfig {
_ => (), _ => (),
} }
} }
pub fn post_process_initialization(&mut self) {
if let Some(radarr_configs) = self.radarr.as_mut() {
for radarr_config in radarr_configs {
radarr_config.post_process_initialization();
}
}
if let Some(sonarr_configs) = self.sonarr.as_mut() {
for sonarr_config in sonarr_configs {
sonarr_config.post_process_initialization();
}
}
}
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Redact, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct ServarrConfig { pub struct ServarrConfig {
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub host: Option<String>, pub host: Option<String>,
#[serde(default, deserialize_with = "deserialize_u16_env_var")]
pub port: Option<u16>, pub port: Option<u16>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub uri: Option<String>, pub uri: Option<String>,
pub api_token: String, #[serde(default, deserialize_with = "deserialize_u16_env_var")]
pub weight: Option<u16>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
#[redact]
pub api_token: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub api_token_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub ssl_cert_path: Option<String>, pub ssl_cert_path: Option<String>,
} }
@@ -232,16 +355,43 @@ impl ServarrConfig {
log_and_print_error("'host' or 'uri' is required for configuration".to_owned()); log_and_print_error("'host' or 'uri' is required for configuration".to_owned());
process::exit(1); process::exit(1);
} }
if self.api_token_file.is_none() && self.api_token.is_none() {
log_and_print_error(
"'api_token' or 'api_token_path' is required for configuration".to_owned(),
);
process::exit(1);
}
}
pub fn post_process_initialization(&mut self) {
if let Some(api_token_file) = self.api_token_file.as_ref() {
if !PathBuf::from(api_token_file).exists() {
log_and_print_error(format!(
"The specified {} API token file does not exist",
api_token_file
));
process::exit(1);
}
let api_token = fs::read_to_string(api_token_file)
.map_err(|e| anyhow!(e))
.unwrap();
self.api_token = Some(api_token.trim().to_owned());
}
} }
} }
impl Default for ServarrConfig { impl Default for ServarrConfig {
fn default() -> Self { fn default() -> Self {
ServarrConfig { ServarrConfig {
name: None,
host: Some("localhost".to_string()), host: Some("localhost".to_string()),
port: None, port: None,
uri: None, uri: None,
api_token: "".to_string(), weight: None,
api_token: Some(String::new()),
api_token_file: None,
ssl_cert_path: None, ssl_cert_path: None,
} }
} }
@@ -251,3 +401,52 @@ pub fn log_and_print_error(error: String) {
error!("{}", error); error!("{}", error);
eprintln!("error: {}", error.red()); eprintln!("error: {}", error.red());
} }
fn deserialize_optional_env_var<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(value) => {
let interpolated = interpolate_env_vars(&value);
Ok(Some(interpolated))
}
None => Ok(None),
}
}
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(value) => {
let interpolated = interpolate_env_vars(&value);
interpolated
.parse::<u16>()
.map(Some)
.map_err(serde::de::Error::custom)
}
None => Ok(None),
}
}
fn interpolate_env_vars(s: &str) -> String {
let result = s.to_string();
let scrubbing_regex = Regex::new(r#"[\s\{\}!\$^\(\)\[\]\\\|`'"]+"#).unwrap();
let var_regex = Regex::new(r"\$\{(.*?)\}").unwrap();
var_regex
.replace_all(s, |caps: &regex::Captures<'_>| {
if let Some(mat) = caps.get(1) {
if let Ok(value) = std::env::var(mat.as_str()) {
return scrubbing_regex.replace_all(&value, "").to_string();
}
}
scrubbing_regex.replace_all(&result, "").to_string()
})
.to_string()
}
+70 -26
View File
@@ -8,7 +8,7 @@ pub mod radarr_context_clues;
#[path = "radarr_tests.rs"] #[path = "radarr_tests.rs"]
mod radarr_tests; mod radarr_tests;
impl<'a> App<'a> { impl App<'_> {
pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) { pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) {
match active_radarr_block { match active_radarr_block {
ActiveRadarrBlock::Blocklist => { ActiveRadarrBlock::Blocklist => {
@@ -17,11 +17,23 @@ impl<'a> App<'a> {
.await; .await;
} }
ActiveRadarrBlock::Collections => { ActiveRadarrBlock::Collections => {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self self
.dispatch_network_event(RadarrEvent::GetCollections.into()) .dispatch_network_event(RadarrEvent::GetCollections.into())
.await; .await;
self
.dispatch_network_event(RadarrEvent::GetMovies.into())
.await;
} }
ActiveRadarrBlock::CollectionDetails => { ActiveRadarrBlock::CollectionDetails => {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self.is_loading = true; self.is_loading = true;
self.populate_movie_collection_table().await; self.populate_movie_collection_table().await;
self.is_loading = false; self.is_loading = false;
@@ -37,6 +49,12 @@ impl<'a> App<'a> {
.await; .await;
} }
ActiveRadarrBlock::Movies => { ActiveRadarrBlock::Movies => {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self self
.dispatch_network_event(RadarrEvent::GetMovies.into()) .dispatch_network_event(RadarrEvent::GetMovies.into())
.await; .await;
@@ -45,6 +63,9 @@ impl<'a> App<'a> {
.await; .await;
} }
ActiveRadarrBlock::Indexers => { ActiveRadarrBlock::Indexers => {
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self self
.dispatch_network_event(RadarrEvent::GetIndexers.into()) .dispatch_network_event(RadarrEvent::GetIndexers.into())
.await; .await;
@@ -56,7 +77,9 @@ impl<'a> App<'a> {
} }
ActiveRadarrBlock::TestIndexer => { ActiveRadarrBlock::TestIndexer => {
self self
.dispatch_network_event(RadarrEvent::TestIndexer(None).into()) .dispatch_network_event(
RadarrEvent::TestIndexer(self.extract_radarr_indexer_id().await).into(),
)
.await; .await;
} }
ActiveRadarrBlock::TestAllIndexers => { ActiveRadarrBlock::TestAllIndexers => {
@@ -72,7 +95,7 @@ impl<'a> App<'a> {
.dispatch_network_event(RadarrEvent::GetQueuedEvents.into()) .dispatch_network_event(RadarrEvent::GetQueuedEvents.into())
.await; .await;
self self
.dispatch_network_event(RadarrEvent::GetLogs(None).into()) .dispatch_network_event(RadarrEvent::GetLogs(500).into())
.await; .await;
} }
ActiveRadarrBlock::SystemUpdates => { ActiveRadarrBlock::SystemUpdates => {
@@ -82,17 +105,23 @@ impl<'a> App<'a> {
} }
ActiveRadarrBlock::AddMovieSearchResults => { ActiveRadarrBlock::AddMovieSearchResults => {
self self
.dispatch_network_event(RadarrEvent::SearchNewMovie(None).into()) .dispatch_network_event(
RadarrEvent::SearchNewMovie(self.extract_movie_search_query().await).into(),
)
.await; .await;
} }
ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => {
self self
.dispatch_network_event(RadarrEvent::GetMovieDetails(None).into()) .dispatch_network_event(
RadarrEvent::GetMovieDetails(self.extract_movie_id().await).into(),
)
.await; .await;
} }
ActiveRadarrBlock::MovieHistory => { ActiveRadarrBlock::MovieHistory => {
self self
.dispatch_network_event(RadarrEvent::GetMovieHistory(None).into()) .dispatch_network_event(
RadarrEvent::GetMovieHistory(self.extract_movie_id().await).into(),
)
.await; .await;
} }
ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => { ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew => {
@@ -102,7 +131,9 @@ impl<'a> App<'a> {
|| movie_details_modal.movie_crew.items.is_empty() => || movie_details_modal.movie_crew.items.is_empty() =>
{ {
self self
.dispatch_network_event(RadarrEvent::GetMovieCredits(None).into()) .dispatch_network_event(
RadarrEvent::GetMovieCredits(self.extract_movie_id().await).into(),
)
.await; .await;
} }
_ => (), _ => (),
@@ -111,7 +142,7 @@ impl<'a> App<'a> {
ActiveRadarrBlock::ManualSearch => match self.data.radarr_data.movie_details_modal.as_ref() { ActiveRadarrBlock::ManualSearch => match self.data.radarr_data.movie_details_modal.as_ref() {
Some(movie_details_modal) if movie_details_modal.movie_releases.items.is_empty() => { Some(movie_details_modal) if movie_details_modal.movie_releases.items.is_empty() => {
self self
.dispatch_network_event(RadarrEvent::GetReleases(None).into()) .dispatch_network_event(RadarrEvent::GetReleases(self.extract_movie_id().await).into())
.await; .await;
} }
_ => (), _ => (),
@@ -119,36 +150,31 @@ impl<'a> App<'a> {
_ => (), _ => (),
} }
self.check_for_prompt_action().await; self.check_for_radarr_prompt_action().await;
self.reset_tick_count(); self.reset_tick_count();
} }
async fn check_for_prompt_action(&mut self) { async fn check_for_radarr_prompt_action(&mut self) {
if self.data.radarr_data.prompt_confirm { if self.data.radarr_data.prompt_confirm {
self.data.radarr_data.prompt_confirm = false; self.data.radarr_data.prompt_confirm = false;
if let Some(radarr_event) = &self.data.radarr_data.prompt_confirm_action { if let Some(radarr_event) = self.data.radarr_data.prompt_confirm_action.take() {
self self.dispatch_network_event(radarr_event.into()).await;
.dispatch_network_event(radarr_event.clone().into())
.await;
self.should_refresh = true; self.should_refresh = true;
self.data.radarr_data.prompt_confirm_action = None;
} }
} }
} }
pub(super) async fn radarr_on_tick( pub(super) async fn radarr_on_tick(&mut self, active_radarr_block: ActiveRadarrBlock) {
&mut self, if self.is_first_render {
active_radarr_block: ActiveRadarrBlock, self.refresh_radarr_metadata().await;
is_first_render: bool,
) {
if is_first_render {
self.refresh_metadata().await;
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
self.is_first_render = false;
return;
} }
if self.should_refresh { if self.should_refresh {
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
self.refresh_metadata().await; self.refresh_radarr_metadata().await;
} }
if self.is_routing { if self.is_routing {
@@ -156,16 +182,15 @@ impl<'a> App<'a> {
self.cancellation_token.cancel(); self.cancellation_token.cancel();
} else { } else {
self.dispatch_by_radarr_block(&active_radarr_block).await; self.dispatch_by_radarr_block(&active_radarr_block).await;
self.refresh_metadata().await;
} }
} }
if self.tick_count % self.tick_until_poll == 0 { if self.tick_count % self.tick_until_poll == 0 {
self.refresh_metadata().await; self.refresh_radarr_metadata().await;
} }
} }
async fn refresh_metadata(&mut self) { async fn refresh_radarr_metadata(&mut self) {
self self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) .dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await; .await;
@@ -201,4 +226,23 @@ impl<'a> App<'a> {
.collection_movies .collection_movies
.set_items(collection_movies); .set_items(collection_movies);
} }
async fn extract_movie_id(&self) -> i64 {
self.data.radarr_data.movies.current_selection().id
}
async fn extract_movie_search_query(&self) -> String {
self
.data
.radarr_data
.add_movie_search
.as_ref()
.expect("Add movie search is empty")
.text
.clone()
}
async fn extract_radarr_indexer_id(&self) -> i64 {
self.data.radarr_data.indexers.current_selection().id
}
} }
+10 -62
View File
@@ -35,60 +35,6 @@ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter"), (DEFAULT_KEYBINDINGS.esc, "cancel filter"),
]; ];
pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
];
pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.clear, "clear blocklist"),
];
pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "edit indexer"),
(
DEFAULT_KEYBINDINGS.settings,
DEFAULT_KEYBINDINGS.settings.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.test, "test indexer"),
(DEFAULT_KEYBINDINGS.test_all, "test all indexers"),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.tasks, "open tasks"),
(DEFAULT_KEYBINDINGS.events, "open events"),
(DEFAULT_KEYBINDINGS.logs, "open logs"),
(DEFAULT_KEYBINDINGS.update, "open updates"),
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
];
pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
@@ -96,7 +42,10 @@ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
), ),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.search, "auto search"), (
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
@@ -108,7 +57,10 @@ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, "auto search"), (
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
@@ -120,17 +72,13 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"), (DEFAULT_KEYBINDINGS.esc, "edit search"),
]; ];
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.confirm, "submit"),
(DEFAULT_KEYBINDINGS.esc, "cancel"),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"), (DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.submit, "show overview/add movie"), (DEFAULT_KEYBINDINGS.submit, "show overview/add movie"),
(DEFAULT_KEYBINDINGS.edit, "edit collection"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
+13 -160
View File
@@ -4,11 +4,10 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
}; };
#[test] #[test]
@@ -113,141 +112,6 @@ mod tests {
assert_eq!(collections_context_clues.next(), None); assert_eq!(collections_context_clues.next(), None);
} }
#[test]
fn test_downloads_context_clues() {
let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter();
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
assert_eq!(downloads_context_clues_iter.next(), None);
}
#[test]
fn test_blocklist_context_clues() {
let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter();
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear);
assert_str_eq!(*description, "clear blocklist");
assert_eq!(blocklist_context_clues_iter.next(), None);
}
#[test]
fn test_root_folders_context_clues() {
let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter();
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc);
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = root_folders_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(root_folders_context_clues_iter.next(), None);
}
#[test]
fn test_indexers_context_clues() {
let mut indexers_context_clues_iter = INDEXERS_CONTEXT_CLUES.iter();
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "edit indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.settings);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.settings.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test);
assert_str_eq!(*description, "test indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test_all);
assert_str_eq!(*description, "test all indexers");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(indexers_context_clues_iter.next(), None);
}
#[test]
fn test_system_context_clues() {
let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter();
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tasks);
assert_str_eq!(*description, "open tasks");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.events);
assert_str_eq!(*description, "open events");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.logs);
assert_str_eq!(*description, "open logs");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "open updates");
let (key_binding, description) = system_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(system_context_clues_iter.next(), None);
}
#[test] #[test]
fn test_movie_details_context_clues() { fn test_movie_details_context_clues() {
let mut movie_details_context_clues_iter = MOVIE_DETAILS_CONTEXT_CLUES.iter(); let mut movie_details_context_clues_iter = MOVIE_DETAILS_CONTEXT_CLUES.iter();
@@ -269,8 +133,8 @@ mod tests {
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap(); let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, "auto search"); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = movie_details_context_clues_iter.next().unwrap(); let (key_binding, description) = movie_details_context_clues_iter.next().unwrap();
@@ -305,8 +169,8 @@ mod tests {
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, "auto search"); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
@@ -349,22 +213,6 @@ mod tests {
assert_eq!(add_movie_search_results_context_clues_iter.next(), None); assert_eq!(add_movie_search_results_context_clues_iter.next(), None);
} }
#[test]
fn test_confirmation_prompt_context_clues() {
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
assert_str_eq!(*description, "submit");
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel");
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
}
#[test] #[test]
fn test_system_tasks_context_clues() { fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
@@ -392,6 +240,11 @@ mod tests {
let (key_binding, description) = collection_details_context_clues_iter.next().unwrap(); let (key_binding, description) = collection_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, "edit collection");
let (key_binding, description) = collection_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(collection_details_context_clues_iter.next(), None); assert_eq!(collection_details_context_clues_iter.next(), None);
File diff suppressed because it is too large Load Diff
+306
View File
@@ -0,0 +1,306 @@
use crate::{
models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
network::sonarr_network::SonarrEvent,
};
use super::App;
pub mod sonarr_context_clues;
#[cfg(test)]
#[path = "sonarr_tests.rs"]
mod sonarr_tests;
impl App<'_> {
pub(super) async fn dispatch_by_sonarr_block(&mut self, active_sonarr_block: &ActiveSonarrBlock) {
match active_sonarr_block {
ActiveSonarrBlock::Series => {
self
.dispatch_network_event(SonarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetLanguageProfiles.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(SonarrEvent::ListSeries.into())
.await;
}
ActiveSonarrBlock::SeriesDetails => {
self
.dispatch_network_event(SonarrEvent::ListSeries.into())
.await;
self.is_loading = true;
self.populate_seasons_table().await;
self.is_loading = false;
}
ActiveSonarrBlock::SeriesHistory => {
self
.dispatch_network_event(
SonarrEvent::GetSeriesHistory(self.extract_series_id().await).into(),
)
.await;
}
ActiveSonarrBlock::SeasonDetails => {
self
.dispatch_network_event(SonarrEvent::GetEpisodes(self.extract_series_id().await).into())
.await;
self
.dispatch_network_event(
SonarrEvent::GetEpisodeFiles(self.extract_series_id().await).into(),
)
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.await;
}
ActiveSonarrBlock::SeasonHistory => {
if !self.data.sonarr_data.seasons.is_empty() {
self
.dispatch_network_event(
SonarrEvent::GetSeasonHistory(self.extract_series_id_season_number_tuple().await)
.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() => {
self
.dispatch_network_event(
SonarrEvent::GetSeasonReleases(self.extract_series_id_season_number_tuple().await)
.into(),
)
.await;
}
_ => (),
}
}
ActiveSonarrBlock::EpisodeDetails | ActiveSonarrBlock::EpisodeFile => {
self
.dispatch_network_event(
SonarrEvent::GetEpisodeDetails(self.extract_episode_id().await).into(),
)
.await;
}
ActiveSonarrBlock::EpisodeHistory => {
self
.dispatch_network_event(
SonarrEvent::GetEpisodeHistory(self.extract_episode_id().await).into(),
)
.await;
}
ActiveSonarrBlock::ManualEpisodeSearch => {
if let Some(season_details_modal) = self.data.sonarr_data.season_details_modal.as_ref() {
if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() {
if episode_details_modal.episode_releases.is_empty() {
self
.dispatch_network_event(
SonarrEvent::GetEpisodeReleases(self.extract_episode_id().await).into(),
)
.await;
}
}
}
}
ActiveSonarrBlock::Downloads => {
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.await;
}
ActiveSonarrBlock::Blocklist => {
self
.dispatch_network_event(SonarrEvent::ListSeries.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetBlocklist.into())
.await;
}
ActiveSonarrBlock::History => {
self
.dispatch_network_event(SonarrEvent::GetHistory(500).into())
.await;
}
ActiveSonarrBlock::RootFolders => {
self
.dispatch_network_event(SonarrEvent::GetRootFolders.into())
.await;
}
ActiveSonarrBlock::Indexers => {
self
.dispatch_network_event(SonarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetIndexers.into())
.await;
}
ActiveSonarrBlock::AllIndexerSettingsPrompt => {
self
.dispatch_network_event(SonarrEvent::GetAllIndexerSettings.into())
.await;
}
ActiveSonarrBlock::TestIndexer => {
self
.dispatch_network_event(
SonarrEvent::TestIndexer(self.extract_sonarr_indexer_id().await).into(),
)
.await;
}
ActiveSonarrBlock::TestAllIndexers => {
self
.dispatch_network_event(SonarrEvent::TestAllIndexers.into())
.await;
}
ActiveSonarrBlock::System => {
self
.dispatch_network_event(SonarrEvent::GetTasks.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetQueuedEvents.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetLogs(500).into())
.await;
}
ActiveSonarrBlock::AddSeriesSearchResults => {
self
.dispatch_network_event(
SonarrEvent::SearchNewSeries(self.extract_add_new_series_search_query().await).into(),
)
.await;
}
ActiveSonarrBlock::SystemUpdates => {
self
.dispatch_network_event(SonarrEvent::GetUpdates.into())
.await;
}
_ => (),
}
self.check_for_sonarr_prompt_action().await;
self.reset_tick_count();
}
async fn check_for_sonarr_prompt_action(&mut self) {
if self.data.sonarr_data.prompt_confirm {
self.data.sonarr_data.prompt_confirm = false;
if let Some(sonarr_event) = self.data.sonarr_data.prompt_confirm_action.take() {
self.dispatch_network_event(sonarr_event.into()).await;
self.should_refresh = true;
}
}
}
pub(super) async fn sonarr_on_tick(&mut self, active_sonarr_block: ActiveSonarrBlock) {
if self.is_first_render {
self.refresh_sonarr_metadata().await;
self.dispatch_by_sonarr_block(&active_sonarr_block).await;
self.is_first_render = false;
return;
}
if self.should_refresh {
self.dispatch_by_sonarr_block(&active_sonarr_block).await;
self.refresh_sonarr_metadata().await;
}
if self.is_routing {
if !self.should_refresh {
self.cancellation_token.cancel();
} else {
self.dispatch_by_sonarr_block(&active_sonarr_block).await;
}
}
if self.tick_count % self.tick_until_poll == 0 {
self.refresh_sonarr_metadata().await;
}
}
async fn refresh_sonarr_metadata(&mut self) {
self
.dispatch_network_event(SonarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetLanguageProfiles.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetDiskSpace.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetStatus.into())
.await;
}
async fn populate_seasons_table(&mut self) {
let seasons = self
.data
.sonarr_data
.series
.current_selection()
.clone()
.seasons
.unwrap_or_default()
.into_iter()
.map(|mut season| {
season.title = Some(format!("Season {}", season.season_number));
season
})
.collect();
self.data.sonarr_data.seasons.set_items(seasons);
}
async fn extract_episode_id(&self) -> i64 {
self
.data
.sonarr_data
.season_details_modal
.as_ref()
.expect("Season details have not been loaded")
.episodes
.current_selection()
.id
}
async fn extract_series_id(&self) -> i64 {
self.data.sonarr_data.series.current_selection().id
}
async fn extract_series_id_season_number_tuple(&self) -> (i64, i64) {
let series_id = self.data.sonarr_data.series.current_selection().id;
let season_number = self
.data
.sonarr_data
.seasons
.current_selection()
.season_number;
(series_id, season_number)
}
async fn extract_add_new_series_search_query(&self) -> String {
self
.data
.sonarr_data
.add_series_search
.as_ref()
.expect("Add series search is empty")
.text
.clone()
}
async fn extract_sonarr_indexer_id(&self) -> i64 {
self.data.sonarr_data.indexers.current_selection().id
}
}
+159
View File
@@ -0,0 +1,159 @@
use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
#[cfg(test)]
#[path = "sonarr_context_clues_tests.rs"]
mod sonarr_context_clues_tests;
pub static ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
];
pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.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.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.submit, "season details"),
(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 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,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(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.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static SEASON_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.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
(
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.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
(
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.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] =
[(DEFAULT_KEYBINDINGS.submit, "details")];
pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(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),
];
@@ -0,0 +1,402 @@
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::{
key_binding::DEFAULT_KEYBINDINGS,
sonarr::sonarr_context_clues::{
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, DETAILS_CONTEXTUAL_CONTEXT_CLUES,
EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXTUAL_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,
},
};
#[test]
fn test_add_series_search_results_context_clues() {
let mut add_series_search_results_context_clues_iter =
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES.iter();
let (key_binding, description) = add_series_search_results_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = add_series_search_results_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "edit search");
assert_eq!(add_series_search_results_context_clues_iter.next(), None);
}
#[test]
fn test_series_context_clues() {
let mut series_context_clues_iter = SERIES_CONTEXT_CLUES.iter();
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, "update all");
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter");
assert_eq!(series_context_clues_iter.next(), None);
}
#[test]
fn test_series_history_context_clues() {
let mut series_history_context_clues_iter = SERIES_HISTORY_CONTEXT_CLUES.iter();
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter/close");
assert_eq!(series_history_context_clues_iter.next(), None);
}
#[test]
fn test_history_context_clues() {
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
let (key_binding, description) = history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter");
assert_eq!(history_context_clues_iter.next(), None);
}
#[test]
fn test_series_details_context_clues() {
let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter();
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.toggle_monitoring);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.toggle_monitoring.desc);
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "season details");
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc);
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = series_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(series_details_context_clues_iter.next(), None);
}
#[test]
fn test_season_details_context_clues() {
let mut season_details_context_clues_iter = SEASON_DETAILS_CONTEXT_CLUES.iter();
let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.toggle_monitoring);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.toggle_monitoring.desc);
let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(season_details_context_clues_iter.next(), None);
}
#[test]
fn test_season_details_contextual_context_clues() {
let mut season_details_contextual_context_clues_iter =
SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter();
let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "episode details");
let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, "delete episode");
assert_eq!(season_details_contextual_context_clues_iter.next(), None);
}
#[test]
fn test_season_history_context_clues() {
let mut season_history_context_clues_iter = SEASON_HISTORY_CONTEXT_CLUES.iter();
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter/close");
assert_eq!(season_history_context_clues_iter.next(), None);
}
#[test]
fn test_manual_season_search_context_clues() {
let mut manual_season_search_context_clues_iter = MANUAL_SEASON_SEARCH_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_season_search_context_clues_iter.next(), None);
}
#[test]
fn test_manual_episode_search_context_clues() {
let mut manual_episode_search_context_clues_iter = MANUAL_EPISODE_SEARCH_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_episode_search_context_clues_iter.next(), None);
}
#[test]
fn details_contextual_context_clues() {
let mut manual_search_contextual_context_clues_iter = DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_search_contextual_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
assert_eq!(manual_search_contextual_context_clues_iter.next(), None);
}
#[test]
fn test_episode_details_context_clues() {
let mut episode_details_context_clues_iter = EPISODE_DETAILS_CONTEXT_CLUES.iter();
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(episode_details_context_clues_iter.next(), None);
}
#[test]
fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
let (key_binding, description) = system_tasks_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "start task");
let (key_binding, description) = system_tasks_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(system_tasks_context_clues_iter.next(), None);
}
}
+881
View File
@@ -0,0 +1,881 @@
#[cfg(test)]
mod tests {
mod sonarr_tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc;
use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data;
use crate::models::servarr_models::Indexer;
use crate::models::sonarr_models::Episode;
use crate::{
app::App,
models::{
servarr_data::sonarr::{
modals::{EpisodeDetailsModal, SeasonDetailsModal},
sonarr_data::ActiveSonarrBlock,
},
sonarr_models::{Season, Series, SonarrRelease},
},
network::{sonarr_network::SonarrEvent, NetworkEvent},
};
#[tokio::test]
async fn test_dispatch_by_blocklist_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::Blocklist)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::ListSeries.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetBlocklist.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_series_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeriesHistory)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetSeriesHistory(1).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_series_details_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.series.set_items(vec![Series {
seasons: Some(vec![Season::default()]),
..Series::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeriesDetails)
.await;
assert!(!app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::ListSeries.into()
);
assert!(!app.data.sonarr_data.seasons.items.is_empty());
assert_eq!(app.tick_count, 0);
assert!(!app.data.sonarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_dispatch_by_season_details_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonDetails)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodes(1).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeFiles(1).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_season_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
app.data.sonarr_data.seasons.set_items(vec![Season {
season_number: 1,
..Season::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonHistory)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetSeasonHistory((1, 1)).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_season_history_block_no_op_when_seasons_table_is_empty() {
let (mut app, _) = construct_app_unit();
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonHistory)
.await;
assert!(!app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_season_search_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
app.data.sonarr_data.seasons.set_items(vec![Season {
season_number: 1,
..Season::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetSeasonReleases((1, 1)).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_season_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::test_default()
};
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_season_search_block_season_releases_non_empty() {
let mut app = App::test_default();
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal
.season_releases
.set_items(vec![SonarrRelease::default()]);
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_episode_details_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data = create_test_sonarr_data();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeDetails)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeDetails(0).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_episode_file_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data = create_test_sonarr_data();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeFile)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeDetails(0).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_episode_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.episodes.set_items(vec![Episode {
id: 1,
..Episode::default()
}]);
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeHistory)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeHistory(1).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_episode_search_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
let mut season_details_modal = SeasonDetailsModal {
episode_details_modal: Some(EpisodeDetailsModal::default()),
..SeasonDetailsModal::default()
};
season_details_modal.episodes.set_items(vec![Episode {
id: 1,
..Episode::default()
}]);
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeReleases(1).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_episode_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::test_default()
};
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_episode_search_block_episode_releases_non_empty() {
let mut app = App::test_default();
let mut episode_details_modal = EpisodeDetailsModal::default();
episode_details_modal
.episode_releases
.set_items(vec![SonarrRelease::default()]);
let season_details_modal = SeasonDetailsModal {
episode_details_modal: Some(episode_details_modal),
..SeasonDetailsModal::default()
};
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
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_sonarr_block(&ActiveSonarrBlock::History)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetHistory(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_downloads_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::Downloads)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert!(!app.data.sonarr_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();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::RootFolders)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetRootFolders.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_series_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::Series)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetLanguageProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::ListSeries.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_indexers_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::Indexers)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetIndexers.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_all_indexer_settings_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::AllIndexerSettingsPrompt)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetAllIndexerSettings.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_indexer_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.indexers.set_items(vec![Indexer {
id: 1,
..Indexer::default()
}]);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::TestIndexer)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::TestIndexer(1).into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_test_all_indexers_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::TestAllIndexers)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::TestAllIndexers.into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::System)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetTasks.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetQueuedEvents.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetLogs(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_system_updates_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SystemUpdates)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetUpdates.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_add_series_search_results_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.add_series_search = Some("test search".into());
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::AddSeriesSearchResults)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::SearchNewSeries("test search".into()).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_check_for_sonarr_prompt_action_no_prompt_confirm() {
let mut app = App::test_default();
app.data.sonarr_data.prompt_confirm = false;
app.check_for_sonarr_prompt_action().await;
assert!(!app.data.sonarr_data.prompt_confirm);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_sonarr_prompt_action() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::GetStatus);
app.check_for_sonarr_prompt_action().await;
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetStatus.into()
);
assert!(app.should_refresh);
assert_eq!(app.data.sonarr_data.prompt_confirm_action, None);
}
#[tokio::test]
async fn test_sonarr_refresh_metadata() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_routing = true;
app.refresh_sonarr_metadata().await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetLanguageProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetStatus.into()
);
assert!(app.is_loading);
}
#[tokio::test]
async fn test_sonarr_on_tick_first_render() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_first_render = true;
app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetLanguageProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetStatus.into()
);
assert!(app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert!(!app.is_first_render);
}
#[tokio::test]
async fn test_sonarr_on_tick_routing() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_routing = true;
app.should_refresh = true;
app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_sonarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (mut app, _) = construct_app_unit();
app.is_routing = true;
app.should_refresh = false;
app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await;
assert!(app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_sonarr_on_tick_should_refresh() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.should_refresh = true;
app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert!(app.should_refresh);
assert!(!app.data.sonarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_sonarr_on_tick_should_refresh_does_not_cancel_prompt_requests() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_loading = true;
app.is_routing = true;
app.should_refresh = true;
app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
assert!(!app.data.sonarr_data.prompt_confirm);
assert!(!app.cancellation_token.is_cancelled());
}
#[tokio::test]
async fn test_sonarr_on_tick_network_tick_frequency() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.tick_count = 2;
app.tick_until_poll = 2;
app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetLanguageProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
}
#[tokio::test]
async fn test_populate_seasons_table_unfiltered() {
let mut app = App::test_default();
app.data.sonarr_data.series.set_items(vec![Series {
seasons: Some(vec![Season::default()]),
..Series::default()
}]);
app.populate_seasons_table().await;
assert!(!app.data.sonarr_data.seasons.items.is_empty());
assert_str_eq!(
app.data.sonarr_data.seasons.items[0]
.title
.as_ref()
.unwrap(),
"Season 0"
);
}
#[tokio::test]
async fn test_populate_seasons_table_filtered() {
let mut app = App::test_default();
app.data.sonarr_data.series.set_filtered_items(vec![Series {
seasons: Some(vec![Season::default()]),
..Series::default()
}]);
app.populate_seasons_table().await;
assert!(!app.data.sonarr_data.seasons.items.is_empty());
assert_str_eq!(
app.data.sonarr_data.seasons.items[0]
.title
.as_ref()
.unwrap(),
"Season 0"
);
}
#[tokio::test]
async fn test_extract_episode_id() {
let mut app = App::test_default();
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.episodes.set_items(vec![Episode {
id: 1,
..Episode::default()
}]);
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
assert_eq!(app.extract_episode_id().await, 1);
}
#[tokio::test]
#[should_panic(expected = "Season details have not been loaded")]
async fn test_extract_episode_id_requires_season_details_modal_to_be_some() {
let app = App::test_default();
assert_eq!(app.extract_episode_id().await, 0);
}
#[tokio::test]
async fn test_extract_series_id() {
let mut app = App::test_default();
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
assert_eq!(app.extract_series_id().await, 1);
}
#[tokio::test]
async fn test_extract_series_id_season_number_tuple() {
let mut app = App::test_default();
app.data.sonarr_data.series.set_items(vec![Series {
id: 1,
..Series::default()
}]);
app.data.sonarr_data.seasons.set_items(vec![Season {
season_number: 1,
..Season::default()
}]);
assert_eq!(app.extract_series_id_season_number_tuple().await, (1, 1));
}
#[tokio::test]
async fn test_extract_add_new_series_search_query() {
let mut app = App::test_default();
app.data.sonarr_data.add_series_search = Some("test search".into());
assert_str_eq!(
app.extract_add_new_series_search_query().await,
"test search"
);
}
#[tokio::test]
#[should_panic(expected = "Add series search is empty")]
async fn test_extract_add_new_series_search_query_panics_when_the_query_is_not_set() {
let app = App::test_default();
app.extract_add_new_series_search_query().await;
}
#[tokio::test]
async fn test_extract_sonarr_indexer_id() {
let mut app = App::test_default();
app.data.sonarr_data.indexers.set_items(vec![Indexer {
id: 1,
..Indexer::default()
}]);
assert_eq!(app.extract_sonarr_indexer_id().await, 1);
}
fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver<NetworkEvent>) {
let (sync_network_tx, sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App {
network_tx: Some(sync_network_tx),
tick_count: 1,
is_first_render: false,
..App::test_default()
};
app.data.sonarr_data.prompt_confirm = true;
(app, sync_network_rx)
}
}
}
+2 -2
View File
@@ -136,7 +136,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let clear_blocklist_command = RadarrCommand::ClearBlocklist.into(); let clear_blocklist_command = RadarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await; let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
@@ -167,7 +167,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let clear_blocklist_command = SonarrCommand::ClearBlocklist.into(); let clear_blocklist_command = SonarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await; let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
+8 -4
View File
@@ -4,6 +4,8 @@ use anyhow::Result;
use clap::{arg, command, ArgAction, Subcommand}; use clap::{arg, command, ArgAction, Subcommand};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::RadarrCommand;
use crate::models::servarr_models::AddRootFolderBody;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
@@ -11,8 +13,6 @@ use crate::{
network::{radarr_network::RadarrEvent, NetworkTrait}, network::{radarr_network::RadarrEvent, NetworkTrait},
}; };
use super::RadarrCommand;
#[cfg(test)] #[cfg(test)]
#[path = "add_command_handler_tests.rs"] #[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests; mod add_command_handler_tests;
@@ -125,6 +125,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
minimum_availability: minimum_availability.to_string(), minimum_availability: minimum_availability.to_string(),
monitored: !disable_monitoring, monitored: !disable_monitoring,
tags, tags,
tag_input_string: None,
add_options: AddMovieOptions { add_options: AddMovieOptions {
monitor: monitor.to_string(), monitor: monitor.to_string(),
search_for_movie: !no_search_for_movie, search_for_movie: !no_search_for_movie,
@@ -132,14 +133,17 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
}; };
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::AddMovie(Some(body)).into()) .handle_network_event(RadarrEvent::AddMovie(body).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrAddCommand::RootFolder { root_folder_path } => { RadarrAddCommand::RootFolder { root_folder_path } => {
let add_root_folder_body = AddRootFolderBody {
path: root_folder_path,
};
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::AddRootFolder(Some(root_folder_path)).into()) .handle_network_event(RadarrEvent::AddRootFolder(add_root_folder_body).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+10 -5
View File
@@ -368,6 +368,7 @@ mod tests {
use super::*; use super::*;
use mockall::predicate::eq; use mockall::predicate::eq;
use crate::models::servarr_models::AddRootFolderBody;
use serde_json::json; use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -381,6 +382,7 @@ mod tests {
minimum_availability: "released".to_owned(), minimum_availability: "released".to_owned(),
monitored: false, monitored: false,
tags: vec![1, 2], tags: vec![1, 2],
tag_input_string: None,
add_options: AddMovieOptions { add_options: AddMovieOptions {
monitor: "movieAndCollection".to_owned(), monitor: "movieAndCollection".to_owned(),
search_for_movie: false, search_for_movie: false,
@@ -390,7 +392,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::AddMovie(Some(expected_add_movie_body)).into(), RadarrEvent::AddMovie(expected_add_movie_body).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -398,7 +400,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_movie_command = RadarrAddCommand::Movie { let add_movie_command = RadarrAddCommand::Movie {
tmdb_id: 1, tmdb_id: 1,
root_folder_path: "/test".to_owned(), root_folder_path: "/test".to_owned(),
@@ -420,11 +422,14 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_handle_add_root_folder_command() { async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned(); let expected_root_folder_path = "/nfs/test".to_owned();
let expected_add_root_folder_body = AddRootFolderBody {
path: expected_root_folder_path.clone(),
};
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(), RadarrEvent::AddRootFolder(expected_add_root_folder_body).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -432,7 +437,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_root_folder_command = RadarrAddCommand::RootFolder { let add_root_folder_command = RadarrAddCommand::RootFolder {
root_folder_path: expected_root_folder_path, root_folder_path: expected_root_folder_path,
}; };
@@ -460,7 +465,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = RadarrAddCommand::Tag { let add_tag_command = RadarrAddCommand::Tag {
name: expected_tag_name, name: expected_tag_name,
}; };
+5 -5
View File
@@ -89,21 +89,21 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => { RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into()) .handle_network_event(RadarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrDeleteCommand::Download { download_id } => { RadarrDeleteCommand::Download { download_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::DeleteDownload(Some(download_id)).into()) .handle_network_event(RadarrEvent::DeleteDownload(download_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrDeleteCommand::Indexer { indexer_id } => { RadarrDeleteCommand::Indexer { indexer_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::DeleteIndexer(Some(indexer_id)).into()) .handle_network_event(RadarrEvent::DeleteIndexer(indexer_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -119,14 +119,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
}; };
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::DeleteMovie(Some(delete_movie_params)).into()) .handle_network_event(RadarrEvent::DeleteMovie(delete_movie_params).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrDeleteCommand::RootFolder { root_folder_id } => { RadarrDeleteCommand::RootFolder { root_folder_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::DeleteRootFolder(Some(root_folder_id)).into()) .handle_network_event(RadarrEvent::DeleteRootFolder(root_folder_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+11 -11
View File
@@ -268,7 +268,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), RadarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -276,7 +276,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_blocklist_item_command = RadarrDeleteCommand::BlocklistItem { let delete_blocklist_item_command = RadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1, blocklist_item_id: 1,
}; };
@@ -299,7 +299,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DeleteDownload(Some(expected_download_id)).into(), RadarrEvent::DeleteDownload(expected_download_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -307,7 +307,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_download_command = RadarrDeleteCommand::Download { download_id: 1 }; let delete_download_command = RadarrDeleteCommand::Download { download_id: 1 };
let result = let result =
@@ -325,7 +325,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(), RadarrEvent::DeleteIndexer(expected_indexer_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -333,7 +333,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_indexer_command = RadarrDeleteCommand::Indexer { indexer_id: 1 }; let delete_indexer_command = RadarrDeleteCommand::Indexer { indexer_id: 1 };
let result = let result =
@@ -355,7 +355,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DeleteMovie(Some(expected_delete_movie_params)).into(), RadarrEvent::DeleteMovie(expected_delete_movie_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -363,7 +363,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_movie_command = RadarrDeleteCommand::Movie { let delete_movie_command = RadarrDeleteCommand::Movie {
movie_id: 1, movie_id: 1,
delete_files_from_disk: true, delete_files_from_disk: true,
@@ -385,7 +385,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(), RadarrEvent::DeleteRootFolder(expected_root_folder_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -393,7 +393,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_root_folder_command = RadarrDeleteCommand::RootFolder { root_folder_id: 1 }; let delete_root_folder_command = RadarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = let result =
@@ -419,7 +419,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_tag_command = RadarrDeleteCommand::Tag { tag_id: 1 }; let delete_tag_command = RadarrDeleteCommand::Tag { tag_id: 1 };
let result = let result =
+7 -11
View File
@@ -379,18 +379,12 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
rss_sync_interval: rss_sync_interval rss_sync_interval: rss_sync_interval
.unwrap_or(previous_indexer_settings.rss_sync_interval), .unwrap_or(previous_indexer_settings.rss_sync_interval),
whitelisted_hardcoded_subs: whitelisted_subtitle_tags whitelisted_hardcoded_subs: whitelisted_subtitle_tags
.clone() .unwrap_or(previous_indexer_settings.whitelisted_hardcoded_subs.text)
.unwrap_or_else(|| {
previous_indexer_settings
.whitelisted_hardcoded_subs
.text
.clone()
})
.into(), .into(),
}; };
self self
.network .network
.handle_network_event(RadarrEvent::EditAllIndexerSettings(Some(params)).into()) .handle_network_event(RadarrEvent::EditAllIndexerSettings(params).into())
.await?; .await?;
"All indexer settings updated".to_owned() "All indexer settings updated".to_owned()
} else { } else {
@@ -420,7 +414,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
}; };
self self
.network .network
.handle_network_event(RadarrEvent::EditCollection(Some(edit_collection_params)).into()) .handle_network_event(RadarrEvent::EditCollection(edit_collection_params).into())
.await?; .await?;
"Collection updated".to_owned() "Collection updated".to_owned()
} }
@@ -455,13 +449,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
api_key, api_key,
seed_ratio, seed_ratio,
tags: tag, tags: tag,
tag_input_string: None,
priority, priority,
clear_tags, clear_tags,
}; };
self self
.network .network
.handle_network_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)).into()) .handle_network_event(RadarrEvent::EditIndexer(edit_indexer_params).into())
.await?; .await?;
"Indexer updated".to_owned() "Indexer updated".to_owned()
} }
@@ -483,12 +478,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
quality_profile_id, quality_profile_id,
root_folder_path, root_folder_path,
tags: tag, tags: tag,
tag_input_string: None,
clear_tags, clear_tags,
}; };
self self
.network .network
.handle_network_event(RadarrEvent::EditMovie(Some(edit_movie_params)).into()) .handle_network_event(RadarrEvent::EditMovie(edit_movie_params).into())
.await?; .await?;
"Movie Updated".to_owned() "Movie Updated".to_owned()
} }
+30 -24
View File
@@ -857,7 +857,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -865,7 +865,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings { let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: true, allow_hardcoded_subs: true,
disable_allow_hardcoded_subs: false, disable_allow_hardcoded_subs: false,
@@ -928,7 +928,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -936,7 +936,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings { let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: false, allow_hardcoded_subs: false,
disable_allow_hardcoded_subs: true, disable_allow_hardcoded_subs: true,
@@ -1000,7 +1000,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1008,7 +1008,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings { let edit_all_indexer_settings_command = RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: false, allow_hardcoded_subs: false,
disable_allow_hardcoded_subs: false, disable_allow_hardcoded_subs: false,
@@ -1047,7 +1047,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), RadarrEvent::EditCollection(expected_edit_collection_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1055,7 +1055,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_collection_command = RadarrEditCommand::Collection { let edit_collection_command = RadarrEditCommand::Collection {
collection_id: 1, collection_id: 1,
enable_monitoring: true, enable_monitoring: true,
@@ -1089,7 +1089,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), RadarrEvent::EditCollection(expected_edit_collection_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1097,7 +1097,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_collection_command = RadarrEditCommand::Collection { let edit_collection_command = RadarrEditCommand::Collection {
collection_id: 1, collection_id: 1,
enable_monitoring: false, enable_monitoring: false,
@@ -1131,7 +1131,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditCollection(Some(expected_edit_collection_params)).into(), RadarrEvent::EditCollection(expected_edit_collection_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1139,7 +1139,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_collection_command = RadarrEditCommand::Collection { let edit_collection_command = RadarrEditCommand::Collection {
collection_id: 1, collection_id: 1,
enable_monitoring: false, enable_monitoring: false,
@@ -1171,6 +1171,7 @@ mod tests {
api_key: Some("testKey".to_owned()), api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()), seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
priority: Some(25), priority: Some(25),
clear_tags: false, clear_tags: false,
}; };
@@ -1178,7 +1179,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), RadarrEvent::EditIndexer(expected_edit_indexer_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1186,7 +1187,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_indexer_command = RadarrEditCommand::Indexer { let edit_indexer_command = RadarrEditCommand::Indexer {
indexer_id: 1, indexer_id: 1,
name: Some("Test".to_owned()), name: Some("Test".to_owned()),
@@ -1224,6 +1225,7 @@ mod tests {
api_key: Some("testKey".to_owned()), api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()), seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
priority: Some(25), priority: Some(25),
clear_tags: false, clear_tags: false,
}; };
@@ -1231,7 +1233,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), RadarrEvent::EditIndexer(expected_edit_indexer_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1239,7 +1241,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_indexer_command = RadarrEditCommand::Indexer { let edit_indexer_command = RadarrEditCommand::Indexer {
indexer_id: 1, indexer_id: 1,
name: Some("Test".to_owned()), name: Some("Test".to_owned()),
@@ -1277,6 +1279,7 @@ mod tests {
api_key: Some("testKey".to_owned()), api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()), seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
priority: Some(25), priority: Some(25),
clear_tags: false, clear_tags: false,
}; };
@@ -1284,7 +1287,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), RadarrEvent::EditIndexer(expected_edit_indexer_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1292,7 +1295,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_indexer_command = RadarrEditCommand::Indexer { let edit_indexer_command = RadarrEditCommand::Indexer {
indexer_id: 1, indexer_id: 1,
name: Some("Test".to_owned()), name: Some("Test".to_owned()),
@@ -1327,13 +1330,14 @@ mod tests {
quality_profile_id: Some(1), quality_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()), root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false, clear_tags: false,
}; };
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), RadarrEvent::EditMovie(expected_edit_movie_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1341,7 +1345,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_movie_command = RadarrEditCommand::Movie { let edit_movie_command = RadarrEditCommand::Movie {
movie_id: 1, movie_id: 1,
enable_monitoring: true, enable_monitoring: true,
@@ -1369,13 +1373,14 @@ mod tests {
quality_profile_id: Some(1), quality_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()), root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false, clear_tags: false,
}; };
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), RadarrEvent::EditMovie(expected_edit_movie_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1383,7 +1388,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_movie_command = RadarrEditCommand::Movie { let edit_movie_command = RadarrEditCommand::Movie {
movie_id: 1, movie_id: 1,
enable_monitoring: false, enable_monitoring: false,
@@ -1411,13 +1416,14 @@ mod tests {
quality_profile_id: Some(1), quality_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()), root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false, clear_tags: false,
}; };
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditMovie(Some(expected_edit_movie_params)).into(), RadarrEvent::EditMovie(expected_edit_movie_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -1425,7 +1431,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_movie_command = RadarrEditCommand::Movie { let edit_movie_command = RadarrEditCommand::Movie {
movie_id: 1, movie_id: 1,
enable_monitoring: false, enable_monitoring: false,
+2 -2
View File
@@ -90,14 +90,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
RadarrGetCommand::MovieDetails { movie_id } => { RadarrGetCommand::MovieDetails { movie_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::GetMovieDetails(Some(movie_id)).into()) .handle_network_event(RadarrEvent::GetMovieDetails(movie_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrGetCommand::MovieHistory { movie_id } => { RadarrGetCommand::MovieHistory { movie_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::GetMovieHistory(Some(movie_id)).into()) .handle_network_event(RadarrEvent::GetMovieHistory(movie_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+8 -8
View File
@@ -138,7 +138,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_all_indexer_settings_command = RadarrGetCommand::AllIndexerSettings; let get_all_indexer_settings_command = RadarrGetCommand::AllIndexerSettings;
let result = RadarrGetCommandHandler::with( let result = RadarrGetCommandHandler::with(
@@ -164,7 +164,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_host_config_command = RadarrGetCommand::HostConfig; let get_host_config_command = RadarrGetCommand::HostConfig;
let result = let result =
@@ -182,7 +182,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::GetMovieDetails(Some(expected_movie_id)).into(), RadarrEvent::GetMovieDetails(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -190,7 +190,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_movie_details_command = RadarrGetCommand::MovieDetails { movie_id: 1 }; let get_movie_details_command = RadarrGetCommand::MovieDetails { movie_id: 1 };
let result = let result =
@@ -208,7 +208,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::GetMovieHistory(Some(expected_movie_id)).into(), RadarrEvent::GetMovieHistory(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -216,7 +216,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_movie_history_command = RadarrGetCommand::MovieHistory { movie_id: 1 }; let get_movie_history_command = RadarrGetCommand::MovieHistory { movie_id: 1 };
let result = let result =
@@ -239,7 +239,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_security_config_command = RadarrGetCommand::SecurityConfig; let get_security_config_command = RadarrGetCommand::SecurityConfig;
let result = let result =
@@ -262,7 +262,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = RadarrGetCommand::SystemStatus; let get_system_status_command = RadarrGetCommand::SystemStatus;
let result = let result =
+4 -4
View File
@@ -131,13 +131,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
} => { } => {
let logs = self let logs = self
.network .network
.handle_network_event(RadarrEvent::GetLogs(Some(events)).into()) .handle_network_event(RadarrEvent::GetLogs(events).into())
.await?; .await?;
if output_in_log_format { if output_in_log_format {
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone(); let log_lines = &self.app.lock().await.data.radarr_data.logs.items;
serde_json::to_string_pretty(&log_lines)? serde_json::to_string_pretty(log_lines)?
} else { } else {
serde_json::to_string_pretty(&logs)? serde_json::to_string_pretty(&logs)?
} }
@@ -152,7 +152,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
RadarrListCommand::MovieCredits { movie_id } => { RadarrListCommand::MovieCredits { movie_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::GetMovieCredits(Some(movie_id)).into()) .handle_network_event(RadarrEvent::GetMovieCredits(movie_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+5 -5
View File
@@ -147,7 +147,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = RadarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) let result = RadarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle() .handle()
@@ -163,7 +163,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::GetMovieCredits(Some(expected_movie_id)).into(), RadarrEvent::GetMovieCredits(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -171,7 +171,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_movie_credits_command = RadarrListCommand::MovieCredits { movie_id: 1 }; let list_movie_credits_command = RadarrListCommand::MovieCredits { movie_id: 1 };
let result = let result =
@@ -189,7 +189,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::GetLogs(Some(expected_events)).into(), RadarrEvent::GetLogs(expected_events).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -197,7 +197,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_logs_command = RadarrListCommand::Logs { let list_logs_command = RadarrListCommand::Logs {
events: 1000, events: 1000,
output_in_log_format: false, output_in_log_format: false,
+6 -6
View File
@@ -209,7 +209,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
}; };
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::DownloadRelease(Some(params)).into()) .handle_network_event(RadarrEvent::DownloadRelease(params).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -217,28 +217,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
println!("Searching for releases. This may take a minute..."); println!("Searching for releases. This may take a minute...");
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::GetReleases(Some(movie_id)).into()) .handle_network_event(RadarrEvent::GetReleases(movie_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrCommand::SearchNewMovie { query } => { RadarrCommand::SearchNewMovie { query } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::SearchNewMovie(Some(query)).into()) .handle_network_event(RadarrEvent::SearchNewMovie(query).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrCommand::StartTask { task_name } => { RadarrCommand::StartTask { task_name } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::StartTask(Some(task_name)).into()) .handle_network_event(RadarrEvent::StartTask(task_name).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrCommand::TestIndexer { indexer_id } => { RadarrCommand::TestIndexer { indexer_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::TestIndexer(Some(indexer_id)).into()) .handle_network_event(RadarrEvent::TestIndexer(indexer_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -253,7 +253,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
RadarrCommand::TriggerAutomaticSearch { movie_id } => { RadarrCommand::TriggerAutomaticSearch { movie_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::TriggerAutomaticSearch(Some(movie_id)).into()) .handle_network_event(RadarrEvent::TriggerAutomaticSearch(movie_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+26 -26
View File
@@ -292,10 +292,10 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let claer_blocklist_command = RadarrCommand::ClearBlocklist; let clear_blocklist_command = RadarrCommand::ClearBlocklist;
let result = RadarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network) let result = RadarrCliHandler::with(&app_arc, clear_blocklist_command, &mut mock_network)
.handle() .handle()
.await; .await;
@@ -313,7 +313,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DownloadRelease(Some(expected_release_download_body)).into(), RadarrEvent::DownloadRelease(expected_release_download_body).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -321,7 +321,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = RadarrCommand::DownloadRelease { let download_release_command = RadarrCommand::DownloadRelease {
guid: "guid".to_owned(), guid: "guid".to_owned(),
indexer_id: 1, indexer_id: 1,
@@ -342,7 +342,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::GetReleases(Some(expected_movie_id)).into(), RadarrEvent::GetReleases(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -350,7 +350,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_search_command = RadarrCommand::ManualSearch { movie_id: 1 }; let manual_search_command = RadarrCommand::ManualSearch { movie_id: 1 };
let result = RadarrCliHandler::with(&app_arc, manual_search_command, &mut mock_network) let result = RadarrCliHandler::with(&app_arc, manual_search_command, &mut mock_network)
@@ -367,7 +367,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::SearchNewMovie(Some(expected_search_query)).into(), RadarrEvent::SearchNewMovie(expected_search_query).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -375,7 +375,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let search_new_movie_command = RadarrCommand::SearchNewMovie { let search_new_movie_command = RadarrCommand::SearchNewMovie {
query: "halo".to_owned(), query: "halo".to_owned(),
}; };
@@ -394,7 +394,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::StartTask(Some(expected_task_name)).into(), RadarrEvent::StartTask(expected_task_name).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -402,7 +402,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let start_task_command = RadarrCommand::StartTask { let start_task_command = RadarrCommand::StartTask {
task_name: RadarrTaskName::ApplicationCheckUpdate, task_name: RadarrTaskName::ApplicationCheckUpdate,
}; };
@@ -421,7 +421,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::TestIndexer(Some(expected_indexer_id)).into(), RadarrEvent::TestIndexer(expected_indexer_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -429,7 +429,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_indexer_command = RadarrCommand::TestIndexer { indexer_id: 1 }; let test_indexer_command = RadarrCommand::TestIndexer { indexer_id: 1 };
let result = RadarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network) let result = RadarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
@@ -451,7 +451,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_all_indexers_command = RadarrCommand::TestAllIndexers; let test_all_indexers_command = RadarrCommand::TestAllIndexers;
let result = RadarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network) let result = RadarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
@@ -468,7 +468,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::TriggerAutomaticSearch(Some(expected_movie_id)).into(), RadarrEvent::TriggerAutomaticSearch(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -476,7 +476,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_search_command = RadarrCommand::TriggerAutomaticSearch { movie_id: 1 }; let trigger_automatic_search_command = RadarrCommand::TriggerAutomaticSearch { movie_id: 1 };
let result = RadarrCliHandler::with( let result = RadarrCliHandler::with(
@@ -505,7 +505,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = RadarrCommand::Add(RadarrAddCommand::Tag { let add_tag_command = RadarrCommand::Add(RadarrAddCommand::Tag {
name: expected_tag_name, name: expected_tag_name,
}); });
@@ -524,7 +524,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), RadarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -532,7 +532,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_blocklist_item_command = let delete_blocklist_item_command =
RadarrCommand::Delete(RadarrDeleteCommand::BlocklistItem { RadarrCommand::Delete(RadarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1, blocklist_item_id: 1,
@@ -584,7 +584,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), RadarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -592,7 +592,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = let edit_all_indexer_settings_command =
RadarrCommand::Edit(RadarrEditCommand::AllIndexerSettings { RadarrCommand::Edit(RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs: true, allow_hardcoded_subs: true,
@@ -632,7 +632,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_all_indexer_settings_command = let get_all_indexer_settings_command =
RadarrCommand::Get(RadarrGetCommand::AllIndexerSettings); RadarrCommand::Get(RadarrGetCommand::AllIndexerSettings);
@@ -654,7 +654,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::GetMovieCredits(Some(expected_movie_id)).into(), RadarrEvent::GetMovieCredits(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -662,7 +662,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_movie_credits_command = let list_movie_credits_command =
RadarrCommand::List(RadarrListCommand::MovieCredits { movie_id: 1 }); RadarrCommand::List(RadarrListCommand::MovieCredits { movie_id: 1 });
@@ -680,7 +680,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::UpdateAndScan(Some(expected_movie_id)).into(), RadarrEvent::UpdateAndScan(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -688,7 +688,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_movie_command = let refresh_movie_command =
RadarrCommand::Refresh(RadarrRefreshCommand::Movie { movie_id: 1 }); RadarrCommand::Refresh(RadarrRefreshCommand::Movie { movie_id: 1 });
+1 -1
View File
@@ -88,7 +88,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
RadarrRefreshCommand::Movie { movie_id } => { RadarrRefreshCommand::Movie { movie_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::UpdateAndScan(Some(movie_id)).into()) .handle_network_event(RadarrEvent::UpdateAndScan(movie_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -96,7 +96,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = RadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network) let result = RadarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle() .handle()
@@ -112,7 +112,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
RadarrEvent::UpdateAndScan(Some(expected_movie_id)).into(), RadarrEvent::UpdateAndScan(expected_movie_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -120,7 +120,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_movie_command = RadarrRefreshCommand::Movie { movie_id: 1 }; let refresh_movie_command = RadarrRefreshCommand::Movie { movie_id: 1 };
let result = let result =
+8 -4
View File
@@ -4,6 +4,8 @@ use anyhow::Result;
use clap::{ArgAction, Subcommand}; use clap::{ArgAction, Subcommand};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::SonarrCommand;
use crate::models::servarr_models::AddRootFolderBody;
use crate::{ use crate::{
app::App, app::App,
cli::{CliCommandHandler, Command}, cli::{CliCommandHandler, Command},
@@ -11,8 +13,6 @@ use crate::{
network::{sonarr_network::SonarrEvent, NetworkTrait}, network::{sonarr_network::SonarrEvent, NetworkTrait},
}; };
use super::SonarrCommand;
#[cfg(test)] #[cfg(test)]
#[path = "add_command_handler_tests.rs"] #[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests; mod add_command_handler_tests;
@@ -140,6 +140,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
series_type: series_type.to_string(), series_type: series_type.to_string(),
season_folder: !disable_season_folders, season_folder: !disable_season_folders,
tags, tags,
tag_input_string: None,
add_options: AddSeriesOptions { add_options: AddSeriesOptions {
monitor: monitor.to_string(), monitor: monitor.to_string(),
search_for_cutoff_unmet_episodes: !no_search_for_series, search_for_cutoff_unmet_episodes: !no_search_for_series,
@@ -148,14 +149,17 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan
}; };
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::AddSeries(Some(body)).into()) .handle_network_event(SonarrEvent::AddSeries(body).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrAddCommand::RootFolder { root_folder_path } => { SonarrAddCommand::RootFolder { root_folder_path } => {
let add_root_folder_body = AddRootFolderBody {
path: root_folder_path,
};
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::AddRootFolder(Some(root_folder_path)).into()) .handle_network_event(SonarrEvent::AddRootFolder(add_root_folder_body).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+10 -5
View File
@@ -469,17 +469,21 @@ mod tests {
use super::*; use super::*;
use mockall::predicate::eq; use mockall::predicate::eq;
use crate::models::servarr_models::AddRootFolderBody;
use serde_json::json; use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
#[tokio::test] #[tokio::test]
async fn test_handle_add_root_folder_command() { async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned(); let expected_root_folder_path = "/nfs/test".to_owned();
let expected_add_root_folder_body = AddRootFolderBody {
path: expected_root_folder_path.clone(),
};
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(), SonarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -487,7 +491,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_root_folder_command = SonarrAddCommand::RootFolder { let add_root_folder_command = SonarrAddCommand::RootFolder {
root_folder_path: expected_root_folder_path, root_folder_path: expected_root_folder_path,
}; };
@@ -511,6 +515,7 @@ mod tests {
series_type: "anime".to_owned(), series_type: "anime".to_owned(),
monitored: false, monitored: false,
tags: vec![1, 2], tags: vec![1, 2],
tag_input_string: None,
season_folder: false, season_folder: false,
add_options: AddSeriesOptions { add_options: AddSeriesOptions {
monitor: "future".to_owned(), monitor: "future".to_owned(),
@@ -522,7 +527,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::AddSeries(Some(expected_add_series_body)).into(), SonarrEvent::AddSeries(expected_add_series_body).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -530,7 +535,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_series_command = SonarrAddCommand::Series { let add_series_command = SonarrAddCommand::Series {
tvdb_id: 1, tvdb_id: 1,
title: "test".to_owned(), title: "test".to_owned(),
@@ -567,7 +572,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = SonarrAddCommand::Tag { let add_tag_command = SonarrAddCommand::Tag {
name: expected_tag_name, name: expected_tag_name,
}; };
+6 -6
View File
@@ -94,35 +94,35 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm
SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => { SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into()) .handle_network_event(SonarrEvent::DeleteBlocklistItem(blocklist_item_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrDeleteCommand::Download { download_id } => { SonarrDeleteCommand::Download { download_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::DeleteDownload(Some(download_id)).into()) .handle_network_event(SonarrEvent::DeleteDownload(download_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrDeleteCommand::EpisodeFile { episode_file_id } => { SonarrDeleteCommand::EpisodeFile { episode_file_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::DeleteEpisodeFile(Some(episode_file_id)).into()) .handle_network_event(SonarrEvent::DeleteEpisodeFile(episode_file_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrDeleteCommand::Indexer { indexer_id } => { SonarrDeleteCommand::Indexer { indexer_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::DeleteIndexer(Some(indexer_id)).into()) .handle_network_event(SonarrEvent::DeleteIndexer(indexer_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrDeleteCommand::RootFolder { root_folder_id } => { SonarrDeleteCommand::RootFolder { root_folder_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::DeleteRootFolder(Some(root_folder_id)).into()) .handle_network_event(SonarrEvent::DeleteRootFolder(root_folder_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -138,7 +138,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm
}; };
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::DeleteSeries(Some(delete_series_params)).into()) .handle_network_event(SonarrEvent::DeleteSeries(delete_series_params).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+37 -11
View File
@@ -301,7 +301,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), SonarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -309,7 +309,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_blocklist_item_command = SonarrDeleteCommand::BlocklistItem { let delete_blocklist_item_command = SonarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1, blocklist_item_id: 1,
}; };
@@ -332,7 +332,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::DeleteDownload(Some(expected_download_id)).into(), SonarrEvent::DeleteDownload(expected_download_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -340,7 +340,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_download_command = SonarrDeleteCommand::Download { download_id: 1 }; let delete_download_command = SonarrDeleteCommand::Download { download_id: 1 };
let result = let result =
@@ -351,6 +351,32 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_handle_delete_episode_file_command() {
let expected_episode_file_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteEpisodeFile(expected_episode_file_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_episode_file_command = SonarrDeleteCommand::EpisodeFile { episode_file_id: 1 };
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_episode_file_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test] #[tokio::test]
async fn test_handle_delete_indexer_command() { async fn test_handle_delete_indexer_command() {
let expected_indexer_id = 1; let expected_indexer_id = 1;
@@ -358,7 +384,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(), SonarrEvent::DeleteIndexer(expected_indexer_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -366,7 +392,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_indexer_command = SonarrDeleteCommand::Indexer { indexer_id: 1 }; let delete_indexer_command = SonarrDeleteCommand::Indexer { indexer_id: 1 };
let result = let result =
@@ -384,7 +410,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(), SonarrEvent::DeleteRootFolder(expected_root_folder_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -392,7 +418,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_root_folder_command = SonarrDeleteCommand::RootFolder { root_folder_id: 1 }; let delete_root_folder_command = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = let result =
@@ -414,7 +440,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::DeleteSeries(Some(expected_delete_series_params)).into(), SonarrEvent::DeleteSeries(expected_delete_series_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -422,7 +448,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_series_command = SonarrDeleteCommand::Series { let delete_series_command = SonarrDeleteCommand::Series {
series_id: 1, series_id: 1,
delete_files_from_disk: true, delete_files_from_disk: true,
@@ -452,7 +478,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_tag_command = SonarrDeleteCommand::Tag { tag_id: 1 }; let delete_tag_command = SonarrDeleteCommand::Tag { tag_id: 1 };
let result = let result =
@@ -333,7 +333,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = SonarrDownloadCommand::Series { let download_release_command = SonarrDownloadCommand::Series {
guid: "guid".to_owned(), guid: "guid".to_owned(),
indexer_id: 1, indexer_id: 1,
@@ -369,7 +369,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = SonarrDownloadCommand::Season { let download_release_command = SonarrDownloadCommand::Season {
guid: "guid".to_owned(), guid: "guid".to_owned(),
indexer_id: 1, indexer_id: 1,
@@ -405,7 +405,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_release_command = SonarrDownloadCommand::Episode { let download_release_command = SonarrDownloadCommand::Episode {
guid: "guid".to_owned(), guid: "guid".to_owned(),
indexer_id: 1, indexer_id: 1,
+5 -3
View File
@@ -274,7 +274,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH
}; };
self self
.network .network
.handle_network_event(SonarrEvent::EditAllIndexerSettings(Some(params)).into()) .handle_network_event(SonarrEvent::EditAllIndexerSettings(params).into())
.await?; .await?;
"All indexer settings updated".to_owned() "All indexer settings updated".to_owned()
} else { } else {
@@ -312,13 +312,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH
api_key, api_key,
seed_ratio, seed_ratio,
tags: tag, tags: tag,
tag_input_string: None,
priority, priority,
clear_tags, clear_tags,
}; };
self self
.network .network
.handle_network_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)).into()) .handle_network_event(SonarrEvent::EditIndexer(edit_indexer_params).into())
.await?; .await?;
"Indexer updated".to_owned() "Indexer updated".to_owned()
} }
@@ -347,12 +348,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandH
language_profile_id, language_profile_id,
root_folder_path, root_folder_path,
tags: tag, tags: tag,
tag_input_string: None,
clear_tags, clear_tags,
}; };
self self
.network .network
.handle_network_event(SonarrEvent::EditSeries(Some(edit_series_params)).into()) .handle_network_event(SonarrEvent::EditSeries(edit_series_params).into())
.await?; .await?;
"Series Updated".to_owned() "Series Updated".to_owned()
} }
+14 -10
View File
@@ -650,7 +650,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), SonarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -658,7 +658,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings { let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(1), maximum_size: Some(1),
minimum_age: Some(1), minimum_age: Some(1),
@@ -689,6 +689,7 @@ mod tests {
api_key: Some("testKey".to_owned()), api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()), seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
priority: Some(25), priority: Some(25),
clear_tags: false, clear_tags: false,
}; };
@@ -696,7 +697,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(), SonarrEvent::EditIndexer(expected_edit_indexer_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -704,7 +705,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_indexer_command = SonarrEditCommand::Indexer { let edit_indexer_command = SonarrEditCommand::Indexer {
indexer_id: 1, indexer_id: 1,
name: Some("Test".to_owned()), name: Some("Test".to_owned()),
@@ -741,13 +742,14 @@ mod tests {
language_profile_id: Some(1), language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()), root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false, clear_tags: false,
}; };
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), SonarrEvent::EditSeries(expected_edit_series_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -755,7 +757,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_series_command = SonarrEditCommand::Series { let edit_series_command = SonarrEditCommand::Series {
series_id: 1, series_id: 1,
enable_monitoring: true, enable_monitoring: true,
@@ -788,13 +790,14 @@ mod tests {
language_profile_id: Some(1), language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()), root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false, clear_tags: false,
}; };
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), SonarrEvent::EditSeries(expected_edit_series_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -802,7 +805,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_series_command = SonarrEditCommand::Series { let edit_series_command = SonarrEditCommand::Series {
series_id: 1, series_id: 1,
enable_monitoring: false, enable_monitoring: false,
@@ -835,13 +838,14 @@ mod tests {
language_profile_id: Some(1), language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()), root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]), tags: Some(vec![1, 2]),
tag_input_string: None,
clear_tags: false, clear_tags: false,
}; };
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(), SonarrEvent::EditSeries(expected_edit_series_params).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -849,7 +853,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_series_command = SonarrEditCommand::Series { let edit_series_command = SonarrEditCommand::Series {
series_id: 1, series_id: 1,
enable_monitoring: false, enable_monitoring: false,
+2 -2
View File
@@ -83,7 +83,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHan
SonarrGetCommand::EpisodeDetails { episode_id } => { SonarrGetCommand::EpisodeDetails { episode_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetEpisodeDetails(Some(episode_id)).into()) .handle_network_event(SonarrEvent::GetEpisodeDetails(episode_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -104,7 +104,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHan
SonarrGetCommand::SeriesDetails { series_id } => { SonarrGetCommand::SeriesDetails { series_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetSeriesDetails(Some(series_id)).into()) .handle_network_event(SonarrEvent::GetSeriesDetails(series_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+8 -8
View File
@@ -139,7 +139,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_all_indexer_settings_command = SonarrGetCommand::AllIndexerSettings; let get_all_indexer_settings_command = SonarrGetCommand::AllIndexerSettings;
let result = SonarrGetCommandHandler::with( let result = SonarrGetCommandHandler::with(
@@ -160,7 +160,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeDetails(Some(expected_episode_id)).into(), SonarrEvent::GetEpisodeDetails(expected_episode_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -168,7 +168,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_episode_details_command = SonarrGetCommand::EpisodeDetails { episode_id: 1 }; let get_episode_details_command = SonarrGetCommand::EpisodeDetails { episode_id: 1 };
let result = let result =
@@ -191,7 +191,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_host_config_command = SonarrGetCommand::HostConfig; let get_host_config_command = SonarrGetCommand::HostConfig;
let result = let result =
@@ -214,7 +214,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_security_config_command = SonarrGetCommand::SecurityConfig; let get_security_config_command = SonarrGetCommand::SecurityConfig;
let result = let result =
@@ -232,7 +232,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetSeriesDetails(Some(expected_series_id)).into(), SonarrEvent::GetSeriesDetails(expected_series_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -240,7 +240,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_series_details_command = SonarrGetCommand::SeriesDetails { series_id: 1 }; let get_series_details_command = SonarrGetCommand::SeriesDetails { series_id: 1 };
let result = let result =
@@ -263,7 +263,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = SonarrGetCommand::SystemStatus; let get_system_status_command = SonarrGetCommand::SystemStatus;
let result = let result =
+50 -7
View File
@@ -33,6 +33,15 @@ pub enum SonarrListCommand {
)] )]
series_id: i64, series_id: i64,
}, },
#[command(about = "List the episode files for the series with the given ID")]
EpisodeFiles {
#[arg(
long,
help = "The Sonarr ID of the series whose episode files you wish to fetch",
required = true
)]
series_id: i64,
},
#[command(about = "Fetch all history events for the episode with the given ID")] #[command(about = "Fetch all history events for the episode with the given ID")]
EpisodeHistory { EpisodeHistory {
#[arg( #[arg(
@@ -67,6 +76,23 @@ pub enum SonarrListCommand {
QueuedEvents, QueuedEvents,
#[command(about = "List all root folders in Sonarr")] #[command(about = "List all root folders in Sonarr")]
RootFolders, RootFolders,
#[command(
about = "Fetch all history events for the given season corresponding to the series with the given ID."
)]
SeasonHistory {
#[arg(
long,
help = "The Sonarr ID of the series whose history you wish to fetch and list",
required = true
)]
series_id: i64,
#[arg(
long,
help = "The season number to fetch history events for",
required = true
)]
season_number: i64,
},
#[command(about = "List all series in your Sonarr library")] #[command(about = "List all series in your Sonarr library")]
Series, Series,
#[command(about = "Fetch all history events for the series with the given ID")] #[command(about = "Fetch all history events for the series with the given ID")]
@@ -137,21 +163,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
SonarrListCommand::Episodes { series_id } => { SonarrListCommand::Episodes { series_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetEpisodes(Some(series_id)).into()) .handle_network_event(SonarrEvent::GetEpisodes(series_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::EpisodeFiles { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodeFiles(series_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrListCommand::EpisodeHistory { episode_id } => { SonarrListCommand::EpisodeHistory { episode_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetEpisodeHistory(Some(episode_id)).into()) .handle_network_event(SonarrEvent::GetEpisodeHistory(episode_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrListCommand::History { events: items } => { SonarrListCommand::History { events: items } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetHistory(Some(items)).into()) .handle_network_event(SonarrEvent::GetHistory(items).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -175,13 +208,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
} => { } => {
let logs = self let logs = self
.network .network
.handle_network_event(SonarrEvent::GetLogs(Some(events)).into()) .handle_network_event(SonarrEvent::GetLogs(events).into())
.await?; .await?;
if output_in_log_format { if output_in_log_format {
let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone(); let log_lines = &self.app.lock().await.data.sonarr_data.logs.items;
serde_json::to_string_pretty(&log_lines)? serde_json::to_string_pretty(log_lines)?
} else { } else {
serde_json::to_string_pretty(&logs)? serde_json::to_string_pretty(&logs)?
} }
@@ -207,6 +240,16 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrListCommand::SeasonHistory {
series_id,
season_number,
} => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetSeasonHistory((series_id, season_number)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Series => { SonarrListCommand::Series => {
let resp = self let resp = self
.network .network
@@ -217,7 +260,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
SonarrListCommand::SeriesHistory { series_id } => { SonarrListCommand::SeriesHistory { series_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetSeriesHistory(Some(series_id)).into()) .handle_network_event(SonarrEvent::GetSeriesHistory(series_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+152 -11
View File
@@ -57,6 +57,18 @@ mod tests {
); );
} }
#[test]
fn test_list_episode_files_requires_series_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-files"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test] #[test]
fn test_list_episode_history_requires_series_id() { fn test_list_episode_history_requires_series_id() {
let result = let result =
@@ -149,6 +161,79 @@ mod tests {
} }
} }
#[test]
fn test_list_episode_files_success() {
let expected_args = SonarrListCommand::EpisodeFiles { series_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"list",
"episode-files",
"--series-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(episode_files_command))) =
result.unwrap().command
{
assert_eq!(episode_files_command, expected_args);
}
}
#[test]
fn test_season_history_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"list",
"season-history",
"--season-number",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_season_history_requires_season_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"list",
"season-history",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_season_history_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"list",
"season-history",
"--series-id",
"1",
"--season-number",
"1",
]);
assert!(result.is_ok());
}
#[test] #[test]
fn test_list_series_history_requires_series_id() { fn test_list_series_history_requires_series_id() {
let result = let result =
@@ -228,7 +313,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = SonarrListCommandHandler::with(&app_arc, list_command, &mut mock_network) let result = SonarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle() .handle()
@@ -244,7 +329,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodes(Some(expected_series_id)).into(), SonarrEvent::GetEpisodes(expected_series_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -252,7 +337,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_episodes_command = SonarrListCommand::Episodes { series_id: 1 }; let list_episodes_command = SonarrListCommand::Episodes { series_id: 1 };
let result = let result =
@@ -263,6 +348,32 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_handle_list_episode_files_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeFiles(expected_series_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_episode_files_command = SonarrListCommand::EpisodeFiles { series_id: 1 };
let result =
SonarrListCommandHandler::with(&app_arc, list_episode_files_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test] #[tokio::test]
async fn test_handle_list_history_command() { async fn test_handle_list_history_command() {
let expected_events = 1000; let expected_events = 1000;
@@ -270,7 +381,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetHistory(Some(expected_events)).into(), SonarrEvent::GetHistory(expected_events).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -278,7 +389,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_history_command = SonarrListCommand::History { events: 1000 }; let list_history_command = SonarrListCommand::History { events: 1000 };
let result = let result =
@@ -296,7 +407,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetLogs(Some(expected_events)).into(), SonarrEvent::GetLogs(expected_events).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -304,7 +415,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_logs_command = SonarrListCommand::Logs { let list_logs_command = SonarrListCommand::Logs {
events: 1000, events: 1000,
output_in_log_format: false, output_in_log_format: false,
@@ -324,7 +435,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetSeriesHistory(Some(expected_series_id)).into(), SonarrEvent::GetSeriesHistory(expected_series_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -332,7 +443,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_series_history_command = SonarrListCommand::SeriesHistory { series_id: 1 }; let list_series_history_command = SonarrListCommand::SeriesHistory { series_id: 1 };
let result = let result =
@@ -350,7 +461,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeHistory(Some(expected_episode_id)).into(), SonarrEvent::GetEpisodeHistory(expected_episode_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -358,7 +469,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_episode_history_command = SonarrListCommand::EpisodeHistory { episode_id: 1 }; let list_episode_history_command = SonarrListCommand::EpisodeHistory { episode_id: 1 };
let result = let result =
@@ -368,5 +479,35 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_list_season_history_command() {
let expected_series_id = 1;
let expected_season_number = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetSeasonHistory((expected_series_id, expected_season_number)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_season_history_command = SonarrListCommand::SeasonHistory {
series_id: 1,
season_number: 1,
};
let result =
SonarrListCommandHandler::with(&app_arc, list_season_history_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
} }
} }
@@ -75,7 +75,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
println!("Searching for episode releases. This may take a minute..."); println!("Searching for episode releases. This may take a minute...");
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into()) .handle_network_event(SonarrEvent::GetEpisodeReleases(episode_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -86,9 +86,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
println!("Searching for season releases. This may take a minute..."); println!("Searching for season releases. This may take a minute...");
let resp = self let resp = self
.network .network
.handle_network_event( .handle_network_event(SonarrEvent::GetSeasonReleases((series_id, season_number)).into())
SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(),
)
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -130,7 +130,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(), SonarrEvent::GetEpisodeReleases(expected_episode_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -138,7 +138,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 }; let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
let result = SonarrManualSearchCommandHandler::with( let result = SonarrManualSearchCommandHandler::with(
@@ -160,7 +160,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetSeasonReleases(Some((expected_series_id, expected_season_number))).into(), SonarrEvent::GetSeasonReleases((expected_series_id, expected_season_number)).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -168,7 +168,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_season_search_command = SonarrManualSearchCommand::Season { let manual_season_search_command = SonarrManualSearchCommand::Season {
series_id: 1, series_id: 1,
season_number: 1, season_number: 1,
+48 -3
View File
@@ -120,6 +120,32 @@ pub enum SonarrCommand {
}, },
#[command(about = "Test all Sonarr indexers")] #[command(about = "Test all Sonarr indexers")]
TestAllIndexers, TestAllIndexers,
#[command(about = "Toggle monitoring for the specified episode")]
ToggleEpisodeMonitoring {
#[arg(
long,
help = "The Sonarr ID of the episode to toggle monitoring on",
required = true
)]
episode_id: i64,
},
#[command(
about = "Toggle monitoring for the specified season that corresponds to the specified series ID"
)]
ToggleSeasonMonitoring {
#[arg(
long,
help = "The Sonarr ID of the series that the season belongs to",
required = true
)]
series_id: i64,
#[arg(
long,
help = "The season number to toggle monitoring for",
required = true
)]
season_number: i64,
},
} }
impl From<SonarrCommand> for Command { impl From<SonarrCommand> for Command {
@@ -219,21 +245,21 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
SonarrCommand::SearchNewSeries { query } => { SonarrCommand::SearchNewSeries { query } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::SearchNewSeries(Some(query)).into()) .handle_network_event(SonarrEvent::SearchNewSeries(query).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrCommand::StartTask { task_name } => { SonarrCommand::StartTask { task_name } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::StartTask(Some(task_name)).into()) .handle_network_event(SonarrEvent::StartTask(task_name).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrCommand::TestIndexer { indexer_id } => { SonarrCommand::TestIndexer { indexer_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::TestIndexer(Some(indexer_id)).into()) .handle_network_event(SonarrEvent::TestIndexer(indexer_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -245,6 +271,25 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrCommand::ToggleEpisodeMonitoring { episode_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::ToggleEpisodeMonitoring(episode_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::ToggleSeasonMonitoring {
series_id,
season_number,
} => {
let resp = self
.network
.handle_network_event(
SonarrEvent::ToggleSeasonMonitoring((series_id, season_number)).into(),
)
.await?;
serde_json::to_string_pretty(&resp)?
}
}; };
Ok(result) Ok(result)
+1 -1
View File
@@ -71,7 +71,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrRefreshCommand>
SonarrRefreshCommand::Series { series_id } => { SonarrRefreshCommand::Series { series_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::UpdateAndScanSeries(Some(series_id)).into()) .handle_network_event(SonarrEvent::UpdateAndScanSeries(series_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -103,7 +103,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let result = SonarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network) let result = SonarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle() .handle()
@@ -119,7 +119,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(), SonarrEvent::UpdateAndScanSeries(expected_series_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -127,7 +127,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_series_command = SonarrRefreshCommand::Series { series_id: 1 }; let refresh_series_command = SonarrRefreshCommand::Series { series_id: 1 };
let result = let result =
+160 -23
View File
@@ -142,6 +142,80 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test]
fn test_toggle_episode_monitoring_requires_episode_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "toggle-episode-monitoring"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_episode_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"toggle-episode-monitoring",
"--episode-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_toggle_season_monitoring_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"toggle-season-monitoring",
"--season-number",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_season_monitoring_requires_season_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"toggle-season-monitoring",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_season_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"toggle-season-monitoring",
"--series-id",
"1",
"--season-number",
"1",
]);
assert!(result.is_ok());
}
} }
mod handler { mod handler {
@@ -198,7 +272,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let claer_blocklist_command = SonarrCommand::ClearBlocklist; let claer_blocklist_command = SonarrCommand::ClearBlocklist;
let result = SonarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network) let result = SonarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
@@ -223,7 +297,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let mark_history_item_as_failed_command = let mark_history_item_as_failed_command =
SonarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 }; SonarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
@@ -253,7 +327,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let add_tag_command = SonarrCommand::Add(SonarrAddCommand::Tag { let add_tag_command = SonarrCommand::Add(SonarrAddCommand::Tag {
name: expected_tag_name, name: expected_tag_name,
}); });
@@ -272,7 +346,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(), SonarrEvent::DeleteBlocklistItem(expected_blocklist_item_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -280,7 +354,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let delete_blocklist_item_command = let delete_blocklist_item_command =
SonarrCommand::Delete(SonarrDeleteCommand::BlocklistItem { SonarrCommand::Delete(SonarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1, blocklist_item_id: 1,
@@ -314,7 +388,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let download_series_release_command = let download_series_release_command =
SonarrCommand::Download(SonarrDownloadCommand::Series { SonarrCommand::Download(SonarrDownloadCommand::Series {
guid: "1234".to_owned(), guid: "1234".to_owned(),
@@ -360,7 +434,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(), SonarrEvent::EditAllIndexerSettings(expected_edit_all_indexer_settings).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -368,7 +442,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let edit_all_indexer_settings_command = let edit_all_indexer_settings_command =
SonarrCommand::Edit(SonarrEditCommand::AllIndexerSettings { SonarrCommand::Edit(SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(1), maximum_size: Some(1),
@@ -396,7 +470,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(), SonarrEvent::GetEpisodeReleases(expected_episode_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -404,7 +478,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command = let manual_episode_search_command =
SonarrCommand::ManualSearch(SonarrManualSearchCommand::Episode { episode_id: 1 }); SonarrCommand::ManualSearch(SonarrManualSearchCommand::Episode { episode_id: 1 });
@@ -424,7 +498,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), SonarrEvent::TriggerAutomaticEpisodeSearch(expected_episode_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -432,7 +506,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let manual_episode_search_command = let manual_episode_search_command =
SonarrCommand::TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand::Episode { SonarrCommand::TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand::Episode {
episode_id: 1, episode_id: 1,
@@ -458,7 +532,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let get_system_status_command = SonarrCommand::Get(SonarrGetCommand::SystemStatus); let get_system_status_command = SonarrCommand::Get(SonarrGetCommand::SystemStatus);
let result = SonarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network) let result = SonarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
@@ -480,7 +554,7 @@ mod tests {
Series::default(), Series::default(),
]))) ])))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_series_command = SonarrCommand::List(SonarrListCommand::Series); let list_series_command = SonarrCommand::List(SonarrListCommand::Series);
let result = SonarrCliHandler::with(&app_arc, list_series_command, &mut mock_network) let result = SonarrCliHandler::with(&app_arc, list_series_command, &mut mock_network)
@@ -497,7 +571,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(), SonarrEvent::UpdateAndScanSeries(expected_series_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -505,7 +579,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let refresh_series_command = let refresh_series_command =
SonarrCommand::Refresh(SonarrRefreshCommand::Series { series_id: 1 }); SonarrCommand::Refresh(SonarrRefreshCommand::Series { series_id: 1 });
@@ -523,7 +597,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::SearchNewSeries(Some(expected_search_query)).into(), SonarrEvent::SearchNewSeries(expected_search_query).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -531,7 +605,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let search_new_series_command = SonarrCommand::SearchNewSeries { let search_new_series_command = SonarrCommand::SearchNewSeries {
query: "halo".to_owned(), query: "halo".to_owned(),
}; };
@@ -550,7 +624,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::StartTask(Some(expected_task_name)).into(), SonarrEvent::StartTask(expected_task_name).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -558,7 +632,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let start_task_command = SonarrCommand::StartTask { let start_task_command = SonarrCommand::StartTask {
task_name: SonarrTaskName::ApplicationUpdateCheck, task_name: SonarrTaskName::ApplicationUpdateCheck,
}; };
@@ -577,7 +651,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::TestIndexer(Some(expected_indexer_id)).into(), SonarrEvent::TestIndexer(expected_indexer_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -585,7 +659,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_indexer_command = SonarrCommand::TestIndexer { indexer_id: 1 }; let test_indexer_command = SonarrCommand::TestIndexer { indexer_id: 1 };
let result = SonarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network) let result = SonarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
@@ -607,7 +681,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let test_all_indexers_command = SonarrCommand::TestAllIndexers; let test_all_indexers_command = SonarrCommand::TestAllIndexers;
let result = SonarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network) let result = SonarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
@@ -616,5 +690,68 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_list_toggle_episode_monitoring_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::ToggleEpisodeMonitoring(expected_episode_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_episode_monitoring_command =
SonarrCommand::ToggleEpisodeMonitoring { episode_id: 1 };
let result = SonarrCliHandler::with(
&app_arc,
toggle_episode_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_toggle_season_monitoring_command() {
let expected_series_id = 1;
let expected_season_number = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::ToggleSeasonMonitoring((expected_series_id, expected_season_number)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_season_monitoring_command = SonarrCommand::ToggleSeasonMonitoring {
series_id: 1,
season_number: 1,
};
let result = SonarrCliHandler::with(
&app_arc,
toggle_season_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
} }
} }
@@ -83,7 +83,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
SonarrTriggerAutomaticSearchCommand::Series { series_id } => { SonarrTriggerAutomaticSearchCommand::Series { series_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into()) .handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(series_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -94,7 +94,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
let resp = self let resp = self
.network .network
.handle_network_event( .handle_network_event(
SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(), SonarrEvent::TriggerAutomaticSeasonSearch((series_id, season_number)).into(),
) )
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
@@ -102,7 +102,7 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => { SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into()) .handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(episode_id).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
@@ -166,7 +166,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(), SonarrEvent::TriggerAutomaticSeriesSearch(expected_series_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -174,7 +174,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_series_search_command = let trigger_automatic_series_search_command =
SonarrTriggerAutomaticSearchCommand::Series { series_id: 1 }; SonarrTriggerAutomaticSearchCommand::Series { series_id: 1 };
@@ -197,11 +197,8 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticSeasonSearch(Some(( SonarrEvent::TriggerAutomaticSeasonSearch((expected_series_id, expected_season_number))
expected_series_id, .into(),
expected_season_number,
)))
.into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -209,7 +206,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_season_search_command = SonarrTriggerAutomaticSearchCommand::Season { let trigger_automatic_season_search_command = SonarrTriggerAutomaticSearchCommand::Season {
series_id: 1, series_id: 1,
season_number: 1, season_number: 1,
@@ -233,7 +230,7 @@ mod tests {
mock_network mock_network
.expect_handle_network_event() .expect_handle_network_event()
.with(eq::<NetworkEvent>( .with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(), SonarrEvent::TriggerAutomaticEpisodeSearch(expected_episode_id).into(),
)) ))
.times(1) .times(1)
.returning(|_| { .returning(|_| {
@@ -241,7 +238,7 @@ mod tests {
json!({"testResponse": "response"}), json!({"testResponse": "response"}),
))) )))
}); });
let app_arc = Arc::new(Mutex::new(App::default())); let app_arc = Arc::new(Mutex::new(App::test_default()));
let trigger_automatic_episode_search_command = let trigger_automatic_episode_search_command =
SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 }; SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
+7
View File
@@ -19,6 +19,7 @@ pub enum Key {
Home, Home,
End, End,
Tab, Tab,
BackTab,
Delete, Delete,
Ctrl(char), Ctrl(char),
Char(char), Char(char),
@@ -40,6 +41,7 @@ impl Display for Key {
Key::Home => write!(f, "<home>"), Key::Home => write!(f, "<home>"),
Key::End => write!(f, "<end>"), Key::End => write!(f, "<end>"),
Key::Tab => write!(f, "<tab>"), Key::Tab => write!(f, "<tab>"),
Key::BackTab => write!(f, "<shift-tab>"),
Key::Delete => write!(f, "<del>"), Key::Delete => write!(f, "<del>"),
_ => write!(f, "<{self:?}>"), _ => write!(f, "<{self:?}>"),
} }
@@ -75,6 +77,11 @@ impl From<KeyEvent> for Key {
KeyEvent { KeyEvent {
code: KeyCode::End, .. code: KeyCode::End, ..
} => Key::End, } => Key::End,
KeyEvent {
code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT,
..
} => Key::BackTab,
KeyEvent { KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} => Key::Tab, } => Key::Tab,
+14
View File
@@ -17,6 +17,7 @@ mod tests {
#[case(Key::Home, "home")] #[case(Key::Home, "home")]
#[case(Key::End, "end")] #[case(Key::End, "end")]
#[case(Key::Tab, "tab")] #[case(Key::Tab, "tab")]
#[case(Key::BackTab, "shift-tab")]
#[case(Key::Delete, "del")] #[case(Key::Delete, "del")]
#[case(Key::Char('q'), "q")] #[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "ctrl-q")] #[case(Key::Ctrl('q'), "ctrl-q")]
@@ -67,6 +68,19 @@ mod tests {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Tab)), Key::Tab); assert_eq!(Key::from(KeyEvent::from(KeyCode::Tab)), Key::Tab);
} }
#[test]
fn test_key_from_back_tab() {
assert_eq!(
Key::from(KeyEvent {
code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}),
Key::BackTab
);
}
#[test] #[test]
fn test_key_from_delete() { fn test_key_from_delete() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Delete)), Key::Delete); assert_eq!(Key::from(KeyEvent::from(KeyCode::Delete)), Key::Delete);
+206 -79
View File
@@ -99,86 +99,96 @@ mod test_utils {
#[macro_export] #[macro_export]
macro_rules! test_iterable_scroll { macro_rules! test_iterable_scroll {
($func:ident, $handler:ident, $data_ref:ident, $block:expr, $context:expr) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $block:expr, $context:expr) => {
#[rstest] #[rstest]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack($block.into());
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]);
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(&key, &mut app, &$block, &$context).handle();
assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 2"); pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection(),
"Test 2"
);
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(&key, &mut app, &$block, &$context).handle();
assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 1"); pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection(),
"Test 1"
);
} }
}; };
($func:ident, $handler:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => {
#[rstest] #[rstest]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack($block.into());
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.set_items(simple_stateful_iterable_vec!($items)); .set_items(simple_stateful_iterable_vec!($items));
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 2" "Test 2"
); );
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 1" "Test 1"
); );
} }
}; };
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => {
#[rstest] #[rstest]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.$data_ref.set_items($items); app.push_navigation_stack($block.into());
app.data.$servarr_data.$data_ref.set_items($items);
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 2" "Test 2"
); );
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 1" "Test 1"
); );
} }
}; };
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => {
#[rstest] #[rstest]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.$data_ref.set_items($items); app.push_navigation_stack($block.into());
app.data.$servarr_data.$data_ref.set_items($items);
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.current_selection() .current_selection()
.$field .$field
@@ -186,12 +196,12 @@ mod test_utils {
"Test 2" "Test 2"
); );
$handler::with(&key, &mut app, &$block, &$context).handle(); $handler::new(key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.current_selection() .current_selection()
.$field .$field
@@ -204,86 +214,96 @@ mod test_utils {
#[macro_export] #[macro_export]
macro_rules! test_iterable_home_and_end { macro_rules! test_iterable_home_and_end {
($func:ident, $handler:ident, $data_ref:ident, $block:expr, $context:expr) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $block:expr, $context:expr) => {
#[test] #[test]
fn $func() { fn $func() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.$data_ref.set_items(vec![ app.push_navigation_stack($block.into());
app.data.$servarr_data.$data_ref.set_items(vec![
"Test 1".to_owned(), "Test 1".to_owned(),
"Test 2".to_owned(), "Test 2".to_owned(),
"Test 3".to_owned(), "Test 3".to_owned(),
]); ]);
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 3"); pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection(),
"Test 3"
);
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 1"); pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection(),
"Test 1"
);
} }
}; };
($func:ident, $handler:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => {
#[test] #[test]
fn $func() { fn $func() {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack($block.into());
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.set_items(extended_stateful_iterable_vec!($items)); .set_items(extended_stateful_iterable_vec!($items));
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 3" "Test 3"
); );
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 1" "Test 1"
); );
} }
}; };
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => {
#[test] #[test]
fn $func() { fn $func() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.$data_ref.set_items($items); app.push_navigation_stack($block.into());
app.data.$servarr_data.$data_ref.set_items($items);
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 3" "Test 3"
); );
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field, app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 1" "Test 1"
); );
} }
}; };
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => {
#[test] #[test]
fn $func() { fn $func() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.$data_ref.set_items($items); app.push_navigation_stack($block.into());
app.data.$servarr_data.$data_ref.set_items($items);
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.current_selection() .current_selection()
.$field .$field
@@ -291,12 +311,12 @@ mod test_utils {
"Test 3" "Test 3"
); );
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); $handler::new(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!( pretty_assertions::assert_str_eq!(
app app
.data .data
.radarr_data .$servarr_data
.$data_ref .$data_ref
.current_selection() .current_selection()
.$field .$field
@@ -310,19 +330,126 @@ mod test_utils {
#[macro_export] #[macro_export]
macro_rules! test_handler_delegation { macro_rules! test_handler_delegation {
($handler:ident, $base:expr, $active_block:expr) => { ($handler:ident, $base:expr, $active_block:expr) => {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack($base.clone().into()); app.data.sonarr_data.history.set_items(vec![
app.push_navigation_stack($active_block.clone().into()); $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()]);
app.push_navigation_stack($base.into());
app.push_navigation_stack($active_block.into());
$handler::with( $handler::new(DEFAULT_KEYBINDINGS.esc.key, &mut app, $active_block, None).handle();
&DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&$active_block,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &$base.into()); pretty_assertions::assert_eq!(app.get_current_route(), $base.into());
};
}
#[macro_export]
macro_rules! assert_delete_prompt {
($handler:ident, $block:expr, $expected_block:expr) => {
let mut app = App::test_default();
$handler::new(DELETE_KEY, &mut app, $block, None).handle();
pretty_assertions::assert_eq!(app.get_current_route(), $expected_block.into());
};
($handler:ident, $app:expr, $block:expr, $expected_block:expr) => {
$handler::new(DELETE_KEY, &mut $app, $block, None).handle();
pretty_assertions::assert_eq!($app.get_current_route(), $expected_block.into());
};
}
#[macro_export]
macro_rules! assert_refresh_key {
($handler:ident, $block:expr) => {
let mut app = App::test_default();
app.push_navigation_stack($block.into());
$handler::new(DEFAULT_KEYBINDINGS.refresh.key, &mut app, $block, None).handle();
pretty_assertions::assert_eq!(app.get_current_route(), $block.into());
assert!(app.should_refresh);
}; };
} }
} }
+88 -5
View File
@@ -1,14 +1,24 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::radarr_models::Movie;
use crate::models::sonarr_models::Series;
use pretty_assertions::assert_eq;
use rstest::rstest; use rstest::rstest;
use tokio_util::sync::CancellationToken;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::handle_events;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::HorizontallyScrollableText;
use crate::models::Route;
#[test] #[test]
fn test_handle_clear_errors() { fn test_handle_clear_errors() {
let mut app = App::default(); let mut app = App::test_default();
app.error = "test error".to_owned().into(); app.error = "test error".to_owned().into();
handle_clear_errors(&mut app); handle_clear_errors(&mut app);
@@ -17,17 +27,90 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_handle_prompt_toggle_left_right(#[values(Key::Left, Key::Right)] key: Key) { #[case(ActiveRadarrBlock::Movies.into(), ActiveRadarrBlock::SearchMovie.into())]
let mut app = App::default(); #[case(ActiveSonarrBlock::Series.into(), ActiveSonarrBlock::SearchSeries.into())]
fn test_handle_events(#[case] base_block: Route, #[case] top_block: Route) {
let mut app = App::test_default();
app.push_navigation_stack(base_block);
app.push_navigation_stack(top_block);
app
.data
.sonarr_data
.series
.set_items(vec![Series::default()]);
app
.data
.radarr_data
.movies
.set_items(vec![Movie::default()]);
handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app);
assert_eq!(app.get_current_route(), base_block);
}
#[rstest]
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)]
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T)
where
T: Into<Route> + Copy,
{
let mut app = App::test_default();
app.error = "Test".into();
app.server_tabs.set_index(index);
handle_events(DEFAULT_KEYBINDINGS.previous_servarr.key, &mut app);
assert_eq!(app.server_tabs.get_active_route(), left_block.into());
assert_eq!(app.get_current_route(), left_block.into());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.cancellation_token.is_cancelled());
app.server_tabs.set_index(index);
app.is_first_render = false;
app.error = "Test".into();
app.cancellation_token = CancellationToken::new();
handle_events(DEFAULT_KEYBINDINGS.next_servarr.key, &mut app);
assert_eq!(app.server_tabs.get_active_route(), right_block.into());
assert_eq!(app.get_current_route(), right_block.into());
assert!(app.is_first_render);
assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.cancellation_token.is_cancelled());
}
#[rstest]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
handle_prompt_toggle(&mut app, &key); handle_prompt_toggle(&mut app, key);
assert!(app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.prompt_confirm);
handle_prompt_toggle(&mut app, &key); handle_prompt_toggle(&mut app, key);
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
#[rstest]
fn test_handle_prompt_toggle_left_right_sonarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Series.into());
assert!(!app.data.sonarr_data.prompt_confirm);
handle_prompt_toggle(&mut app, key);
assert!(app.data.sonarr_data.prompt_confirm);
handle_prompt_toggle(&mut app, key);
assert!(!app.data.sonarr_data.prompt_confirm);
}
} }
+65 -24
View File
@@ -1,11 +1,13 @@
use radarr_handlers::RadarrHandler; use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::matches_key;
use crate::models::{HorizontallyScrollableText, Route}; use crate::models::{HorizontallyScrollableText, Route};
mod radarr_handlers; mod radarr_handlers;
mod sonarr_handlers;
#[cfg(test)] #[cfg(test)]
#[path = "handlers_tests.rs"] #[path = "handlers_tests.rs"]
@@ -14,45 +16,48 @@ mod handlers_tests;
#[cfg(test)] #[cfg(test)]
#[path = "handler_test_utils.rs"] #[path = "handler_test_utils.rs"]
pub mod handler_test_utils; pub mod handler_test_utils;
mod table_handler;
pub trait KeyEventHandler<'a, 'b, T: Into<Route>> { pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn handle_key_event(&mut self) { fn handle_key_event(&mut self) {
let key = self.get_key(); let key = self.get_key();
match key { match key {
_ if *key == DEFAULT_KEYBINDINGS.up.key => { _ if matches_key!(up, key, self.ignore_alt_navigation()) => {
if self.is_ready() { if self.is_ready() {
self.handle_scroll_up(); self.handle_scroll_up();
} }
} }
_ if *key == DEFAULT_KEYBINDINGS.down.key => { _ if matches_key!(down, key, self.ignore_alt_navigation()) => {
if self.is_ready() { if self.is_ready() {
self.handle_scroll_down(); self.handle_scroll_down();
} }
} }
_ if *key == DEFAULT_KEYBINDINGS.home.key => { _ if matches_key!(home, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_home(); self.handle_home();
} }
} }
_ if *key == DEFAULT_KEYBINDINGS.end.key => { _ if matches_key!(end, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_end(); self.handle_end();
} }
} }
_ if *key == DEFAULT_KEYBINDINGS.delete.key => { _ if matches_key!(delete, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_delete(); self.handle_delete();
} }
} }
_ if *key == DEFAULT_KEYBINDINGS.left.key || *key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(left, key, self.ignore_alt_navigation())
|| matches_key!(right, key, self.ignore_alt_navigation()) =>
{
self.handle_left_right_action() self.handle_left_right_action()
} }
_ if *key == DEFAULT_KEYBINDINGS.submit.key => { _ if matches_key!(submit, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_submit(); self.handle_submit();
} }
} }
_ if *key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(), _ if matches_key!(esc, key) => self.handle_esc(),
_ => { _ => {
if self.is_ready() { if self.is_ready() {
self.handle_char_key_event(); self.handle_char_key_event();
@@ -65,9 +70,10 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route>> {
self.handle_key_event(); self.handle_key_event();
} }
fn accepts(active_block: &'a T) -> bool; fn accepts(active_block: T) -> bool;
fn with(key: &'a Key, app: &'a mut App<'b>, active_block: &'a T, context: &'a Option<T>) -> Self; fn new(key: Key, app: &'a mut App<'b>, active_block: T, context: Option<T>) -> Self;
fn get_key(&self) -> &Key; fn get_key(&self) -> Key;
fn ignore_alt_navigation(&self) -> bool;
fn is_ready(&self) -> bool; fn is_ready(&self) -> bool;
fn handle_scroll_up(&mut self); fn handle_scroll_up(&mut self);
fn handle_scroll_down(&mut self); fn handle_scroll_down(&mut self);
@@ -81,8 +87,26 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route>> {
} }
pub fn handle_events(key: Key, app: &mut App<'_>) { pub fn handle_events(key: Key, app: &mut App<'_>) {
if let Route::Radarr(active_radarr_block, context) = *app.get_current_route() { if matches_key!(next_servarr, key) {
RadarrHandler::with(&key, app, &active_radarr_block, &context).handle() app.reset();
app.server_tabs.next();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel();
} else if matches_key!(previous_servarr, key) {
app.reset();
app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel();
} else {
match app.get_current_route() {
Route::Radarr(active_radarr_block, context) => {
RadarrHandler::new(key, app, active_radarr_block, context).handle()
}
Route::Sonarr(active_sonarr_block, context) => {
SonarrHandler::new(key, app, active_sonarr_block, context).handle()
}
_ => (),
}
} }
} }
@@ -92,13 +116,17 @@ fn handle_clear_errors(app: &mut App<'_>) {
} }
} }
fn handle_prompt_toggle(app: &mut App<'_>, key: &Key) { fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
match key { match key {
_ if *key == DEFAULT_KEYBINDINGS.left.key || *key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(left, key) || matches_key!(right, key) => match app.get_current_route() {
if let Route::Radarr(_, _) = *app.get_current_route() { Route::Radarr(_, _) => {
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm; app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
} }
} Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
},
_ => (), _ => (),
} }
} }
@@ -107,10 +135,10 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: &Key) {
macro_rules! handle_text_box_left_right_keys { macro_rules! handle_text_box_left_right_keys {
($self:expr, $key:expr, $input:expr) => { ($self:expr, $key:expr, $input:expr) => {
match $self.key { match $self.key {
_ if *$key == DEFAULT_KEYBINDINGS.left.key => { _ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key => {
$input.scroll_left(); $input.scroll_left();
} }
_ if *$key == DEFAULT_KEYBINDINGS.right.key => { _ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.right.key => {
$input.scroll_right(); $input.scroll_right();
} }
_ => (), _ => (),
@@ -122,13 +150,26 @@ macro_rules! handle_text_box_left_right_keys {
macro_rules! handle_text_box_keys { macro_rules! handle_text_box_keys {
($self:expr, $key:expr, $input:expr) => { ($self:expr, $key:expr, $input:expr) => {
match $self.key { match $self.key {
_ if *$key == DEFAULT_KEYBINDINGS.backspace.key => { _ if $crate::matches_key!(backspace, $key) => {
$input.pop(); $input.pop();
} }
Key::Char(character) => { Key::Char(character) => {
$input.push(*character); $input.push(character);
} }
_ => (), _ => (),
} }
}; };
} }
#[macro_export]
macro_rules! handle_prompt_left_right_keys {
($self:expr, $confirm_prompt:expr, $data:ident) => {
if $self.app.data.$data.selected_block.get_active_block() == $confirm_prompt {
handle_prompt_toggle($self.app, $self.key);
} else if $crate::matches_key!(left, $self.key) {
$self.app.data.$data.selected_block.left();
} else {
$self.app.data.$data.selected_block.right();
}
};
}
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime; use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -14,246 +15,6 @@ mod tests {
use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie}; use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::servarr_models::{Language, Quality, QualityWrapper}; use crate::models::servarr_models::{Language, Quality, QualityWrapper};
use crate::models::stateful_table::SortOption;
mod test_handle_scroll_up_and_down {
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use crate::models::radarr_models::BlocklistItem;
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
use super::*;
test_iterable_scroll!(
test_blocklist_scroll,
BlocklistHandler,
blocklist,
simple_stateful_iterable_vec!(BlocklistItem, String, source_title),
ActiveRadarrBlock::Blocklist,
None,
source_title,
to_string
);
#[rstest]
fn test_blocklist_scroll_no_op_when_not_ready(
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
) {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.blocklist
.set_items(simple_stateful_iterable_vec!(
BlocklistItem,
String,
source_title
));
BlocklistHandler::with(&key, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
assert_str_eq!(
app
.data
.radarr_data
.blocklist
.current_selection()
.source_title
.to_string(),
"Test 1"
);
BlocklistHandler::with(&key, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
assert_str_eq!(
app
.data
.radarr_data
.blocklist
.current_selection()
.source_title
.to_string(),
"Test 1"
);
}
#[rstest]
fn test_blocklist_sort_scroll(
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
) {
let blocklist_field_vec = sort_options();
let mut app = App::default();
app.data.radarr_data.blocklist.sorting(sort_options());
if key == Key::Up {
for i in (0..blocklist_field_vec.len()).rev() {
BlocklistHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[i]
);
}
} else {
for i in 0..blocklist_field_vec.len() {
BlocklistHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[(i + 1) % blocklist_field_vec.len()]
);
}
}
}
}
mod test_handle_home_end {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::models::radarr_models::BlocklistItem;
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
use super::*;
test_iterable_home_and_end!(
test_blocklist_home_and_end,
BlocklistHandler,
blocklist,
extended_stateful_iterable_vec!(BlocklistItem, String, source_title),
ActiveRadarrBlock::Blocklist,
None,
source_title,
to_string
);
#[test]
fn test_blocklist_home_and_end_no_op_when_not_ready() {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.blocklist
.set_items(extended_stateful_iterable_vec!(
BlocklistItem,
String,
source_title
));
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_str_eq!(
app
.data
.radarr_data
.blocklist
.current_selection()
.source_title
.to_string(),
"Test 1"
);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_str_eq!(
app
.data
.radarr_data
.blocklist
.current_selection()
.source_title
.to_string(),
"Test 1"
);
}
#[test]
fn test_blocklist_sort_home_end() {
let blocklist_field_vec = sort_options();
let mut app = App::default();
app.data.radarr_data.blocklist.sorting(sort_options());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[blocklist_field_vec.len() - 1]
);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[0]
);
}
}
mod test_handle_delete { mod test_handle_delete {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -264,30 +25,27 @@ mod tests {
#[test] #[test]
fn test_delete_blocklist_item_prompt() { fn test_delete_blocklist_item_prompt() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); BlocklistHandler::new(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::DeleteBlocklistItemPrompt.into() ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()
); );
} }
#[test] #[test]
fn test_delete_blocklist_item_no_op_when_not_ready() { fn test_delete_blocklist_item_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); BlocklistHandler::new(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
} }
} }
@@ -299,49 +57,46 @@ mod tests {
#[rstest] #[rstest]
fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) { fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(3); app.data.radarr_data.main_tabs.set_index(3);
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.left.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into() ActiveRadarrBlock::Downloads.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
); );
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
} }
#[rstest] #[rstest]
fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) { fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(3); app.data.radarr_data.main_tabs.set_index(3);
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.right.key, DEFAULT_KEYBINDINGS.right.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::RootFolders.into() ActiveRadarrBlock::RootFolders.into()
); );
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into() ActiveRadarrBlock::RootFolders.into()
); );
} }
@@ -354,13 +109,13 @@ mod tests {
active_radarr_block: ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); BlocklistHandler::new(key, &mut app, active_radarr_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.prompt_confirm);
BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); BlocklistHandler::new(key, &mut app, active_radarr_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
@@ -378,38 +133,35 @@ mod tests {
#[test] #[test]
fn test_blocklist_submit() { fn test_blocklist_submit() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::BlocklistItemDetails.into() ActiveRadarrBlock::BlocklistItemDetails.into()
); );
} }
#[test] #[test]
fn test_blocklist_submit_no_op_when_not_ready() { fn test_blocklist_submit_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); BlocklistHandler::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
} }
#[rstest] #[rstest]
#[case( #[case(
ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt, ActiveRadarrBlock::DeleteBlocklistItemPrompt,
RadarrEvent::DeleteBlocklistItem(None) RadarrEvent::DeleteBlocklistItem(3)
)] )]
#[case( #[case(
ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
@@ -421,20 +173,20 @@ mod tests {
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent, #[case] expected_action: RadarrEvent,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.prompt_confirm = true;
app.push_navigation_stack(base_route.into()); app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.prompt_confirm);
assert_eq!( assert_eq!(
app.data.radarr_data.prompt_confirm_action, app.data.radarr_data.prompt_confirm_action,
Some(expected_action) Some(expected_action)
); );
assert_eq!(app.get_current_route(), &base_route.into()); assert_eq!(app.get_current_route(), base_route.into());
} }
#[rstest] #[rstest]
@@ -445,47 +197,16 @@ mod tests {
)] )]
prompt_block: ActiveRadarrBlock, prompt_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); BlocklistHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_blocklist_sort_prompt_submit() {
let mut app = App::default();
app.data.radarr_data.blocklist.sort_asc = true;
app.data.radarr_data.blocklist.sorting(sort_options());
app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
let mut expected_vec = blocklist_vec();
expected_vec.sort_by(|a, b| a.id.cmp(&b.id));
expected_vec.reverse();
BlocklistHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.data.radarr_data.blocklist.items, expected_vec);
} }
} }
@@ -512,71 +233,45 @@ mod tests {
#[case] base_block: ActiveRadarrBlock, #[case] base_block: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack(base_block.into()); app.push_navigation_stack(base_block.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.prompt_confirm = true;
BlocklistHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle(); BlocklistHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_eq!(app.get_current_route(), &base_block.into()); assert_eq!(app.get_current_route(), base_block.into());
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
#[test] #[test]
fn test_esc_blocklist_item_details() { fn test_esc_blocklist_item_details() {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into()); app.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into());
BlocklistHandler::with( BlocklistHandler::new(
&ESC_KEY, ESC_KEY,
&mut app, &mut app,
&ActiveRadarrBlock::BlocklistItemDetails, ActiveRadarrBlock::BlocklistItemDetails,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_blocklist_sort_prompt_block_esc() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
BlocklistHandler::with(
&ESC_KEY,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
} }
#[rstest] #[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) { fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.error = "test error".to_owned().into(); app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
DownloadsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); DownloadsHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert!(app.error.text.is_empty()); assert!(app.error.text.is_empty());
} }
} }
@@ -591,139 +286,83 @@ mod tests {
#[test] #[test]
fn test_refresh_blocklist_key() { fn test_refresh_blocklist_key() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.refresh.key, DEFAULT_KEYBINDINGS.refresh.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert!(app.should_refresh); assert!(app.should_refresh);
} }
#[test] #[test]
fn test_refresh_blocklist_key_no_op_when_not_ready() { fn test_refresh_blocklist_key_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.refresh.key, DEFAULT_KEYBINDINGS.refresh.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert!(!app.should_refresh); assert!(!app.should_refresh);
} }
#[test] #[test]
fn test_clear_blocklist_key() { fn test_clear_blocklist_key() {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.clear.key, DEFAULT_KEYBINDINGS.clear.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into() ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()
); );
} }
#[test] #[test]
fn test_clear_blocklist_key_no_op_when_not_ready() { fn test_clear_blocklist_key_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.clear.key, DEFAULT_KEYBINDINGS.clear.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_sort_key() {
let mut app = App::default();
app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.sort.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::BlocklistSortPrompt.into()
);
assert_eq!(
app.data.radarr_data.blocklist.sort.as_ref().unwrap().items,
blocklist_sorting_options()
);
assert!(!app.data.radarr_data.blocklist.sort_asc);
}
#[test]
fn test_sort_key_no_op_when_not_ready() {
let mut app = App::default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.sort.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert!(app.data.radarr_data.blocklist.sort.is_none());
assert!(!app.data.radarr_data.blocklist.sort_asc);
} }
#[rstest] #[rstest]
#[case( #[case(
ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt, ActiveRadarrBlock::DeleteBlocklistItemPrompt,
RadarrEvent::DeleteBlocklistItem(None) RadarrEvent::DeleteBlocklistItem(3)
)] )]
#[case( #[case(
ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
@@ -735,16 +374,16 @@ mod tests {
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent, #[case] expected_action: RadarrEvent,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(base_route.into()); app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with( BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.confirm.key, DEFAULT_KEYBINDINGS.confirm.key,
&mut app, &mut app,
&prompt_block, prompt_block,
&None, None,
) )
.handle(); .handle();
@@ -753,7 +392,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action, app.data.radarr_data.prompt_confirm_action,
Some(expected_action) Some(expected_action)
); );
assert_eq!(app.get_current_route(), &base_route.into()); assert_eq!(app.get_current_route(), base_route.into());
} }
} }
@@ -896,23 +535,55 @@ mod tests {
fn test_blocklist_handler_accepts() { fn test_blocklist_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| { ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if BLOCKLIST_BLOCKS.contains(&active_radarr_block) { if BLOCKLIST_BLOCKS.contains(&active_radarr_block) {
assert!(BlocklistHandler::accepts(&active_radarr_block)); assert!(BlocklistHandler::accepts(active_radarr_block));
} else { } else {
assert!(!BlocklistHandler::accepts(&active_radarr_block)); assert!(!BlocklistHandler::accepts(active_radarr_block));
} }
}) })
} }
#[rstest]
fn test_blocklist_handler_ignore_alt_navigation(
#[values(true, false)] should_ignore_quit_key: bool,
) {
let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key;
let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key);
}
#[test]
fn test_extract_blocklist_item_id() {
let mut app = App::test_default();
app.data.radarr_data.blocklist.set_items(blocklist_vec());
let blocklist_item_id = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::Blocklist,
None,
)
.extract_blocklist_item_id();
assert_eq!(blocklist_item_id, 3);
}
#[test] #[test]
fn test_blocklist_handler_not_ready_when_loading() { fn test_blocklist_handler_not_ready_when_loading() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
let handler = BlocklistHandler::with( let handler = BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
); );
assert!(!handler.is_ready()); assert!(!handler.is_ready());
@@ -920,14 +591,14 @@ mod tests {
#[test] #[test]
fn test_blocklist_handler_not_ready_when_blocklist_is_empty() { fn test_blocklist_handler_not_ready_when_blocklist_is_empty() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = false; app.is_loading = false;
let handler = BlocklistHandler::with( let handler = BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
); );
assert!(!handler.is_ready()); assert!(!handler.is_ready());
@@ -935,7 +606,7 @@ mod tests {
#[test] #[test]
fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() { fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = false; app.is_loading = false;
app app
.data .data
@@ -943,11 +614,11 @@ mod tests {
.blocklist .blocklist
.set_items(vec![BlocklistItem::default()]); .set_items(vec![BlocklistItem::default()]);
let handler = BlocklistHandler::with( let handler = BlocklistHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Blocklist,
&None, None,
); );
assert!(handler.is_ready()); assert!(handler.is_ready());
@@ -1029,15 +700,4 @@ mod tests {
}, },
] ]
} }
fn sort_options() -> Vec<SortOption<BlocklistItem>> {
vec![SortOption {
name: "Test 1",
cmp_fn: Some(|a, b| {
b.source_title
.to_lowercase()
.cmp(&a.source_title.to_lowercase())
}),
}]
}
} }
+57 -105
View File
@@ -1,35 +1,64 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::BlocklistItem; use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "blocklist_handler_tests.rs"] #[path = "blocklist_handler_tests.rs"]
mod blocklist_handler_tests; mod blocklist_handler_tests;
pub(super) struct BlocklistHandler<'a, 'b> { pub(super) struct BlocklistHandler<'a, 'b> {
key: &'a Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>, _context: Option<ActiveRadarrBlock>,
}
impl BlocklistHandler<'_, '_> {
handle_table_events!(
self,
blocklist,
self.app.data.radarr_data.blocklist,
BlocklistItem
);
fn extract_blocklist_item_id(&self) -> i64 {
self.app.data.radarr_data.blocklist.current_selection().id
}
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { fn handle(&mut self) {
BLOCKLIST_BLOCKS.contains(active_block) let blocklist_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Blocklist.into())
.sorting_block(ActiveRadarrBlock::BlocklistSortPrompt.into())
.sort_by_fn(|a: &BlocklistItem, b: &BlocklistItem| a.id.cmp(&b.id))
.sort_options(blocklist_sorting_options());
if !self.handle_blocklist_table_events(blocklist_table_handling_config) {
self.handle_key_event();
}
} }
fn with( fn accepts(active_block: ActiveRadarrBlock) -> bool {
key: &'a Key, BLOCKLIST_BLOCKS.contains(&active_block)
}
fn ignore_alt_navigation(&self) -> bool {
self.app.should_ignore_quit_key
}
fn new(
key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock, active_block: ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>, context: Option<ActiveRadarrBlock>,
) -> Self { ) -> Self {
BlocklistHandler { BlocklistHandler {
key, key,
@@ -39,7 +68,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
} }
} }
fn get_key(&self) -> &Key { fn get_key(&self) -> Key {
self.key self.key
} }
@@ -47,72 +76,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
!self.app.is_loading && !self.app.data.radarr_data.blocklist.is_empty() !self.app.is_loading && !self.app.data.radarr_data.blocklist.is_empty()
} }
fn handle_scroll_up(&mut self) { fn handle_scroll_up(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_up(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) { fn handle_scroll_down(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_down(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_down(),
_ => (),
}
}
fn handle_home(&mut self) { fn handle_home(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_top(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_to_top(),
_ => (),
}
}
fn handle_end(&mut self) { fn handle_end(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_bottom(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_to_bottom(),
_ => (),
}
}
fn handle_delete(&mut self) { fn handle_delete(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist { if self.active_radarr_block == ActiveRadarrBlock::Blocklist {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::DeleteBlocklistItemPrompt.into());
@@ -132,8 +105,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::DeleteBlocklistItemPrompt => { ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if self.app.data.radarr_data.prompt_confirm { if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem(
Some(RadarrEvent::DeleteBlocklistItem(None)); self.extract_blocklist_item_id(),
));
} }
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -145,18 +119,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
ActiveRadarrBlock::BlocklistSortPrompt => {
self
.app
.data
.radarr_data
.blocklist
.items
.sort_by(|a, b| a.id.cmp(&b.id));
self.app.data.radarr_data.blocklist.apply_sorting();
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::Blocklist => { ActiveRadarrBlock::Blocklist => {
self self
.app .app
@@ -173,7 +135,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false; self.app.data.radarr_data.prompt_confirm = false;
} }
ActiveRadarrBlock::BlocklistItemDetails | ActiveRadarrBlock::BlocklistSortPrompt => { ActiveRadarrBlock::BlocklistItemDetails => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
_ => handle_clear_errors(self.app), _ => handle_clear_errors(self.app),
@@ -184,38 +146,28 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key { ActiveRadarrBlock::Blocklist => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if *key == DEFAULT_KEYBINDINGS.clear.key => { _ if matches_key!(clear, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into());
} }
_ if *key == DEFAULT_KEYBINDINGS.sort.key => {
self
.app
.data
.radarr_data
.blocklist
.sorting(blocklist_sorting_options());
self
.app
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
}
_ => (), _ => (),
}, },
ActiveRadarrBlock::DeleteBlocklistItemPrompt => { ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem(
Some(RadarrEvent::DeleteBlocklistItem(None)); self.extract_blocklist_item_id(),
));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
} }
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
@@ -1,35 +1,59 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::CollectionMovie;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS, ActiveRadarrBlock, ADD_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
}; };
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Scrollable}; use crate::models::BlockSelectionState;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "collection_details_handler_tests.rs"] #[path = "collection_details_handler_tests.rs"]
mod collection_details_handler_tests; mod collection_details_handler_tests;
pub(super) struct CollectionDetailsHandler<'a, 'b> { pub(super) struct CollectionDetailsHandler<'a, 'b> {
key: &'a Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>, _context: Option<ActiveRadarrBlock>,
}
impl CollectionDetailsHandler<'_, '_> {
handle_table_events!(
self,
collection_movies,
self.app.data.radarr_data.collection_movies,
CollectionMovie
);
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { fn handle(&mut self) {
COLLECTION_DETAILS_BLOCKS.contains(active_block) let collection_movies_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::CollectionDetails.into());
if !self.handle_collection_movies_table_events(collection_movies_table_handling_config) {
self.handle_key_event();
}
} }
fn with( fn accepts(active_block: ActiveRadarrBlock) -> bool {
key: &'a Key, COLLECTION_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_alt_navigation(&self) -> bool {
self.app.should_ignore_quit_key
}
fn new(
key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock, active_block: ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>, _context: Option<ActiveRadarrBlock>,
) -> CollectionDetailsHandler<'a, 'b> { ) -> CollectionDetailsHandler<'a, 'b> {
CollectionDetailsHandler { CollectionDetailsHandler {
key, key,
@@ -39,7 +63,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
} }
} }
fn get_key(&self) -> &Key { fn get_key(&self) -> Key {
self.key self.key
} }
@@ -47,41 +71,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
!self.app.is_loading && !self.app.data.radarr_data.collection_movies.is_empty() !self.app.is_loading && !self.app.data.radarr_data.collection_movies.is_empty()
} }
fn handle_scroll_up(&mut self) { fn handle_scroll_up(&mut self) {}
if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block {
self.app.data.radarr_data.collection_movies.scroll_up()
}
}
fn handle_scroll_down(&mut self) { fn handle_scroll_down(&mut self) {}
if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block {
self.app.data.radarr_data.collection_movies.scroll_down()
}
}
fn handle_home(&mut self) { fn handle_home(&mut self) {}
if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block {
self.app.data.radarr_data.collection_movies.scroll_to_top();
}
}
fn handle_end(&mut self) { fn handle_end(&mut self) {}
if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block {
self
.app
.data
.radarr_data
.collection_movies
.scroll_to_bottom();
}
}
fn handle_delete(&mut self) {} fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {} fn handle_left_right_action(&mut self) {}
fn handle_submit(&mut self) { fn handle_submit(&mut self) {
if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block { if ActiveRadarrBlock::CollectionDetails == self.active_radarr_block {
let tmdb_id = self let tmdb_id = self
.app .app
.data .data
@@ -111,7 +114,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
.into(), .into(),
); );
self.app.data.radarr_data.selected_block = self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS);
self.app.data.radarr_data.add_movie_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.add_movie_modal = Some((&self.app.data.radarr_data).into());
} }
} }
@@ -129,19 +132,19 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
} }
fn handle_char_key_event(&mut self) { fn handle_char_key_event(&mut self) {
if *self.active_radarr_block == ActiveRadarrBlock::CollectionDetails if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails
&& *self.key == DEFAULT_KEYBINDINGS.edit.key && matches_key!(edit, self.key)
{ {
self.app.push_navigation_stack( self.app.push_navigation_stack(
( (
ActiveRadarrBlock::EditCollectionPrompt, ActiveRadarrBlock::EditCollectionPrompt,
Some(*self.active_radarr_block), Some(self.active_radarr_block),
) )
.into(), .into(),
); );
self.app.data.radarr_data.edit_collection_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.edit_collection_modal = Some((&self.app.data.radarr_data).into());
self.app.data.radarr_data.selected_block = self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
} }
} }
} }
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_str_eq; use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -12,142 +13,6 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS,
}; };
use crate::models::HorizontallyScrollableText;
mod test_handle_scroll_up_and_down {
use rstest::rstest;
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
use super::*;
test_iterable_scroll!(
test_collection_details_scroll,
CollectionDetailsHandler,
collection_movies,
simple_stateful_iterable_vec!(CollectionMovie, HorizontallyScrollableText),
ActiveRadarrBlock::CollectionDetails,
None,
title,
to_string
);
#[rstest]
fn test_collection_details_scroll_no_op_when_not_ready(
#[values(
DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key
)]
key: Key,
) {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.collection_movies
.set_items(simple_stateful_iterable_vec!(
CollectionMovie,
HorizontallyScrollableText
));
CollectionDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::CollectionDetails, &None)
.handle();
assert_str_eq!(
app
.data
.radarr_data
.collection_movies
.current_selection()
.title
.to_string(),
"Test 1"
);
CollectionDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::CollectionDetails, &None)
.handle();
assert_str_eq!(
app
.data
.radarr_data
.collection_movies
.current_selection()
.title
.to_string(),
"Test 1"
);
}
}
mod test_handle_home_end {
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
use super::*;
test_iterable_home_and_end!(
test_collection_details_home_end,
CollectionDetailsHandler,
collection_movies,
extended_stateful_iterable_vec!(CollectionMovie, HorizontallyScrollableText),
ActiveRadarrBlock::CollectionDetails,
None,
title,
to_string
);
#[test]
fn test_collection_details_home_end_no_op_when_not_ready() {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.collection_movies
.set_items(extended_stateful_iterable_vec!(
CollectionMovie,
HorizontallyScrollableText
));
CollectionDetailsHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
)
.handle();
assert_str_eq!(
app
.data
.radarr_data
.collection_movies
.current_selection()
.title
.to_string(),
"Test 1"
);
CollectionDetailsHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
)
.handle();
assert_str_eq!(
app
.data
.radarr_data
.collection_movies
.current_selection()
.title
.to_string(),
"Test 1"
);
}
}
mod test_handle_submit { mod test_handle_submit {
use bimap::BiMap; use bimap::BiMap;
@@ -163,7 +28,7 @@ mod tests {
#[test] #[test]
fn test_collection_details_submit() { fn test_collection_details_submit() {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
@@ -171,24 +36,24 @@ mod tests {
.set_items(vec![CollectionMovie::default()]); .set_items(vec![CollectionMovie::default()]);
app.data.radarr_data.quality_profile_map = app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]);
app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS);
app app
.data .data
.radarr_data .radarr_data
.selected_block .selected_block
.set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1); .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1);
CollectionDetailsHandler::with( CollectionDetailsHandler::new(
&SUBMIT_KEY, SUBMIT_KEY,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&( (
ActiveRadarrBlock::AddMoviePrompt, ActiveRadarrBlock::AddMoviePrompt,
Some(ActiveRadarrBlock::CollectionDetails) Some(ActiveRadarrBlock::CollectionDetails)
) )
@@ -205,7 +70,7 @@ mod tests {
.is_empty()); .is_empty());
assert_eq!( assert_eq!(
app.data.radarr_data.selected_block.get_active_block(), app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::AddMovieSelectRootFolder ActiveRadarrBlock::AddMovieSelectRootFolder
); );
assert!(!app assert!(!app
.data .data
@@ -240,7 +105,7 @@ mod tests {
#[test] #[test]
fn test_collection_details_submit_no_op_when_not_ready() { fn test_collection_details_submit_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
app app
@@ -249,24 +114,24 @@ mod tests {
.collection_movies .collection_movies
.set_items(vec![CollectionMovie::default()]); .set_items(vec![CollectionMovie::default()]);
CollectionDetailsHandler::with( CollectionDetailsHandler::new(
&SUBMIT_KEY, SUBMIT_KEY,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::CollectionDetails.into() ActiveRadarrBlock::CollectionDetails.into()
); );
assert!(app.data.radarr_data.add_movie_modal.is_none()); assert!(app.data.radarr_data.add_movie_modal.is_none());
} }
#[test] #[test]
fn test_collection_details_submit_movie_already_in_library() { fn test_collection_details_submit_movie_already_in_library() {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
@@ -278,17 +143,17 @@ mod tests {
.movies .movies
.set_items(vec![Movie::default()]); .set_items(vec![Movie::default()]);
CollectionDetailsHandler::with( CollectionDetailsHandler::new(
&SUBMIT_KEY, SUBMIT_KEY,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::ViewMovieOverview.into() ActiveRadarrBlock::ViewMovieOverview.into()
); );
} }
} }
@@ -302,7 +167,7 @@ mod tests {
#[rstest] #[rstest]
fn test_esc_collection_details(#[values(true, false)] is_ready: bool) { fn test_esc_collection_details(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
@@ -312,38 +177,38 @@ mod tests {
.collection_movies .collection_movies
.set_items(vec![CollectionMovie::default()]); .set_items(vec![CollectionMovie::default()]);
CollectionDetailsHandler::with( CollectionDetailsHandler::new(
&ESC_KEY, ESC_KEY,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::Collections.into() ActiveRadarrBlock::Collections.into()
); );
assert!(app.data.radarr_data.collection_movies.items.is_empty()); assert!(app.data.radarr_data.collection_movies.items.is_empty());
} }
#[test] #[test]
fn test_esc_view_movie_overview() { fn test_esc_view_movie_overview() {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into()); app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into());
CollectionDetailsHandler::with( CollectionDetailsHandler::new(
&ESC_KEY, ESC_KEY,
&mut app, &mut app,
&ActiveRadarrBlock::ViewMovieOverview, ActiveRadarrBlock::ViewMovieOverview,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::CollectionDetails.into() ActiveRadarrBlock::CollectionDetails.into()
); );
} }
} }
@@ -367,13 +232,13 @@ mod tests {
test_edit_collection_key!( test_edit_collection_key!(
CollectionDetailsHandler, CollectionDetailsHandler,
ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
ActiveRadarrBlock::CollectionDetails Some(ActiveRadarrBlock::CollectionDetails)
); );
} }
#[test] #[test]
fn test_edit_key_no_op_when_not_ready() { fn test_edit_key_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
let mut radarr_data = create_test_radarr_data(); let mut radarr_data = create_test_radarr_data();
@@ -387,17 +252,17 @@ mod tests {
}]); }]);
app.data.radarr_data = radarr_data; app.data.radarr_data = radarr_data;
CollectionDetailsHandler::with( CollectionDetailsHandler::new(
&DEFAULT_KEYBINDINGS.edit.key, DEFAULT_KEYBINDINGS.edit.key,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::CollectionDetails.into() ActiveRadarrBlock::CollectionDetails.into()
); );
assert!(app.data.radarr_data.edit_collection_modal.is_none()); assert!(app.data.radarr_data.edit_collection_modal.is_none());
} }
@@ -407,23 +272,39 @@ mod tests {
fn test_collection_details_handler_accepts() { fn test_collection_details_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| { ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block) { if COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block) {
assert!(CollectionDetailsHandler::accepts(&active_radarr_block)); assert!(CollectionDetailsHandler::accepts(active_radarr_block));
} else { } else {
assert!(!CollectionDetailsHandler::accepts(&active_radarr_block)); assert!(!CollectionDetailsHandler::accepts(active_radarr_block));
} }
}); });
} }
#[rstest]
fn test_collection_details_handler_ignore_alt_navigation(
#[values(true, false)] should_ignore_quit_key: bool,
) {
let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key;
let handler = CollectionDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key);
}
#[test] #[test]
fn test_collection_details_handler_not_ready_when_loading() { fn test_collection_details_handler_not_ready_when_loading() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
let handler = CollectionDetailsHandler::with( let handler = CollectionDetailsHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
); );
assert!(!handler.is_ready()); assert!(!handler.is_ready());
@@ -431,14 +312,14 @@ mod tests {
#[test] #[test]
fn test_collection_details_handler_not_ready_when_collection_movies_is_empty() { fn test_collection_details_handler_not_ready_when_collection_movies_is_empty() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = false; app.is_loading = false;
let handler = CollectionDetailsHandler::with( let handler = CollectionDetailsHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
); );
assert!(!handler.is_ready()); assert!(!handler.is_ready());
@@ -446,7 +327,7 @@ mod tests {
#[test] #[test]
fn test_collection_details_handler_ready_when_not_loading_and_collection_movies_is_not_empty() { fn test_collection_details_handler_ready_when_not_loading_and_collection_movies_is_not_empty() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = false; app.is_loading = false;
app app
.data .data
@@ -454,11 +335,11 @@ mod tests {
.collection_movies .collection_movies
.set_items(vec![CollectionMovie::default()]); .set_items(vec![CollectionMovie::default()]);
let handler = CollectionDetailsHandler::with( let handler = CollectionDetailsHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::CollectionDetails, ActiveRadarrBlock::CollectionDetails,
&None, None,
); );
assert!(handler.is_ready()); assert!(handler.is_ready());
File diff suppressed because it is too large Load Diff
@@ -1,33 +1,82 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
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::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
use crate::models::Scrollable; use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "edit_collection_handler_tests.rs"] #[path = "edit_collection_handler_tests.rs"]
mod edit_collection_handler_tests; mod edit_collection_handler_tests;
pub(super) struct EditCollectionHandler<'a, 'b> { pub(super) struct EditCollectionHandler<'a, 'b> {
key: &'a Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>, context: Option<ActiveRadarrBlock>,
}
impl EditCollectionHandler<'_, '_> {
fn build_edit_collection_params(&mut self) -> EditCollectionParams {
let edit_collection_modal = self
.app
.data
.radarr_data
.edit_collection_modal
.take()
.expect("EditCollectionModal is None");
let collection_id = self.app.data.radarr_data.collections.current_selection().id;
let EditCollectionModal {
path,
search_on_add,
minimum_availability_list,
monitored,
quality_profile_list,
} = edit_collection_modal;
let quality_profile = quality_profile_list.current_selection();
let quality_profile_id = *self
.app
.data
.radarr_data
.quality_profile_map
.iter()
.filter(|(_, value)| *value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
let root_folder_path = path.text;
let search_on_add = search_on_add.unwrap_or_default();
let minimum_availability = *minimum_availability_list.current_selection();
EditCollectionParams {
collection_id,
monitored,
minimum_availability: Some(minimum_availability),
quality_profile_id: Some(quality_profile_id),
root_folder_path: Some(root_folder_path),
search_on_add: Some(search_on_add),
}
}
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { fn accepts(active_block: ActiveRadarrBlock) -> bool {
EDIT_COLLECTION_BLOCKS.contains(active_block) EDIT_COLLECTION_BLOCKS.contains(&active_block)
} }
fn with( fn ignore_alt_navigation(&self) -> bool {
key: &'a Key, self.app.should_ignore_quit_key
}
fn new(
key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock, active_block: ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>, context: Option<ActiveRadarrBlock>,
) -> EditCollectionHandler<'a, 'b> { ) -> EditCollectionHandler<'a, 'b> {
EditCollectionHandler { EditCollectionHandler {
key, key,
@@ -37,7 +86,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
} }
} }
fn get_key(&self) -> &Key { fn get_key(&self) -> Key {
self.key self.key
} }
@@ -65,9 +114,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
.unwrap() .unwrap()
.quality_profile_list .quality_profile_list
.scroll_up(), .scroll_up(),
ActiveRadarrBlock::EditCollectionPrompt => { ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.up(),
self.app.data.radarr_data.selected_block.previous()
}
_ => (), _ => (),
} }
} }
@@ -92,7 +139,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
.unwrap() .unwrap()
.quality_profile_list .quality_profile_list
.scroll_down(), .scroll_down(),
ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.next(), ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.down(),
_ => (), _ => (),
} }
} }
@@ -192,8 +239,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
match self.app.data.radarr_data.selected_block.get_active_block() { match self.app.data.radarr_data.selected_block.get_active_block() {
ActiveRadarrBlock::EditCollectionConfirmPrompt => { ActiveRadarrBlock::EditCollectionConfirmPrompt => {
if self.app.data.radarr_data.prompt_confirm { if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
Some(RadarrEvent::EditCollection(None)); self.build_edit_collection_params(),
));
self.app.should_refresh = true; self.app.should_refresh = true;
} }
@@ -203,8 +251,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
| ActiveRadarrBlock::EditCollectionSelectQualityProfile => { | ActiveRadarrBlock::EditCollectionSelectQualityProfile => {
self.app.push_navigation_stack( self.app.push_navigation_stack(
( (
*self.app.data.radarr_data.selected_block.get_active_block(), self.app.data.radarr_data.selected_block.get_active_block(),
*self.context, self.context,
) )
.into(), .into(),
) )
@@ -212,8 +260,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
ActiveRadarrBlock::EditCollectionRootFolderPathInput => { ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.push_navigation_stack( self.app.push_navigation_stack(
( (
*self.app.data.radarr_data.selected_block.get_active_block(), self.app.data.radarr_data.selected_block.get_active_block(),
*self.context, self.context,
) )
.into(), .into(),
); );
@@ -308,11 +356,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
} }
ActiveRadarrBlock::EditCollectionPrompt => { ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditCollectionConfirmPrompt == ActiveRadarrBlock::EditCollectionConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None)); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
self.build_edit_collection_params(),
));
self.app.should_refresh = true; self.app.should_refresh = true;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
File diff suppressed because it is too large Load Diff
+59 -277
View File
@@ -1,18 +1,18 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler; use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler;
use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler; use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::Collection; use crate::models::radarr_models::Collection;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
}; };
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{handle_table_events, matches_key};
mod collection_details_handler; mod collection_details_handler;
mod edit_collection_handler; mod edit_collection_handler;
@@ -22,38 +22,65 @@ mod edit_collection_handler;
mod collections_handler_tests; mod collections_handler_tests;
pub(super) struct CollectionsHandler<'a, 'b> { pub(super) struct CollectionsHandler<'a, 'b> {
key: &'a Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>, context: Option<ActiveRadarrBlock>,
}
impl CollectionsHandler<'_, '_> {
handle_table_events!(
self,
collections,
self.app.data.radarr_data.collections,
Collection
);
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'a, 'b> {
fn handle(&mut self) { fn handle(&mut self) {
match self.active_radarr_block { let collections_table_handling_config =
_ if CollectionDetailsHandler::accepts(self.active_radarr_block) => { TableHandlingConfig::new(ActiveRadarrBlock::Collections.into())
CollectionDetailsHandler::with(self.key, self.app, self.active_radarr_block, self.context) .sorting_block(ActiveRadarrBlock::CollectionsSortPrompt.into())
.handle(); .sort_by_fn(|a: &Collection, b: &Collection| a.id.cmp(&b.id))
.sort_options(collections_sorting_options())
.searching_block(ActiveRadarrBlock::SearchCollection.into())
.search_error_block(ActiveRadarrBlock::SearchCollectionError.into())
.search_field_fn(|collection| &collection.title.text)
.filtering_block(ActiveRadarrBlock::FilterCollections.into())
.filter_error_block(ActiveRadarrBlock::FilterCollectionsError.into())
.filter_field_fn(|collection| &collection.title.text);
if !self.handle_collections_table_events(collections_table_handling_config) {
match self.active_radarr_block {
_ if CollectionDetailsHandler::accepts(self.active_radarr_block) => {
CollectionDetailsHandler::new(self.key, self.app, self.active_radarr_block, self.context)
.handle();
}
_ if EditCollectionHandler::accepts(self.active_radarr_block) => {
EditCollectionHandler::new(self.key, self.app, self.active_radarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
} }
_ if EditCollectionHandler::accepts(self.active_radarr_block) => {
EditCollectionHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
} }
} }
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { fn accepts(active_block: ActiveRadarrBlock) -> bool {
CollectionDetailsHandler::accepts(active_block) CollectionDetailsHandler::accepts(active_block)
|| EditCollectionHandler::accepts(active_block) || EditCollectionHandler::accepts(active_block)
|| COLLECTIONS_BLOCKS.contains(active_block) || COLLECTIONS_BLOCKS.contains(&active_block)
} }
fn with( fn ignore_alt_navigation(&self) -> bool {
key: &'a Key, self.app.should_ignore_quit_key
}
fn new(
key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock, active_block: ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>, context: Option<ActiveRadarrBlock>,
) -> CollectionsHandler<'a, 'b> { ) -> CollectionsHandler<'a, 'b> {
CollectionsHandler { CollectionsHandler {
key, key,
@@ -63,7 +90,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
} }
} }
fn get_key(&self) -> &Key { fn get_key(&self) -> Key {
self.key self.key
} }
@@ -71,105 +98,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
!self.app.is_loading && !self.app.data.radarr_data.collections.is_empty() !self.app.is_loading && !self.app.data.radarr_data.collections.is_empty()
} }
fn handle_scroll_up(&mut self) { fn handle_scroll_up(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_up(),
ActiveRadarrBlock::CollectionsSortPrompt => self
.app
.data
.radarr_data
.collections
.sort
.as_mut()
.unwrap()
.scroll_up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) { fn handle_scroll_down(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_down(),
ActiveRadarrBlock::CollectionsSortPrompt => self
.app
.data
.radarr_data
.collections
.sort
.as_mut()
.unwrap()
.scroll_down(),
_ => (),
}
}
fn handle_home(&mut self) { fn handle_home(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_to_top(),
ActiveRadarrBlock::SearchCollection => self
.app
.data
.radarr_data
.collections
.search
.as_mut()
.unwrap()
.scroll_home(),
ActiveRadarrBlock::FilterCollections => self
.app
.data
.radarr_data
.collections
.filter
.as_mut()
.unwrap()
.scroll_home(),
ActiveRadarrBlock::CollectionsSortPrompt => self
.app
.data
.radarr_data
.collections
.sort
.as_mut()
.unwrap()
.scroll_to_top(),
_ => (),
}
}
fn handle_end(&mut self) { fn handle_end(&mut self) {}
match self.active_radarr_block {
ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_to_bottom(),
ActiveRadarrBlock::SearchCollection => self
.app
.data
.radarr_data
.collections
.search
.as_mut()
.unwrap()
.reset_offset(),
ActiveRadarrBlock::FilterCollections => self
.app
.data
.radarr_data
.collections
.filter
.as_mut()
.unwrap()
.reset_offset(),
ActiveRadarrBlock::CollectionsSortPrompt => self
.app
.data
.radarr_data
.collections
.sort
.as_mut()
.unwrap()
.scroll_to_bottom(),
_ => (),
}
}
fn handle_delete(&mut self) {} fn handle_delete(&mut self) {}
@@ -177,34 +112,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Collections => handle_change_tab_left_right_keys(self.app, self.key), ActiveRadarrBlock::Collections => handle_change_tab_left_right_keys(self.app, self.key),
ActiveRadarrBlock::UpdateAllCollectionsPrompt => handle_prompt_toggle(self.app, self.key), ActiveRadarrBlock::UpdateAllCollectionsPrompt => handle_prompt_toggle(self.app, self.key),
ActiveRadarrBlock::SearchCollection => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.collections
.search
.as_mut()
.unwrap()
)
}
ActiveRadarrBlock::FilterCollections => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.collections
.filter
.as_mut()
.unwrap()
)
}
_ => (), _ => (),
} }
} }
@@ -214,44 +121,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
ActiveRadarrBlock::Collections => self ActiveRadarrBlock::Collections => self
.app .app
.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()), .push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()),
ActiveRadarrBlock::SearchCollection => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
if self.app.data.radarr_data.collections.search.is_some() {
let has_match = self
.app
.data
.radarr_data
.collections
.apply_search(|collection| &collection.title.text);
if !has_match {
self
.app
.push_navigation_stack(ActiveRadarrBlock::SearchCollectionError.into());
}
}
}
ActiveRadarrBlock::FilterCollections => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
if self.app.data.radarr_data.collections.filter.is_some() {
let has_matches = self
.app
.data
.radarr_data
.collections
.apply_filter(|collection| &collection.title.text);
if !has_matches {
self
.app
.push_navigation_stack(ActiveRadarrBlock::FilterCollectionsError.into());
}
}
}
ActiveRadarrBlock::UpdateAllCollectionsPrompt => { ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if self.app.data.radarr_data.prompt_confirm { if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -259,44 +128,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
ActiveRadarrBlock::CollectionsSortPrompt => {
self
.app
.data
.radarr_data
.collections
.items
.sort_by(|a, b| a.id.cmp(&b.id));
self.app.data.radarr_data.collections.apply_sorting();
self.app.pop_navigation_stack();
}
_ => (), _ => (),
} }
} }
fn handle_esc(&mut self) { fn handle_esc(&mut self) {
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::FilterCollections | ActiveRadarrBlock::FilterCollectionsError => {
self.app.pop_navigation_stack();
self.app.data.radarr_data.collections.reset_filter();
self.app.should_ignore_quit_key = false;
}
ActiveRadarrBlock::SearchCollection | ActiveRadarrBlock::SearchCollectionError => {
self.app.pop_navigation_stack();
self.app.data.radarr_data.collections.reset_search();
self.app.should_ignore_quit_key = false;
}
ActiveRadarrBlock::UpdateAllCollectionsPrompt => { ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false; self.app.data.radarr_data.prompt_confirm = false;
} }
ActiveRadarrBlock::CollectionsSortPrompt => {
self.app.pop_navigation_stack();
}
_ => { _ => {
self.app.data.radarr_data.collections.reset_search();
self.app.data.radarr_data.collections.reset_filter();
handle_clear_errors(self.app); handle_clear_errors(self.app);
} }
} }
@@ -306,87 +148,27 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Collections => match self.key { ActiveRadarrBlock::Collections => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.search.key => { _ if matches_key!(edit, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); .push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
self.app.data.radarr_data.collections.search =
Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
}
_ if *key == DEFAULT_KEYBINDINGS.filter.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::FilterCollections.into());
self.app.data.radarr_data.collections.reset_filter();
self.app.data.radarr_data.collections.filter =
Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
}
_ if *key == DEFAULT_KEYBINDINGS.edit.key => {
self.app.push_navigation_stack(
(
ActiveRadarrBlock::EditCollectionPrompt,
Some(ActiveRadarrBlock::Collections),
)
.into(),
);
self.app.data.radarr_data.edit_collection_modal = self.app.data.radarr_data.edit_collection_modal =
Some((&self.app.data.radarr_data).into()); Some((&self.app.data.radarr_data).into());
self.app.data.radarr_data.selected_block = self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
} }
_ if *key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
} }
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if *key == DEFAULT_KEYBINDINGS.sort.key => {
self
.app
.data
.radarr_data
.collections
.sorting(collections_sorting_options());
self
.app
.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into());
}
_ => (), _ => (),
}, },
ActiveRadarrBlock::SearchCollection => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.radarr_data
.collections
.search
.as_mut()
.unwrap()
)
}
ActiveRadarrBlock::FilterCollections => {
handle_text_box_keys!(
self,
key,
self
.app
.data
.radarr_data
.collections
.filter
.as_mut()
.unwrap()
)
}
ActiveRadarrBlock::UpdateAllCollectionsPrompt => { ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -1,119 +1,18 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_str_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler; use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::download_record;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::DownloadRecord; use crate::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
mod test_handle_scroll_up_and_down {
use rstest::rstest;
use crate::models::radarr_models::DownloadRecord;
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
use super::*;
test_iterable_scroll!(
test_downloads_scroll,
DownloadsHandler,
downloads,
DownloadRecord,
ActiveRadarrBlock::Downloads,
None,
title
);
#[rstest]
fn test_downloads_scroll_no_op_when_not_ready(
#[values(
DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key
)]
key: Key,
) {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.downloads
.set_items(simple_stateful_iterable_vec!(DownloadRecord));
DownloadsHandler::with(&key, &mut app, &ActiveRadarrBlock::Downloads, &None).handle();
assert_str_eq!(
app.data.radarr_data.downloads.current_selection().title,
"Test 1"
);
DownloadsHandler::with(&key, &mut app, &ActiveRadarrBlock::Downloads, &None).handle();
assert_str_eq!(
app.data.radarr_data.downloads.current_selection().title,
"Test 1"
);
}
}
mod test_handle_home_end {
use crate::models::radarr_models::DownloadRecord;
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
use super::*;
test_iterable_home_and_end!(
test_downloads_home_end,
DownloadsHandler,
downloads,
DownloadRecord,
ActiveRadarrBlock::Downloads,
None,
title
);
#[test]
fn test_downloads_home_end_no_op_when_not_ready() {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.downloads
.set_items(extended_stateful_iterable_vec!(DownloadRecord));
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
)
.handle();
assert_str_eq!(
app.data.radarr_data.downloads.current_selection().title,
"Test 1"
);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
)
.handle();
assert_str_eq!(
app.data.radarr_data.downloads.current_selection().title,
"Test 1"
);
}
}
mod test_handle_delete { mod test_handle_delete {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -123,24 +22,24 @@ mod tests {
#[test] #[test]
fn test_delete_download_prompt() { fn test_delete_download_prompt() {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); DownloadsHandler::new(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::DeleteDownloadPrompt.into() ActiveRadarrBlock::DeleteDownloadPrompt.into()
); );
} }
#[test] #[test]
fn test_delete_download_prompt_no_op_when_not_ready() { fn test_delete_download_prompt_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
app app
@@ -149,12 +48,9 @@ mod tests {
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); DownloadsHandler::new(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
} }
} }
@@ -166,50 +62,47 @@ mod tests {
#[rstest] #[rstest]
fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) { fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(2); app.data.radarr_data.main_tabs.set_index(2);
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.left.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Collections.into() ActiveRadarrBlock::Collections.into()
); );
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::Collections.into() ActiveRadarrBlock::Collections.into()
); );
} }
#[rstest] #[rstest]
fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) { fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.data.radarr_data.main_tabs.set_index(2); app.data.radarr_data.main_tabs.set_index(2);
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.right.key, DEFAULT_KEYBINDINGS.right.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Blocklist.into() ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
); );
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
} }
#[rstest] #[rstest]
@@ -221,13 +114,13 @@ mod tests {
active_radarr_block: ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
DownloadsHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); DownloadsHandler::new(key, &mut app, active_radarr_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.prompt_confirm);
DownloadsHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); DownloadsHandler::new(key, &mut app, active_radarr_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
@@ -247,7 +140,7 @@ mod tests {
#[case( #[case(
ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::DeleteDownloadPrompt, ActiveRadarrBlock::DeleteDownloadPrompt,
RadarrEvent::DeleteDownload(None) RadarrEvent::DeleteDownload(1)
)] )]
#[case( #[case(
ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
@@ -259,24 +152,24 @@ mod tests {
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent, #[case] expected_action: RadarrEvent,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![download_record()]);
app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.prompt_confirm = true;
app.push_navigation_stack(base_route.into()); app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.prompt_confirm);
assert_eq!( assert_eq!(
app.data.radarr_data.prompt_confirm_action, app.data.radarr_data.prompt_confirm_action,
Some(expected_action) Some(expected_action)
); );
assert_eq!(app.get_current_route(), &base_route.into()); assert_eq!(app.get_current_route(), base_route.into());
} }
#[rstest] #[rstest]
@@ -286,7 +179,7 @@ mod tests {
#[case] base_route: ActiveRadarrBlock, #[case] base_route: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
@@ -295,11 +188,11 @@ mod tests {
app.push_navigation_stack(base_route.into()); app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(app.get_current_route(), &base_route.into()); assert_eq!(app.get_current_route(), base_route.into());
} }
} }
@@ -318,31 +211,28 @@ mod tests {
#[case] base_block: ActiveRadarrBlock, #[case] base_block: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app.push_navigation_stack(base_block.into()); app.push_navigation_stack(base_block.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.prompt_confirm = true;
DownloadsHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle(); DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle();
assert_eq!(app.get_current_route(), &base_block.into()); assert_eq!(app.get_current_route(), base_block.into());
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
#[rstest] #[rstest]
fn test_default_esc(#[values(true, false)] is_ready: bool) { fn test_default_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.error = "test error".to_owned().into(); app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
DownloadsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); DownloadsHandler::new(ESC_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert!(app.error.text.is_empty()); assert!(app.error.text.is_empty());
} }
} }
@@ -357,30 +247,30 @@ mod tests {
#[test] #[test]
fn test_update_downloads_key() { fn test_update_downloads_key() {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.update.key, DEFAULT_KEYBINDINGS.update.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::UpdateDownloadsPrompt.into() ActiveRadarrBlock::UpdateDownloadsPrompt.into()
); );
} }
#[test] #[test]
fn test_update_downloads_key_no_op_when_not_ready() { fn test_update_downloads_key_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
app app
@@ -389,23 +279,20 @@ mod tests {
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.update.key, DEFAULT_KEYBINDINGS.update.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
} }
#[test] #[test]
fn test_refresh_downloads_key() { fn test_refresh_downloads_key() {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
@@ -413,24 +300,21 @@ mod tests {
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.refresh.key, DEFAULT_KEYBINDINGS.refresh.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert!(app.should_refresh); assert!(app.should_refresh);
} }
#[test] #[test]
fn test_refresh_downloads_key_no_op_when_not_ready() { fn test_refresh_downloads_key_no_op_when_not_ready() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
app app
@@ -439,18 +323,15 @@ mod tests {
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.refresh.key, DEFAULT_KEYBINDINGS.refresh.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
) )
.handle(); .handle();
assert_eq!( assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert!(!app.should_refresh); assert!(!app.should_refresh);
} }
@@ -458,7 +339,7 @@ mod tests {
#[case( #[case(
ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
ActiveRadarrBlock::DeleteDownloadPrompt, ActiveRadarrBlock::DeleteDownloadPrompt,
RadarrEvent::DeleteDownload(None) RadarrEvent::DeleteDownload(1)
)] )]
#[case( #[case(
ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
@@ -470,20 +351,20 @@ mod tests {
#[case] prompt_block: ActiveRadarrBlock, #[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent, #[case] expected_action: RadarrEvent,
) { ) {
let mut app = App::default(); let mut app = App::test_default();
app app
.data .data
.radarr_data .radarr_data
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![download_record()]);
app.push_navigation_stack(base_route.into()); app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into()); app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with( DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.confirm.key, DEFAULT_KEYBINDINGS.confirm.key,
&mut app, &mut app,
&prompt_block, prompt_block,
&None, None,
) )
.handle(); .handle();
@@ -492,7 +373,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action, app.data.radarr_data.prompt_confirm_action,
Some(expected_action) Some(expected_action)
); );
assert_eq!(app.get_current_route(), &base_route.into()); assert_eq!(app.get_current_route(), base_route.into());
} }
} }
@@ -500,23 +381,59 @@ mod tests {
fn test_downloads_handler_accepts() { fn test_downloads_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| { ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_radarr_block) { if DOWNLOADS_BLOCKS.contains(&active_radarr_block) {
assert!(DownloadsHandler::accepts(&active_radarr_block)); assert!(DownloadsHandler::accepts(active_radarr_block));
} else { } else {
assert!(!DownloadsHandler::accepts(&active_radarr_block)); assert!(!DownloadsHandler::accepts(active_radarr_block));
} }
}) })
} }
#[rstest]
fn test_downloads_handler_ignore_alt_navigation(
#[values(true, false)] should_ignore_quit_key: bool,
) {
let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key);
}
#[test]
fn test_extract_download_id() {
let mut app = App::test_default();
app
.data
.radarr_data
.downloads
.set_items(vec![download_record()]);
let download_id = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::Downloads,
None,
)
.extract_download_id();
assert_eq!(download_id, 1);
}
#[test] #[test]
fn test_downloads_handler_not_ready_when_loading() { fn test_downloads_handler_not_ready_when_loading() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = true; app.is_loading = true;
let handler = DownloadsHandler::with( let handler = DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
); );
assert!(!handler.is_ready()); assert!(!handler.is_ready());
@@ -524,14 +441,14 @@ mod tests {
#[test] #[test]
fn test_downloads_handler_not_ready_when_downloads_is_empty() { fn test_downloads_handler_not_ready_when_downloads_is_empty() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = false; app.is_loading = false;
let handler = DownloadsHandler::with( let handler = DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
); );
assert!(!handler.is_ready()); assert!(!handler.is_ready());
@@ -539,7 +456,7 @@ mod tests {
#[test] #[test]
fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() { fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() {
let mut app = App::default(); let mut app = App::test_default();
app.is_loading = false; app.is_loading = false;
app app
@@ -547,11 +464,11 @@ mod tests {
.radarr_data .radarr_data
.downloads .downloads
.set_items(vec![DownloadRecord::default()]); .set_items(vec![DownloadRecord::default()]);
let handler = DownloadsHandler::with( let handler = DownloadsHandler::new(
&DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
&ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Downloads,
&None, None,
); );
assert!(handler.is_ready()); assert!(handler.is_ready());
+52 -39
View File
@@ -1,33 +1,60 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "downloads_handler_tests.rs"] #[path = "downloads_handler_tests.rs"]
mod downloads_handler_tests; mod downloads_handler_tests;
pub(super) struct DownloadsHandler<'a, 'b> { pub(super) struct DownloadsHandler<'a, 'b> {
key: &'a Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock, active_radarr_block: ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>, _context: Option<ActiveRadarrBlock>,
}
impl DownloadsHandler<'_, '_> {
handle_table_events!(
self,
downloads,
self.app.data.radarr_data.downloads,
DownloadRecord
);
fn extract_download_id(&self) -> i64 {
self.app.data.radarr_data.downloads.current_selection().id
}
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { fn handle(&mut self) {
DOWNLOADS_BLOCKS.contains(active_block) let downloads_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Downloads.into());
if !self.handle_downloads_table_events(downloads_table_handling_config) {
self.handle_key_event();
}
} }
fn with( fn accepts(active_block: ActiveRadarrBlock) -> bool {
key: &'a Key, DOWNLOADS_BLOCKS.contains(&active_block)
}
fn ignore_alt_navigation(&self) -> bool {
self.app.should_ignore_quit_key
}
fn new(
key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock, active_block: ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>, _context: Option<ActiveRadarrBlock>,
) -> DownloadsHandler<'a, 'b> { ) -> DownloadsHandler<'a, 'b> {
DownloadsHandler { DownloadsHandler {
key, key,
@@ -37,7 +64,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
} }
} }
fn get_key(&self) -> &Key { fn get_key(&self) -> Key {
self.key self.key
} }
@@ -45,32 +72,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
!self.app.is_loading && !self.app.data.radarr_data.downloads.is_empty() !self.app.is_loading && !self.app.data.radarr_data.downloads.is_empty()
} }
fn handle_scroll_up(&mut self) { fn handle_scroll_up(&mut self) {}
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
self.app.data.radarr_data.downloads.scroll_up()
}
}
fn handle_scroll_down(&mut self) { fn handle_scroll_down(&mut self) {}
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
self.app.data.radarr_data.downloads.scroll_down()
}
}
fn handle_home(&mut self) { fn handle_home(&mut self) {}
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
self.app.data.radarr_data.downloads.scroll_to_top()
}
}
fn handle_end(&mut self) { fn handle_end(&mut self) {}
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
self.app.data.radarr_data.downloads.scroll_to_bottom()
}
}
fn handle_delete(&mut self) { fn handle_delete(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Downloads { if self.active_radarr_block == ActiveRadarrBlock::Downloads {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::DeleteDownloadPrompt.into()) .push_navigation_stack(ActiveRadarrBlock::DeleteDownloadPrompt.into())
@@ -91,7 +102,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::DeleteDownloadPrompt => { ActiveRadarrBlock::DeleteDownloadPrompt => {
if self.app.data.radarr_data.prompt_confirm { if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None)); self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteDownload(self.extract_download_id()));
} }
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -121,26 +133,27 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key { ActiveRadarrBlock::Downloads => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into());
} }
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ => (), _ => (),
}, },
ActiveRadarrBlock::DeleteDownloadPrompt => { ActiveRadarrBlock::DeleteDownloadPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None)); self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteDownload(self.extract_download_id()));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
} }
ActiveRadarrBlock::UpdateDownloadsPrompt => { ActiveRadarrBlock::UpdateDownloadsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);

Some files were not shown because too many files have changed in this diff Show More