Compare commits

...

291 Commits

Author SHA1 Message Date
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
5a1b92547d fix(ci): Forgot to also pull in the most recent changes [skip ci] 2024-11-25 20:02:37 -07:00
github-actions[bot]
9e44713985 chore: Bump the version in Cargo.lock 2024-11-26 02:59:13 +00:00
github-actions[bot]
e93837fef7 bump: version 0.3.5 → 0.3.6 [skip ci] 2024-11-26 02:59:11 +00:00
6006c9d0e8 fix(ci): Ensure the Release Crate job fetches the most recent commit before publishing the crate [skip ci] 2024-11-25 19:58:34 -07:00
github-actions[bot]
c93543186a chore: Bump the version in Cargo.lock 2024-11-26 02:49:11 +00:00
github-actions[bot]
e1b74d7a36 bump: version 0.3.3 → 0.3.4 [skip ci] 2024-11-26 02:49:07 +00:00
6375bc3413 docs(README): Updated the README to not use GitHub markdown symbols and to directly use the unicode symbols so it can be displayed correctly in Crates.io [skip ci] 2024-11-25 19:47:57 -07:00
github-actions[bot]
9c99e0d2ef chore: Bump the version in Cargo.lock 2024-11-26 02:43:36 +00:00
github-actions[bot]
518ccaadc8 bump: version 0.3.2 → 0.3.3 [skip ci] 2024-11-26 02:43:34 +00:00
a766b395c1 fix(ci): Properly prefix version tags with 'v' [skip ci] 2024-11-25 19:41:11 -07:00
1746869b45 fix(ci): Bump the version in the Cargo.lock file and commit it as well when releasing [skip ci] 2024-11-25 19:39:36 -07:00
github-actions[bot]
533366b90b bump: version 0.3.1 → 0.3.2 [skip ci] 2024-11-26 02:33:19 +00:00
5c68df7246 fix(ci): Updated the Cargo.lock file [skip ci] 2024-11-25 19:28:40 -07:00
cc26d9a655 Merge remote-tracking branch 'origin/main' 2024-11-25 19:24:45 -07:00
02a1303557 fix(ci): Use a different GitHub action to release the crate to Crates.io [skip ci] 2024-11-25 19:24:24 -07:00
github-actions[bot]
817ffcf2b5 bump: version 0.3.0 → 0.3.1 [skip ci] 2024-11-26 02:20:25 +00:00
4a8b7eb837 fix(ci): Don't manually push the tags and let Commitizen do it [skip ci] 2024-11-25 19:19:59 -07:00
fcf81ad7d9 fix(ci): Fixed a typo in the version creation on GitHub [skip ci] 2024-11-25 19:17:35 -07:00
89a1b0dec7 ci: Undo selection for release steps [skip ci] 2024-11-25 19:12:38 -07:00
face51ae0c ci: Testing choices for release action [skip ci] 2024-11-25 19:11:32 -07:00
5ca6757fb0 ci: Set the default start_from job to be empty [skip ci] 2024-11-25 19:09:35 -07:00
4f4633c6b3 ci: Creating an enumerated selection of release jobs [skip ci] 2024-11-25 19:06:00 -07:00
17c70dfef2 ci: Adding fields to allow more dynamic executions of releases for testing purposes [skip ci] 2024-11-25 19:03:48 -07:00
92daaebf9d ci: specify the tag to use in release-plz [skip ci] 2024-11-25 18:59:41 -07:00
0c70191687 ci: Fixed release-plz to only release and not create the release MR 2024-11-25 18:57:12 -07:00
github-actions[bot]
e2a5f2f1b5 bump: version 0.2.2 → 0.3.0 [skip ci] 2024-11-26 01:52:23 +00:00
1893abd773 ci: Fix the GitHub release to use commitizen to fetch the new tag 2024-11-25 18:51:48 -07:00
41bb08418f ci: Fixed a typo in the GitHub release workflow 2024-11-25 18:49:12 -07:00
cf1794145c ci: Install conventional-changelog to generate the changelog for the GitHub release [skip ci] 2024-11-25 18:46:42 -07:00
7d9a25e599 ci: Populate the GitHub release with the changelog [skip ci] 2024-11-25 18:44:45 -07:00
github-actions[bot]
cd2175b5ad bump: version 0.2.2 → 0.3.0 [skip ci] 2024-11-26 01:38:08 +00:00
98ff5184e1 ci: Create a new GitHub tag as well when releasing [skip ci] 2024-11-25 18:37:30 -07:00
github-actions[bot]
da785355d6 bump: version 0.2.2 → 0.3.0 [skip ci] 2024-11-26 01:34:37 +00:00
4cabd5f0d0 ci: Update the release flow [skip ci] 2024-11-25 18:34:01 -07:00
a41f03c6b2 ci: Fetch all tags first before running Commitizen [skip ci] 2024-11-25 18:27:25 -07:00
cde86cf9fd fix: Reverted to old version to fix release [skip ci] 2024-11-25 18:24:00 -07:00
8d4981f10f ci: Release-plz fix [skip ci] 2024-11-25 18:21:13 -07:00
93d023c54a docs(README): Added Sonarr support to the README [skip ci] 2024-11-25 18:17:07 -07:00
bae450f23e ci: Fixed the commitizen commit to skip CI [skip ci] 2024-11-25 18:14:20 -07:00
42339e65d4 Merge remote-tracking branch 'origin/main' 2024-11-25 18:10:08 -07:00
0bb839c0a0 ci: Fixed the commitizen bump to not require a full re-run of test suites when releasing [skip ci] 2024-11-25 18:08:49 -07:00
github-actions[bot]
6381d7b742 bump: version 0.2.2 → 0.3.0 2024-11-26 01:04:58 +00:00
2efbdad4f0 ci: Changed ordering of git user commit [skip ci] 2024-11-25 18:04:18 -07:00
30bf0c22fa ci: Configure the git user for the Commitizen commit [skip ci] 2024-11-25 18:03:04 -07:00
169d6c7364 ci: Set up the release workflow to use a deploy key [skip ci] 2024-11-25 18:00:57 -07:00
e1fb5f570e ci: Created deploy key for releases to push directly to main branch [skip ci] 2024-11-25 17:56:29 -07:00
92362a5d8c ci: Fix the push to the release branch [skip ci] 2024-11-25 17:47:10 -07:00
7c88901185 ci: Update the bump step of the release workflow [skip ci] 2024-11-25 17:42:16 -07:00
5bce9b240e ci: Attempting to fix the release workflow 2024-11-25 17:35:55 -07:00
e675168798 ci: Configure the git user for Commitizen [skip ci] 2024-11-25 17:27:02 -07:00
8aa88d7343 ci: Tweaking the bump task to retrieve tags [skip ci] 2024-11-25 17:24:39 -07:00
Alex Clarke
9e6879c0f2 Merge pull request #18 from Dark-Alex-17/sonarr-cli-support
Sonarr CLI support
2024-11-25 17:04:03 -07:00
eb856e28d7 fix(minimal-versions): Addressed concerns with the minimal-versions CI checks 2024-11-25 16:58:15 -07:00
866b0c7537 fix(lint): Addressed linter complaints 2024-11-25 16:55:14 -07:00
3ef5c1911d docs(README): Updated the README to include new features that are available in the Sonarr CLI release 2024-11-25 16:52:08 -07:00
80787d1187 style(lint): Added allow dead code directives around certain structs that are causing linter complaints because these will either be used once sonarr UI work begins, or in future Servarr developments that will make life easier 2024-11-25 16:50:28 -07:00
ad0b3989ed fix(cli): Corrected some copy/paste typos 2024-11-25 16:43:29 -07:00
c7a0e33485 fix(network): Force sonarr to save edits to indexers 2024-11-25 16:28:21 -07:00
ee312a21eb feat(cli): Support for editing a sonarr series 2024-11-25 16:19:48 -07:00
3af22cceac feat(models): Added the ActiveSonarrBlocks for editing a series 2024-11-25 16:04:35 -07:00
c29e2ca9ae feat(network): Support for editing a series in Sonarr 2024-11-25 16:02:49 -07:00
06c9baf8df feat(models): Created the EditSeriesModal 2024-11-25 15:44:07 -07:00
97dc5054e9 feat(cli): Support for editing Sonarr indexers 2024-11-25 15:23:28 -07:00
d43862a3a7 feat(network): Support for editing a sonarr indexer 2024-11-25 15:17:13 -07:00
1dd4cd74c3 feat(cli): Support for deleting an episode file from disk 2024-11-25 14:46:33 -07:00
4f86cce497 feat(network): Support for deleting an episode file from disk in Sonarr 2024-11-25 14:43:53 -07:00
3968983002 feat(cli): Support for editing all indexer settings in Sonarr 2024-11-25 14:25:10 -07:00
4c7e8f0cf6 feat(models): Added the ActiveSonarrBlocks for editing all indexer settings 2024-11-25 14:01:47 -07:00
1e3141e4ee feat(network): Support for editing all sonarr indexer settings 2024-11-25 13:58:34 -07:00
45542cd3a9 feat(cli): Support for searching for new series to add to Sonarr 2024-11-24 14:58:30 -07:00
da3bb795b7 feat(network): Support for searching for new series 2024-11-24 14:54:41 -07:00
53a59cdb4c feat(cli): Support for adding a series to Sonarr 2024-11-24 14:38:23 -07:00
8125bd5ae0 feat(cli): Support for adding a series to Sonarr 2024-11-24 14:29:13 -07:00
5ba3f2b1ba feat(network): Support for adding a new series to Sonarr 2024-11-24 13:18:02 -07:00
c98828aec7 feat(cli): Support for fetching all sonarr language profiles 2024-11-24 11:36:14 -07:00
5ed278ec9c feat(network): Support for fetching all Sonarr language profiles 2024-11-24 11:34:09 -07:00
c8a2fea9cd feat(cli): Support for deleting a series from Sonarr 2024-11-23 12:47:22 -07:00
cac54c5447 feat(network): Support for deleting a series from Sonarr 2024-11-23 12:42:11 -07:00
374819b4f3 fix(network): Made the overview field nullable in the Sonarr series model 2024-11-23 12:23:33 -07:00
4d92c350de fix(network): Added filtering for full seasons specifically in the UI when performing a manual full season search and added a message to the CLI that noes to only try to download a full season if that release includes 'fullSeason: true' 2024-11-23 12:15:41 -07:00
3be9321df6 refactor(cli): the trigger-automatic-search commands now all have their own dedicated subcommand to keep things cleaner. Now they look like 'trigger-automatic-search episode/series/season' and their corresponding flags 2024-11-22 20:42:34 -07:00
746064c430 refactor(cli): Added an additional delegation test to ensure manual-search commands are delegated to the manual-search command handler 2024-11-22 20:26:34 -07:00
ffc00691cb refactor(cli): Moved the manual-season-search and manual-episode-search commands into their own dedicated handler so the commands can now be manual-search episode or manual-search season 2024-11-22 20:22:52 -07:00
1b5979c36c feat(cli): Support for downloading an episode release in Sonarr 2024-11-22 20:04:57 -07:00
5ed3372ae2 feat(cli): Support for downloading a season release in Sonarr 2024-11-22 20:00:41 -07:00
8002a5aa1e feat(cli): Support for downloading a Series release in Sonarr 2024-11-22 19:53:54 -07:00
896c50909a feat(network): Support for downloading releases from Sonarr 2024-11-22 19:33:29 -07:00
cea4632a22 feat(cli): Support for refreshing Sonarr downloads 2024-11-22 19:20:34 -07:00
7fdec15ba9 feat(network): Support for updating Sonarr downloads 2024-11-22 19:18:42 -07:00
eb06787bb2 feat(cli): Support for refreshing a specific series in Sonarr 2024-11-22 19:13:57 -07:00
c3577a0724 feat(network): Support for updating and scanning a series in Sonarr 2024-11-22 19:08:13 -07:00
8864e2c867 feat(cli): Support for refreshing all Sonarr series data 2024-11-22 18:59:55 -07:00
581975b941 feat(network): Support for updating all series in Sonarr 2024-11-22 18:44:51 -07:00
b8e4deb80f feat(cli): Support for triggering an automatic episode search in Sonarr 2024-11-22 18:35:38 -07:00
40bb22ef7c feat(cli): Support for triggering an automatic season search in Sonarr 2024-11-22 18:32:35 -07:00
74e9ea17ac feat(cli): Support for triggering an automatic series search in Sonarr 2024-11-22 18:22:33 -07:00
a11bce603d feat(network): Support for triggering an automatic episode search in Sonarr 2024-11-22 18:18:23 -07:00
c754275af3 feat(network): Support for triggering an automatic season search in Sonarr 2024-11-22 18:04:27 -07:00
3497a54c39 feat(network): Support for triggering an automatic series search in Sonarr 2024-11-22 17:53:09 -07:00
8807adea83 feat(cli): Support for testing all Sonarr indexers at once 2024-11-22 17:38:11 -07:00
6896fcc134 feat(network): Support for testing all Sonarr indexers at once 2024-11-22 17:35:36 -07:00
68830a8789 feat(cli): Support for testing an individual Sonarr indexer 2024-11-22 17:22:41 -07:00
2dce587ea8 feat(network): Added the ability to test an individual indexer in Sonarr 2024-11-22 17:18:47 -07:00
9403bdcbcb fix(network): Not all Sonarr tasks return the lastDuration field and was causing a crash 2024-11-22 17:10:06 -07:00
aa13735533 feat(cli): Support for starting a Sonarr task 2024-11-22 17:01:00 -07:00
33db3efacf feat(network): Support for starting a Sonarr task 2024-11-22 16:57:09 -07:00
8df74585bc feat(cli): Support for listing Sonarr updates 2024-11-22 16:48:09 -07:00
16ca8841a1 feat(network): Support for fetching Sonarr updates 2024-11-22 16:46:36 -07:00
22fbe025d9 feat(cli): Support for listing all Sonarr tasks 2024-11-22 16:37:21 -07:00
c54bd2bab0 feat(network): Support for fetching all Sonarr tasks 2024-11-22 16:35:39 -07:00
539ad75fe6 feat(cli): Support for marking a Sonarr history item as 'failed' 2024-11-22 16:21:43 -07:00
9476caa392 feat(network): Support for marking a Sonarr history item as failed 2024-11-22 16:13:35 -07:00
df3cf70682 feat(cli): Support for listing the available disk space for all provisioned root folders in both Radarr and Sonarr 2024-11-22 15:57:05 -07:00
a881d1f33a feat(network): Support for listing disk space on a Sonarr instance 2024-11-22 15:54:11 -07:00
d96316577a feat(cli): Support for listing all Sonarr tags 2024-11-22 15:35:30 -07:00
eefe6392df feat(cli): Support for adding a root folder to Sonarr 2024-11-22 15:25:33 -07:00
1cc95e2cd1 feat(cli): CLI support for adding a tag to Sonarr 2024-11-22 15:22:45 -07:00
c5328917de feat(network): Support for fetching and listing all Sonarr tags 2024-11-22 15:05:56 -07:00
208acafc73 feat(network): Support for deleting tags from Sonarr 2024-11-22 15:02:30 -07:00
57eced64c0 feat(network): Support for adding tags to Sonarr 2024-11-22 14:58:14 -07:00
ce701c1ab7 feat(network): Support for adding a root folder to Sonarr 2024-11-22 14:42:17 -07:00
b24e3bf9db feat(cli): Support for deleting a root folder from Sonarr 2024-11-21 16:46:00 -07:00
1227796e78 feat(network): Support for deleting a Sonarr root folder 2024-11-21 16:43:38 -07:00
bb1c08277e feat(cli): Support for fetching all Sonarr root folders 2024-11-21 16:39:20 -07:00
16538a3158 feat(network): Support for fetching all Sonarr root folders 2024-11-21 16:37:23 -07:00
f4c647342b feat(cli): Support for deleting a Sonarr indexer 2024-11-21 16:22:30 -07:00
72cb334b6a feat(network): Support for deleting an indexer from Sonarr 2024-11-21 16:19:53 -07:00
1a65a7f3e7 feat(cli): Support for deleting a download from Sonarr 2024-11-21 14:49:36 -07:00
6a0049eb8f feat(network): Support for deleting a download from Sonarr 2024-11-21 14:46:18 -07:00
d2e3750de6 style(README): Added a key and proper GitHub markdown emoji names instead of direct unicode symbols for the features tables 2024-11-21 13:04:21 -07:00
4cdad182ef docs(README): Updated the readme to use a table of checkmarks to indicate which features are present in the CLI and TUI 2024-11-21 12:44:57 -07:00
4ed1e99a15 Merge remote-tracking branch 'origin/main' into sonarr-cli-support 2024-11-21 12:23:19 -07:00
71870d9396 feat(cli): Support for fetching episode history events from Sonarr 2024-11-20 20:03:53 -07:00
fa4ec709c0 feat(network): Support for fetching episode history 2024-11-20 19:59:32 -07:00
d7d223400e style(linter): Removed unused imports that were missed on the last commit 2024-11-20 19:36:01 -07:00
34157ef32f feat(cli): Added a spinner to the CLI for long running commands like fetching releases 2024-11-20 19:33:40 -07:00
f5631376af fix(network): Fixed an issue with dynamic typing in responses from Sonarr for history items 2024-11-20 19:22:13 -07:00
df1eea22ab feat(cli): Support for fetching history for a given series ID 2024-11-20 14:58:54 -07:00
86d93377ac feat(network): Support for fetching Sonarr series history for a given series ID 2024-11-20 14:54:16 -07:00
5872a6ba72 feat(cli): Support for fetching all Sonarr history events 2024-11-20 14:13:20 -07:00
6da1ae93ef feat(network): Support to fetch all Sonarr history events 2024-11-20 14:06:44 -07:00
b8c60bf59a test(models): Fixed the test for the default series_history value in the sonarr_data 2024-11-20 13:25:24 -07:00
bd2d2875a5 feat(models): Added an additional History tab to the mocked tabs for viewing all Sonarr history at once 2024-11-20 13:24:44 -07:00
9d782af020 feat(models): Stubbed out the necessary ActiveSonarrBlocks for the UI mockup 2024-11-20 13:12:08 -07:00
a711c3d16c chore(modals): Removed the unnecessary season_details field from the SeasonDetailsModal 2024-11-19 17:24:41 -07:00
268cc13d27 feat(cli): Added support for manually searching for episode releases in Sonarr 2024-11-19 17:22:27 -07:00
a8328d3636 feat(network): Added support for fetching episode releases in Sonarr 2024-11-19 17:17:12 -07:00
d82a7f7674 feat(cli): Added CLI support for fetching series details in Sonarr 2024-11-19 17:01:48 -07:00
5e63c34a9f feat(network): Added support for fetching series details for a given series ID in Sonarr 2024-11-19 16:56:48 -07:00
540db5993b feat(cli): Added support for manually searching for season releases for Sonarr 2024-11-19 16:39:21 -07:00
16bf06426f fix(config): The CLI panics if the servarr you specify has no config defined 2024-11-19 16:29:25 -07:00
cc02832512 feat(network): Added support for fetching season releases for Sonarr 2024-11-19 15:59:35 -07:00
2876913f48 feat(cli): Added support for listing Sonarr queued events 2024-11-19 12:03:19 -07:00
6b64b5ecc4 feat(network): Added support for fetching Sonarr queued events 2024-11-19 12:01:07 -07:00
9ceb55a314 feat(cli): Added CLI support for fetching all indexer settings for Sonarr 2024-11-18 21:25:50 -07:00
7870bb4b5b feat(network): Added netwwork support for fetching all indexer settings for Sonarr 2024-11-18 21:19:20 -07:00
4fc2d3c94b feat(cli): Added Sonarr support for fetching host and security configs 2024-11-18 20:59:27 -07:00
a012945df2 feat(network): Added network support for fetching host and security configs from Sonarr 2024-11-18 20:49:07 -07:00
f094cf5ad3 feat(cli): Added CLI support for listing Sonarr indexers 2024-11-18 19:57:55 -07:00
d8979221c8 feat(network): Added the GetIndexers network call for Sonarr 2024-11-18 19:54:42 -07:00
aaa4e67f43 chore: published the managarr-tui-widget to crates.io so I changed the Cargo.toml to now pull from crates.io instead of directly using the repo for the widget 2024-11-18 18:47:49 -07:00
Alex Clarke
fff38704ab chore(readme): Updated the README to explicitly mention creating the config file first for docker installs 2024-11-16 18:04:47 -07:00
d5e6d64d0f fix: Imported a missing macro in the panic hook 2024-11-15 18:43:36 -07:00
003f319385 feat(cli): Added sonarr support for listing downloads, listing quality profiles, and fetching detailed information about an episode 2024-11-15 18:41:13 -07:00
e14b7072c6 feat(network): Added get quality profiles and get episode details events for Sonarr 2024-11-15 18:19:03 -07:00
1fe95d057b feat(cli): Sonarr CLI support for fetching all episodes for a given series 2024-11-15 15:14:34 -07:00
6dffc90e92 feat(sonarr_network): Added support for fetching episodes for a specified series to the network events 2024-11-15 14:57:19 -07:00
295cd56a1f feat(models): Added the Episode model to Sonarr models 2024-11-15 12:48:35 -07:00
214c89e8b5 feat(models): Created the StatefulTree struct for displaying seasons and episodes (and any other structured data) for the UI. 2024-11-15 12:08:35 -07:00
29047c3007 feat(sonarr): Added CLI support for listing Sonarr logs 2024-11-11 14:06:46 -07:00
a8f3bed402 feat(sonarr): Added the ability to fetch Sonarr logs 2024-11-11 14:00:07 -07:00
1ca9265a2a feat(sonarr): Added blocklist commands (List, Clear, Delete) 2024-11-11 13:45:32 -07:00
60d61b9e31 feat: Added initial Sonarr CLI support and the initial network handler setup for the TUI 2024-11-10 21:23:55 -07:00
b6f5b9d08c feat: Added a new command to the main managarr CLI: tail-logs, to enable users to tail the Managarr logs without needing to know where the log file itself is located 2024-11-09 16:18:43 -07:00
28f7bc6a4c ci: Updated the release workflow to use commitizen to bump the version, generate the changelog, and push those changes to the repo [skip ci] 2024-11-06 17:21:41 -07:00
5b42129f55 fix: Yet another typo in the release workflow [skip ci] 2024-11-06 17:15:17 -07:00
4f06b2b947 fix: Fixed typo in release name [skip ci] 2024-11-06 17:14:33 -07:00
0f98050a12 fix: Removed the release-patch workflow [skip ci] 2024-11-06 17:14:01 -07:00
14839642dc fix: Fixed a typo in the new release workflow name [skip ci] 2024-11-06 17:13:17 -07:00
eccc1a2df1 fix: Release-plz to perform the release and to use Commitizen for bumping and generating the CHANGELOG [skip ci] 2024-11-06 17:12:39 -07:00
9df929a8e3 fix: Updated the Commitizen config to also always commit the Cargo.lock when doing the version bump 2024-11-06 17:07:43 -07:00
1e008f9778 bump: version 0.2.1 → 0.2.2 2024-11-06 17:03:46 -07:00
Alex Clarke
fa811da5c2 Merge pull request #16 from Dark-Alex-17/commitizen-config
Added Commitizen to enforce commit styles
2024-11-06 16:53:40 -07:00
48ad17c6f1 style: Updated the contributing doc to also explain how to install commitizen 2024-11-06 16:48:27 -07:00
3cd15f34cd style: Test install for commitizen 2024-11-06 16:39:26 -07:00
53ca14e64d fix(handler): Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading 2024-11-06 16:17:23 -07:00
0d8803d35d fix(ui): Fixed a bug that would freeze all user input while background network requests were running 2024-11-06 15:50:47 -07:00
8c90221a81 perf(network): Improved performance and reactiveness of the UI by speeding up network requests and clearing the channel whenever a request is cancelled/the UI is routing 2024-11-06 14:52:48 -07:00
a708f71d57 fix(radarr_ui): Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly 2024-11-06 11:29:49 -07:00
2a13f74a2b Updated the release workflows to use the correct flags and commands 2024-11-05 18:33:17 -07:00
276 changed files with 57545 additions and 12312 deletions
+10
View File
@@ -0,0 +1,10 @@
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver"
version_provider = "cargo"
update_changelog_on_bump = true
major_version_zero = true
[tool.commitizen.hooks]
pre-commit = "git add Cargo.toml Cargo.lock"
+11 -2
View File
@@ -11,8 +11,6 @@ name: Check
env:
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:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
@@ -24,14 +22,18 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Run cargo fmt
run: cargo fmt -- --check
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
clippy:
name: ${{ matrix.toolchain }} / clippy
runs-on: ubuntu-latest
@@ -45,12 +47,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Run clippy action
uses: clechasseur/rs-clippy-check@v3
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
doc:
@@ -61,8 +66,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- name: Run cargo doc
run: cargo doc --no-deps --all-features
env:
@@ -73,9 +80,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install 1.82.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.82.0
- name: cargo +1.82.0 check
run: cargo check
-38
View File
@@ -1,38 +0,0 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create major release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
bump: major
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
-38
View File
@@ -1,38 +0,0 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create minor release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
bump: minor
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
-38
View File
@@ -1,38 +0,0 @@
# Adapted from https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-release-plz.yml
# Thanks to joshka for permission to use this template!
name: Create patch release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
jobs:
release-plz:
# see https://release-plz.ieni.dev/docs/github
# for more information
name: Release-plz
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
with:
bump: patch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+351
View File
@@ -0,0 +1,351 @@
name: Create release
permissions:
pull-requests: write
contents: write
on:
workflow_dispatch:
inputs:
bump_type:
description: "Specify the type of version bump"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
jobs:
build-release-artifacts:
name: build-release
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
- 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@v3
with:
name: artifacts
path: artifacts
publish-github-release:
name: publish-github-release
needs: [build-release-artifacts]
runs-on: ubuntu-latest
steps:
- name: Configure SSH for Git
run: |
mkdir -p ~/.ssh
echo "${{ secrets.RELEASE_BOT_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
- name: Checkout repository
uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v3
with:
name: artifacts
path: artifacts
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install Commitizen
run: |
python -m pip install --upgrade pip
pip install commitizen
npm install -g conventional-changelog-cli
- name: Configure Git user
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump version with Commitizen
run: |
cz bump --yes --increment ${{ github.event.inputs.bump_type }}
- name: Amend commit message to include '[skip ci]'
run: |
git commit --amend --no-edit -m "$(git log -1 --pretty=%B) [skip ci]"
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Update the Cargo.lock
run: |
cargo update
git add Cargo.lock
git commit -m "chore: Bump the version in Cargo.lock"
- name: Get the new version tag
id: version
run: |
NEW_TAG=$(cz version --project)
echo "New version: $NEW_TAG"
echo "version=$NEW_TAG" >> $GITHUB_ENV
echo "$NEW_TAG" > artifacts/release-version
- name: Get the previous version tag
id: prev_version
run: |
PREV_TAG=$(git describe --tags --abbrev=0 ${GITHUB_SHA}^)
echo "Previous tag: $PREV_TAG"
echo "prev_version=$PREV_TAG" >> $GITHUB_ENV
- name: Generate changelog for the version bump
id: changelog
run: |
changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }})
echo "$changelog" > changelog.md
echo "changelog_body=$(cat 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.version }}
name: "v${{ env.version }}"
body: ${{ env.changelog_body }}
draft: false
prerelease: false
- name: Push changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git push origin --follow-tags
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: artifacts
path: artifacts
publish-docker-image:
needs: [build-release-artifacts]
name: Publishing Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Get release artifacts
uses: actions/download-artifact@v3
with:
name: artifacts
path: artifacts
- name: Validate release environment variables
run: |
echo "Release version: ${{ env.version }}"
- 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:
tags: darkalex17/managarr:latest, darkalex17/managarr:x86_64, darkalex17/managarr:x86_64-${{ env.version }}
push: true
- name: Push to Docker Hub
uses: docker/build-push-action@v5
with:
file: Dockerfile.arm64
tags: darkalex17/managarr:arm64, darkalex17/managarr:arm64-${{ env.version }}
push: true
publish-crate:
needs: publish-github-release
name: Publish Crate
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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
with:
path: ~/.cargo/bin
key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }}
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- uses: katyo/publish-crates@v2
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+23
View File
@@ -34,16 +34,20 @@ jobs:
toolchain: [stable, beta]
steps:
- uses: actions/checkout@v4
- name: Install ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
# enable this ci template to run regardless of whether the lockfile is checked in or not
- name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo test --locked
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
# that this crate is compatible with the minimal version that this crate and its dependencies
@@ -71,18 +75,25 @@ jobs:
name: ubuntu / stable / minimal-versions
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install nightly for -Zdirect-minimal-versions
uses: dtolnay/rust-toolchain@nightly
- name: rustup default stable
run: rustup default stable
- 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:
# run cargo test on mac and windows
runs-on: ${{ matrix.os }}
@@ -100,15 +111,20 @@ jobs:
# if: runner.os == 'Windows'
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo test
run: cargo test --locked --all-features --all-targets
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
coverage:
# use llvm-cov to build and collect coverage and outputs in a format that
# is compatible with codecov.io
@@ -136,21 +152,28 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: cargo install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo llvm-cov
run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info
- name: Record Rust version
run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV"
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
with:
+8
View File
@@ -0,0 +1,8 @@
repos:
- hooks:
- id: commitizen
- id: commitizen-branch
stages:
- pre-push
repo: https://github.com/commitizen-tools/commitizen
rev: v3.30.0
+266 -1
View File
@@ -5,7 +5,272 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## v0.4.0 (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)
### Fix
- **ci**: Ensure the Release Crate job fetches the most recent commit before publishing the crate [skip ci]
## v0.3.4 (2024-11-26)
## v0.3.3 (2024-11-26)
### Fix
- **ci**: Properly prefix version tags with 'v' [skip ci]
- **ci**: Bump the version in the Cargo.lock file and commit it as well when releasing [skip ci]
## v0.3.2 (2024-11-26)
### Fix
- **ci**: Updated the Cargo.lock file [skip ci]
- **ci**: Use a different GitHub action to release the crate to Crates.io [skip ci]
- **ci**: Don't manually push the tags and let Commitizen do it [skip ci]
## v0.3.1 (2024-11-26)
### Fix
- **ci**: Don't manually push the tags and let Commitizen do it [skip ci]
- **ci**: Fixed a typo in the version creation on GitHub [skip ci]
## v0.3.0 (2024-11-26)
### Feat
- **cli**: Support for editing a sonarr series
- **models**: Added the ActiveSonarrBlocks for editing a series
- **network**: Support for editing a series in Sonarr
- **models**: Created the EditSeriesModal
- **cli**: Support for editing Sonarr indexers
- **network**: Support for editing a sonarr indexer
- **cli**: Support for deleting an episode file from disk
- **network**: Support for deleting an episode file from disk in Sonarr
- **cli**: Support for editing all indexer settings in Sonarr
- **models**: Added the ActiveSonarrBlocks for editing all indexer settings
- **network**: Support for editing all sonarr indexer settings
- **cli**: Support for searching for new series to add to Sonarr
- **network**: Support for searching for new series
- **cli**: Support for adding a series to Sonarr
- **cli**: Support for adding a series to Sonarr
- **network**: Support for adding a new series to Sonarr
- **cli**: Support for fetching all sonarr language profiles
- **network**: Support for fetching all Sonarr language profiles
- **cli**: Support for deleting a series from Sonarr
- **network**: Support for deleting a series from Sonarr
- **cli**: Support for downloading an episode release in Sonarr
- **cli**: Support for downloading a season release in Sonarr
- **cli**: Support for downloading a Series release in Sonarr
- **network**: Support for downloading releases from Sonarr
- **cli**: Support for refreshing Sonarr downloads
- **network**: Support for updating Sonarr downloads
- **cli**: Support for refreshing a specific series in Sonarr
- **network**: Support for updating and scanning a series in Sonarr
- **cli**: Support for refreshing all Sonarr series data
- **network**: Support for updating all series in Sonarr
- **cli**: Support for triggering an automatic episode search in Sonarr
- **cli**: Support for triggering an automatic season search in Sonarr
- **cli**: Support for triggering an automatic series search in Sonarr
- **network**: Support for triggering an automatic episode search in Sonarr
- **network**: Support for triggering an automatic season search in Sonarr
- **network**: Support for triggering an automatic series search in Sonarr
- **cli**: Support for testing all Sonarr indexers at once
- **network**: Support for testing all Sonarr indexers at once
- **cli**: Support for testing an individual Sonarr indexer
- **network**: Added the ability to test an individual indexer in Sonarr
- **cli**: Support for starting a Sonarr task
- **network**: Support for starting a Sonarr task
- **cli**: Support for listing Sonarr updates
- **network**: Support for fetching Sonarr updates
- **cli**: Support for listing all Sonarr tasks
- **network**: Support for fetching all Sonarr tasks
- **cli**: Support for marking a Sonarr history item as 'failed'
- **network**: Support for marking a Sonarr history item as failed
- **cli**: Support for listing the available disk space for all provisioned root folders in both Radarr and Sonarr
- **network**: Support for listing disk space on a Sonarr instance
- **cli**: Support for listing all Sonarr tags
- **cli**: Support for adding a root folder to Sonarr
- **cli**: CLI support for adding a tag to Sonarr
- **network**: Support for fetching and listing all Sonarr tags
- **network**: Support for deleting tags from Sonarr
- **network**: Support for adding tags to Sonarr
- **network**: Support for adding a root folder to Sonarr
- **cli**: Support for deleting a root folder from Sonarr
- **network**: Support for deleting a Sonarr root folder
- **cli**: Support for fetching all Sonarr root folders
- **network**: Support for fetching all Sonarr root folders
- **cli**: Support for deleting a Sonarr indexer
- **network**: Support for deleting an indexer from Sonarr
- **cli**: Support for deleting a download from Sonarr
- **network**: Support for deleting a download from Sonarr
- **cli**: Support for fetching episode history events from Sonarr
- **network**: Support for fetching episode history
- **cli**: Added a spinner to the CLI for long running commands like fetching releases
- **cli**: Support for fetching history for a given series ID
- **network**: Support for fetching Sonarr series history for a given series ID
- **cli**: Support for fetching all Sonarr history events
- **network**: Support to fetch all Sonarr history events
- **models**: Added an additional History tab to the mocked tabs for viewing all Sonarr history at once
- **models**: Stubbed out the necessary ActiveSonarrBlocks for the UI mockup
- **cli**: Added support for manually searching for episode releases in Sonarr
- **network**: Added support for fetching episode releases in Sonarr
- **cli**: Added CLI support for fetching series details in Sonarr
- **network**: Added support for fetching series details for a given series ID in Sonarr
- **cli**: Added support for manually searching for season releases for Sonarr
- **network**: Added support for fetching season releases for Sonarr
- **cli**: Added support for listing Sonarr queued events
- **network**: Added support for fetching Sonarr queued events
- **cli**: Added CLI support for fetching all indexer settings for Sonarr
- **network**: Added netwwork support for fetching all indexer settings for Sonarr
- **cli**: Added Sonarr support for fetching host and security configs
- **network**: Added network support for fetching host and security configs from Sonarr
- **cli**: Added CLI support for listing Sonarr indexers
- **network**: Added the GetIndexers network call for Sonarr
- **cli**: Added sonarr support for listing downloads, listing quality profiles, and fetching detailed information about an episode
- **network**: Added get quality profiles and get episode details events for Sonarr
- **cli**: Sonarr CLI support for fetching all episodes for a given series
- **sonarr_network**: Added support for fetching episodes for a specified series to the network events
- **models**: Added the Episode model to Sonarr models
- **models**: Created the StatefulTree struct for displaying seasons and episodes (and any other structured data) for the UI.
- **sonarr**: Added CLI support for listing Sonarr logs
- **sonarr**: Added the ability to fetch Sonarr logs
- **sonarr**: Added blocklist commands (List, Clear, Delete)
- Added initial Sonarr CLI support and the initial network handler setup for the TUI
- Added a new command to the main managarr CLI: tail-logs, to enable users to tail the Managarr logs without needing to know where the log file itself is located
### Fix
- Reverted to old version to fix release [skip ci]
- **minimal-versions**: Addressed concerns with the minimal-versions CI checks
- **lint**: Addressed linter complaints
- **cli**: Corrected some copy/paste typos
- **network**: Force sonarr to save edits to indexers
- **network**: Made the overview field nullable in the Sonarr series model
- **network**: Added filtering for full seasons specifically in the UI when performing a manual full season search and added a message to the CLI that noes to only try to download a full season if that release includes 'fullSeason: true'
- **network**: Not all Sonarr tasks return the lastDuration field and was causing a crash
- **network**: Fixed an issue with dynamic typing in responses from Sonarr for history items
- **config**: The CLI panics if the servarr you specify has no config defined
- Imported a missing macro in the panic hook
### Refactor
- **cli**: the trigger-automatic-search commands now all have their own dedicated subcommand to keep things cleaner. Now they look like 'trigger-automatic-search episode/series/season' and their corresponding flags
- **cli**: Added an additional delegation test to ensure manual-search commands are delegated to the manual-search command handler
- **cli**: Moved the manual-season-search and manual-episode-search commands into their own dedicated handler so the commands can now be manual-search episode or manual-search season
## v0.2.2 (2024-11-06)
### Fix
- **handler**: Fixed a bug in the movie details handler that would allow key events to be processed before the data was finished loading
- **ui**: Fixed a bug that would freeze all user input while background network requests were running
- **radarr_ui**: Fixed a race condition bug in the movie details UI that would panic if the user changes tabs too quickly
### Perf
- **network**: Improved performance and reactiveness of the UI by speeding up network requests and clearing the channel whenever a request is cancelled/the UI is routing
## v0.2.1 (2024-11-06)
## [0.2.1](https://github.com/Dark-Alex-17/managarr/compare/v0.2.0...v0.2.1) - 2024-11-06
+32
View File
@@ -1,6 +1,7 @@
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
## Rust
You'll need to have the stable Rust toolchain installed in order to develop Managarr.
The Rust toolchain (stable) can be installed via rustup using the following command:
@@ -11,6 +12,37 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
This will install `rustup`, `rustc` and `cargo`. For more information, refer to the [official Rust installation documentation](https://www.rust-lang.org/tools/install).
## Commitizen
[Commitizen](https://github.com/commitizen-tools/commitizen?tab=readme-ov-file) is a nifty tool that helps us write better commit messages. It ensures that our
commits have a consistent style and makes it easier to generate CHANGELOGS. Additionally,
Commitizen is used to run pre-commit checks to enforce style constraints.
To install `commitizen` and the `pre-commit` prerequisite, run the following command:
```shell
python3 -m pip install commitizen pre-commit
```
### Commitizen Quick Guide
To see an example commit to get an idea for the Commitizen style, run:
```shell
cz example
```
To see the allowed types of commits and their descriptions, run:
```shell
cz info
```
If you'd like to create a commit using Commitizen with an interactive prompt to help you get
comfortable with the style, use:
```shell
cz commit
```
## Setup workspace
1. Clone this repo
Generated
+857 -511
View File
File diff suppressed because it is too large Load Diff
+16 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "managarr"
version = "0.2.1"
version = "0.4.0"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
@@ -15,20 +15,20 @@ exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[dependencies]
anyhow = "1.0.68"
backtrace = "0.3.67"
backtrace = "0.3.74"
bimap = { version = "0.6.3", features = ["serde"] }
chrono = { version = "0.4.38", features = ["serde"] }
confy = { version = "0.6.0", default-features = false, features = [
"yaml_conf",
] }
crossterm = "0.27.0"
crossterm = "0.28.1"
derivative = "2.2.0"
human-panic = "1.1.3"
human-panic = "2.0.2"
indoc = "2.0.0"
log = "0.4.17"
log4rs = { version = "1.2.0", features = ["file_appender"] }
regex = "1.11.1"
reqwest = { version = "0.11.14", features = ["json"] }
reqwest = { version = "0.12.9", features = ["json"] }
serde_yaml = "0.9.16"
serde_json = "1.0.91"
serde = { version = "1.0.214", features = ["derive"] }
@@ -36,7 +36,10 @@ strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4"
tokio = { version = "1.36.0", features = ["full"] }
tokio-util = "0.7.8"
ratatui = { version = "0.28.0", features = ["all-widgets"] }
ratatui = { version = "0.29.0", features = [
"all-widgets",
"unstable-widget-ref",
] }
urlencoding = "2.1.2"
clap = { version = "4.5.20", features = ["derive", "cargo", "env"] }
clap_complete = "4.5.33"
@@ -45,13 +48,19 @@ ctrlc = "3.4.5"
colored = "2.1.0"
async-trait = "0.1.83"
dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0"
indicatif = "0.17.9"
derive_setters = "0.1.6"
deunicode = "1.6.0"
paste = "1.0.15"
openssl = { version = "0.10.68", features = ["vendored"] }
[dev-dependencies]
assert_cmd = "2.0.16"
mockall = "0.13.0"
mockito = "1.0.0"
pretty_assertions = "1.3.0"
rstest = "0.18.2"
rstest = "0.23.0"
[dev-dependencies.cargo-husky]
version = "1"
+3 -3
View File
@@ -1,4 +1,4 @@
FROM clux/muslrust:stable AS builder
FROM messense/rust-musl-cross:x86_64-musl AS builder
WORKDIR /usr/src
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
@@ -6,7 +6,7 @@ RUN USER=root cargo new --bin managarr-temp
WORKDIR /usr/src/managarr-temp
COPY Cargo.* .
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN cargo build --release
# remove src from empty project
RUN rm -r src
COPY src ./src
@@ -15,7 +15,7 @@ RUN rm ./target/x86_64-unknown-linux-musl/release/deps/managarr*
RUN --mount=type=cache,target=/volume/target \
--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 .
FROM debian:stable-slim
+27
View File
@@ -0,0 +1,27 @@
FROM messense/rust-musl-cross:armv7-musleabihf AS builder
WORKDIR /usr/src
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
RUN USER=root cargo new --bin managarr-temp
RUN apt update && apt install -y libssl-dev pkg-config
WORKDIR /usr/src/managarr-temp
COPY Cargo.* .
RUN cargo build --release
# remove src from empty project
RUN rm -r src
COPY src ./src
# remove previous deps
RUN rm ./target/armv7-unknown-linux-musleabihf/release/deps/managarr*
RUN --mount=type=cache,target=/volume/target \
--mount=type=cache,target=/root/.cargo/registry \
cargo build --release --bin managarr
RUN mv target/armv7-unknown-linux-musleabihf/release/managarr .
FROM debian:stable-slim
# Copy the compiled binary from the builder container
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
ENTRYPOINT [ "/usr/local/bin/managarr" ]
+137 -58
View File
@@ -8,15 +8,17 @@
![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)
![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)
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?
- [x] ![radarr_logo](logos/radarr.png) [Radarr](https://wiki.servarr.com/radarr)
- [ ] ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr)
- [x] ![sonarr_logo](logos/sonarr.png) [Sonarr](https://wiki.servarr.com/en/sonarr)
- [ ] ![readarr_logo](logos/readarr.png) [Readarr](https://wiki.servarr.com/en/readarr)
- [ ] ![lidarr_logo](logos/lidarr.png) [Lidarr](https://wiki.servarr.com/en/lidarr)
- [ ] ![prowlarr_logo](logos/prowlarr.png) [Prowlarr](https://wiki.servarr.com/en/prowlarr)
@@ -46,34 +48,94 @@ cargo install --locked managarr
### Docker
Run Managarr as a docker container by mounting your `config.yml` file to `/root/.config/managarr/config.yml`. For example:
```shell
docker run --rm -it -v ~/.config/managarr:/root/.config/managarr darkalex17/managarr
docker run --rm -it -v /home/aclarke/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr:latest
```
For ARM64 users, you can use the `arm64` tag:
```shell
docker run --rm -it -v /home/aclarke/.config/managarr/config.yml:/root/.config/managarr/config.yml darkalex17/managarr:arm64
```
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.
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
### 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
Key:
| Symbol | Status |
|--------|-----------|
| ✅ | Supported |
| ❌ | Missing |
| 🕒 | Planned |
| 🚫 | Won't Add |
### Radarr
- [x] View your library, downloads, collections, and blocklist
- [x] View details of a specific movie including description, history, downloaded file info, or the credits
- [x] View details of any collection and the movies in them
- [x] View your host and security configs from the CLI to programmatically fetch the API token, among other settings
- [x] Search your library or collections
- [x] Add movies to your library
- [x] Delete movies, downloads, and indexers
- [x] Trigger automatic searches for movies
- [x] Trigger refresh and disk scan for movies, downloads, and collections
- [x] Manually search for movies
- [x] Edit your movies, collections, and indexers
- [x] Manage your tags
- [x] Manage your root folders
- [x] Manage your blocklist
- [x] View and browse logs, tasks, events queues, and updates
- [x] Manually trigger scheduled tasks
| TUI | CLI | Feature |
|-----|-----|----------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, collections, and blocklist |
| ✅ | ✅ | View details of a specific movie including description, history, downloaded file info, or the credits |
| ✅ | ✅ | View details of any collection and the movies in them |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library or collections |
| ✅ | ✅ | Add movies to your library |
| ✅ | ✅ | Delete movies, downloads, and indexers |
| ✅ | ✅ | Trigger automatic searches for movies |
| ✅ | ✅ | Trigger refresh and disk scan for movies, downloads, and collections |
| ✅ | ✅ | Manually search for movies |
| ✅ | ✅ | Edit your movies, collections, and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Sonarr
- [ ] Support for Sonarr
| TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | 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 |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Readarr
@@ -105,13 +167,13 @@ Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your
All management features available in the TUI are also available in the CLI. However, the CLI is
equipped with additional features to allow for more advanced usage and automation.
The CLI can be helpful for automating tasks or for use in scripts. For example, you can use the CLI to trigger a search for a movie, or to add a movie to your library.
The CLI can be helpful for automating tasks or for use in scripts. For example, you can use the CLI to trigger a search for a movie, or to add a movie to your Radarr library.
To see all available commands, simply run `managarr --help`:
```shell
$ managarr --help
managarr 0.2.1
managarr 0.4.0
Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs
@@ -120,43 +182,50 @@ Usage: managarr [OPTIONS] [COMMAND]
Commands:
radarr Commands for manging your Radarr instance
sonarr Commands for manging your Sonarr instance
completions Generate shell completions for the Managarr CLI
tail-logs Tail Managarr logs
help Print this message or the help of the given subcommand(s)
Options:
--config <CONFIG> The Managarr configuration file to use
-h, --help Print help
-V, --version Print version
--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=]
-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 Radarr, 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:
```shell
$ managarr radarr --help
Commands for manging your Radarr instance
$ managarr sonarr --help
Commands for manging your Sonarr instance
Usage: managarr radarr [OPTIONS] <COMMAND>
Usage: managarr sonarr [OPTIONS] <COMMAND>
Commands:
add Commands to add or create new resources within your Radarr instance
delete Commands to delete resources from your Radarr instance
edit Commands to edit resources in your Radarr instance
get Commands to fetch details of the resources in your Radarr instance
list Commands to list attributes from your Radarr instance
refresh Commands to refresh the data in your Radarr instance
clear-blocklist Clear the blocklist
download-release Manually download the given release for the specified movie ID
manual-search Trigger a manual search of releases for the movie with the given ID
search-new-movie Search for a new film to add to Radarr
start-task Start the specified Radarr task
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 indexers
trigger-automatic-search Trigger an automatic search for the movie with the specified ID
help Print this message or the help of the given subcommand(s)
add Commands to add or create new resources within your Sonarr instance
delete Commands to delete resources from your Sonarr instance
edit Commands to edit resources in your Sonarr instance
get Commands to fetch details of the resources in your Sonarr instance
download Commands to download releases in your Sonarr instance
list Commands to list attributes from your Sonarr instance
refresh Commands to refresh the data in your Sonarr instance
manual-search Commands to manually search for releases
trigger-automatic-search Commands to trigger automatic searches for releases of different resources in your Sonarr instance
clear-blocklist Clear the blocklist
mark-history-item-as-failed Mark the Sonarr history item with the given ID as 'failed'
search-new-series Search for a new series to add to Sonarr
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-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)
Options:
--config <CONFIG> The Managarr configuration file to use
-h, --help Print help
--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=]
-h, --help Print help
```
**Pro Tip:** The CLI is even more powerful and useful when used in conjunction with the `jq` CLI tool. This allows you to parse the JSON response from the Managarr CLI and use it in your scripts; For example, to extract the `movieId` of the movie "Ad Astra", you would run:
@@ -170,7 +239,7 @@ $ managarr radarr list movies | jq '.[] | select(.title == "Ad Astra") | .id'
Managarr assumes reasonable defaults to connect to each service (i.e. Radarr is on localhost:7878),
but all servers will require you to input the API token.
The configuration file is located somewhere different for each OS
The configuration file is located somewhere different for each OS.
### Linux
```
@@ -236,9 +305,10 @@ tautulli:
## Environment Variables
Managarr supports using environment variables on startup so you don't have to always specify certain flags:
| Variable | Description | Equivalent Flag |
| --------------------------------------- | -------------------------------- | -------------------------------- |
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
| Variable | Description | Equivalent Flag |
|-----------------------------------------|--------------------------------------------------------------------------------|----------------------------------|
| `MANAGARR_CONFIG_FILE` | Set the path to the config file | `--config` |
| `MANAGARR_DISABLE_SPINNER` | Disable the CLI spinner (this can be useful when scripting and parsing output) | `--disable-spinner` |
## Track My Progress for the Beta release (With Sonarr Support!)
Progress for the beta release can be followed on my [Wekan Board](https://wekan.alexjclarke.com/b/dHoGjBb44MHM9HSv4/managarr)
@@ -246,13 +316,22 @@ with all items tagged `Beta`.
## Screenshots
![library](screenshots/library.png)
![manual_search](screenshots/manual_search.png)
![logs](screenshots/logs.png)
![new_movie_search](screenshots/new_movie_search.png)
![add_new_movie](screenshots/add_new_movie.png)
![collection_details](screenshots/collection_details.png)
![indexers](screenshots/indexers.png)
### Radarr
![radarr_library](screenshots/radarr/radarr_library.png)
![manual_search](screenshots/radarr/manual_search.png)
![new_movie_search](screenshots/radarr/new_movie_search.png)
![add_new_movie](screenshots/radarr/add_new_movie.png)
![collection_details](screenshots/radarr/collection_details.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
* [ratatui](https://github.com/tui-rs-revival/ratatui)
@@ -264,7 +343,7 @@ with all items tagged `Beta`.
## Servarr Requirements
* [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/)
* [Lidarr v1](https://lidarr.audio/docs/api/)
* [Whisparr >= v3](https://whisparr.com/docs/api/)
+6
View File
@@ -0,0 +1,6 @@
{
"name": "managarr",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+1
View File
@@ -0,0 +1 @@
{}

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

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 382 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: 211 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: 203 KiB

+89 -32
View File
@@ -5,9 +5,10 @@ mod tests {
use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::app::{App, Data, RadarrConfig, DEFAULT_ROUTE};
use crate::app::{App, AppConfig, Data, ServarrConfig, DEFAULT_ROUTE};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::{HorizontallyScrollableText, Route, TabRoute};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, TabRoute};
use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent;
@@ -18,6 +19,7 @@ mod tests {
assert_eq!(app.navigation_stack, vec![DEFAULT_ROUTE]);
assert!(app.network_tx.is_none());
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!(
@@ -34,7 +36,7 @@ mod tests {
},
TabRoute {
title: "Sonarr",
route: Route::Sonarr,
route: ActiveSonarrBlock::Series.into(),
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
contextual_help: None,
},
@@ -47,20 +49,18 @@ mod tests {
assert!(!app.is_routing);
assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key);
assert!(!app.cli_mode);
}
#[test]
fn test_navigation_stack_methods() {
let mut app = App::default();
assert_eq!(app.get_current_route(), &DEFAULT_ROUTE);
assert_eq!(app.get_current_route(), DEFAULT_ROUTE);
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert!(app.is_routing);
app.is_routing = false;
@@ -68,26 +68,30 @@ mod tests {
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
assert!(app.is_routing);
app.is_routing = false;
app.pop_navigation_stack();
assert_eq!(app.get_current_route(), &DEFAULT_ROUTE);
assert_eq!(app.get_current_route(), DEFAULT_ROUTE);
assert!(app.is_routing);
app.is_routing = false;
app.pop_navigation_stack();
assert_eq!(app.get_current_route(), &DEFAULT_ROUTE);
assert_eq!(app.get_current_route(), DEFAULT_ROUTE);
assert!(app.is_routing);
}
#[test]
fn test_reset_cancellation_token() {
let mut app = App::default();
let mut app = App {
is_loading: true,
should_refresh: false,
..App::default()
};
app.cancellation_token.cancel();
assert!(app.cancellation_token.is_cancelled());
@@ -96,6 +100,8 @@ mod tests {
assert!(!app.cancellation_token.is_cancelled());
assert!(!new_token.is_cancelled());
assert!(!app.is_loading);
assert!(app.should_refresh);
}
#[test]
@@ -112,15 +118,23 @@ mod tests {
#[test]
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 {
tick_count: 2,
error: "Test error".to_owned().into(),
data: Data {
radarr_data: RadarrData {
version: "test".to_owned(),
..RadarrData::default()
},
},
is_first_render: false,
data,
..App::default()
};
@@ -128,7 +142,9 @@ mod tests {
assert_eq!(app.tick_count, 0);
assert_eq!(app.error, HorizontallyScrollableText::default());
assert!(app.is_first_render);
assert!(app.data.radarr_data.version.is_empty());
assert!(app.data.sonarr_data.version.is_empty());
}
#[test]
@@ -146,7 +162,7 @@ mod tests {
}
#[tokio::test]
async fn test_on_tick_first_render() {
async fn test_dispatch_network_event() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App {
@@ -157,7 +173,32 @@ mod tests {
assert_eq!(app.tick_count, 0);
app.on_tick(true).await;
app
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into()
);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_on_tick_first_render() {
let (sync_network_tx, mut sync_network_rx) = mpsc::channel::<NetworkEvent>(500);
let mut app = App {
tick_until_poll: 2,
network_tx: Some(sync_network_tx),
is_first_render: true,
..App::default()
};
assert_eq!(app.tick_count, 0);
app.on_tick().await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
@@ -172,7 +213,11 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -180,11 +225,15 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovies.into()
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovies.into()
);
assert!(!app.is_routing);
assert!(!app.should_refresh);
@@ -200,7 +249,7 @@ mod tests {
..App::default()
};
app.on_tick(false).await;
app.on_tick().await;
assert!(!app.is_routing);
}
@@ -213,18 +262,26 @@ mod tests {
..App::default()
};
app.on_tick(false).await;
app.on_tick().await;
assert!(!app.should_refresh);
}
#[test]
fn test_radarr_config_default() {
let radarr_config = RadarrConfig::default();
fn test_app_config_default() {
let app_config = AppConfig::default();
assert_eq!(radarr_config.host, Some("localhost".to_string()));
assert_eq!(radarr_config.port, Some(7878));
assert_eq!(radarr_config.uri, None);
assert!(radarr_config.api_token.is_empty());
assert_eq!(radarr_config.ssl_cert_path, None);
assert!(app_config.radarr.is_none());
assert!(app_config.sonarr.is_none());
}
#[test]
fn test_servarr_config_default() {
let servarr_config = ServarrConfig::default();
assert_eq!(servarr_config.host, Some("localhost".to_string()));
assert_eq!(servarr_config.port, None);
assert_eq!(servarr_config.uri, None);
assert!(servarr_config.api_token.is_empty());
assert_eq!(servarr_config.ssl_cert_path, None);
}
}
+69 -2
View File
@@ -14,10 +14,77 @@ pub fn build_context_clue_string(context_clues: &[(KeyBinding, &str)]) -> String
.join(" | ")
}
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.tab, "change servarr"),
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [
(
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),
];
pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] =
[(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 {
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};
#[test]
@@ -24,8 +28,13 @@ mod test {
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tab);
assert_str_eq!(*description, "change servarr");
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_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();
@@ -44,4 +53,160 @@ mod test {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
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);
}
}
+21 -6
View File
@@ -15,8 +15,11 @@ generate_keybindings! {
left,
right,
backspace,
next_servarr,
previous_servarr,
clear,
search,
auto_search,
settings,
filter,
sort,
@@ -25,12 +28,12 @@ generate_keybindings! {
tasks,
test,
test_all,
toggle_monitoring,
refresh,
update,
events,
home,
end,
tab,
delete,
submit,
confirm,
@@ -69,16 +72,28 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Backspace,
desc: "backspace",
},
next_servarr: KeyBinding {
key: Key::Tab,
desc: "next servarr",
},
previous_servarr: KeyBinding {
key: Key::BackTab,
desc: "previous servarr",
},
clear: KeyBinding {
key: Key::Char('c'),
desc: "clear",
},
auto_search: KeyBinding {
key: Key::Char('S'),
desc: "auto search",
},
search: KeyBinding {
key: Key::Char('s'),
desc: "search",
},
settings: KeyBinding {
key: Key::Char('s'),
key: Key::Char('S'),
desc: "settings",
},
filter: KeyBinding {
@@ -113,6 +128,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Char('T'),
desc: "test all",
},
toggle_monitoring: KeyBinding {
key: Key::Char('m'),
desc: "toggle monitoring",
},
refresh: KeyBinding {
key: Key::Ctrl('r'),
desc: "refresh",
@@ -129,10 +148,6 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::End,
desc: "end",
},
tab: KeyBinding {
key: Key::Tab,
desc: "tab",
},
delete: KeyBinding {
key: Key::Delete,
desc: "delete",
+5 -2
View File
@@ -13,9 +13,12 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")]
#[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.auto_search, Key::Char('S'), "auto 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.sort, Key::Char('o'), "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")]
@@ -24,11 +27,11 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")]
#[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.update, Key::Char('u'), "update")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")]
#[case(DEFAULT_KEYBINDINGS.tab, Key::Tab, "tab")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
+64 -28
View File
@@ -1,6 +1,6 @@
use std::process;
use anyhow::anyhow;
use anyhow::{anyhow, Error};
use colored::Colorize;
use log::{debug, error};
use serde::{Deserialize, Serialize};
@@ -8,7 +8,9 @@ use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::cli::Command;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
use crate::network::NetworkEvent;
@@ -19,6 +21,7 @@ pub mod context_clues;
pub mod key_binding;
mod key_binding_tests;
pub mod radarr;
pub mod sonarr;
const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies, None);
@@ -26,6 +29,7 @@ pub struct App<'a> {
navigation_stack: Vec<Route>,
network_tx: Option<Sender<NetworkEvent>>,
cancellation_token: CancellationToken,
pub is_first_render: bool,
pub server_tabs: TabState,
pub error: HorizontallyScrollableText,
pub tick_until_poll: u64,
@@ -35,6 +39,7 @@ pub struct App<'a> {
pub is_loading: bool,
pub should_refresh: bool,
pub should_ignore_quit_key: bool,
pub cli_mode: bool,
pub config: AppConfig,
pub data: Data<'a>,
}
@@ -56,7 +61,10 @@ impl<'a> App<'a> {
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
debug!("Dispatching network event: {action:?}");
self.is_loading = true;
if !self.should_refresh {
self.is_loading = true;
}
if let Some(network_tx) = &self.network_tx {
if let Err(e) = network_tx.send(action).await {
self.is_loading = false;
@@ -70,26 +78,26 @@ impl<'a> App<'a> {
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)]
pub fn reset(&mut self) {
self.reset_tick_count();
self.error = HorizontallyScrollableText::default();
self.is_first_render = true;
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() {
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 let Route::Radarr(active_radarr_block, _) = self.get_current_route() {
self
.radarr_on_tick(*active_radarr_block, is_first_render)
.await;
match self.get_current_route() {
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
_ => (),
}
self.is_routing = false;
@@ -113,6 +121,8 @@ impl<'a> App<'a> {
pub fn reset_cancellation_token(&mut self) -> CancellationToken {
self.cancellation_token = CancellationToken::new();
self.should_refresh = true;
self.is_loading = false;
self.cancellation_token.clone()
}
@@ -122,8 +132,8 @@ impl<'a> App<'a> {
self.push_navigation_stack(route);
}
pub fn get_current_route(&self) -> &Route {
self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE)
pub fn get_current_route(&self) -> Route {
*self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE)
}
}
@@ -134,6 +144,7 @@ impl<'a> Default for App<'a> {
network_tx: None,
cancellation_token: CancellationToken::new(),
error: HorizontallyScrollableText::default(),
is_first_render: true,
server_tabs: TabState::new(vec![
TabRoute {
title: "Radarr",
@@ -146,7 +157,7 @@ impl<'a> Default for App<'a> {
},
TabRoute {
title: "Sonarr",
route: Route::Sonarr,
route: ActiveSonarrBlock::Series.into(),
help: format!("{} ", build_context_clue_string(&SERVARR_CONTEXT_CLUES)),
contextual_help: None,
},
@@ -158,6 +169,7 @@ impl<'a> Default for App<'a> {
is_routing: false,
should_refresh: false,
should_ignore_quit_key: false,
cli_mode: false,
config: AppConfig::default(),
data: Data::default(),
}
@@ -167,25 +179,49 @@ impl<'a> Default for App<'a> {
#[derive(Default)]
pub struct Data<'a> {
pub radarr_data: RadarrData<'a>,
pub sonarr_data: SonarrData<'a>,
}
pub trait ServarrConfig {
fn validate(&self);
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig {
pub radarr: RadarrConfig,
pub radarr: Option<ServarrConfig>,
pub sonarr: Option<ServarrConfig>,
}
impl ServarrConfig for AppConfig {
fn validate(&self) {
self.radarr.validate();
impl AppConfig {
pub fn validate(&self) {
if let Some(radarr_config) = &self.radarr {
radarr_config.validate();
}
if let Some(sonarr_config) = &self.sonarr {
sonarr_config.validate();
}
}
pub fn verify_config_present_for_cli(&self, command: &Command) {
let msg = |servarr: &str| {
log_and_print_error(format!(
"{} configuration missing; Unable to run any {} commands.",
servarr, servarr
))
};
match command {
Command::Radarr(_) if self.radarr.is_none() => {
msg("Radarr");
process::exit(1);
}
Command::Sonarr(_) if self.sonarr.is_none() => {
msg("Sonarr");
process::exit(1);
}
_ => (),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RadarrConfig {
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServarrConfig {
pub host: Option<String>,
pub port: Option<u16>,
pub uri: Option<String>,
@@ -193,20 +229,20 @@ pub struct RadarrConfig {
pub ssl_cert_path: Option<String>,
}
impl ServarrConfig for RadarrConfig {
impl ServarrConfig {
fn validate(&self) {
if self.host.is_none() && self.uri.is_none() {
log_and_print_error("'host' or 'uri' is required for Radarr configuration".to_owned());
log_and_print_error("'host' or 'uri' is required for configuration".to_owned());
process::exit(1);
}
}
}
impl Default for RadarrConfig {
impl Default for ServarrConfig {
fn default() -> Self {
RadarrConfig {
ServarrConfig {
host: Some("localhost".to_string()),
port: Some(7878),
port: None,
uri: None,
api_token: "".to_string(),
ssl_cert_path: None,
+40 -29
View File
@@ -17,11 +17,23 @@ impl<'a> App<'a> {
.await;
}
ActiveRadarrBlock::Collections => {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetCollections.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetMovies.into())
.await;
}
ActiveRadarrBlock::CollectionDetails => {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self.is_loading = true;
self.populate_movie_collection_table().await;
self.is_loading = false;
@@ -37,6 +49,12 @@ impl<'a> App<'a> {
.await;
}
ActiveRadarrBlock::Movies => {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetMovies.into())
.await;
@@ -45,6 +63,9 @@ impl<'a> App<'a> {
.await;
}
ActiveRadarrBlock::Indexers => {
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetIndexers.into())
.await;
@@ -119,11 +140,11 @@ impl<'a> App<'a> {
_ => (),
}
self.check_for_prompt_action().await;
self.check_for_radarr_prompt_action().await;
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 {
self.data.radarr_data.prompt_confirm = false;
if let Some(radarr_event) = &self.data.radarr_data.prompt_confirm_action {
@@ -136,49 +157,33 @@ impl<'a> App<'a> {
}
}
pub(super) async fn radarr_on_tick(
&mut self,
active_radarr_block: ActiveRadarrBlock,
is_first_render: bool,
) {
if is_first_render {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetOverview.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
pub(super) async fn radarr_on_tick(&mut self, active_radarr_block: ActiveRadarrBlock) {
if self.is_first_render {
self.refresh_radarr_metadata().await;
self.dispatch_by_radarr_block(&active_radarr_block).await;
self.is_first_render = false;
return;
}
if self.should_refresh {
self.dispatch_by_radarr_block(&active_radarr_block).await;
self.refresh_radarr_metadata().await;
}
if self.is_routing {
if self.is_loading && !self.should_refresh {
if !self.should_refresh {
self.cancellation_token.cancel();
} 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 {
self.refresh_metadata().await;
self.refresh_radarr_metadata().await;
}
}
async fn refresh_metadata(&mut self) {
async fn refresh_radarr_metadata(&mut self) {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
@@ -191,6 +196,12 @@ impl<'a> App<'a> {
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetDiskSpace.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetStatus.into())
.await;
}
async fn populate_movie_collection_table(&mut self) {
+10 -62
View File
@@ -35,60 +35,6 @@ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [
(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] = [
(
DEFAULT_KEYBINDINGS.refresh,
@@ -96,7 +42,10 @@ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.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),
];
@@ -108,7 +57,10 @@ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.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),
];
@@ -120,17 +72,13 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(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] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(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.edit, "edit collection"),
(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::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
};
#[test]
@@ -113,141 +112,6 @@ mod tests {
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]
fn test_movie_details_context_clues() {
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();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, "auto search");
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
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();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, "auto search");
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
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);
}
#[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_system_tasks_context_clues() {
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();
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_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(collection_details_context_clues_iter.next(), None);
+68 -71
View File
@@ -6,7 +6,7 @@ mod tests {
use crate::app::radarr::ActiveRadarrBlock;
use crate::app::App;
use crate::models::radarr_models::{Collection, CollectionMovie, Credit, Release};
use crate::models::radarr_models::{Collection, CollectionMovie, Credit, RadarrRelease};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::network::radarr_network::RadarrEvent;
@@ -38,17 +38,25 @@ mod tests {
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetCollections.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovies.into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_collection_details_block() {
let (mut app, _) = construct_app_unit();
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.radarr_data.collections.set_items(vec![Collection {
movies: Some(vec![CollectionMovie::default()]),
@@ -60,6 +68,14 @@ mod tests {
.await;
assert!(!app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert!(!app.data.radarr_data.collection_movies.items.is_empty());
assert_eq!(app.tick_count, 0);
assert!(!app.data.radarr_data.prompt_confirm);
@@ -80,6 +96,14 @@ mod tests {
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::AddMovie(None).into()
@@ -132,6 +156,14 @@ mod tests {
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetMovies.into()
@@ -153,6 +185,10 @@ mod tests {
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetIndexers.into()
@@ -430,7 +466,7 @@ mod tests {
let mut movie_details_modal = MovieDetailsModal::default();
movie_details_modal
.movie_releases
.set_items(vec![Release::default()]);
.set_items(vec![RadarrRelease::default()]);
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
app
@@ -459,22 +495,22 @@ mod tests {
}
#[tokio::test]
async fn test_check_for_prompt_action_no_prompt_confirm() {
async fn test_check_for_radarr_prompt_action_no_prompt_confirm() {
let mut app = App::default();
app.data.radarr_data.prompt_confirm = false;
app.check_for_prompt_action().await;
app.check_for_radarr_prompt_action().await;
assert!(!app.data.radarr_data.prompt_confirm);
assert!(!app.should_refresh);
}
#[tokio::test]
async fn test_check_for_prompt_action() {
async fn test_check_for_radarr_prompt_action() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::GetStatus);
app.check_for_prompt_action().await;
app.check_for_radarr_prompt_action().await;
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(
@@ -490,7 +526,7 @@ mod tests {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_routing = true;
app.refresh_metadata().await;
app.refresh_radarr_metadata().await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -508,14 +544,23 @@ mod tests {
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into()
);
assert!(app.is_loading);
}
#[tokio::test]
async fn test_radarr_on_tick_first_render() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_first_render = true;
app.radarr_on_tick(ActiveRadarrBlock::Downloads, true).await;
app.radarr_on_tick(ActiveRadarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -531,86 +576,44 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
RadarrEvent::GetDownloads.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetStatus.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
assert!(!app.data.radarr_data.prompt_confirm);
assert!(!app.is_first_render);
}
#[tokio::test]
async fn test_radarr_on_tick_routing() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.is_routing = true;
app.should_refresh = true;
app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
app.radarr_on_tick(ActiveRadarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.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!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
assert!(!app.data.radarr_data.prompt_confirm);
}
#[tokio::test]
async fn test_radarr_on_tick_routing_while_long_request_is_running_should_cancel_request() {
let (mut app, mut sync_network_rx) = construct_app_unit();
let (mut app, _) = construct_app_unit();
app.is_routing = true;
app.is_loading = true;
app.should_refresh = false;
app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
app.radarr_on_tick(ActiveRadarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.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!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
assert!(!app.data.radarr_data.prompt_confirm);
assert!(app.cancellation_token.is_cancelled());
}
@@ -619,15 +622,12 @@ mod tests {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.should_refresh = true;
app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
app.radarr_on_tick(ActiveRadarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -639,9 +639,7 @@ mod tests {
app.is_routing = true;
app.should_refresh = true;
app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
app.radarr_on_tick(ActiveRadarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -659,9 +657,7 @@ mod tests {
app.tick_count = 2;
app.tick_until_poll = 2;
app
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
app.radarr_on_tick(ActiveRadarrBlock::Downloads).await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -717,6 +713,7 @@ mod tests {
let mut app = App {
network_tx: Some(sync_network_tx),
tick_count: 1,
is_first_render: false,
..App::default()
};
app.data.radarr_data.prompt_confirm = true;
+245
View File
@@ -0,0 +1,245 @@
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<'a> App<'a> {
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(None).into())
.await;
}
ActiveSonarrBlock::SeasonDetails => {
self
.dispatch_network_event(SonarrEvent::GetEpisodes(None).into())
.await;
self
.dispatch_network_event(SonarrEvent::GetEpisodeFiles(None).into())
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.await;
}
ActiveSonarrBlock::SeasonHistory => {
self
.dispatch_network_event(SonarrEvent::GetSeasonHistory(None).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(None).into())
.await;
}
_ => (),
}
}
ActiveSonarrBlock::EpisodeDetails | ActiveSonarrBlock::EpisodeFile => {
self
.dispatch_network_event(SonarrEvent::GetEpisodeDetails(None).into())
.await;
}
ActiveSonarrBlock::EpisodeHistory => {
self
.dispatch_network_event(SonarrEvent::GetEpisodeHistory(None).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(None).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(None).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(None).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(None).into())
.await;
}
ActiveSonarrBlock::AddSeriesSearchResults => {
self
.dispatch_network_event(SonarrEvent::SearchNewSeries(None).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 {
self
.dispatch_network_event(sonarr_event.clone().into())
.await;
self.should_refresh = true;
self.data.sonarr_data.prompt_confirm_action = None;
}
}
}
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);
}
}
+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);
}
}
+743
View File
@@ -0,0 +1,743 @@
#[cfg(test)]
mod tests {
mod sonarr_tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc;
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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeriesHistory)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetSeriesHistory(None).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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonDetails)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodes(None).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeFiles(None).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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonHistory)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetSeasonHistory(None).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() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetSeasonReleases(None).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::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::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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeDetails)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeDetails(None).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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeFile)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeDetails(None).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();
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeHistory)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeHistory(None).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 season_details_modal = SeasonDetailsModal {
episode_details_modal: Some(EpisodeDetailsModal::default()),
..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_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetEpisodeReleases(None).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::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::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(None).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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::TestIndexer)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::TestIndexer(None).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(None).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
.dispatch_by_sonarr_block(&ActiveSonarrBlock::AddSeriesSearchResults)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::SearchNewSeries(None).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::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::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::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"
);
}
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::default()
};
app.data.sonarr_data.prompt_confirm = true;
(app, sync_network_rx)
}
}
}
+57 -10
View File
@@ -10,19 +10,28 @@ mod tests {
use crate::{
app::App,
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand},
cli::{handle_command, mutex_flags_or_option, radarr::RadarrCommand, sonarr::SonarrCommand},
models::{
radarr_models::{BlocklistItem, BlocklistResponse, RadarrSerdeable},
radarr_models::{
BlocklistItem as RadarrBlocklistItem, BlocklistResponse as RadarrBlocklistResponse,
RadarrSerdeable,
},
sonarr_models::{
BlocklistItem as SonarrBlocklistItem, BlocklistResponse as SonarrBlocklistResponse,
SonarrSerdeable,
},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
network::{
radarr_network::RadarrEvent, sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent,
},
Cli,
};
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_subcommand_requires_subcommand() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr"]);
#[rstest]
fn test_servarr_subcommand_requires_subcommand(#[values("radarr", "sonarr")] subcommand: &str) {
let result = Cli::command().try_get_matches_from(["managarr", subcommand]);
assert!(result.is_err());
assert_eq!(
@@ -39,6 +48,13 @@ mod tests {
assert!(result.is_ok());
}
#[test]
fn test_sonarr_subcommand_delegates_to_sonarr() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series"]);
assert!(result.is_ok());
}
#[test]
fn test_completions_requires_argument() {
let result = Cli::command().try_get_matches_from(["managarr", "completions"]);
@@ -106,8 +122,8 @@ mod tests {
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::BlocklistResponse(
BlocklistResponse {
records: vec![BlocklistItem::default()],
RadarrBlocklistResponse {
records: vec![RadarrBlocklistItem::default()],
},
)))
});
@@ -121,9 +137,40 @@ mod tests {
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let claer_blocklist_command = RadarrCommand::ClearBlocklist.into();
let clear_blocklist_command = RadarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, claer_blocklist_command, &mut mock_network).await;
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_cli_handler_delegates_sonarr_commands_to_the_sonarr_cli_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse(
SonarrBlocklistResponse {
records: vec![SonarrBlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let clear_blocklist_command = SonarrCommand::ClearBlocklist.into();
let result = handle_command(&app_arc, clear_blocklist_command, &mut mock_network).await;
assert!(result.is_ok());
}
+28 -21
View File
@@ -4,11 +4,13 @@ use anyhow::Result;
use clap::{command, Subcommand};
use clap_complete::Shell;
use radarr::{RadarrCliHandler, RadarrCommand};
use sonarr::{SonarrCliHandler, SonarrCommand};
use tokio::sync::Mutex;
use crate::{app::App, network::NetworkTrait};
pub mod radarr;
pub mod sonarr;
#[cfg(test)]
#[path = "cli_tests.rs"]
@@ -19,6 +21,9 @@ pub enum Command {
#[command(subcommand, about = "Commands for manging your Radarr instance")]
Radarr(RadarrCommand),
#[command(subcommand, about = "Commands for manging your Sonarr instance")]
Sonarr(SonarrCommand),
#[command(
arg_required_else_help = true,
about = "Generate shell completions for the Managarr CLI"
@@ -27,24 +32,39 @@ pub enum Command {
#[arg(value_enum)]
shell: Shell,
},
#[command(about = "Tail Managarr logs")]
TailLogs {
#[arg(long, help = "Disable colored log output")]
no_color: bool,
},
}
pub trait CliCommandHandler<'a, 'b, T: Into<Command>> {
fn with(app: &'a Arc<Mutex<App<'b>>>, command: T, network: &'a mut dyn NetworkTrait) -> Self;
async fn handle(self) -> Result<()>;
async fn handle(self) -> Result<String>;
}
pub(crate) async fn handle_command(
app: &Arc<Mutex<App<'_>>>,
command: Command,
network: &mut dyn NetworkTrait,
) -> Result<()> {
if let Command::Radarr(radarr_command) = command {
RadarrCliHandler::with(app, radarr_command, network)
.handle()
.await?
}
Ok(())
) -> Result<String> {
let result = match command {
Command::Radarr(radarr_command) => {
RadarrCliHandler::with(app, radarr_command, network)
.handle()
.await?
}
Command::Sonarr(sonarr_command) => {
SonarrCliHandler::with(app, sonarr_command, network)
.handle()
.await?
}
_ => String::new(),
};
Ok(result)
}
#[inline]
@@ -68,16 +88,3 @@ pub fn mutex_flags_or_default(positive: bool, negative: bool, default_value: boo
default_value
}
}
#[macro_export]
macro_rules! execute_network_event {
($self:ident, $event:expr) => {
let resp = $self.network.handle_network_event($event.into()).await?;
let json = serde_json::to_string_pretty(&resp)?;
println!("{}", json);
};
($self:ident, $event:expr, $happy_output:expr) => {
$self.network.handle_network_event($event.into()).await?;
println!("{}", $happy_output);
};
}
+24 -16
View File
@@ -7,8 +7,7 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
models::radarr_models::{AddMovieBody, AddOptions, MinimumAvailability, Monitor},
models::radarr_models::{AddMovieBody, AddMovieOptions, MinimumAvailability, MovieMonitor},
network::{radarr_network::RadarrEvent, NetworkTrait},
};
@@ -47,7 +46,7 @@ pub enum RadarrAddCommand {
default_value_t = MinimumAvailability::default()
)]
minimum_availability: MinimumAvailability,
#[arg(long, help = "Should Radarr monitor this film")]
#[arg(long, help = "Disable monitoring for this film")]
disable_monitoring: bool,
#[arg(
long,
@@ -60,9 +59,9 @@ pub enum RadarrAddCommand {
long,
help = "What Radarr should monitor",
value_enum,
default_value_t = Monitor::default()
default_value_t = MovieMonitor::default()
)]
monitor: Monitor,
monitor: MovieMonitor,
#[arg(
long,
help = "Tell Radarr to not start a search for this film once it's added to your library"
@@ -106,8 +105,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrAddCommand::Movie {
tmdb_id,
root_folder_path,
@@ -126,24 +125,33 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrAddCommand> for RadarrAddCommandHan
minimum_availability: minimum_availability.to_string(),
monitored: !disable_monitoring,
tags,
add_options: AddOptions {
add_options: AddMovieOptions {
monitor: monitor.to_string(),
search_for_movie: !no_search_for_movie,
},
};
execute_network_event!(self, RadarrEvent::AddMovie(Some(body)));
let resp = self
.network
.handle_network_event(RadarrEvent::AddMovie(Some(body)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrAddCommand::RootFolder { root_folder_path } => {
execute_network_event!(
self,
RadarrEvent::AddRootFolder(Some(root_folder_path.clone()))
);
let resp = self
.network
.handle_network_event(RadarrEvent::AddRootFolder(Some(root_folder_path)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrAddCommand::Tag { name } => {
execute_network_event!(self, RadarrEvent::AddTag(name.clone()));
let resp = self
.network
.handle_network_event(RadarrEvent::AddTag(name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}
};
Ok(())
Ok(result)
}
}
+10 -7
View File
@@ -7,9 +7,10 @@ mod tests {
radarr::{add_command_handler::RadarrAddCommand, RadarrCommand},
Command,
},
models::radarr_models::{MinimumAvailability, Monitor},
models::radarr_models::{MinimumAvailability, MovieMonitor},
Cli,
};
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_add_command_from() {
@@ -111,6 +112,8 @@ mod tests {
"/test",
"--quality-profile-id",
"1",
"--tmdb-id",
"1",
flag,
]);
@@ -187,7 +190,7 @@ mod tests {
minimum_availability: MinimumAvailability::default(),
disable_monitoring: false,
tag: vec![],
monitor: Monitor::default(),
monitor: MovieMonitor::default(),
no_search_for_movie: false,
};
@@ -219,7 +222,7 @@ mod tests {
minimum_availability: MinimumAvailability::default(),
disable_monitoring: false,
tag: vec![1, 2],
monitor: Monitor::default(),
monitor: MovieMonitor::default(),
no_search_for_movie: false,
};
@@ -255,7 +258,7 @@ mod tests {
minimum_availability: MinimumAvailability::Released,
disable_monitoring: true,
tag: vec![1, 2],
monitor: Monitor::MovieAndCollection,
monitor: MovieMonitor::MovieAndCollection,
no_search_for_movie: true,
};
@@ -356,7 +359,7 @@ mod tests {
app::App,
cli::{radarr::add_command_handler::RadarrAddCommandHandler, CliCommandHandler},
models::{
radarr_models::{AddMovieBody, AddOptions, RadarrSerdeable},
radarr_models::{AddMovieBody, AddMovieOptions, RadarrSerdeable},
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
@@ -378,7 +381,7 @@ mod tests {
minimum_availability: "released".to_owned(),
monitored: false,
tags: vec![1, 2],
add_options: AddOptions {
add_options: AddMovieOptions {
monitor: "movieAndCollection".to_owned(),
search_for_movie: false,
},
@@ -403,7 +406,7 @@ mod tests {
minimum_availability: MinimumAvailability::Released,
disable_monitoring: true,
tag: vec![1, 2],
monitor: Monitor::MovieAndCollection,
monitor: MovieMonitor::MovieAndCollection,
no_search_for_movie: true,
};
+34 -14
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
models::radarr_models::DeleteMovieParams,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
@@ -85,19 +84,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
execute_network_event!(
self,
RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id))
);
let resp = self
.network
.handle_network_event(RadarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrDeleteCommand::Download { download_id } => {
execute_network_event!(self, RadarrEvent::DeleteDownload(Some(download_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::DeleteDownload(Some(download_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrDeleteCommand::Indexer { indexer_id } => {
execute_network_event!(self, RadarrEvent::DeleteIndexer(Some(indexer_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::DeleteIndexer(Some(indexer_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrDeleteCommand::Movie {
movie_id,
@@ -109,16 +117,28 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrDeleteCommand> for RadarrDeleteComm
delete_movie_files: delete_files_from_disk,
add_list_exclusion,
};
execute_network_event!(self, RadarrEvent::DeleteMovie(Some(delete_movie_params)));
let resp = self
.network
.handle_network_event(RadarrEvent::DeleteMovie(Some(delete_movie_params)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrDeleteCommand::RootFolder { root_folder_id } => {
execute_network_event!(self, RadarrEvent::DeleteRootFolder(Some(root_folder_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::DeleteRootFolder(Some(root_folder_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrDeleteCommand::Tag { tag_id } => {
execute_network_event!(self, RadarrEvent::DeleteTag(tag_id));
let resp = self
.network
.handle_network_event(RadarrEvent::DeleteTag(tag_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}
};
Ok(())
Ok(result)
}
}
@@ -8,6 +8,7 @@ mod tests {
Cli,
};
use clap::{error::ErrorKind, CommandFactory, Parser};
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_delete_command_from() {
+28 -27
View File
@@ -7,12 +7,11 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
execute_network_event,
models::{
radarr_models::{
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
MinimumAvailability, RadarrSerdeable,
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability, RadarrSerdeable,
},
servarr_models::EditIndexerParams,
Serdeable,
},
network::{radarr_network::RadarrEvent, NetworkTrait},
@@ -339,8 +338,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrEditCommand::AllIndexerSettings {
allow_hardcoded_subs,
disable_allow_hardcoded_subs,
@@ -389,11 +388,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
})
.into(),
};
execute_network_event!(
self,
RadarrEvent::EditAllIndexerSettings(Some(params)),
"All indexer settings updated"
);
self
.network
.handle_network_event(RadarrEvent::EditAllIndexerSettings(Some(params)).into())
.await?;
"All indexer settings updated".to_owned()
} else {
String::new()
}
}
RadarrEditCommand::Collection {
@@ -417,11 +418,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
root_folder_path,
search_on_add: search_on_add_value,
};
execute_network_event!(
self,
RadarrEvent::EditCollection(Some(edit_collection_params)),
"Collection Updated"
);
self
.network
.handle_network_event(RadarrEvent::EditCollection(Some(edit_collection_params)).into())
.await?;
"Collection updated".to_owned()
}
RadarrEditCommand::Indexer {
indexer_id,
@@ -458,11 +459,11 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
clear_tags,
};
execute_network_event!(
self,
RadarrEvent::EditIndexer(Some(edit_indexer_params)),
"Indexer updated"
);
self
.network
.handle_network_event(RadarrEvent::EditIndexer(Some(edit_indexer_params)).into())
.await?;
"Indexer updated".to_owned()
}
RadarrEditCommand::Movie {
movie_id,
@@ -485,14 +486,14 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrEditCommand> for RadarrEditCommandH
clear_tags,
};
execute_network_event!(
self,
RadarrEvent::EditMovie(Some(edit_movie_params)),
"Movie updated"
);
self
.network
.handle_network_event(RadarrEvent::EditMovie(Some(edit_movie_params)).into())
.await?;
"Movie Updated".to_owned()
}
}
};
Ok(())
Ok(result)
}
}
+4 -2
View File
@@ -8,6 +8,7 @@ mod tests {
Cli,
};
use clap::{error::ErrorKind, CommandFactory, Parser};
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_edit_command_from() {
@@ -809,9 +810,10 @@ mod tests {
},
models::{
radarr_models::{
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
MinimumAvailability, RadarrSerdeable,
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability,
RadarrSerdeable,
},
servarr_models::EditIndexerParams,
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
+34 -11
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
@@ -72,28 +71,52 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrGetCommand> for RadarrGetCommandHan
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrGetCommand::AllIndexerSettings => {
execute_network_event!(self, RadarrEvent::GetAllIndexerSettings);
let resp = self
.network
.handle_network_event(RadarrEvent::GetAllIndexerSettings.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrGetCommand::HostConfig => {
execute_network_event!(self, RadarrEvent::GetHostConfig);
let resp = self
.network
.handle_network_event(RadarrEvent::GetHostConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrGetCommand::MovieDetails { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieDetails(Some(movie_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::GetMovieDetails(Some(movie_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrGetCommand::MovieHistory { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieHistory(Some(movie_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::GetMovieHistory(Some(movie_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrGetCommand::SecurityConfig => {
execute_network_event!(self, RadarrEvent::GetSecurityConfig);
let resp = self
.network
.handle_network_event(RadarrEvent::GetSecurityConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrGetCommand::SystemStatus => {
execute_network_event!(self, RadarrEvent::GetStatus);
let resp = self
.network
.handle_network_event(RadarrEvent::GetStatus.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}
};
Ok(())
Ok(result)
}
}
+2 -1
View File
@@ -1,5 +1,5 @@
#[cfg(test)]
mod test {
mod tests {
use clap::error::ErrorKind;
use clap::CommandFactory;
@@ -7,6 +7,7 @@ mod test {
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_get_command_from() {
+76 -22
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
@@ -25,6 +24,8 @@ pub enum RadarrListCommand {
Collections,
#[command(about = "List all active downloads in Radarr")]
Downloads,
#[command(about = "List disk space details for all provisioned root folders in Radarr")]
DiskSpace,
#[command(about = "List all Radarr indexers")]
Indexers,
#[command(about = "Fetch Radarr logs")]
@@ -56,7 +57,7 @@ pub enum RadarrListCommand {
RootFolders,
#[command(about = "List all Radarr tags")]
Tags,
#[command(about = "List tasks")]
#[command(about = "List all Radarr tasks")]
Tasks,
#[command(about = "List all Radarr updates")]
Updates,
@@ -87,19 +88,42 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrListCommand::Blocklist => {
execute_network_event!(self, RadarrEvent::GetBlocklist);
let resp = self
.network
.handle_network_event(RadarrEvent::GetBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Collections => {
execute_network_event!(self, RadarrEvent::GetCollections);
let resp = self
.network
.handle_network_event(RadarrEvent::GetCollections.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Downloads => {
execute_network_event!(self, RadarrEvent::GetDownloads);
let resp = self
.network
.handle_network_event(RadarrEvent::GetDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::DiskSpace => {
let resp = self
.network
.handle_network_event(RadarrEvent::GetDiskSpace.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Indexers => {
execute_network_event!(self, RadarrEvent::GetIndexers);
let resp = self
.network
.handle_network_event(RadarrEvent::GetIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Logs {
events,
@@ -113,39 +137,69 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
if output_in_log_format {
let log_lines = self.app.lock().await.data.radarr_data.logs.items.clone();
let json = serde_json::to_string_pretty(&log_lines)?;
println!("{}", json);
serde_json::to_string_pretty(&log_lines)?
} else {
let json = serde_json::to_string_pretty(&logs)?;
println!("{}", json);
serde_json::to_string_pretty(&logs)?
}
}
RadarrListCommand::Movies => {
execute_network_event!(self, RadarrEvent::GetMovies);
let resp = self
.network
.handle_network_event(RadarrEvent::GetMovies.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::MovieCredits { movie_id } => {
execute_network_event!(self, RadarrEvent::GetMovieCredits(Some(movie_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::GetMovieCredits(Some(movie_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::QualityProfiles => {
execute_network_event!(self, RadarrEvent::GetQualityProfiles);
let resp = self
.network
.handle_network_event(RadarrEvent::GetQualityProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::QueuedEvents => {
execute_network_event!(self, RadarrEvent::GetQueuedEvents);
let resp = self
.network
.handle_network_event(RadarrEvent::GetQueuedEvents.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::RootFolders => {
execute_network_event!(self, RadarrEvent::GetRootFolders);
let resp = self
.network
.handle_network_event(RadarrEvent::GetRootFolders.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Tags => {
execute_network_event!(self, RadarrEvent::GetTags);
let resp = self
.network
.handle_network_event(RadarrEvent::GetTags.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Tasks => {
execute_network_event!(self, RadarrEvent::GetTasks);
let resp = self
.network
.handle_network_event(RadarrEvent::GetTasks.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Updates => {
execute_network_event!(self, RadarrEvent::GetUpdates);
let resp = self
.network
.handle_network_event(RadarrEvent::GetUpdates.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}
};
Ok(())
Ok(result)
}
}
+6 -3
View File
@@ -7,6 +7,7 @@ mod tests {
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_list_command_from() {
@@ -29,6 +30,7 @@ mod tests {
"blocklist",
"collections",
"downloads",
"disk-space",
"indexers",
"movies",
"quality-profiles",
@@ -80,8 +82,8 @@ mod tests {
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
assert_eq!(refresh_command, expected_args);
if let Some(Command::Radarr(RadarrCommand::List(credits_command))) = result.unwrap().command {
assert_eq!(credits_command, expected_args);
}
}
@@ -121,6 +123,7 @@ mod tests {
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
#[case(RadarrListCommand::QualityProfiles, RadarrEvent::GetQualityProfiles)]
@@ -130,7 +133,7 @@ mod tests {
#[case(RadarrListCommand::Tasks, RadarrEvent::GetTasks)]
#[case(RadarrListCommand::Updates, RadarrEvent::GetUpdates)]
#[tokio::test]
async fn test_handle_list_blocklist_command(
async fn test_handle_list_command(
#[case] list_command: RadarrListCommand,
#[case] expected_radarr_event: RadarrEvent,
) {
+50 -18
View File
@@ -12,8 +12,7 @@ use tokio::sync::Mutex;
use crate::app::App;
use crate::cli::CliCommandHandler;
use crate::execute_network_event;
use crate::models::radarr_models::{ReleaseDownloadBody, TaskName};
use crate::models::radarr_models::{RadarrReleaseDownloadBody, RadarrTaskName};
use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkTrait;
use anyhow::Result;
@@ -86,7 +85,7 @@ pub enum RadarrCommand {
ManualSearch {
#[arg(
long,
help = "The Radarr ID of the movie whose releases you wish to fetch and list",
help = "The Radarr ID of the movie whose releases you wish to fetch",
required = true
)]
movie_id: i64,
@@ -108,7 +107,7 @@ pub enum RadarrCommand {
value_enum,
required = true
)]
task_name: TaskName,
task_name: RadarrTaskName,
},
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
@@ -117,7 +116,7 @@ pub enum RadarrCommand {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(about = "Test all indexers")]
#[command(about = "Test all Radarr indexers")]
TestAllIndexers,
#[command(about = "Trigger an automatic search for the movie with the specified ID")]
TriggerAutomaticSearch {
@@ -155,8 +154,8 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrCommand::Add(add_command) => {
RadarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
@@ -192,41 +191,74 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.network
.handle_network_event(RadarrEvent::GetBlocklist.into())
.await?;
execute_network_event!(self, RadarrEvent::ClearBlocklist);
let resp = self
.network
.handle_network_event(RadarrEvent::ClearBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::DownloadRelease {
guid,
indexer_id,
movie_id,
} => {
let params = ReleaseDownloadBody {
let params = RadarrReleaseDownloadBody {
guid,
indexer_id,
movie_id,
};
execute_network_event!(self, RadarrEvent::DownloadRelease(Some(params)));
let resp = self
.network
.handle_network_event(RadarrEvent::DownloadRelease(Some(params)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::ManualSearch { movie_id } => {
println!("Searching for releases. This may take a minute...");
execute_network_event!(self, RadarrEvent::GetReleases(Some(movie_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::GetReleases(Some(movie_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::SearchNewMovie { query } => {
execute_network_event!(self, RadarrEvent::SearchNewMovie(Some(query)));
let resp = self
.network
.handle_network_event(RadarrEvent::SearchNewMovie(Some(query)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::StartTask { task_name } => {
execute_network_event!(self, RadarrEvent::StartTask(Some(task_name)));
let resp = self
.network
.handle_network_event(RadarrEvent::StartTask(Some(task_name)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::TestIndexer { indexer_id } => {
execute_network_event!(self, RadarrEvent::TestIndexer(Some(indexer_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::TestIndexer(Some(indexer_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::TestAllIndexers => {
execute_network_event!(self, RadarrEvent::TestAllIndexers);
println!("Testing all Radarr indexers. This may take a minute...");
let resp = self
.network
.handle_network_event(RadarrEvent::TestAllIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::TriggerAutomaticSearch { movie_id } => {
execute_network_event!(self, RadarrEvent::TriggerAutomaticSearch(Some(movie_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::TriggerAutomaticSearch(Some(movie_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}
};
Ok(())
Ok(result)
}
}
+14 -14
View File
@@ -31,7 +31,7 @@ mod tests {
assert!(result.is_ok());
}
#[rstest]
#[test]
fn test_download_release_requires_movie_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
@@ -50,7 +50,7 @@ mod tests {
);
}
#[rstest]
#[test]
fn test_download_release_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
@@ -69,7 +69,7 @@ mod tests {
);
}
#[rstest]
#[test]
fn test_download_release_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
@@ -105,7 +105,7 @@ mod tests {
assert!(result.is_ok());
}
#[rstest]
#[test]
fn test_manual_search_requires_movie_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "manual-search"]);
@@ -129,7 +129,7 @@ mod tests {
assert!(result.is_ok());
}
#[rstest]
#[test]
fn test_search_new_movie_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "search-new-movie"]);
@@ -153,7 +153,7 @@ mod tests {
assert!(result.is_ok());
}
#[rstest]
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "start-task"]);
@@ -164,7 +164,7 @@ mod tests {
);
}
#[rstest]
#[test]
fn test_start_task_task_name_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
@@ -191,7 +191,7 @@ mod tests {
assert!(result.is_ok());
}
#[rstest]
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "radarr", "test-indexer"]);
@@ -215,7 +215,7 @@ mod tests {
assert!(result.is_ok());
}
#[rstest]
#[test]
fn test_trigger_automatic_search_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "trigger-automatic-search"]);
@@ -261,8 +261,8 @@ mod tests {
},
models::{
radarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrSerdeable, ReleaseDownloadBody,
TaskName,
BlocklistItem, BlocklistResponse, IndexerSettings, RadarrReleaseDownloadBody,
RadarrSerdeable, RadarrTaskName,
},
Serdeable,
},
@@ -304,7 +304,7 @@ mod tests {
#[tokio::test]
async fn test_download_release_command() {
let expected_release_download_body = ReleaseDownloadBody {
let expected_release_download_body = RadarrReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
movie_id: 1,
@@ -389,7 +389,7 @@ mod tests {
#[tokio::test]
async fn test_start_task_command() {
let expected_task_name = TaskName::ApplicationCheckUpdate;
let expected_task_name = RadarrTaskName::ApplicationCheckUpdate;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
@@ -404,7 +404,7 @@ mod tests {
});
let app_arc = Arc::new(Mutex::new(App::default()));
let start_task_command = RadarrCommand::StartTask {
task_name: TaskName::ApplicationCheckUpdate,
task_name: RadarrTaskName::ApplicationCheckUpdate,
};
let result = RadarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
+25 -10
View File
@@ -7,7 +7,6 @@ use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
execute_network_event,
network::{radarr_network::RadarrEvent, NetworkTrait},
};
@@ -19,7 +18,7 @@ mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RadarrRefreshCommand {
#[command(about = "Refresh all movie data for all movies in your library")]
#[command(about = "Refresh all movie data for all movies in your Radarr library")]
AllMovies,
#[command(about = "Refresh movie data and scan disk for the movie with the given ID")]
Movie {
@@ -63,22 +62,38 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrRefreshCommand>
}
}
async fn handle(self) -> Result<()> {
match self.command {
async fn handle(self) -> Result<String> {
let result = match self.command {
RadarrRefreshCommand::AllMovies => {
execute_network_event!(self, RadarrEvent::UpdateAllMovies);
let resp = self
.network
.handle_network_event(RadarrEvent::UpdateAllMovies.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrRefreshCommand::Collections => {
execute_network_event!(self, RadarrEvent::UpdateCollections);
let resp = self
.network
.handle_network_event(RadarrEvent::UpdateCollections.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrRefreshCommand::Downloads => {
execute_network_event!(self, RadarrEvent::UpdateDownloads);
let resp = self
.network
.handle_network_event(RadarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrRefreshCommand::Movie { movie_id } => {
execute_network_event!(self, RadarrEvent::UpdateAndScan(Some(movie_id)));
let resp = self
.network
.handle_network_event(RadarrEvent::UpdateAndScan(Some(movie_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}
};
Ok(())
Ok(result)
}
}
@@ -7,6 +7,7 @@ mod tests {
use crate::cli::radarr::RadarrCommand;
use crate::cli::Command;
use crate::Cli;
use pretty_assertions::assert_eq;
#[test]
fn test_radarr_refresh_command_from() {
@@ -81,7 +82,7 @@ mod tests {
#[case(RadarrRefreshCommand::Collections, RadarrEvent::UpdateCollections)]
#[case(RadarrRefreshCommand::Downloads, RadarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_list_blocklist_command(
async fn test_handle_refresh_command(
#[case] refresh_command: RadarrRefreshCommand,
#[case] expected_radarr_event: RadarrEvent,
) {
+172
View File
@@ -0,0 +1,172 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, Subcommand};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::sonarr_models::{AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "add_command_handler_tests.rs"]
mod add_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrAddCommand {
#[command(about = "Add a new series to your Sonarr library")]
Series {
#[arg(
long,
help = "The TVDB ID of the series you wish to add to your library",
required = true
)]
tvdb_id: i64,
#[arg(long, help = "The title of the series", required = true)]
title: String,
#[arg(
long,
help = "The root folder path where all series data and metadata should live",
required = true
)]
root_folder_path: String,
#[arg(
long,
help = "The ID of the quality profile to use for this series",
required = true
)]
quality_profile_id: i64,
#[arg(
long,
help = "The ID of the language profile to use for this series",
required = true
)]
language_profile_id: i64,
#[arg(
long,
help = "The type of series",
value_enum,
default_value_t = SeriesType::default()
)]
series_type: SeriesType,
#[arg(long, help = "Disable monitoring for this series")]
disable_monitoring: bool,
#[arg(long, help = "Don't use season folders for this series")]
disable_season_folders: bool,
#[arg(
long,
help = "Tag IDs to tag the series with",
value_parser,
action = ArgAction::Append
)]
tag: Vec<i64>,
#[arg(
long,
help = "What Sonarr should monitor",
value_enum,
default_value_t = SeriesMonitor::default()
)]
monitor: SeriesMonitor,
#[arg(
long,
help = "Tell Sonarr to not start a search for this series once it's added to your library"
)]
no_search_for_series: bool,
},
#[command(about = "Add a new root folder")]
RootFolder {
#[arg(long, help = "The path of the new root folder", required = true)]
root_folder_path: String,
},
#[command(about = "Add new tag")]
Tag {
#[arg(long, help = "The name of the tag to be added", required = true)]
name: String,
},
}
impl From<SonarrAddCommand> for Command {
fn from(value: SonarrAddCommand) -> Self {
Command::Sonarr(SonarrCommand::Add(value))
}
}
pub(super) struct SonarrAddCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrAddCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrAddCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrAddCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrAddCommand::Series {
tvdb_id,
title,
root_folder_path,
quality_profile_id,
language_profile_id,
series_type,
disable_monitoring,
disable_season_folders,
tag: tags,
monitor,
no_search_for_series,
} => {
let body = AddSeriesBody {
tvdb_id,
title,
monitored: !disable_monitoring,
root_folder_path,
quality_profile_id,
language_profile_id,
series_type: series_type.to_string(),
season_folder: !disable_season_folders,
tags,
add_options: AddSeriesOptions {
monitor: monitor.to_string(),
search_for_cutoff_unmet_episodes: !no_search_for_series,
search_for_missing_episodes: !no_search_for_series,
},
};
let resp = self
.network
.handle_network_event(SonarrEvent::AddSeries(Some(body)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrAddCommand::RootFolder { root_folder_path } => {
let resp = self
.network
.handle_network_event(SonarrEvent::AddRootFolder(Some(root_folder_path)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrAddCommand::Tag { name } => {
let resp = self
.network
.handle_network_event(SonarrEvent::AddTag(name).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
+582
View File
@@ -0,0 +1,582 @@
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, CommandFactory, Parser};
use pretty_assertions::assert_eq;
use crate::{
cli::{
sonarr::{add_command_handler::SonarrAddCommand, SonarrCommand},
Command,
},
Cli,
};
#[test]
fn test_sonarr_add_command_from() {
let command = SonarrAddCommand::Tag {
name: String::new(),
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::Add(command)));
}
mod cli {
use crate::models::sonarr_models::{SeriesMonitor, SeriesType};
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_add_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "root-folder"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_root_folder_success() {
let expected_args = SonarrAddCommand::RootFolder {
root_folder_path: "/nfs/test".to_owned(),
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"add",
"root-folder",
"--root-folder-path",
"/nfs/test",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_series_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "series"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_series_requires_tvdb_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--root-folder-path",
"test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--title",
"test",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_series_requires_title() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--tvdb-id",
"1",
"--root-folder-path",
"test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_series_requires_root_folder_path() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--tvdb-id",
"1",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--title",
"test",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_series_requires_quality_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--tvdb-id",
"1",
"--root-folder-path",
"test",
"--language-profile-id",
"1",
"--title",
"test",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_series_requires_language_profile_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--tvdb-id",
"1",
"--root-folder-path",
"test",
"--quality-profile-id",
"1",
"--title",
"test",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_add_series_assert_argument_flags_require_args(
#[values("--series-type", "--tag", "--monitor")] flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--tvdb-id",
"1",
"--title",
"test",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
flag,
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_series_all_arguments_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--title",
"test",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--tvdb-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_add_series_series_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--tvdb-id",
"1",
"--title",
"test",
"--series-type",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_series_monitor_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"add",
"series",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--tvdb-id",
"--title",
"test",
"1",
"--monitor",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_add_series_defaults() {
let expected_args = SonarrAddCommand::Series {
tvdb_id: 1,
title: "test".to_owned(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: SeriesType::default(),
disable_monitoring: false,
disable_season_folders: false,
tag: vec![],
monitor: SeriesMonitor::default(),
no_search_for_series: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"add",
"series",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--title",
"test",
"--tvdb-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_series_tags_is_repeatable() {
let expected_args = SonarrAddCommand::Series {
tvdb_id: 1,
title: "test".to_owned(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: SeriesType::default(),
disable_monitoring: false,
disable_season_folders: false,
tag: vec![1, 2],
monitor: SeriesMonitor::default(),
no_search_for_series: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"add",
"series",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--tvdb-id",
"1",
"--title",
"test",
"--tag",
"1",
"--tag",
"2",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_series_all_args_defined() {
let expected_args = SonarrAddCommand::Series {
tvdb_id: 1,
title: "test".to_owned(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: SeriesType::Anime,
disable_monitoring: true,
disable_season_folders: true,
tag: vec![1, 2],
monitor: SeriesMonitor::Future,
no_search_for_series: true,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"add",
"series",
"--root-folder-path",
"/test",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--series-type",
"anime",
"--disable-monitoring",
"--disable-season-folders",
"--tvdb-id",
"1",
"--title",
"test",
"--tag",
"1",
"--tag",
"2",
"--monitor",
"future",
"--no-search-for-series",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
#[test]
fn test_add_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "add", "tag"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_add_tag_success() {
let expected_args = SonarrAddCommand::Tag {
name: "test".to_owned(),
};
let result = Cli::try_parse_from(["managarr", "sonarr", "add", "tag", "--name", "test"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Add(add_command))) = result.unwrap().command {
assert_eq!(add_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use crate::{
app::App,
cli::{sonarr::add_command_handler::SonarrAddCommandHandler, CliCommandHandler},
models::{
sonarr_models::{
AddSeriesBody, AddSeriesOptions, SeriesMonitor, SeriesType, SonarrSerdeable,
},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
use super::*;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_handle_add_root_folder_command() {
let expected_root_folder_path = "/nfs/test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_root_folder_command = SonarrAddCommand::RootFolder {
root_folder_path: expected_root_folder_path,
};
let result =
SonarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_add_series_command() {
let expected_add_series_body = AddSeriesBody {
tvdb_id: 1,
title: "test".to_owned(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: "anime".to_owned(),
monitored: false,
tags: vec![1, 2],
season_folder: false,
add_options: AddSeriesOptions {
monitor: "future".to_owned(),
search_for_cutoff_unmet_episodes: false,
search_for_missing_episodes: false,
},
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::AddSeries(Some(expected_add_series_body)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_series_command = SonarrAddCommand::Series {
tvdb_id: 1,
title: "test".to_owned(),
root_folder_path: "/test".to_owned(),
quality_profile_id: 1,
language_profile_id: 1,
series_type: SeriesType::Anime,
disable_monitoring: true,
disable_season_folders: true,
tag: vec![1, 2],
monitor: SeriesMonitor::Future,
no_search_for_series: true,
};
let result = SonarrAddCommandHandler::with(&app_arc, add_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_add_tag_command() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_tag_command = SonarrAddCommand::Tag {
name: expected_tag_name,
};
let result = SonarrAddCommandHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+156
View File
@@ -0,0 +1,156 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::sonarr_models::DeleteSeriesParams,
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "delete_command_handler_tests.rs"]
mod delete_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrDeleteCommand {
#[command(about = "Delete the specified item from the Sonarr blocklist")]
BlocklistItem {
#[arg(
long,
help = "The ID of the blocklist item to remove from the blocklist",
required = true
)]
blocklist_item_id: i64,
},
#[command(about = "Delete the specified download")]
Download {
#[arg(long, help = "The ID of the download to delete", required = true)]
download_id: i64,
},
#[command(about = "Delete the specified episode file from disk")]
EpisodeFile {
#[arg(long, help = "The ID of the episode file to delete", required = true)]
episode_file_id: i64,
},
#[command(about = "Delete the indexer with the given ID")]
Indexer {
#[arg(long, help = "The ID of the indexer to delete", required = true)]
indexer_id: i64,
},
#[command(about = "Delete the root folder with the given ID")]
RootFolder {
#[arg(long, help = "The ID of the root folder to delete", required = true)]
root_folder_id: i64,
},
#[command(about = "Delete a series from your Sonarr library")]
Series {
#[arg(long, help = "The ID of the series to delete", required = true)]
series_id: i64,
#[arg(long, help = "Delete the series files from disk as well")]
delete_files_from_disk: bool,
#[arg(long, help = "Add a list exclusion for this series")]
add_list_exclusion: bool,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
tag_id: i64,
},
}
impl From<SonarrDeleteCommand> for Command {
fn from(value: SonarrDeleteCommand) -> Self {
Command::Sonarr(SonarrCommand::Delete(value))
}
}
pub(super) struct SonarrDeleteCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrDeleteCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrDeleteCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let resp = match self.command {
SonarrDeleteCommand::BlocklistItem { blocklist_item_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteBlocklistItem(Some(blocklist_item_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::Download { download_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteDownload(Some(download_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::EpisodeFile { episode_file_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteEpisodeFile(Some(episode_file_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::Indexer { indexer_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteIndexer(Some(indexer_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::RootFolder { root_folder_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteRootFolder(Some(root_folder_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::Series {
series_id,
delete_files_from_disk,
add_list_exclusion,
} => {
let delete_series_params = DeleteSeriesParams {
id: series_id,
delete_series_files: delete_files_from_disk,
add_list_exclusion,
};
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteSeries(Some(delete_series_params)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::Tag { tag_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteTag(tag_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(resp)
}
}
@@ -0,0 +1,466 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
sonarr::{delete_command_handler::SonarrDeleteCommand, SonarrCommand},
Command,
},
Cli,
};
use clap::{error::ErrorKind, CommandFactory, Parser};
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_delete_command_from() {
let command = SonarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::Delete(command)));
}
mod cli {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_delete_blocklist_item_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "blocklist-item"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_blocklist_item_success() {
let expected_args = SonarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"delete",
"blocklist-item",
"--blocklist-item-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_download_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "download"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_download_success() {
let expected_args = SonarrDeleteCommand::Download { download_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"delete",
"download",
"--download-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_episode_file_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "episode-file"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_episode_file_success() {
let expected_args = SonarrDeleteCommand::EpisodeFile { episode_file_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"delete",
"episode-file",
"--episode-file-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "indexer"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_indexer_success() {
let expected_args = SonarrDeleteCommand::Indexer { indexer_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"delete",
"indexer",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_root_folder_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "root-folder"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_root_folder_success() {
let expected_args = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"delete",
"root-folder",
"--root-folder-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_series_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "series"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_series_defaults() {
let expected_args = SonarrDeleteCommand::Series {
series_id: 1,
delete_files_from_disk: false,
add_list_exclusion: false,
};
let result =
Cli::try_parse_from(["managarr", "sonarr", "delete", "series", "--series-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_series_all_args_defined() {
let expected_args = SonarrDeleteCommand::Series {
series_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"delete",
"series",
"--series-id",
"1",
"--delete-files-from-disk",
"--add-list-exclusion",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "tag"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_tag_success() {
let expected_args = SonarrDeleteCommand::Tag { tag_id: 1 };
let result = Cli::try_parse_from(["managarr", "sonarr", "delete", "tag", "--tag-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler},
CliCommandHandler,
},
models::{
sonarr_models::{DeleteSeriesParams, SonarrSerdeable},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_delete_blocklist_item_command() {
let expected_blocklist_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_blocklist_item_command = SonarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
};
let result = SonarrDeleteCommandHandler::with(
&app_arc,
delete_blocklist_item_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_download_command() {
let expected_download_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteDownload(Some(expected_download_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_download_command = SonarrDeleteCommand::Download { download_id: 1 };
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteIndexer(Some(expected_indexer_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_indexer_command = SonarrDeleteCommand::Indexer { indexer_id: 1 };
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_indexer_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_root_folder_command() {
let expected_root_folder_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteRootFolder(Some(expected_root_folder_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_root_folder_command = SonarrDeleteCommand::RootFolder { root_folder_id: 1 };
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_series_command() {
let expected_delete_series_params = DeleteSeriesParams {
id: 1,
delete_series_files: true,
add_list_exclusion: true,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteSeries(Some(expected_delete_series_params)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_series_command = SonarrDeleteCommand::Series {
series_id: 1,
delete_files_from_disk: true,
add_list_exclusion: true,
};
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_tag_command() {
let expected_tag_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteTag(expected_tag_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_tag_command = SonarrDeleteCommand::Tag { tag_id: 1 };
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+169
View File
@@ -0,0 +1,169 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
models::sonarr_models::SonarrReleaseDownloadBody,
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "download_command_handler_tests.rs"]
mod download_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrDownloadCommand {
#[command(about = "Manually download the given series release for the specified series ID")]
Series {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
#[arg(
long,
help = "The series ID that the release is associated with",
required = true
)]
series_id: i64,
},
#[command(
about = "Manually download the given season release corresponding to the series specified with the series ID"
)]
Season {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
#[arg(
long,
help = "The series ID that the release is associated with",
required = true
)]
series_id: i64,
#[arg(
long,
help = "The season number that the release corresponds to",
required = true
)]
season_number: i64,
},
#[command(about = "Manually download the given episode release for the specified episode ID")]
Episode {
#[arg(long, help = "The GUID of the release to download", required = true)]
guid: String,
#[arg(
long,
help = "The indexer ID to download the release from",
required = true
)]
indexer_id: i64,
#[arg(
long,
help = "The episode ID that the release is associated with",
required = true
)]
episode_id: i64,
},
}
impl From<SonarrDownloadCommand> for Command {
fn from(value: SonarrDownloadCommand) -> Self {
Command::Sonarr(SonarrCommand::Download(value))
}
}
pub(super) struct SonarrDownloadCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrDownloadCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDownloadCommand>
for SonarrDownloadCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrDownloadCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrDownloadCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrDownloadCommand::Series {
guid,
indexer_id,
series_id,
} => {
let params = SonarrReleaseDownloadBody {
guid,
indexer_id,
series_id: Some(series_id),
..SonarrReleaseDownloadBody::default()
};
let resp = self
.network
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDownloadCommand::Season {
guid,
indexer_id,
series_id,
season_number,
} => {
let params = SonarrReleaseDownloadBody {
guid,
indexer_id,
series_id: Some(series_id),
season_number: Some(season_number),
..SonarrReleaseDownloadBody::default()
};
let resp = self
.network
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDownloadCommand::Episode {
guid,
indexer_id,
episode_id,
} => {
let params = SonarrReleaseDownloadBody {
guid,
indexer_id,
episode_id: Some(episode_id),
..SonarrReleaseDownloadBody::default()
};
let resp = self
.network
.handle_network_event(SonarrEvent::DownloadRelease(params).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,423 @@
#[cfg(test)]
mod tests {
use crate::{
cli::{
sonarr::{download_command_handler::SonarrDownloadCommand, SonarrCommand},
Command,
},
Cli,
};
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_download_command_from() {
let command = SonarrDownloadCommand::Series {
guid: "Test".to_owned(),
indexer_id: 1,
series_id: 1,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::Download(command)));
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_download_series_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"series",
"--indexer-id",
"1",
"--guid",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_series_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"series",
"--indexer-id",
"1",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_series_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"series",
"--guid",
"1",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_series_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"series",
"--guid",
"1",
"--series-id",
"1",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_download_season_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"season",
"--indexer-id",
"1",
"--season-number",
"1",
"--guid",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_season_requires_season_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"season",
"--indexer-id",
"1",
"--series-id",
"1",
"--guid",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_season_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"season",
"--indexer-id",
"1",
"--season-number",
"1",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_season_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"season",
"--guid",
"1",
"--season-number",
"1",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_season_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"season",
"--guid",
"1",
"--series-id",
"1",
"--season-number",
"1",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_download_episode_requires_episode_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"episode",
"--indexer-id",
"1",
"--guid",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_episode_requires_guid() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"episode",
"--indexer-id",
"1",
"--episode-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_episode_requires_indexer_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"episode",
"--guid",
"1",
"--episode-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_download_episode_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"download",
"episode",
"--guid",
"1",
"--episode-id",
"1",
"--indexer-id",
"1",
]);
assert!(result.is_ok());
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler},
CliCommandHandler,
},
models::{
sonarr_models::{SonarrReleaseDownloadBody, SonarrSerdeable},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_download_series_release_command() {
let expected_release_download_body = SonarrReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
series_id: Some(1),
..SonarrReleaseDownloadBody::default()
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let download_release_command = SonarrDownloadCommand::Series {
guid: "guid".to_owned(),
indexer_id: 1,
series_id: 1,
};
let result =
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_download_season_release_command() {
let expected_release_download_body = SonarrReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
series_id: Some(1),
season_number: Some(1),
..SonarrReleaseDownloadBody::default()
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let download_release_command = SonarrDownloadCommand::Season {
guid: "guid".to_owned(),
indexer_id: 1,
series_id: 1,
season_number: 1,
};
let result =
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_download_episode_release_command() {
let expected_release_download_body = SonarrReleaseDownloadBody {
guid: "guid".to_owned(),
indexer_id: 1,
episode_id: Some(1),
..SonarrReleaseDownloadBody::default()
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DownloadRelease(expected_release_download_body).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let download_release_command = SonarrDownloadCommand::Episode {
guid: "guid".to_owned(),
indexer_id: 1,
episode_id: 1,
};
let result =
SonarrDownloadCommandHandler::with(&app_arc, download_release_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+363
View File
@@ -0,0 +1,363 @@
use std::sync::Arc;
use anyhow::Result;
use clap::{ArgAction, ArgGroup, Subcommand};
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{mutex_flags_or_option, CliCommandHandler, Command},
models::{
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
Serdeable,
},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "edit_command_handler_tests.rs"]
mod edit_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrEditCommand {
#[command(
about = "Edit and indexer settings that apply to all indexers",
group(
ArgGroup::new("edit_settings")
.args([
"maximum_size",
"minimum_age",
"retention",
"rss_sync_interval",
]).required(true)
.multiple(true))
)]
AllIndexerSettings {
#[arg(
long,
help = "The maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
)]
maximum_size: Option<i64>,
#[arg(
long,
help = "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
)]
minimum_age: Option<i64>,
#[arg(
long,
help = "Usenet only: The retention time in days to retain releases. Set to zero to set for unlimited retention"
)]
retention: Option<i64>,
#[arg(
long,
help = "The RSS sync interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
)]
rss_sync_interval: Option<i64>,
},
#[command(
about = "Edit preferences for the specified indexer",
group(
ArgGroup::new("edit_indexer")
.args([
"name",
"enable_rss",
"disable_rss",
"enable_automatic_search",
"disable_automatic_search",
"enable_interactive_search",
"disable_automatic_search",
"url",
"api_key",
"seed_ratio",
"tag",
"priority",
"clear_tags"
]).required(true)
.multiple(true))
)]
Indexer {
#[arg(
long,
help = "The ID of the indexer whose settings you wish to edit",
required = true
)]
indexer_id: i64,
#[arg(long, help = "The name of the indexer")]
name: Option<String>,
#[arg(
long,
help = "Indicate to Sonarr that this indexer should be used when Sonarr periodically looks for releases via RSS Sync",
conflicts_with = "disable_rss"
)]
enable_rss: bool,
#[arg(
long,
help = "Disable using this indexer when Sonarr periodically looks for releases via RSS Sync",
conflicts_with = "enable_rss"
)]
disable_rss: bool,
#[arg(
long,
help = "Indicate to Sonarr that this indexer should be used when automatic searches are performed via the UI or by Sonarr",
conflicts_with = "disable_automatic_search"
)]
enable_automatic_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever automatic searches are performed via the UI or by Sonarr",
conflicts_with = "enable_automatic_search"
)]
disable_automatic_search: bool,
#[arg(
long,
help = "Indicate to Sonarr that this indexer should be used when an interactive search is used",
conflicts_with = "disable_interactive_search"
)]
enable_interactive_search: bool,
#[arg(
long,
help = "Disable using this indexer whenever an interactive search is performed",
conflicts_with = "enable_interactive_search"
)]
disable_interactive_search: bool,
#[arg(long, help = "The URL of the indexer")]
url: Option<String>,
#[arg(long, help = "The API key used to access the indexer's API")]
api_key: Option<String>,
#[arg(
long,
help = "The ratio a torrent should reach before stopping; Empty uses the download client's default. Ratio should be at least 1.0 and follow the indexer's rules"
)]
seed_ratio: Option<String>,
#[arg(
long,
help = "Only use this indexer for series with at least one matching tag ID. Leave blank to use with all series.",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(
long,
help = "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Sonarr will still use all enabled indexers for RSS Sync and Searching"
)]
priority: Option<i64>,
#[arg(long, help = "Clear all tags on this indexer", conflicts_with = "tag")]
clear_tags: bool,
},
#[command(
about = "Edit preferences for the specified series",
group(
ArgGroup::new("edit_series")
.args([
"enable_monitoring",
"disable_monitoring",
"enable_season_folders",
"disable_season_folders",
"series_type",
"quality_profile_id",
"language_profile_id",
"root_folder_path",
"tag",
"clear_tags"
]).required(true)
.multiple(true))
)]
Series {
#[arg(
long,
help = "The ID of the series whose settings you want to edit",
required = true
)]
series_id: i64,
#[arg(
long,
help = "Enable monitoring of this series in Sonarr so Sonarr will automatically download this series if it is available",
conflicts_with = "disable_monitoring"
)]
enable_monitoring: bool,
#[arg(
long,
help = "Disable monitoring of this series so Sonarr does not automatically download the series if it is found to be available",
conflicts_with = "enable_monitoring"
)]
disable_monitoring: bool,
#[arg(
long,
help = "The minimum availability to monitor for this film",
value_enum
)]
#[arg(
long,
help = "Enable sorting episodes of this series into season folders",
conflicts_with = "disable_season_folders"
)]
enable_season_folders: bool,
#[arg(
long,
help = "Disable sorting episodes of this series into season folders",
conflicts_with = "enable_season_folders"
)]
disable_season_folders: bool,
#[arg(long, help = "The type of series", value_enum)]
series_type: Option<SeriesType>,
#[arg(long, help = "The ID of the quality profile to use for this series")]
quality_profile_id: Option<i64>,
#[arg(long, help = "The ID of the language profile to use for this series")]
language_profile_id: Option<i64>,
#[arg(
long,
help = "The root folder path where all film data and metadata should live"
)]
root_folder_path: Option<String>,
#[arg(
long,
help = "Tag IDs to tag this series with",
value_parser,
action = ArgAction::Append,
conflicts_with = "clear_tags"
)]
tag: Option<Vec<i64>>,
#[arg(long, help = "Clear all tags on this series", conflicts_with = "tag")]
clear_tags: bool,
},
}
impl From<SonarrEditCommand> for Command {
fn from(value: SonarrEditCommand) -> Self {
Command::Sonarr(SonarrCommand::Edit(value))
}
}
pub(super) struct SonarrEditCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrEditCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrEditCommand> for SonarrEditCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrEditCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrEditCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrEditCommand::AllIndexerSettings {
maximum_size,
minimum_age,
retention,
rss_sync_interval,
} => {
if let Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(previous_indexer_settings)) = self
.network
.handle_network_event(SonarrEvent::GetAllIndexerSettings.into())
.await?
{
let params = IndexerSettings {
id: 1,
maximum_size: maximum_size.unwrap_or(previous_indexer_settings.maximum_size),
minimum_age: minimum_age.unwrap_or(previous_indexer_settings.minimum_age),
retention: retention.unwrap_or(previous_indexer_settings.retention),
rss_sync_interval: rss_sync_interval
.unwrap_or(previous_indexer_settings.rss_sync_interval),
};
self
.network
.handle_network_event(SonarrEvent::EditAllIndexerSettings(Some(params)).into())
.await?;
"All indexer settings updated".to_owned()
} else {
String::new()
}
}
SonarrEditCommand::Indexer {
indexer_id,
name,
enable_rss,
disable_rss,
enable_automatic_search,
disable_automatic_search,
enable_interactive_search,
disable_interactive_search,
url,
api_key,
seed_ratio,
tag,
priority,
clear_tags,
} => {
let rss_value = mutex_flags_or_option(enable_rss, disable_rss);
let automatic_search_value =
mutex_flags_or_option(enable_automatic_search, disable_automatic_search);
let interactive_search_value =
mutex_flags_or_option(enable_interactive_search, disable_interactive_search);
let edit_indexer_params = EditIndexerParams {
indexer_id,
name,
enable_rss: rss_value,
enable_automatic_search: automatic_search_value,
enable_interactive_search: interactive_search_value,
url,
api_key,
seed_ratio,
tags: tag,
priority,
clear_tags,
};
self
.network
.handle_network_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)).into())
.await?;
"Indexer updated".to_owned()
}
SonarrEditCommand::Series {
series_id,
enable_monitoring,
disable_monitoring,
enable_season_folders,
disable_season_folders,
series_type,
quality_profile_id,
language_profile_id,
root_folder_path,
tag,
clear_tags,
} => {
let monitored_value = mutex_flags_or_option(enable_monitoring, disable_monitoring);
let season_folders_value =
mutex_flags_or_option(enable_season_folders, disable_season_folders);
let edit_series_params = EditSeriesParams {
series_id,
monitored: monitored_value,
use_season_folders: season_folders_value,
series_type,
quality_profile_id,
language_profile_id,
root_folder_path,
tags: tag,
clear_tags,
};
self
.network
.handle_network_event(SonarrEvent::EditSeries(Some(edit_series_params)).into())
.await?;
"Series Updated".to_owned()
}
};
Ok(result)
}
}
@@ -0,0 +1,874 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{edit_command_handler::SonarrEditCommand, SonarrCommand},
Command,
};
#[test]
fn test_sonarr_edit_command_from() {
let command = SonarrEditCommand::AllIndexerSettings {
maximum_size: None,
minimum_age: None,
retention: None,
rss_sync_interval: None,
};
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::Edit(command)));
}
mod cli {
use crate::{models::sonarr_models::SeriesType, Cli};
use super::*;
use clap::{error::ErrorKind, CommandFactory, Parser};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_edit_all_indexer_settings_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "all-indexer-settings"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[rstest]
fn test_edit_all_indexer_settings_assert_argument_flags_require_args(
#[values(
"--maximum-size",
"--minimum-age",
"--retention",
"--rss-sync-interval"
)]
flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"all-indexer-settings",
flag,
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_all_indexer_settings_only_requires_at_least_one_argument() {
let expected_args = SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: None,
retention: None,
rss_sync_interval: None,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"all-indexer-settings",
"--maximum-size",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_all_indexer_settings_all_arguments_defined() {
let expected_args = SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"all-indexer-settings",
"--maximum-size",
"1",
"--minimum-age",
"1",
"--retention",
"1",
"--rss-sync-interval",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_indexer_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "indexer"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_indexer_with_indexer_id_still_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_indexer_rss_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-rss",
"--disable-rss",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_automatic_search_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-automatic-search",
"--disable-automatic-search",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_interactive_search_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--enable-interactive-search",
"--disable-interactive-search",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_indexer_tag_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--tag",
"1",
"--clear-tags",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[rstest]
fn test_edit_indexer_assert_argument_flags_require_args(
#[values("--name", "--url", "--api-key", "--seed-ratio", "--tag", "--priority")] flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
flag,
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_indexer_only_requires_at_least_one_argument_plus_indexer_id() {
let expected_args = SonarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: false,
disable_rss: false,
enable_automatic_search: false,
disable_automatic_search: false,
enable_interactive_search: false,
disable_interactive_search: false,
url: None,
api_key: None,
seed_ratio: None,
tag: None,
priority: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--name",
"Test",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_indexer_tag_argument_is_repeatable() {
let expected_args = SonarrEditCommand::Indexer {
indexer_id: 1,
name: None,
enable_rss: false,
disable_rss: false,
enable_automatic_search: false,
disable_automatic_search: false,
enable_interactive_search: false,
disable_interactive_search: false,
url: None,
api_key: None,
seed_ratio: None,
tag: Some(vec![1, 2]),
priority: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--tag",
"1",
"--tag",
"2",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_indexer_all_arguments_defined() {
let expected_args = SonarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: true,
disable_rss: false,
enable_automatic_search: true,
disable_automatic_search: false,
enable_interactive_search: true,
disable_interactive_search: false,
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tag: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"indexer",
"--indexer-id",
"1",
"--name",
"Test",
"--enable-rss",
"--enable-automatic-search",
"--enable-interactive-search",
"--url",
"http://test.com",
"--api-key",
"testKey",
"--seed-ratio",
"1.2",
"--tag",
"1",
"--tag",
"2",
"--priority",
"25",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_series_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "edit", "series"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_series_with_series_id_still_requires_arguments() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_edit_series_monitoring_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--enable-monitoring",
"--disable-monitoring",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_series_season_folders_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--enable-season-folders",
"--disable-season-folders",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[test]
fn test_edit_series_tag_flags_conflict() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--tag",
"1",
"--clear-tags",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::ArgumentConflict);
}
#[rstest]
fn test_edit_series_assert_argument_flags_require_args(
#[values(
"--series-type",
"--quality-profile-id",
"--language-profile-id",
"--root-folder-path",
"--tag"
)]
flag: &str,
) {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
flag,
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_series_series_type_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--series-type",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_edit_series_only_requires_at_least_one_argument_plus_series_id() {
let expected_args = SonarrEditCommand::Series {
series_id: 1,
enable_monitoring: false,
disable_monitoring: false,
enable_season_folders: false,
disable_season_folders: false,
series_type: None,
quality_profile_id: None,
language_profile_id: None,
root_folder_path: Some("/nfs/test".to_owned()),
tag: None,
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--root-folder-path",
"/nfs/test",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_series_tag_argument_is_repeatable() {
let expected_args = SonarrEditCommand::Series {
series_id: 1,
enable_monitoring: false,
disable_monitoring: false,
enable_season_folders: false,
disable_season_folders: false,
series_type: None,
quality_profile_id: None,
language_profile_id: None,
root_folder_path: None,
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--tag",
"1",
"--tag",
"2",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
#[test]
fn test_edit_series_all_arguments_defined() {
let expected_args = SonarrEditCommand::Series {
series_id: 1,
enable_monitoring: true,
disable_monitoring: false,
enable_season_folders: true,
disable_season_folders: false,
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"edit",
"series",
"--series-id",
"1",
"--enable-monitoring",
"--enable-season-folders",
"--series-type",
"anime",
"--quality-profile-id",
"1",
"--language-profile-id",
"1",
"--root-folder-path",
"/nfs/test",
"--tag",
"1",
"--tag",
"2",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Edit(edit_command))) = result.unwrap().command {
assert_eq!(edit_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler},
CliCommandHandler,
},
models::{
servarr_models::EditIndexerParams,
sonarr_models::{EditSeriesParams, IndexerSettings, SeriesType, SonarrSerdeable},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_edit_all_indexer_settings_command() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 1,
minimum_age: 1,
retention: 1,
rss_sync_interval: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
IndexerSettings {
id: 1,
maximum_size: 2,
minimum_age: 2,
retention: 2,
rss_sync_interval: 2,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_all_indexer_settings_command = SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
};
let result = SonarrEditCommandHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_edit_indexer_command() {
let expected_edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: Some(true),
enable_automatic_search: Some(true),
enable_interactive_search: Some(true),
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tags: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::EditIndexer(Some(expected_edit_indexer_params)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_indexer_command = SonarrEditCommand::Indexer {
indexer_id: 1,
name: Some("Test".to_owned()),
enable_rss: true,
disable_rss: false,
enable_automatic_search: true,
disable_automatic_search: false,
enable_interactive_search: true,
disable_interactive_search: false,
url: Some("http://test.com".to_owned()),
api_key: Some("testKey".to_owned()),
seed_ratio: Some("1.2".to_owned()),
tag: Some(vec![1, 2]),
priority: Some(25),
clear_tags: false,
};
let result =
SonarrEditCommandHandler::with(&app_arc, edit_indexer_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_edit_series_command() {
let expected_edit_series_params = EditSeriesParams {
series_id: 1,
monitored: Some(true),
use_season_folders: Some(true),
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_series_command = SonarrEditCommand::Series {
series_id: 1,
enable_monitoring: true,
disable_monitoring: false,
enable_season_folders: true,
disable_season_folders: false,
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_edit_series_command_handles_disable_monitoring_flag_properly() {
let expected_edit_series_params = EditSeriesParams {
series_id: 1,
monitored: Some(false),
use_season_folders: Some(false),
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_series_command = SonarrEditCommand::Series {
series_id: 1,
enable_monitoring: false,
disable_monitoring: true,
enable_season_folders: false,
disable_season_folders: true,
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_edit_series_command_no_monitoring_boolean_flags_returns_none_value() {
let expected_edit_series_params = EditSeriesParams {
series_id: 1,
monitored: None,
use_season_folders: None,
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tags: Some(vec![1, 2]),
clear_tags: false,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::EditSeries(Some(expected_edit_series_params)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_series_command = SonarrEditCommand::Series {
series_id: 1,
enable_monitoring: false,
disable_monitoring: false,
enable_season_folders: false,
disable_season_folders: false,
series_type: Some(SeriesType::Anime),
quality_profile_id: Some(1),
language_profile_id: Some(1),
root_folder_path: Some("/nfs/test".to_owned()),
tag: Some(vec![1, 2]),
clear_tags: false,
};
let result = SonarrEditCommandHandler::with(&app_arc, edit_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+122
View File
@@ -0,0 +1,122 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "get_command_handler_tests.rs"]
mod get_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrGetCommand {
#[command(about = "Get the shared settings for all indexers")]
AllIndexerSettings,
#[command(about = "Get detailed information for the episode with the given ID")]
EpisodeDetails {
#[arg(
long,
help = "The Sonarr ID of the episode whose details you wish to fetch",
required = true
)]
episode_id: i64,
},
#[command(about = "Fetch the host config for your Sonarr instance")]
HostConfig,
#[command(about = "Fetch the security config for your Sonarr instance")]
SecurityConfig,
#[command(about = "Get detailed information for the series with the given ID")]
SeriesDetails {
#[arg(
long,
help = "The Sonarr ID of the series whose details you wish to fetch",
required = true
)]
series_id: i64,
},
#[command(about = "Get the system status")]
SystemStatus,
}
impl From<SonarrGetCommand> for Command {
fn from(value: SonarrGetCommand) -> Self {
Command::Sonarr(SonarrCommand::Get(value))
}
}
pub(super) struct SonarrGetCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrGetCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrGetCommand> for SonarrGetCommandHandler<'a, 'b> {
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrGetCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrGetCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrGetCommand::AllIndexerSettings => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetAllIndexerSettings.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrGetCommand::EpisodeDetails { episode_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodeDetails(Some(episode_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrGetCommand::HostConfig => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetHostConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrGetCommand::SecurityConfig => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetSecurityConfig.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrGetCommand::SeriesDetails { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetSeriesDetails(Some(series_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrGetCommand::SystemStatus => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetStatus.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
+277
View File
@@ -0,0 +1,277 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{get_command_handler::SonarrGetCommand, SonarrCommand},
Command,
};
use crate::Cli;
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_get_command_from() {
let command = SonarrGetCommand::SystemStatus;
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::Get(command)));
}
mod cli {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_all_indexer_settings_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "all-indexer-settings"]);
assert!(result.is_ok());
}
#[test]
fn test_system_status_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "system-status"]);
assert!(result.is_ok());
}
#[test]
fn test_episode_details_requires_episode_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "episode-details"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_episode_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"get",
"episode-details",
"--episode-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_get_host_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "host-config"]);
assert!(result.is_ok());
}
#[test]
fn test_get_security_config_has_no_arg_requirements() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "security-config"]);
assert!(result.is_ok());
}
#[test]
fn test_series_details_requires_series_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "get", "series-details"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_series_details_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"get",
"series-details",
"--series-id",
"1",
]);
assert!(result.is_ok());
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler},
CliCommandHandler,
},
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_get_all_indexer_settings_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_all_indexer_settings_command = SonarrGetCommand::AllIndexerSettings;
let result = SonarrGetCommandHandler::with(
&app_arc,
get_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_episode_details_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeDetails(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_episode_details_command = SonarrGetCommand::EpisodeDetails { episode_id: 1 };
let result =
SonarrGetCommandHandler::with(&app_arc, get_episode_details_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_host_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::GetHostConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_host_config_command = SonarrGetCommand::HostConfig;
let result =
SonarrGetCommandHandler::with(&app_arc, get_host_config_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_security_config_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::GetSecurityConfig.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_security_config_command = SonarrGetCommand::SecurityConfig;
let result =
SonarrGetCommandHandler::with(&app_arc, get_security_config_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_series_details_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetSeriesDetails(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_series_details_command = SonarrGetCommand::SeriesDetails { series_id: 1 };
let result =
SonarrGetCommandHandler::with(&app_arc, get_series_details_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_get_system_status_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_system_status_command = SonarrGetCommand::SystemStatus;
let result =
SonarrGetCommandHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+294
View File
@@ -0,0 +1,294 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "list_command_handler_tests.rs"]
mod list_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrListCommand {
#[command(about = "List all items in the Sonarr blocklist")]
Blocklist,
#[command(about = "List all active downloads in Sonarr")]
Downloads,
#[command(about = "List disk space details for all provisioned root folders in Sonarr")]
DiskSpace,
#[command(about = "List the episodes for the series with the given ID")]
Episodes {
#[arg(
long,
help = "The Sonarr ID of the series whose episodes you wish to fetch",
required = true
)]
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")]
EpisodeHistory {
#[arg(
long,
help = "The Sonarr ID of the episode whose history you wish to fetch",
required = true
)]
episode_id: i64,
},
#[command(about = "Fetch all Sonarr history events")]
History {
#[arg(long, help = "How many history events to fetch", default_value_t = 500)]
events: u64,
},
#[command(about = "List all Sonarr indexers")]
Indexers,
#[command(about = "List all Sonarr language profiles")]
LanguageProfiles,
#[command(about = "Fetch Sonarr logs")]
Logs {
#[arg(long, help = "How many log events to fetch", default_value_t = 500)]
events: u64,
#[arg(
long,
help = "Output the logs in the same format as they appear in the log files"
)]
output_in_log_format: bool,
},
#[command(about = "List all Sonarr quality profiles")]
QualityProfiles,
#[command(about = "List all queued events")]
QueuedEvents,
#[command(about = "List all root folders in Sonarr")]
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")]
Series,
#[command(about = "Fetch all history events for the series with the given ID")]
SeriesHistory {
#[arg(
long,
help = "The Sonarr ID of the series whose history you wish to fetch",
required = true
)]
series_id: i64,
},
#[command(about = "List all Sonarr tags")]
Tags,
#[command(about = "List all Sonarr tasks")]
Tasks,
#[command(about = "List all Sonarr updates")]
Updates,
}
impl From<SonarrListCommand> for Command {
fn from(value: SonarrListCommand) -> Self {
Command::Sonarr(SonarrCommand::List(value))
}
}
pub(super) struct SonarrListCommandHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: SonarrListCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: SonarrListCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrListCommandHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrListCommand::Blocklist => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Downloads => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::DiskSpace => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetDiskSpace.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Episodes { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodes(Some(series_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::EpisodeFiles { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodeFiles(Some(series_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::EpisodeHistory { episode_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodeHistory(Some(episode_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::History { events: items } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetHistory(Some(items)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Indexers => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::LanguageProfiles => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetLanguageProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Logs {
events,
output_in_log_format,
} => {
let logs = self
.network
.handle_network_event(SonarrEvent::GetLogs(Some(events)).into())
.await?;
if output_in_log_format {
let log_lines = self.app.lock().await.data.sonarr_data.logs.items.clone();
serde_json::to_string_pretty(&log_lines)?
} else {
serde_json::to_string_pretty(&logs)?
}
}
SonarrListCommand::QualityProfiles => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetQualityProfiles.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::QueuedEvents => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetQueuedEvents.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::RootFolders => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetRootFolders.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::SeasonHistory {
series_id,
season_number,
} => {
let resp = self
.network
.handle_network_event(
SonarrEvent::GetSeasonHistory(Some((series_id, season_number))).into(),
)
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Series => {
let resp = self
.network
.handle_network_event(SonarrEvent::ListSeries.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::SeriesHistory { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetSeriesHistory(Some(series_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Tags => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetTags.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Tasks => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetTasks.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Updates => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetUpdates.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,513 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
Command,
};
use crate::Cli;
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_list_command_from() {
let command = SonarrListCommand::Series;
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::List(command)));
}
mod cli {
use super::*;
use clap::{error::ErrorKind, Parser};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_list_commands_have_no_arg_requirements(
#[values(
"blocklist",
"series",
"downloads",
"disk-space",
"quality-profiles",
"indexers",
"queued-events",
"root-folders",
"tags",
"tasks",
"updates",
"language-profiles"
)]
subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", subcommand]);
assert!(result.is_ok());
}
#[test]
fn test_list_episodes_requires_series_id() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episodes"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[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]
fn test_list_episode_history_requires_series_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-history"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_episode_history_success() {
let expected_args = SonarrListCommand::EpisodeHistory { episode_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"list",
"episode-history",
"--episode-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(episode_history_command))) =
result.unwrap().command
{
assert_eq!(episode_history_command, expected_args);
}
}
#[test]
fn test_list_history_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "history", "--events"]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_history_default_values() {
let expected_args = SonarrListCommand::History { events: 500 };
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "history"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(history_command))) = result.unwrap().command {
assert_eq!(history_command, expected_args);
}
}
#[test]
fn test_list_logs_events_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "logs", "--events"]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_logs_default_values() {
let expected_args = SonarrListCommand::Logs {
events: 500,
output_in_log_format: false,
};
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "logs"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(logs_command))) = result.unwrap().command {
assert_eq!(logs_command, expected_args);
}
}
#[test]
fn test_list_episodes_success() {
let expected_args = SonarrListCommand::Episodes { series_id: 1 };
let result =
Cli::try_parse_from(["managarr", "sonarr", "list", "episodes", "--series-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(episodes_command))) = result.unwrap().command
{
assert_eq!(episodes_command, expected_args);
}
}
#[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]
fn test_list_series_history_requires_series_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "series-history"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_list_series_history_success() {
let expected_args = SonarrListCommand::SeriesHistory { series_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"list",
"series-history",
"--series-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(series_command))) = result.unwrap().command {
assert_eq!(series_command, expected_args);
}
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use rstest::rstest;
use serde_json::json;
use tokio::sync::Mutex;
use crate::cli::sonarr::list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
use crate::cli::CliCommandHandler;
use crate::models::sonarr_models::SonarrSerdeable;
use crate::models::Serdeable;
use crate::network::sonarr_network::SonarrEvent;
use crate::{
app::App,
network::{MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)]
#[case(SonarrListCommand::Downloads, SonarrEvent::GetDownloads)]
#[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)]
#[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)]
#[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)]
#[case(SonarrListCommand::QueuedEvents, SonarrEvent::GetQueuedEvents)]
#[case(SonarrListCommand::RootFolders, SonarrEvent::GetRootFolders)]
#[case(SonarrListCommand::Series, SonarrEvent::ListSeries)]
#[case(SonarrListCommand::Tags, SonarrEvent::GetTags)]
#[case(SonarrListCommand::Tasks, SonarrEvent::GetTasks)]
#[case(SonarrListCommand::Updates, SonarrEvent::GetUpdates)]
#[case(SonarrListCommand::LanguageProfiles, SonarrEvent::GetLanguageProfiles)]
#[tokio::test]
async fn test_handle_list_command(
#[case] list_command: SonarrListCommand,
#[case] expected_sonarr_event: SonarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let result = SonarrListCommandHandler::with(&app_arc, list_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_list_episodes_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodes(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_episodes_command = SonarrListCommand::Episodes { series_id: 1 };
let result =
SonarrListCommandHandler::with(&app_arc, list_episodes_command, &mut mock_network)
.handle()
.await;
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(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::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]
async fn test_handle_list_history_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetHistory(Some(expected_events)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_history_command = SonarrListCommand::History { events: 1000 };
let result =
SonarrListCommandHandler::with(&app_arc, list_history_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetLogs(Some(expected_events)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_logs_command = SonarrListCommand::Logs {
events: 1000,
output_in_log_format: false,
};
let result = SonarrListCommandHandler::with(&app_arc, list_logs_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_list_series_history_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetSeriesHistory(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_series_history_command = SonarrListCommand::SeriesHistory { series_id: 1 };
let result =
SonarrListCommandHandler::with(&app_arc, list_series_history_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_list_episode_history_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeHistory(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_episode_history_command = SonarrListCommand::EpisodeHistory { episode_id: 1 };
let result =
SonarrListCommandHandler::with(&app_arc, list_episode_history_command, &mut mock_network)
.handle()
.await;
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(Some((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::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());
}
}
}
@@ -0,0 +1,99 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "manual_search_command_handler_tests.rs"]
mod manual_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrManualSearchCommand {
#[command(about = "Trigger a manual search of releases for the episode with the given ID")]
Episode {
#[arg(
long,
help = "The Sonarr ID of the episode whose releases you wish to fetch and list",
required = true
)]
episode_id: i64,
},
#[command(
about = "Trigger a manual search of releases for the given season corresponding to the series with the given ID.\nNote that when downloading a season release, ensure that the release includes 'fullSeason: true', otherwise you'll run into issues"
)]
Season {
#[arg(
long,
help = "The Sonarr ID of the series whose releases you wish to fetch and list",
required = true
)]
series_id: i64,
#[arg(long, help = "The season number to search for", required = true)]
season_number: i64,
},
}
impl From<SonarrManualSearchCommand> for Command {
fn from(value: SonarrManualSearchCommand) -> Self {
Command::Sonarr(SonarrCommand::ManualSearch(value))
}
}
pub(super) struct SonarrManualSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrManualSearchCommand>
for SonarrManualSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrManualSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrManualSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrManualSearchCommand::Episode { episode_id } => {
println!("Searching for episode releases. This may take a minute...");
let resp = self
.network
.handle_network_event(SonarrEvent::GetEpisodeReleases(Some(episode_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrManualSearchCommand::Season {
series_id,
season_number,
} => {
println!("Searching for season releases. This may take a minute...");
let resp = self
.network
.handle_network_event(
SonarrEvent::GetSeasonReleases(Some((series_id, season_number))).into(),
)
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,188 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{manual_search_command_handler::SonarrManualSearchCommand, SonarrCommand},
Command,
};
use crate::Cli;
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_manual_search_command_from() {
let command = SonarrManualSearchCommand::Episode { episode_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Sonarr(SonarrCommand::ManualSearch(command))
);
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_manual_season_search_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"manual-search",
"season",
"--season-number",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_season_search_requires_season_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"manual-search",
"season",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_season_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"manual-search",
"season",
"--series-id",
"1",
"--season-number",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_manual_episode_search_requires_episode_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "manual-search", "episode"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_manual_episode_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"manual-search",
"episode",
"--episode-id",
"1",
]);
assert!(result.is_ok());
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::manual_search_command_handler::{
SonarrManualSearchCommand, SonarrManualSearchCommandHandler,
},
CliCommandHandler,
},
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_manual_episode_search_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let manual_episode_search_command = SonarrManualSearchCommand::Episode { episode_id: 1 };
let result = SonarrManualSearchCommandHandler::with(
&app_arc,
manual_episode_search_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_manual_season_search_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::GetSeasonReleases(Some((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::default()));
let manual_season_search_command = SonarrManualSearchCommand::Season {
series_id: 1,
season_number: 1,
};
let result = SonarrManualSearchCommandHandler::with(
&app_arc,
manual_season_search_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+297
View File
@@ -0,0 +1,297 @@
use std::sync::Arc;
use add_command_handler::{SonarrAddCommand, SonarrAddCommandHandler};
use anyhow::Result;
use clap::Subcommand;
use delete_command_handler::{SonarrDeleteCommand, SonarrDeleteCommandHandler};
use download_command_handler::{SonarrDownloadCommand, SonarrDownloadCommandHandler};
use edit_command_handler::{SonarrEditCommand, SonarrEditCommandHandler};
use get_command_handler::{SonarrGetCommand, SonarrGetCommandHandler};
use list_command_handler::{SonarrListCommand, SonarrListCommandHandler};
use manual_search_command_handler::{SonarrManualSearchCommand, SonarrManualSearchCommandHandler};
use refresh_command_handler::{SonarrRefreshCommand, SonarrRefreshCommandHandler};
use tokio::sync::Mutex;
use trigger_automatic_search_command_handler::{
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
};
use crate::{
app::App,
models::sonarr_models::SonarrTaskName,
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::{CliCommandHandler, Command};
mod add_command_handler;
mod delete_command_handler;
mod download_command_handler;
mod edit_command_handler;
mod get_command_handler;
mod list_command_handler;
mod manual_search_command_handler;
mod refresh_command_handler;
mod trigger_automatic_search_command_handler;
#[cfg(test)]
#[path = "sonarr_command_tests.rs"]
mod sonarr_command_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrCommand {
#[command(
subcommand,
about = "Commands to add or create new resources within your Sonarr instance"
)]
Add(SonarrAddCommand),
#[command(
subcommand,
about = "Commands to delete resources from your Sonarr instance"
)]
Delete(SonarrDeleteCommand),
#[command(
subcommand,
about = "Commands to edit resources in your Sonarr instance"
)]
Edit(SonarrEditCommand),
#[command(
subcommand,
about = "Commands to fetch details of the resources in your Sonarr instance"
)]
Get(SonarrGetCommand),
#[command(
subcommand,
about = "Commands to download releases in your Sonarr instance"
)]
Download(SonarrDownloadCommand),
#[command(
subcommand,
about = "Commands to list attributes from your Sonarr instance"
)]
List(SonarrListCommand),
#[command(
subcommand,
about = "Commands to refresh the data in your Sonarr instance"
)]
Refresh(SonarrRefreshCommand),
#[command(subcommand, about = "Commands to manually search for releases")]
ManualSearch(SonarrManualSearchCommand),
#[command(
subcommand,
about = "Commands to trigger automatic searches for releases of different resources in your Sonarr instance"
)]
TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand),
#[command(about = "Clear the blocklist")]
ClearBlocklist,
#[command(about = "Mark the Sonarr history item with the given ID as 'failed'")]
MarkHistoryItemAsFailed {
#[arg(
long,
help = "The Sonarr ID of the history item you wish to mark as 'failed'",
required = true
)]
history_item_id: i64,
},
#[command(about = "Search for a new series to add to Sonarr")]
SearchNewSeries {
#[arg(
long,
help = "The title of the series you want to search for",
required = true
)]
query: String,
},
#[command(about = "Start the specified Sonarr task")]
StartTask {
#[arg(
long,
help = "The name of the task to trigger",
value_enum,
required = true
)]
task_name: SonarrTaskName,
},
#[command(
about = "Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}'"
)]
TestIndexer {
#[arg(long, help = "The ID of the indexer to test", required = true)]
indexer_id: i64,
},
#[command(about = "Test all Sonarr indexers")]
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 {
fn from(sonarr_command: SonarrCommand) -> Command {
Command::Sonarr(sonarr_command)
}
}
pub(super) struct SonarrCliHandler<'a, 'b> {
app: &'a Arc<Mutex<App<'b>>>,
command: SonarrCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, 'b> {
fn with(
app: &'a Arc<Mutex<App<'b>>>,
command: SonarrCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrCliHandler {
app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrCommand::Add(add_command) => {
SonarrAddCommandHandler::with(self.app, add_command, self.network)
.handle()
.await?
}
SonarrCommand::Delete(delete_command) => {
SonarrDeleteCommandHandler::with(self.app, delete_command, self.network)
.handle()
.await?
}
SonarrCommand::Edit(edit_command) => {
SonarrEditCommandHandler::with(self.app, edit_command, self.network)
.handle()
.await?
}
SonarrCommand::Download(download_command) => {
SonarrDownloadCommandHandler::with(self.app, download_command, self.network)
.handle()
.await?
}
SonarrCommand::Get(get_command) => {
SonarrGetCommandHandler::with(self.app, get_command, self.network)
.handle()
.await?
}
SonarrCommand::List(list_command) => {
SonarrListCommandHandler::with(self.app, list_command, self.network)
.handle()
.await?
}
SonarrCommand::Refresh(refresh_command) => {
SonarrRefreshCommandHandler::with(self.app, refresh_command, self.network)
.handle()
.await?
}
SonarrCommand::ManualSearch(manual_search_command) => {
SonarrManualSearchCommandHandler::with(self.app, manual_search_command, self.network)
.handle()
.await?
}
SonarrCommand::TriggerAutomaticSearch(trigger_automatic_search_command) => {
SonarrTriggerAutomaticSearchCommandHandler::with(
self.app,
trigger_automatic_search_command,
self.network,
)
.handle()
.await?
}
SonarrCommand::ClearBlocklist => {
self
.network
.handle_network_event(SonarrEvent::GetBlocklist.into())
.await?;
let resp = self
.network
.handle_network_event(SonarrEvent::ClearBlocklist.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::MarkHistoryItemAsFailed { history_item_id } => {
let _ = self
.network
.handle_network_event(SonarrEvent::MarkHistoryItemAsFailed(history_item_id).into())
.await?;
"Sonarr history item marked as 'failed'".to_owned()
}
SonarrCommand::SearchNewSeries { query } => {
let resp = self
.network
.handle_network_event(SonarrEvent::SearchNewSeries(Some(query)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::StartTask { task_name } => {
let resp = self
.network
.handle_network_event(SonarrEvent::StartTask(Some(task_name)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::TestIndexer { indexer_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::TestIndexer(Some(indexer_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::TestAllIndexers => {
println!("Testing all Sonarr indexers. This may take a minute...");
let resp = self
.network
.handle_network_event(SonarrEvent::TestAllIndexers.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrCommand::ToggleEpisodeMonitoring { episode_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::ToggleEpisodeMonitoring(Some(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(Some((series_id, season_number))).into(),
)
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
+89
View File
@@ -0,0 +1,89 @@
use std::sync::Arc;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "refresh_command_handler_tests.rs"]
mod refresh_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrRefreshCommand {
#[command(about = "Refresh all series data for all series in your Sonarr library")]
AllSeries,
#[command(about = "Refresh series data and scan disk for the series with the given ID")]
Series {
#[arg(
long,
help = "The ID of the series to refresh information on and to scan the disk for",
required = true
)]
series_id: i64,
},
#[command(about = "Refresh all downloads in Sonarr")]
Downloads,
}
impl From<SonarrRefreshCommand> for Command {
fn from(value: SonarrRefreshCommand) -> Self {
Command::Sonarr(SonarrCommand::Refresh(value))
}
}
pub(super) struct SonarrRefreshCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrRefreshCommand>
for SonarrRefreshCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrRefreshCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrRefreshCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> anyhow::Result<String> {
let result = match self.command {
SonarrRefreshCommand::AllSeries => {
let resp = self
.network
.handle_network_event(SonarrEvent::UpdateAllSeries.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrRefreshCommand::Series { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::UpdateAndScanSeries(Some(series_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrRefreshCommand::Downloads => {
let resp = self
.network
.handle_network_event(SonarrEvent::UpdateDownloads.into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,141 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::cli::{
sonarr::{refresh_command_handler::SonarrRefreshCommand, SonarrCommand},
Command,
};
use crate::Cli;
use clap::CommandFactory;
#[test]
fn test_sonarr_refresh_command_from() {
let command = SonarrRefreshCommand::AllSeries;
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(SonarrCommand::Refresh(command)));
}
mod cli {
use super::*;
use clap::{error::ErrorKind, Parser};
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_refresh_commands_have_no_arg_requirements(
#[values("all-series", "downloads")] subcommand: &str,
) {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", subcommand]);
assert!(result.is_ok());
}
#[test]
fn test_refresh_series_requires_series_id() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "refresh", "series"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_refresh_series_success() {
let expected_args = SonarrRefreshCommand::Series { series_id: 1 };
let result = Cli::try_parse_from([
"managarr",
"sonarr",
"refresh",
"series",
"--series-id",
"1",
]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Refresh(refresh_command))) =
result.unwrap().command
{
assert_eq!(refresh_command, expected_args);
}
}
}
mod handler {
use rstest::rstest;
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{app::App, cli::sonarr::refresh_command_handler::SonarrRefreshCommandHandler};
use crate::{
cli::{sonarr::refresh_command_handler::SonarrRefreshCommand, CliCommandHandler},
network::sonarr_network::SonarrEvent,
};
use crate::{
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{MockNetworkTrait, NetworkEvent},
};
#[rstest]
#[case(SonarrRefreshCommand::AllSeries, SonarrEvent::UpdateAllSeries)]
#[case(SonarrRefreshCommand::Downloads, SonarrEvent::UpdateDownloads)]
#[tokio::test]
async fn test_handle_refresh_command(
#[case] refresh_command: SonarrRefreshCommand,
#[case] expected_sonarr_event: SonarrEvent,
) {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(expected_sonarr_event.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let result = SonarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_refresh_series_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let refresh_series_command = SonarrRefreshCommand::Series { series_id: 1 };
let result =
SonarrRefreshCommandHandler::with(&app_arc, refresh_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+758
View File
@@ -0,0 +1,758 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{list_command_handler::SonarrListCommand, SonarrCommand},
Command,
};
use crate::Cli;
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_command_from() {
let command = SonarrCommand::List(SonarrListCommand::Series);
let result = Command::from(command.clone());
assert_eq!(result, Command::Sonarr(command));
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_commands_that_have_no_arg_requirements(
#[values("clear-blocklist", "test-all-indexers")] subcommand: &str,
) {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", subcommand]);
assert!(result.is_ok());
}
#[test]
fn test_mark_history_item_as_failed_requires_history_item_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "mark-history-item-as-failed"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_mark_history_item_as_failed_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"mark-history-item-as-failed",
"--history-item-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_search_new_series_requires_query() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "search-new-series"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_search_new_series_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"search-new-series",
"--query",
"halo",
]);
assert!(result.is_ok());
}
#[test]
fn test_start_task_requires_task_name() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "start-task"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_start_task_task_name_validation() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"start-task",
"--task-name",
"test",
]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_start_task_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"start-task",
"--task-name",
"application-update-check",
]);
assert!(result.is_ok());
}
#[test]
fn test_test_indexer_requires_indexer_id() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "test-indexer"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_test_indexer_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"test-indexer",
"--indexer-id",
"1",
]);
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 {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::{
add_command_handler::SonarrAddCommand, delete_command_handler::SonarrDeleteCommand,
download_command_handler::SonarrDownloadCommand, edit_command_handler::SonarrEditCommand,
get_command_handler::SonarrGetCommand, list_command_handler::SonarrListCommand,
manual_search_command_handler::SonarrManualSearchCommand,
refresh_command_handler::SonarrRefreshCommand,
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand,
SonarrCliHandler, SonarrCommand,
},
CliCommandHandler,
},
models::{
sonarr_models::{
BlocklistItem, BlocklistResponse, IndexerSettings, Series, SonarrReleaseDownloadBody,
SonarrSerdeable, SonarrTaskName,
},
Serdeable,
},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_handle_clear_blocklist_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::GetBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::BlocklistResponse(
BlocklistResponse {
records: vec![BlocklistItem::default()],
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::ClearBlocklist.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let claer_blocklist_command = SonarrCommand::ClearBlocklist;
let result = SonarrCliHandler::with(&app_arc, claer_blocklist_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_mark_history_item_as_failed_command() {
let expected_history_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::MarkHistoryItemAsFailed(expected_history_item_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let mark_history_item_as_failed_command =
SonarrCommand::MarkHistoryItemAsFailed { history_item_id: 1 };
let result = SonarrCliHandler::with(
&app_arc,
mark_history_item_as_failed_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_add_commands_to_the_add_command_handler() {
let expected_tag_name = "test".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::AddTag(expected_tag_name.clone()).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let add_tag_command = SonarrCommand::Add(SonarrAddCommand::Tag {
name: expected_tag_name,
});
let result = SonarrCliHandler::with(&app_arc, add_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_delete_commands_to_the_delete_command_handler() {
let expected_blocklist_item_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteBlocklistItem(Some(expected_blocklist_item_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_blocklist_item_command =
SonarrCommand::Delete(SonarrDeleteCommand::BlocklistItem {
blocklist_item_id: 1,
});
let result =
SonarrCliHandler::with(&app_arc, delete_blocklist_item_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_download_commands_to_the_download_command_handler() {
let expected_params = SonarrReleaseDownloadBody {
guid: "1234".to_owned(),
indexer_id: 1,
series_id: Some(1),
..SonarrReleaseDownloadBody::default()
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DownloadRelease(expected_params).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let download_series_release_command =
SonarrCommand::Download(SonarrDownloadCommand::Series {
guid: "1234".to_owned(),
indexer_id: 1,
series_id: 1,
});
let result =
SonarrCliHandler::with(&app_arc, download_series_release_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_edit_commands_to_the_edit_command_handler() {
let expected_edit_all_indexer_settings = IndexerSettings {
id: 1,
maximum_size: 1,
minimum_age: 1,
retention: 1,
rss_sync_interval: 1,
};
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetAllIndexerSettings.into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::IndexerSettings(
IndexerSettings {
id: 1,
maximum_size: 2,
minimum_age: 2,
retention: 2,
rss_sync_interval: 2,
},
)))
});
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::EditAllIndexerSettings(Some(expected_edit_all_indexer_settings)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let edit_all_indexer_settings_command =
SonarrCommand::Edit(SonarrEditCommand::AllIndexerSettings {
maximum_size: Some(1),
minimum_age: Some(1),
retention: Some(1),
rss_sync_interval: Some(1),
});
let result = SonarrCliHandler::with(
&app_arc,
edit_all_indexer_settings_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_manual_search_commands_to_the_manual_search_command_handler(
) {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetEpisodeReleases(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let manual_episode_search_command =
SonarrCommand::ManualSearch(SonarrManualSearchCommand::Episode { episode_id: 1 });
let result =
SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_trigger_automatic_search_commands_to_the_trigger_automatic_search_command_handler(
) {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let manual_episode_search_command =
SonarrCommand::TriggerAutomaticSearch(SonarrTriggerAutomaticSearchCommand::Episode {
episode_id: 1,
});
let result =
SonarrCliHandler::with(&app_arc, manual_episode_search_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_get_commands_to_the_get_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::GetStatus.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let get_system_status_command = SonarrCommand::Get(SonarrGetCommand::SystemStatus);
let result = SonarrCliHandler::with(&app_arc, get_system_status_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_list_commands_to_the_list_command_handler() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::ListSeries.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::SeriesVec(vec![
Series::default(),
])))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let list_series_command = SonarrCommand::List(SonarrListCommand::Series);
let result = SonarrCliHandler::with(&app_arc, list_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sonarr_cli_handler_delegates_refresh_commands_to_the_refresh_command_handler() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::UpdateAndScanSeries(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let refresh_series_command =
SonarrCommand::Refresh(SonarrRefreshCommand::Series { series_id: 1 });
let result = SonarrCliHandler::with(&app_arc, refresh_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_search_new_series_command() {
let expected_search_query = "halo".to_owned();
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::SearchNewSeries(Some(expected_search_query)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let search_new_series_command = SonarrCommand::SearchNewSeries {
query: "halo".to_owned(),
};
let result = SonarrCliHandler::with(&app_arc, search_new_series_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_start_task_command() {
let expected_task_name = SonarrTaskName::ApplicationUpdateCheck;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::StartTask(Some(expected_task_name)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let start_task_command = SonarrCommand::StartTask {
task_name: SonarrTaskName::ApplicationUpdateCheck,
};
let result = SonarrCliHandler::with(&app_arc, start_task_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_test_indexer_command() {
let expected_indexer_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::TestIndexer(Some(expected_indexer_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let test_indexer_command = SonarrCommand::TestIndexer { indexer_id: 1 };
let result = SonarrCliHandler::with(&app_arc, test_indexer_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_test_all_indexers_command() {
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(SonarrEvent::TestAllIndexers.into()))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let test_all_indexers_command = SonarrCommand::TestAllIndexers;
let result = SonarrCliHandler::with(&app_arc, test_all_indexers_command, &mut mock_network)
.handle()
.await;
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(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::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(Some((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::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());
}
}
}
@@ -0,0 +1,113 @@
use std::sync::Arc;
use anyhow::Result;
use clap::Subcommand;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{CliCommandHandler, Command},
network::{sonarr_network::SonarrEvent, NetworkTrait},
};
use super::SonarrCommand;
#[cfg(test)]
#[path = "trigger_automatic_search_command_handler_tests.rs"]
mod trigger_automatic_search_command_handler_tests;
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SonarrTriggerAutomaticSearchCommand {
#[command(about = "Trigger an automatic search for the series with the specified ID")]
Series {
#[arg(
long,
help = "The ID of the series you want to trigger an automatic search for",
required = true
)]
series_id: i64,
},
#[command(
about = "Trigger an automatic search for the given season corresponding to the series with the given ID"
)]
Season {
#[arg(
long,
help = "The Sonarr ID of the series whose season you wish to trigger an automatic search for",
required = true
)]
series_id: i64,
#[arg(long, help = "The season number to search for", required = true)]
season_number: i64,
},
#[command(about = "Trigger an automatic search for the episode with the specified ID")]
Episode {
#[arg(
long,
help = "The ID of the episode you want to trigger an automatic search for",
required = true
)]
episode_id: i64,
},
}
impl From<SonarrTriggerAutomaticSearchCommand> for Command {
fn from(value: SonarrTriggerAutomaticSearchCommand) -> Self {
Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(value))
}
}
pub(super) struct SonarrTriggerAutomaticSearchCommandHandler<'a, 'b> {
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
}
impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrTriggerAutomaticSearchCommand>
for SonarrTriggerAutomaticSearchCommandHandler<'a, 'b>
{
fn with(
_app: &'a Arc<Mutex<App<'b>>>,
command: SonarrTriggerAutomaticSearchCommand,
network: &'a mut dyn NetworkTrait,
) -> Self {
SonarrTriggerAutomaticSearchCommandHandler {
_app,
command,
network,
}
}
async fn handle(self) -> Result<String> {
let result = match self.command {
SonarrTriggerAutomaticSearchCommand::Series { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::TriggerAutomaticSeriesSearch(Some(series_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrTriggerAutomaticSearchCommand::Season {
series_id,
season_number,
} => {
let resp = self
.network
.handle_network_event(
SonarrEvent::TriggerAutomaticSeasonSearch(Some((series_id, season_number))).into(),
)
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrTriggerAutomaticSearchCommand::Episode { episode_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::TriggerAutomaticEpisodeSearch(Some(episode_id)).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(result)
}
}
@@ -0,0 +1,259 @@
#[cfg(test)]
mod tests {
use crate::cli::{
sonarr::{
trigger_automatic_search_command_handler::SonarrTriggerAutomaticSearchCommand, SonarrCommand,
},
Command,
};
use crate::Cli;
use clap::CommandFactory;
use pretty_assertions::assert_eq;
#[test]
fn test_sonarr_trigger_automatic_search_command_from() {
let command = SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
let result = Command::from(command.clone());
assert_eq!(
result,
Command::Sonarr(SonarrCommand::TriggerAutomaticSearch(command))
);
}
mod cli {
use super::*;
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
#[test]
fn test_trigger_automatic_series_search_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"series",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_series_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"series",
"--series-id",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_trigger_automatic_season_search_requires_series_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"season",
"--season-number",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_season_search_requires_season_number() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"season",
"--series-id",
"1",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_season_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"season",
"--series-id",
"1",
"--season-number",
"1",
]);
assert!(result.is_ok());
}
#[test]
fn test_trigger_automatic_episode_search_requires_episode_id() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"episode",
]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_trigger_automatic_episode_search_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"trigger-automatic-search",
"episode",
"--episode-id",
"1",
]);
assert!(result.is_ok());
}
}
mod handler {
use std::sync::Arc;
use mockall::predicate::eq;
use serde_json::json;
use tokio::sync::Mutex;
use crate::{
app::App,
cli::{
sonarr::trigger_automatic_search_command_handler::{
SonarrTriggerAutomaticSearchCommand, SonarrTriggerAutomaticSearchCommandHandler,
},
CliCommandHandler,
},
models::{sonarr_models::SonarrSerdeable, Serdeable},
network::{sonarr_network::SonarrEvent, MockNetworkTrait, NetworkEvent},
};
#[tokio::test]
async fn test_trigger_automatic_series_search_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticSeriesSearch(Some(expected_series_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let trigger_automatic_series_search_command =
SonarrTriggerAutomaticSearchCommand::Series { series_id: 1 };
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_series_search_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_trigger_automatic_season_search_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::TriggerAutomaticSeasonSearch(Some((
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::default()));
let trigger_automatic_season_search_command = SonarrTriggerAutomaticSearchCommand::Season {
series_id: 1,
season_number: 1,
};
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_season_search_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_trigger_automatic_episode_search_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::TriggerAutomaticEpisodeSearch(Some(expected_episode_id)).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let trigger_automatic_episode_search_command =
SonarrTriggerAutomaticSearchCommand::Episode { episode_id: 1 };
let result = SonarrTriggerAutomaticSearchCommandHandler::with(
&app_arc,
trigger_automatic_episode_search_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+7
View File
@@ -19,6 +19,7 @@ pub enum Key {
Home,
End,
Tab,
BackTab,
Delete,
Ctrl(char),
Char(char),
@@ -40,6 +41,7 @@ impl Display for Key {
Key::Home => write!(f, "<home>"),
Key::End => write!(f, "<end>"),
Key::Tab => write!(f, "<tab>"),
Key::BackTab => write!(f, "<shift-tab>"),
Key::Delete => write!(f, "<del>"),
_ => write!(f, "<{self:?}>"),
}
@@ -75,6 +77,11 @@ impl From<KeyEvent> for Key {
KeyEvent {
code: KeyCode::End, ..
} => Key::End,
KeyEvent {
code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT,
..
} => Key::BackTab,
KeyEvent {
code: KeyCode::Tab, ..
} => Key::Tab,
+14
View File
@@ -17,6 +17,7 @@ mod tests {
#[case(Key::Home, "home")]
#[case(Key::End, "end")]
#[case(Key::Tab, "tab")]
#[case(Key::BackTab, "shift-tab")]
#[case(Key::Delete, "del")]
#[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "ctrl-q")]
@@ -67,6 +68,19 @@ mod tests {
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]
fn test_key_from_delete() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Delete)), Key::Delete);
+195 -68
View File
@@ -99,86 +99,96 @@ mod test_utils {
#[macro_export]
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]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default();
app.push_navigation_stack($block.into());
app
.data
.radarr_data
.$servarr_data
.$data_ref
.set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]);
$handler::with(&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();
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]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default();
app.push_navigation_stack($block.into());
app
.data
.radarr_data
.$servarr_data
.$data_ref
.set_items(simple_stateful_iterable_vec!($items));
$handler::with(&key, &mut app, &$block, &$context).handle();
$handler::with(key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 2"
);
$handler::with(&key, &mut app, &$block, &$context).handle();
$handler::with(key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"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]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::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::with(key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 2"
);
$handler::with(&key, &mut app, &$block, &$context).handle();
$handler::with(key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"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]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::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::with(key, &mut app, $block, $context).handle();
assert_str_eq!(
pretty_assertions::assert_str_eq!(
app
.data
.radarr_data
.$servarr_data
.$data_ref
.current_selection()
.$field
@@ -186,12 +196,12 @@ mod test_utils {
"Test 2"
);
$handler::with(&key, &mut app, &$block, &$context).handle();
$handler::with(key, &mut app, $block, $context).handle();
assert_str_eq!(
pretty_assertions::assert_str_eq!(
app
.data
.radarr_data
.$servarr_data
.$data_ref
.current_selection()
.$field
@@ -204,86 +214,96 @@ mod test_utils {
#[macro_export]
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]
fn $func() {
let mut app = App::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 2".to_owned(),
"Test 3".to_owned(),
]);
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle();
$handler::with(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::with(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]
fn $func() {
let mut app = App::default();
app.push_navigation_stack($block.into());
app
.data
.radarr_data
.$servarr_data
.$data_ref
.set_items(extended_stateful_iterable_vec!($items));
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle();
$handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 3"
);
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle();
$handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"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]
fn $func() {
let mut app = App::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::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"Test 3"
);
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle();
$handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
pretty_assertions::assert_str_eq!(
app.data.$servarr_data.$data_ref.current_selection().$field,
"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]
fn $func() {
let mut app = App::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::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle();
assert_str_eq!(
pretty_assertions::assert_str_eq!(
app
.data
.radarr_data
.$servarr_data
.$data_ref
.current_selection()
.$field
@@ -291,12 +311,12 @@ mod test_utils {
"Test 3"
);
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle();
$handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle();
assert_str_eq!(
pretty_assertions::assert_str_eq!(
app
.data
.radarr_data
.$servarr_data
.$data_ref
.current_selection()
.$field
@@ -311,18 +331,125 @@ mod test_utils {
macro_rules! test_handler_delegation {
($handler:ident, $base:expr, $active_block:expr) => {
let mut app = App::default();
app.push_navigation_stack($base.clone().into());
app.push_navigation_stack($active_block.clone().into());
app.data.sonarr_data.history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app
.data
.sonarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.sonarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.sonarr_data
.blocklist
.set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]);
app.data.sonarr_data.add_searched_series =
Some($crate::models::stateful_table::StatefulTable::default());
app
.data
.radarr_data
.movies
.set_items(vec![$crate::models::radarr_models::Movie::default()]);
app
.data
.radarr_data
.collections
.set_items(vec![$crate::models::radarr_models::Collection::default()]);
app.data.radarr_data.collection_movies.set_items(vec![
$crate::models::radarr_models::CollectionMovie::default(),
]);
app
.data
.radarr_data
.indexers
.set_items(vec![$crate::models::servarr_models::Indexer::default()]);
app
.data
.radarr_data
.root_folders
.set_items(vec![$crate::models::servarr_models::RootFolder::default()]);
app
.data
.radarr_data
.blocklist
.set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]);
app.data.radarr_data.add_searched_movies =
Some($crate::models::stateful_table::StatefulTable::default());
let mut movie_details_modal =
$crate::models::servarr_data::radarr::modals::MovieDetailsModal::default();
movie_details_modal.movie_history.set_items(vec![
$crate::models::radarr_models::MovieHistoryItem::default(),
]);
movie_details_modal
.movie_cast
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_crew
.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal
.movie_releases
.set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]);
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
let mut season_details_modal =
$crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default();
season_details_modal.season_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
season_details_modal.episode_details_modal =
Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default());
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
let mut series_history = $crate::models::stateful_table::StatefulTable::default();
series_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app.data.sonarr_data.series_history = Some(series_history);
app
.data
.sonarr_data
.series
.set_items(vec![$crate::models::sonarr_models::Series::default()]);
app.push_navigation_stack($base.into());
app.push_navigation_stack($active_block.into());
$handler::with(
&DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&$active_block,
&None,
)
.handle();
$handler::with(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::default();
$handler::with(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::with(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::default();
app.push_navigation_stack($block.into());
$handler::with(DEFAULT_KEYBINDINGS.refresh.key, &mut app, $block, None).handle();
pretty_assertions::assert_eq!(app.get_current_route(), $block.into());
assert!(app.should_refresh);
};
}
}
+82 -3
View File
@@ -1,10 +1,19 @@
#[cfg(test)]
mod tests {
use crate::models::radarr_models::Movie;
use crate::models::sonarr_models::Series;
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::handle_events;
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]
fn test_handle_clear_errors() {
@@ -17,17 +26,87 @@ mod tests {
}
#[rstest]
fn test_handle_prompt_toggle_left_right(#[values(Key::Left, Key::Right)] key: Key) {
#[case(ActiveRadarrBlock::Movies.into(), ActiveRadarrBlock::SearchMovie.into())]
#[case(ActiveSonarrBlock::Series.into(), ActiveSonarrBlock::SearchSeries.into())]
fn test_handle_events(#[case] base_block: Route, #[case] top_block: Route) {
let mut app = App::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::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());
app.server_tabs.set_index(index);
app.is_first_render = false;
app.error = "Test".into();
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());
}
#[rstest]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
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);
handle_prompt_toggle(&mut app, &key);
handle_prompt_toggle(&mut app, key);
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::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);
}
}
+60 -22
View File
@@ -1,4 +1,5 @@
use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
@@ -6,6 +7,7 @@ use crate::event::Key;
use crate::models::{HorizontallyScrollableText, Route};
mod radarr_handlers;
mod sonarr_handlers;
#[cfg(test)]
#[path = "handlers_tests.rs"]
@@ -14,45 +16,46 @@ mod handlers_tests;
#[cfg(test)]
#[path = "handler_test_utils.rs"]
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) {
let key = self.get_key();
match key {
_ if *key == DEFAULT_KEYBINDINGS.up.key => {
_ if key == DEFAULT_KEYBINDINGS.up.key => {
if self.is_ready() {
self.handle_scroll_up();
}
}
_ if *key == DEFAULT_KEYBINDINGS.down.key => {
_ if key == DEFAULT_KEYBINDINGS.down.key => {
if self.is_ready() {
self.handle_scroll_down();
}
}
_ if *key == DEFAULT_KEYBINDINGS.home.key => {
_ if key == DEFAULT_KEYBINDINGS.home.key => {
if self.is_ready() {
self.handle_home();
}
}
_ if *key == DEFAULT_KEYBINDINGS.end.key => {
_ if key == DEFAULT_KEYBINDINGS.end.key => {
if self.is_ready() {
self.handle_end();
}
}
_ if *key == DEFAULT_KEYBINDINGS.delete.key => {
_ if key == DEFAULT_KEYBINDINGS.delete.key => {
if self.is_ready() {
self.handle_delete();
}
}
_ if *key == DEFAULT_KEYBINDINGS.left.key || *key == DEFAULT_KEYBINDINGS.right.key => {
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => {
self.handle_left_right_action()
}
_ if *key == DEFAULT_KEYBINDINGS.submit.key => {
_ if key == DEFAULT_KEYBINDINGS.submit.key => {
if self.is_ready() {
self.handle_submit();
}
}
_ if *key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(),
_ if key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(),
_ => {
if self.is_ready() {
self.handle_char_key_event();
@@ -65,9 +68,9 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route>> {
self.handle_key_event();
}
fn accepts(active_block: &'a T) -> bool;
fn with(key: &'a Key, app: &'a mut App<'b>, active_block: &'a T, context: &'a Option<T>) -> Self;
fn get_key(&self) -> &Key;
fn accepts(active_block: T) -> bool;
fn with(key: Key, app: &'a mut App<'b>, active_block: T, context: Option<T>) -> Self;
fn get_key(&self) -> Key;
fn is_ready(&self) -> bool;
fn handle_scroll_up(&mut self);
fn handle_scroll_down(&mut self);
@@ -81,8 +84,24 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route>> {
}
pub fn handle_events(key: Key, app: &mut App<'_>) {
if let Route::Radarr(active_radarr_block, context) = *app.get_current_route() {
RadarrHandler::with(&key, app, &active_radarr_block, &context).handle()
if key == DEFAULT_KEYBINDINGS.next_servarr.key {
app.reset();
app.server_tabs.next();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
} else if key == DEFAULT_KEYBINDINGS.previous_servarr.key {
app.reset();
app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
} else {
match app.get_current_route() {
Route::Radarr(active_radarr_block, context) => {
RadarrHandler::with(key, app, active_radarr_block, context).handle()
}
Route::Sonarr(active_sonarr_block, context) => {
SonarrHandler::with(key, app, active_sonarr_block, context).handle()
}
_ => (),
}
}
}
@@ -92,11 +111,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 {
_ if *key == DEFAULT_KEYBINDINGS.left.key || *key == DEFAULT_KEYBINDINGS.right.key => {
if let Route::Radarr(_, _) = *app.get_current_route() {
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm;
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => {
match app.get_current_route() {
Route::Radarr(_, _) => {
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 +132,10 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: &Key) {
macro_rules! handle_text_box_left_right_keys {
($self:expr, $key:expr, $input:expr) => {
match $self.key {
_ if *$key == DEFAULT_KEYBINDINGS.left.key => {
_ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key => {
$input.scroll_left();
}
_ if *$key == DEFAULT_KEYBINDINGS.right.key => {
_ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.right.key => {
$input.scroll_right();
}
_ => (),
@@ -122,13 +147,26 @@ macro_rules! handle_text_box_left_right_keys {
macro_rules! handle_text_box_keys {
($self:expr, $key:expr, $input:expr) => {
match $self.key {
_ if *$key == DEFAULT_KEYBINDINGS.backspace.key => {
_ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.backspace.key => {
$input.pop();
}
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 $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key {
$self.app.data.$data.selected_block.left();
} else {
$self.app.data.$data.selected_block.right();
}
};
}
@@ -11,250 +11,9 @@ mod tests {
use crate::event::Key;
use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::{
BlocklistItem, BlocklistItemMovie, Language, Quality, QualityWrapper,
};
use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
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]
);
}
}
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
mod test_handle_delete {
use pretty_assertions::assert_eq;
@@ -268,11 +27,11 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
BlocklistHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()
ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()
);
}
@@ -283,12 +42,9 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
BlocklistHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
}
}
@@ -305,21 +61,18 @@ mod tests {
app.data.radarr_data.main_tabs.set_index(3);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
DEFAULT_KEYBINDINGS.left.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
}
#[rstest]
@@ -329,20 +82,20 @@ mod tests {
app.data.radarr_data.main_tabs.set_index(3);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::RootFolders.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::RootFolders.into()
);
}
@@ -357,11 +110,11 @@ mod tests {
) {
let mut app = App::default();
BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle();
BlocklistHandler::with(key, &mut app, active_radarr_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm);
BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle();
BlocklistHandler::with(key, &mut app, active_radarr_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -383,11 +136,11 @@ mod tests {
app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
BlocklistHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::BlocklistItemDetails.into()
ActiveRadarrBlock::BlocklistItemDetails.into()
);
}
@@ -398,12 +151,9 @@ mod tests {
app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
BlocklistHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
@@ -428,14 +178,14 @@ mod tests {
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle();
BlocklistHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
assert_eq!(app.get_current_route(), base_route.into());
}
#[rstest]
@@ -451,42 +201,11 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle();
BlocklistHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(
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);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
}
}
@@ -518,9 +237,9 @@ mod tests {
app.push_navigation_stack(prompt_block.into());
app.data.radarr_data.prompt_confirm = true;
BlocklistHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle();
BlocklistHandler::with(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);
}
@@ -531,37 +250,14 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into());
BlocklistHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::BlocklistItemDetails,
&None,
ActiveRadarrBlock::BlocklistItemDetails,
None,
)
.handle();
assert_eq!(
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()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
@@ -572,12 +268,9 @@ mod tests {
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::with(ESC_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert!(app.error.text.is_empty());
}
}
@@ -597,17 +290,14 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert!(app.should_refresh);
}
@@ -619,17 +309,14 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
assert!(!app.should_refresh);
}
@@ -639,16 +326,16 @@ mod tests {
app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.clear.key,
DEFAULT_KEYBINDINGS.clear.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()
ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()
);
}
@@ -660,64 +347,14 @@ mod tests {
app.data.radarr_data.blocklist.set_items(blocklist_vec());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.clear.key,
DEFAULT_KEYBINDINGS.clear.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
)
.handle();
assert_eq!(
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);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
@@ -742,10 +379,10 @@ mod tests {
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
prompt_block,
None,
)
.handle();
@@ -754,7 +391,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
assert_eq!(app.get_current_route(), base_route.into());
}
}
@@ -897,9 +534,9 @@ mod tests {
fn test_blocklist_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if BLOCKLIST_BLOCKS.contains(&active_radarr_block) {
assert!(BlocklistHandler::accepts(&active_radarr_block));
assert!(BlocklistHandler::accepts(active_radarr_block));
} else {
assert!(!BlocklistHandler::accepts(&active_radarr_block));
assert!(!BlocklistHandler::accepts(active_radarr_block));
}
})
}
@@ -910,10 +547,10 @@ mod tests {
app.is_loading = true;
let handler = BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
);
assert!(!handler.is_ready());
@@ -925,10 +562,10 @@ mod tests {
app.is_loading = false;
let handler = BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
);
assert!(!handler.is_ready());
@@ -945,10 +582,10 @@ mod tests {
.set_items(vec![BlocklistItem::default()]);
let handler = BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
ActiveRadarrBlock::Blocklist,
None,
);
assert!(handler.is_ready());
@@ -960,6 +597,7 @@ mod tests {
id: 3,
source_title: "test 1".to_owned(),
languages: vec![Language {
id: 1,
name: "telgu".to_owned(),
}],
quality: QualityWrapper {
@@ -968,6 +606,7 @@ mod tests {
},
},
custom_formats: Some(vec![Language {
id: 2,
name: "nikki".to_owned(),
}]),
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
@@ -980,6 +619,7 @@ mod tests {
id: 2,
source_title: "test 2".to_owned(),
languages: vec![Language {
id: 3,
name: "chinese".to_owned(),
}],
quality: QualityWrapper {
@@ -989,9 +629,11 @@ mod tests {
},
custom_formats: Some(vec![
Language {
id: 4,
name: "alex".to_owned(),
},
Language {
id: 5,
name: "English".to_owned(),
},
]),
@@ -1005,6 +647,7 @@ mod tests {
id: 1,
source_title: "test 3".to_owned(),
languages: vec![Language {
id: 1,
name: "english".to_owned(),
}],
quality: QualityWrapper {
@@ -1013,6 +656,7 @@ mod tests {
},
},
custom_formats: Some(vec![Language {
id: 2,
name: "English".to_owned(),
}]),
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
@@ -1023,15 +667,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())
}),
}]
}
}
+42 -99
View File
@@ -1,12 +1,13 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
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::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption;
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
#[cfg(test)]
@@ -14,22 +15,43 @@ use crate::network::radarr_network::RadarrEvent;
mod blocklist_handler_tests;
pub(super) struct BlocklistHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> BlocklistHandler<'a, 'b> {
handle_table_events!(
self,
blocklist,
self.app.data.radarr_data.blocklist,
BlocklistItem
);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
BLOCKLIST_BLOCKS.contains(active_block)
fn handle(&mut self) {
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 accepts(active_block: ActiveRadarrBlock) -> bool {
BLOCKLIST_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
) -> Self {
BlocklistHandler {
key,
@@ -39,7 +61,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -47,72 +69,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
!self.app.is_loading && !self.app.data.radarr_data.blocklist.is_empty()
}
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_up(&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_scroll_down(&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_home(&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_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist {
if self.active_radarr_block == ActiveRadarrBlock::Blocklist {
self
.app
.push_navigation_stack(ActiveRadarrBlock::DeleteBlocklistItemPrompt.into());
@@ -145,18 +111,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
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 => {
self
.app
@@ -173,7 +127,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false;
}
ActiveRadarrBlock::BlocklistItemDetails | ActiveRadarrBlock::BlocklistSortPrompt => {
ActiveRadarrBlock::BlocklistItemDetails => {
self.app.pop_navigation_stack();
}
_ => handle_clear_errors(self.app),
@@ -184,29 +138,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
_ if *key == DEFAULT_KEYBINDINGS.clear.key => {
_ if key == DEFAULT_KEYBINDINGS.clear.key => {
self
.app
.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 => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteBlocklistItem(None));
@@ -215,7 +158,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
}
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
@@ -1,35 +1,56 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::CollectionMovie;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Scrollable};
use crate::models::BlockSelectionState;
#[cfg(test)]
#[path = "collection_details_handler_tests.rs"]
mod collection_details_handler_tests;
pub(super) struct CollectionDetailsHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> CollectionDetailsHandler<'a, 'b> {
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> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
COLLECTION_DETAILS_BLOCKS.contains(active_block)
fn handle(&mut self) {
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 accepts(active_block: ActiveRadarrBlock) -> bool {
COLLECTION_DETAILS_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
) -> CollectionDetailsHandler<'a, 'b> {
CollectionDetailsHandler {
key,
@@ -39,7 +60,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -47,41 +68,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
!self.app.is_loading && !self.app.data.radarr_data.collection_movies.is_empty()
}
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_up(&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_scroll_down(&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_home(&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_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&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
.app
.data
@@ -111,7 +111,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
.into(),
);
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());
}
}
@@ -129,19 +129,19 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
}
fn handle_char_key_event(&mut self) {
if *self.active_radarr_block == ActiveRadarrBlock::CollectionDetails
&& *self.key == DEFAULT_KEYBINDINGS.edit.key
if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails
&& self.key == DEFAULT_KEYBINDINGS.edit.key
{
self.app.push_navigation_stack(
(
ActiveRadarrBlock::EditCollectionPrompt,
Some(*self.active_radarr_block),
Some(self.active_radarr_block),
)
.into(),
);
self.app.data.radarr_data.edit_collection_modal = Some((&self.app.data.radarr_data).into());
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
}
}
}
@@ -12,142 +12,6 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{
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 {
use bimap::BiMap;
@@ -171,24 +35,24 @@ mod tests {
.set_items(vec![CollectionMovie::default()]);
app.data.radarr_data.quality_profile_map =
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
.data
.radarr_data
.selected_block
.set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1);
.set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1);
CollectionDetailsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&(
(
ActiveRadarrBlock::AddMoviePrompt,
Some(ActiveRadarrBlock::CollectionDetails)
)
@@ -205,7 +69,7 @@ mod tests {
.is_empty());
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::AddMovieSelectRootFolder
ActiveRadarrBlock::AddMovieSelectRootFolder
);
assert!(!app
.data
@@ -250,16 +114,16 @@ mod tests {
.set_items(vec![CollectionMovie::default()]);
CollectionDetailsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::CollectionDetails.into()
ActiveRadarrBlock::CollectionDetails.into()
);
assert!(app.data.radarr_data.add_movie_modal.is_none());
}
@@ -279,16 +143,16 @@ mod tests {
.set_items(vec![Movie::default()]);
CollectionDetailsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::ViewMovieOverview.into()
ActiveRadarrBlock::ViewMovieOverview.into()
);
}
}
@@ -313,16 +177,16 @@ mod tests {
.set_items(vec![CollectionMovie::default()]);
CollectionDetailsHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
assert!(app.data.radarr_data.collection_movies.items.is_empty());
}
@@ -334,16 +198,16 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into());
CollectionDetailsHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::ViewMovieOverview,
&None,
ActiveRadarrBlock::ViewMovieOverview,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::CollectionDetails.into()
ActiveRadarrBlock::CollectionDetails.into()
);
}
}
@@ -367,7 +231,7 @@ mod tests {
test_edit_collection_key!(
CollectionDetailsHandler,
ActiveRadarrBlock::CollectionDetails,
ActiveRadarrBlock::CollectionDetails
Some(ActiveRadarrBlock::CollectionDetails)
);
}
@@ -388,16 +252,16 @@ mod tests {
app.data.radarr_data = radarr_data;
CollectionDetailsHandler::with(
&DEFAULT_KEYBINDINGS.edit.key,
DEFAULT_KEYBINDINGS.edit.key,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::CollectionDetails.into()
ActiveRadarrBlock::CollectionDetails.into()
);
assert!(app.data.radarr_data.edit_collection_modal.is_none());
}
@@ -407,9 +271,9 @@ mod tests {
fn test_collection_details_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block) {
assert!(CollectionDetailsHandler::accepts(&active_radarr_block));
assert!(CollectionDetailsHandler::accepts(active_radarr_block));
} else {
assert!(!CollectionDetailsHandler::accepts(&active_radarr_block));
assert!(!CollectionDetailsHandler::accepts(active_radarr_block));
}
});
}
@@ -420,10 +284,10 @@ mod tests {
app.is_loading = true;
let handler = CollectionDetailsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
);
assert!(!handler.is_ready());
@@ -435,10 +299,10 @@ mod tests {
app.is_loading = false;
let handler = CollectionDetailsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
);
assert!(!handler.is_ready());
@@ -455,10 +319,10 @@ mod tests {
.set_items(vec![CollectionMovie::default()]);
let handler = CollectionDetailsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::CollectionDetails,
&None,
ActiveRadarrBlock::CollectionDetails,
None,
);
assert!(handler.is_ready());
File diff suppressed because it is too large Load Diff
@@ -12,22 +12,22 @@ use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
mod edit_collection_handler_tests;
pub(super) struct EditCollectionHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
EDIT_COLLECTION_BLOCKS.contains(active_block)
fn accepts(active_block: ActiveRadarrBlock) -> bool {
EDIT_COLLECTION_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
) -> EditCollectionHandler<'a, 'b> {
EditCollectionHandler {
key,
@@ -37,7 +37,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -65,9 +65,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
.unwrap()
.quality_profile_list
.scroll_up(),
ActiveRadarrBlock::EditCollectionPrompt => {
self.app.data.radarr_data.selected_block.previous()
}
ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.up(),
_ => (),
}
}
@@ -92,7 +90,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
.unwrap()
.quality_profile_list
.scroll_down(),
ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.next(),
ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.down(),
_ => (),
}
}
@@ -203,8 +201,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
| ActiveRadarrBlock::EditCollectionSelectQualityProfile => {
self.app.push_navigation_stack(
(
*self.app.data.radarr_data.selected_block.get_active_block(),
*self.context,
self.app.data.radarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
)
@@ -212,8 +210,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.push_navigation_stack(
(
*self.app.data.radarr_data.selected_block.get_active_block(),
*self.context,
self.app.data.radarr_data.selected_block.get_active_block(),
self.context,
)
.into(),
);
@@ -308,8 +306,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
}
ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditCollectionConfirmPrompt
&& *key == DEFAULT_KEYBINDINGS.confirm.key
== ActiveRadarrBlock::EditCollectionConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None));
@@ -44,10 +44,10 @@ mod tests {
if key == Key::Up {
for i in (0..minimum_availability_vec.len()).rev() {
EditCollectionHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
&None,
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
None,
)
.handle();
@@ -66,10 +66,10 @@ mod tests {
} else {
for i in 0..minimum_availability_vec.len() {
EditCollectionHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
&None,
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
None,
)
.handle();
@@ -104,10 +104,10 @@ mod tests {
.set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]);
EditCollectionHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectQualityProfile,
&None,
ActiveRadarrBlock::EditCollectionSelectQualityProfile,
None,
)
.handle();
@@ -124,10 +124,10 @@ mod tests {
);
EditCollectionHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectQualityProfile,
&None,
ActiveRadarrBlock::EditCollectionSelectQualityProfile,
None,
)
.handle();
@@ -149,26 +149,21 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.next();
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.down();
EditCollectionHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
)
.handle();
EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None)
.handle();
if key == Key::Up {
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::EditCollectionToggleMonitored
ActiveRadarrBlock::EditCollectionToggleMonitored
);
} else {
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::EditCollectionSelectQualityProfile
ActiveRadarrBlock::EditCollectionSelectQualityProfile
);
}
}
@@ -181,20 +176,15 @@ mod tests {
app.is_loading = true;
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.next();
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.down();
EditCollectionHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
)
.handle();
EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None)
.handle();
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::EditCollectionSelectMinimumAvailability
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability
);
}
}
@@ -224,10 +214,10 @@ mod tests {
.set_items(minimum_availability_vec.clone());
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
&None,
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
None,
)
.handle();
@@ -244,10 +234,10 @@ mod tests {
);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
&None,
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
None,
)
.handle();
@@ -282,10 +272,10 @@ mod tests {
]);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectQualityProfile,
&None,
ActiveRadarrBlock::EditCollectionSelectQualityProfile,
None,
)
.handle();
@@ -302,10 +292,10 @@ mod tests {
);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::EditCollectionSelectQualityProfile,
&None,
ActiveRadarrBlock::EditCollectionSelectQualityProfile,
None,
)
.handle();
@@ -331,10 +321,10 @@ mod tests {
});
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -352,10 +342,10 @@ mod tests {
);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -386,23 +376,13 @@ mod tests {
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::default();
EditCollectionHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
)
.handle();
EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
EditCollectionHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
)
.handle();
EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None)
.handle();
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -416,10 +396,10 @@ mod tests {
});
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
DEFAULT_KEYBINDINGS.left.key,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -437,10 +417,10 @@ mod tests {
);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -484,10 +464,10 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into());
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -503,7 +483,7 @@ mod tests {
.is_empty());
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditCollectionPrompt.into()
ActiveRadarrBlock::EditCollectionPrompt.into()
);
}
@@ -514,24 +494,24 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
.set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
}
@@ -544,24 +524,24 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.data.radarr_data.prompt_confirm = true;
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
.set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
@@ -579,24 +559,24 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.data.radarr_data.prompt_confirm = true;
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
.set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditCollectionPrompt.into()
ActiveRadarrBlock::EditCollectionPrompt.into()
);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert!(!app.should_refresh);
@@ -611,18 +591,18 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app.push_navigation_stack(current_route);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&Some(ActiveRadarrBlock::Collections),
ActiveRadarrBlock::EditCollectionPrompt,
Some(ActiveRadarrBlock::Collections),
)
.handle();
assert_eq!(app.get_current_route(), &current_route);
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
app
.data
@@ -635,14 +615,14 @@ mod tests {
);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&Some(ActiveRadarrBlock::Collections),
ActiveRadarrBlock::EditCollectionPrompt,
Some(ActiveRadarrBlock::Collections),
)
.handle();
assert_eq!(app.get_current_route(), &current_route);
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
app
.data
@@ -664,23 +644,23 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 2);
.set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 2);
app.push_navigation_stack(current_route);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&Some(ActiveRadarrBlock::Collections),
ActiveRadarrBlock::EditCollectionPrompt,
Some(ActiveRadarrBlock::Collections),
)
.handle();
assert_eq!(app.get_current_route(), &current_route);
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
app
.data
@@ -693,14 +673,14 @@ mod tests {
);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&Some(ActiveRadarrBlock::Collections),
ActiveRadarrBlock::EditCollectionPrompt,
Some(ActiveRadarrBlock::Collections),
)
.handle();
assert_eq!(app.get_current_route(), &current_route);
assert_eq!(app.get_current_route(), current_route);
assert_eq!(
app
.data
@@ -731,20 +711,20 @@ mod tests {
.into(),
);
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(index);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(0, index);
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&Some(ActiveRadarrBlock::Collections),
ActiveRadarrBlock::EditCollectionPrompt,
Some(ActiveRadarrBlock::Collections),
)
.handle();
assert_eq!(
app.get_current_route(),
&(selected_block, Some(ActiveRadarrBlock::Collections)).into()
(selected_block, Some(ActiveRadarrBlock::Collections)).into()
);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
@@ -768,16 +748,16 @@ mod tests {
app.push_navigation_stack(active_radarr_block.into());
EditCollectionHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&active_radarr_block,
&Some(ActiveRadarrBlock::Collections),
active_radarr_block,
Some(ActiveRadarrBlock::Collections),
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditCollectionPrompt.into()
ActiveRadarrBlock::EditCollectionPrompt.into()
);
if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
@@ -806,17 +786,17 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into());
EditCollectionHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
assert!(!app.should_ignore_quit_key);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditCollectionPrompt.into()
ActiveRadarrBlock::EditCollectionPrompt.into()
);
}
@@ -828,16 +808,16 @@ mod tests {
app.data.radarr_data = create_test_radarr_data();
EditCollectionHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
let radarr_data = &app.data.radarr_data;
@@ -860,11 +840,11 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(active_radarr_block.into());
EditCollectionHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle();
EditCollectionHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
}
}
@@ -890,10 +870,10 @@ mod tests {
});
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
DEFAULT_KEYBINDINGS.backspace.key,
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -916,10 +896,10 @@ mod tests {
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
EditCollectionHandler::with(
&Key::Char('h'),
Key::Char('h'),
&mut app,
&ActiveRadarrBlock::EditCollectionRootFolderPathInput,
&None,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
)
.handle();
@@ -943,24 +923,24 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
.set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1);
EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
@@ -974,9 +954,9 @@ mod tests {
fn test_edit_collection_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block) {
assert!(EditCollectionHandler::accepts(&active_radarr_block));
assert!(EditCollectionHandler::accepts(active_radarr_block));
} else {
assert!(!EditCollectionHandler::accepts(&active_radarr_block));
assert!(!EditCollectionHandler::accepts(active_radarr_block));
}
});
}
@@ -987,10 +967,10 @@ mod tests {
app.is_loading = true;
let handler = EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
);
assert!(!handler.is_ready());
@@ -1002,10 +982,10 @@ mod tests {
app.is_loading = false;
let handler = EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
);
assert!(!handler.is_ready());
@@ -1018,10 +998,10 @@ mod tests {
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
let handler = EditCollectionHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::EditCollectionPrompt,
&None,
ActiveRadarrBlock::EditCollectionPrompt,
None,
);
assert!(handler.is_ready());
+58 -274
View File
@@ -1,18 +1,19 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
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::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::Collection;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable};
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
mod collection_details_handler;
mod edit_collection_handler;
@@ -22,38 +23,66 @@ mod edit_collection_handler;
mod collections_handler_tests;
pub(super) struct CollectionsHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> CollectionsHandler<'a, 'b> {
handle_table_events!(
self,
collections,
self.app.data.radarr_data.collections,
Collection
);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'a, 'b> {
fn handle(&mut self) {
match self.active_radarr_block {
_ if CollectionDetailsHandler::accepts(self.active_radarr_block) => {
CollectionDetailsHandler::with(self.key, self.app, self.active_radarr_block, self.context)
let collections_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Collections.into())
.sorting_block(ActiveRadarrBlock::CollectionsSortPrompt.into())
.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::with(
self.key,
self.app,
self.active_radarr_block,
self.context,
)
.handle();
}
_ if EditCollectionHandler::accepts(self.active_radarr_block) => {
EditCollectionHandler::with(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)
|| EditCollectionHandler::accepts(active_block)
|| COLLECTIONS_BLOCKS.contains(active_block)
|| COLLECTIONS_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
) -> CollectionsHandler<'a, 'b> {
CollectionsHandler {
key,
@@ -63,7 +92,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -71,105 +100,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
!self.app.is_loading && !self.app.data.radarr_data.collections.is_empty()
}
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_up(&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_scroll_down(&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_home(&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_end(&mut self) {}
fn handle_delete(&mut self) {}
@@ -177,34 +114,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
match self.active_radarr_block {
ActiveRadarrBlock::Collections => handle_change_tab_left_right_keys(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 +123,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
ActiveRadarrBlock::Collections => self
.app
.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 => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -259,44 +130,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
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) {
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 => {
self.app.pop_navigation_stack();
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);
}
}
@@ -306,87 +150,27 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Collections => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.search.key => {
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::SearchCollection.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(),
);
.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
self.app.data.radarr_data.edit_collection_modal =
Some((&self.app.data.radarr_data).into());
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 key == DEFAULT_KEYBINDINGS.update.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
}
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
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 => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -1,6 +1,5 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -11,109 +10,6 @@ mod tests {
use crate::models::radarr_models::DownloadRecord;
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 {
use pretty_assertions::assert_eq;
@@ -130,11 +26,11 @@ mod tests {
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle();
DownloadsHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::DeleteDownloadPrompt.into()
ActiveRadarrBlock::DeleteDownloadPrompt.into()
);
}
@@ -149,12 +45,9 @@ mod tests {
.downloads
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle();
DownloadsHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
}
}
@@ -171,20 +64,20 @@ mod tests {
app.data.radarr_data.main_tabs.set_index(2);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
DEFAULT_KEYBINDINGS.left.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Collections.into()
);
}
@@ -195,21 +88,18 @@ mod tests {
app.data.radarr_data.main_tabs.set_index(2);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into());
}
#[rstest]
@@ -223,11 +113,11 @@ mod tests {
) {
let mut app = App::default();
DownloadsHandler::with(&key, &mut app, &active_radarr_block, &None).handle();
DownloadsHandler::with(key, &mut app, active_radarr_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm);
DownloadsHandler::with(&key, &mut app, &active_radarr_block, &None).handle();
DownloadsHandler::with(key, &mut app, active_radarr_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -269,14 +159,14 @@ mod tests {
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle();
DownloadsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
assert_eq!(app.get_current_route(), base_route.into());
}
#[rstest]
@@ -295,11 +185,11 @@ mod tests {
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle();
DownloadsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
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());
}
}
@@ -323,9 +213,9 @@ mod tests {
app.push_navigation_stack(prompt_block.into());
app.data.radarr_data.prompt_confirm = true;
DownloadsHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle();
DownloadsHandler::with(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);
}
@@ -337,12 +227,9 @@ mod tests {
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::with(ESC_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert!(app.error.text.is_empty());
}
}
@@ -365,16 +252,16 @@ mod tests {
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.update.key,
DEFAULT_KEYBINDINGS.update.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::UpdateDownloadsPrompt.into()
ActiveRadarrBlock::UpdateDownloadsPrompt.into()
);
}
@@ -390,17 +277,14 @@ mod tests {
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.update.key,
DEFAULT_KEYBINDINGS.update.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
}
#[test]
@@ -414,17 +298,14 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Downloads.into());
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert!(app.should_refresh);
}
@@ -440,17 +321,14 @@ mod tests {
.set_items(vec![DownloadRecord::default()]);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into());
assert!(!app.should_refresh);
}
@@ -480,10 +358,10 @@ mod tests {
app.push_navigation_stack(prompt_block.into());
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&prompt_block,
&None,
prompt_block,
None,
)
.handle();
@@ -492,7 +370,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
assert_eq!(app.get_current_route(), base_route.into());
}
}
@@ -500,9 +378,9 @@ mod tests {
fn test_downloads_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if DOWNLOADS_BLOCKS.contains(&active_radarr_block) {
assert!(DownloadsHandler::accepts(&active_radarr_block));
assert!(DownloadsHandler::accepts(active_radarr_block));
} else {
assert!(!DownloadsHandler::accepts(&active_radarr_block));
assert!(!DownloadsHandler::accepts(active_radarr_block));
}
})
}
@@ -513,10 +391,10 @@ mod tests {
app.is_loading = true;
let handler = DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
@@ -528,10 +406,10 @@ mod tests {
app.is_loading = false;
let handler = DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
);
assert!(!handler.is_ready());
@@ -548,10 +426,10 @@ mod tests {
.downloads
.set_items(vec![DownloadRecord::default()]);
let handler = DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
ActiveRadarrBlock::Downloads,
None,
);
assert!(handler.is_ready());
+39 -35
View File
@@ -1,10 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
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::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
#[cfg(test)]
@@ -12,22 +14,40 @@ use crate::network::radarr_network::RadarrEvent;
mod downloads_handler_tests;
pub(super) struct DownloadsHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> DownloadsHandler<'a, 'b> {
handle_table_events!(
self,
downloads,
self.app.data.radarr_data.downloads,
DownloadRecord
);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
DOWNLOADS_BLOCKS.contains(active_block)
fn handle(&mut self) {
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 accepts(active_block: ActiveRadarrBlock) -> bool {
DOWNLOADS_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
) -> DownloadsHandler<'a, 'b> {
DownloadsHandler {
key,
@@ -37,7 +57,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -45,32 +65,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
!self.app.is_loading && !self.app.data.radarr_data.downloads.is_empty()
}
fn handle_scroll_up(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
self.app.data.radarr_data.downloads.scroll_up()
}
}
fn handle_scroll_up(&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_scroll_down(&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_home(&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_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Downloads {
if self.active_radarr_block == ActiveRadarrBlock::Downloads {
self
.app
.push_navigation_stack(ActiveRadarrBlock::DeleteDownloadPrompt.into())
@@ -121,18 +125,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.update.key => {
_ if key == DEFAULT_KEYBINDINGS.update.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into());
}
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::DeleteDownloadPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None));
@@ -140,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
}
}
ActiveRadarrBlock::UpdateDownloadsPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
@@ -4,29 +4,29 @@ use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys};
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
mod edit_indexer_handler_tests;
pub(super) struct EditIndexerHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
EDIT_INDEXER_BLOCKS.contains(active_block)
fn accepts(active_block: ActiveRadarrBlock) -> bool {
EDIT_INDEXER_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
) -> EditIndexerHandler<'a, 'b> {
EditIndexerHandler {
key,
@@ -36,7 +36,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -45,14 +45,42 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
}
fn handle_scroll_up(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::EditIndexerPrompt {
self.app.data.radarr_data.selected_block.previous();
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
self.app.data.radarr_data.selected_block.up();
}
ActiveRadarrBlock::EditIndexerPriorityInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.priority += 1;
}
_ => (),
}
}
fn handle_scroll_down(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::EditIndexerPrompt {
self.app.data.radarr_data.selected_block.next();
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
self.app.data.radarr_data.selected_block.down();
}
ActiveRadarrBlock::EditIndexerPriorityInput => {
let edit_indexer_modal = self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
if edit_indexer_modal.priority > 0 {
edit_indexer_modal.priority -= 1;
}
}
_ => (),
}
}
@@ -183,15 +211,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
fn handle_left_right_action(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditIndexerConfirmPrompt
{
handle_prompt_toggle(self.app, self.key);
} else {
let len = self.app.data.radarr_data.selected_block.blocks.len();
let idx = self.app.data.radarr_data.selected_block.index;
self.app.data.radarr_data.selected_block.index = (idx + 5) % len;
}
handle_prompt_left_right_keys!(
self,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
radarr_data
);
}
ActiveRadarrBlock::EditIndexerNameInput => {
handle_text_box_left_right_keys!(
@@ -270,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
fn handle_submit(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
let selected_block = *self.app.data.radarr_data.selected_block.get_active_block();
let selected_block = self.app.data.radarr_data.selected_block.get_active_block();
match selected_block {
ActiveRadarrBlock::EditIndexerConfirmPrompt => {
let radarr_data = &mut self.app.data.radarr_data;
@@ -291,6 +315,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
self.app.push_navigation_stack(selected_block.into());
self.app.should_ignore_quit_key = true;
}
ActiveRadarrBlock::EditIndexerPriorityInput => self
.app
.push_navigation_stack(ActiveRadarrBlock::EditIndexerPriorityInput.into()),
ActiveRadarrBlock::EditIndexerToggleEnableRss => {
let indexer = self
.app
@@ -334,6 +361,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
}
ActiveRadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(),
_ => (),
}
}
@@ -349,6 +377,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerUrlInput
| ActiveRadarrBlock::EditIndexerApiKeyInput
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerPriorityInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
@@ -431,8 +460,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
}
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditIndexerConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
== ActiveRadarrBlock::EditIndexerConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None));
File diff suppressed because it is too large Load Diff
@@ -6,29 +6,29 @@ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys};
#[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"]
mod edit_indexer_settings_handler_tests;
pub(super) struct IndexerSettingsHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
INDEXER_SETTINGS_BLOCKS.contains(active_block)
fn accepts(active_block: ActiveRadarrBlock) -> bool {
INDEXER_SETTINGS_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
_context: Option<ActiveRadarrBlock>,
) -> IndexerSettingsHandler<'a, 'b> {
IndexerSettingsHandler {
key,
@@ -38,7 +38,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -50,7 +50,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
match self.active_radarr_block {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
self.app.data.radarr_data.selected_block.previous();
self.app.data.radarr_data.selected_block.up();
}
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => {
indexer_settings.minimum_age += 1;
@@ -75,7 +75,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
match self.active_radarr_block {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
self.app.data.radarr_data.selected_block.next()
self.app.data.radarr_data.selected_block.down()
}
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => {
if indexer_settings.minimum_age > 0 {
@@ -105,7 +105,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
fn handle_home(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
if self.active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
self
.app
.data
@@ -119,7 +119,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
fn handle_end(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
if self.active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput {
self
.app
.data
@@ -137,15 +137,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
fn handle_left_right_action(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::IndexerSettingsConfirmPrompt
{
handle_prompt_toggle(self.app, self.key);
} else {
let len = self.app.data.radarr_data.selected_block.blocks.len();
let idx = self.app.data.radarr_data.selected_block.index;
self.app.data.radarr_data.selected_block.index = (idx + 5) % len;
}
handle_prompt_left_right_keys!(
self,
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
radarr_data
);
}
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
handle_text_box_left_right_keys!(
@@ -187,7 +183,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
| ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput => {
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(),
None,
)
.into(),
@@ -258,8 +254,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& *self.key == DEFAULT_KEYBINDINGS.confirm.key
== ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -27,7 +27,7 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(&$key, &mut app, &$block, &None).handle();
IndexerSettingsHandler::with($key, &mut app, $block, None).handle();
if $key == Key::Up {
assert_eq!(
@@ -64,7 +64,7 @@ mod tests {
0
);
IndexerSettingsHandler::with(&Key::Up, &mut app, &$block, &None).handle();
IndexerSettingsHandler::with(Key::Up, &mut app, $block, None).handle();
assert_eq!(
app
@@ -77,7 +77,7 @@ mod tests {
1
);
IndexerSettingsHandler::with(&$key, &mut app, &$block, &None).handle();
IndexerSettingsHandler::with($key, &mut app, $block, None).handle();
assert_eq!(
app
.data
@@ -98,26 +98,26 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.next();
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.down();
IndexerSettingsHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
if key == Key::Up {
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::IndexerSettingsMinimumAgeInput
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput
);
} else {
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::IndexerSettingsMaximumSizeInput
ActiveRadarrBlock::IndexerSettingsMaximumSizeInput
);
}
}
@@ -130,20 +130,20 @@ mod tests {
app.is_loading = true;
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.next();
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.down();
IndexerSettingsHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&ActiveRadarrBlock::IndexerSettingsRetentionInput
ActiveRadarrBlock::IndexerSettingsRetentionInput
);
}
@@ -218,10 +218,10 @@ mod tests {
});
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -239,10 +239,10 @@ mod tests {
);
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -276,24 +276,24 @@ mod tests {
fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::default();
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.index = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1;
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1;
IndexerSettingsHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert!(app.data.radarr_data.prompt_confirm);
IndexerSettingsHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
@@ -323,44 +323,44 @@ mod tests {
)]
fn test_left_right_block_toggle(
#[values(Key::Left, Key::Right)] key: Key,
#[case] starting_index: usize,
#[case] starting_y_index: usize,
#[case] left_block: ActiveRadarrBlock,
#[case] right_block: ActiveRadarrBlock,
) {
let mut app = App::default();
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.index = starting_index;
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.y = starting_y_index;
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&left_block
left_block
);
IndexerSettingsHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&right_block
right_block
);
IndexerSettingsHandler::with(
&key,
key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.selected_block.get_active_block(),
&left_block
left_block
);
}
@@ -373,10 +373,10 @@ mod tests {
});
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
DEFAULT_KEYBINDINGS.left.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -394,10 +394,10 @@ mod tests {
);
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -438,23 +438,23 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert!(!app.should_refresh);
assert_eq!(app.data.radarr_data.indexer_settings, None);
@@ -466,24 +466,24 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.radarr_data.prompt_confirm = true;
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditAllIndexerSettings(None))
@@ -502,71 +502,80 @@ mod tests {
app.data.radarr_data.prompt_confirm = true;
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(!app.should_refresh);
}
#[rstest]
#[case(ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, 0)]
#[case(ActiveRadarrBlock::IndexerSettingsRetentionInput, 1)]
#[case(ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, 2)]
#[case(ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, 5)]
#[case(ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, 6)]
#[case(ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, 0, 0)]
#[case(ActiveRadarrBlock::IndexerSettingsRetentionInput, 1, 0)]
#[case(ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, 2, 0)]
#[case(ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, 0, 1)]
#[case(ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, 1, 1)]
fn test_edit_indexer_settings_prompt_submit_selected_block(
#[case] selected_block: ActiveRadarrBlock,
#[case] index: usize,
#[case] y_index: usize,
#[case] x_index: usize,
) {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(index);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(x_index, y_index);
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), &selected_block.into());
assert_eq!(app.get_current_route(), selected_block.into());
}
#[rstest]
fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready(
#[values(0, 1, 2, 5, 6)] index: usize,
#[values((0, 0), (1, 0), (2, 0), (0, 1), (1, 1))] index: (usize, usize),
) {
let mut app = App::default();
app.is_loading = true;
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(index);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(index.1, index.0);
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
}
@@ -576,20 +585,20 @@ mod tests {
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(7);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(1, 2);
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into()
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into()
);
assert!(app.should_ignore_quit_key);
}
@@ -599,21 +608,21 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(3);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(0, 3);
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
app
@@ -626,16 +635,16 @@ mod tests {
);
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
!app
@@ -653,21 +662,21 @@ mod tests {
let mut app = App::default();
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(8);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app.data.radarr_data.selected_block.set_index(1, 3);
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
app
@@ -680,16 +689,16 @@ mod tests {
);
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert!(
!app
@@ -716,10 +725,10 @@ mod tests {
);
IndexerSettingsHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -735,7 +744,7 @@ mod tests {
.is_empty());
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
}
@@ -755,11 +764,11 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.push_navigation_stack(active_radarr_block.into());
IndexerSettingsHandler::with(&SUBMIT_KEY, &mut app, &active_radarr_block, &None).handle();
IndexerSettingsHandler::with(SUBMIT_KEY, &mut app, active_radarr_block, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
}
}
@@ -783,14 +792,14 @@ mod tests {
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.indexer_settings, None);
}
@@ -806,14 +815,14 @@ mod tests {
app.should_ignore_quit_key = true;
IndexerSettingsHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.should_ignore_quit_key);
assert_eq!(
app.data.radarr_data.indexer_settings,
@@ -838,9 +847,9 @@ mod tests {
app.push_navigation_stack(active_radarr_block.into());
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle();
IndexerSettingsHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.indexer_settings,
Some(IndexerSettings::default())
@@ -870,10 +879,10 @@ mod tests {
});
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
DEFAULT_KEYBINDINGS.backspace.key,
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -896,10 +905,10 @@ mod tests {
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&Key::Char('h'),
Key::Char('h'),
&mut app,
&ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
&None,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
)
.handle();
@@ -922,23 +931,23 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
app
.data
.radarr_data
.selected_block
.set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
.set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1);
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::EditAllIndexerSettings(None))
@@ -952,9 +961,9 @@ mod tests {
fn test_indexer_settings_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if INDEXER_SETTINGS_BLOCKS.contains(&active_radarr_block) {
assert!(IndexerSettingsHandler::accepts(&active_radarr_block));
assert!(IndexerSettingsHandler::accepts(active_radarr_block));
} else {
assert!(!IndexerSettingsHandler::accepts(&active_radarr_block));
assert!(!IndexerSettingsHandler::accepts(active_radarr_block));
}
})
}
@@ -965,10 +974,10 @@ mod tests {
app.is_loading = true;
let handler = IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
);
assert!(!handler.is_ready());
@@ -980,10 +989,10 @@ mod tests {
app.is_loading = false;
let handler = IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
);
assert!(!handler.is_ready());
@@ -996,10 +1005,10 @@ mod tests {
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
let handler = IndexerSettingsHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::AllIndexerSettingsPrompt,
&None,
ActiveRadarrBlock::AllIndexerSettingsPrompt,
None,
);
assert!(handler.is_ready());
@@ -1,6 +1,5 @@
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
@@ -9,115 +8,12 @@ mod tests {
use crate::event::Key;
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::Indexer;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
};
use crate::models::servarr_models::Indexer;
use crate::test_handler_delegation;
mod test_handle_scroll_up_and_down {
use rstest::rstest;
use crate::models::radarr_models::Indexer;
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
use super::*;
test_iterable_scroll!(
test_indexers_scroll,
IndexersHandler,
indexers,
simple_stateful_iterable_vec!(Indexer, String, protocol),
ActiveRadarrBlock::Indexers,
None,
protocol
);
#[rstest]
fn test_indexers_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
.indexers
.set_items(simple_stateful_iterable_vec!(Indexer, String, protocol));
IndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
assert_str_eq!(
app.data.radarr_data.indexers.current_selection().protocol,
"Test 1"
);
IndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
assert_str_eq!(
app.data.radarr_data.indexers.current_selection().protocol,
"Test 1"
);
}
}
mod test_handle_home_end {
use crate::models::radarr_models::Indexer;
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
use super::*;
test_iterable_home_and_end!(
test_indexers_home_end,
IndexersHandler,
indexers,
extended_stateful_iterable_vec!(Indexer, String, protocol),
ActiveRadarrBlock::Indexers,
None,
protocol
);
#[test]
fn test_indexers_home_end_no_op_when_not_ready() {
let mut app = App::default();
app.is_loading = true;
app
.data
.radarr_data
.indexers
.set_items(extended_stateful_iterable_vec!(Indexer, String, protocol));
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
)
.handle();
assert_str_eq!(
app.data.radarr_data.indexers.current_selection().protocol,
"Test 1"
);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
)
.handle();
assert_str_eq!(
app.data.radarr_data.indexers.current_selection().protocol,
"Test 1"
);
}
}
mod test_handle_delete {
use pretty_assertions::assert_eq;
@@ -134,11 +30,11 @@ mod tests {
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
IndexersHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::DeleteIndexerPrompt.into()
ActiveRadarrBlock::DeleteIndexerPrompt.into()
);
}
@@ -153,9 +49,9 @@ mod tests {
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
IndexersHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
}
@@ -172,20 +68,20 @@ mod tests {
app.data.radarr_data.main_tabs.set_index(5);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
DEFAULT_KEYBINDINGS.left.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::RootFolders.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::RootFolders.into()
);
}
@@ -196,18 +92,18 @@ mod tests {
app.data.radarr_data.main_tabs.set_index(5);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::System.into()
ActiveRadarrBlock::System.into()
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into());
}
#[rstest]
@@ -216,34 +112,22 @@ mod tests {
) {
let mut app = App::default();
IndexersHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
)
.handle();
IndexersHandler::with(key, &mut app, ActiveRadarrBlock::DeleteIndexerPrompt, None).handle();
assert!(app.data.radarr_data.prompt_confirm);
IndexersHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
)
.handle();
IndexersHandler::with(key, &mut app, ActiveRadarrBlock::DeleteIndexerPrompt, None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use crate::models::radarr_models::{Indexer, IndexerField};
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
use crate::models::servarr_models::{Indexer, IndexerField};
use bimap::BiMap;
use pretty_assertions::assert_eq;
use serde_json::{Number, Value};
@@ -308,11 +192,11 @@ mod tests {
radarr_data.indexers.set_items(vec![indexer]);
app.data.radarr_data = radarr_data;
IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
IndexersHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditIndexerPrompt.into()
ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert_eq!(
app.data.radarr_data.edit_indexer_modal,
@@ -325,12 +209,12 @@ mod tests {
if torrent_protocol {
assert_eq!(
app.data.radarr_data.selected_block.blocks,
&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS
);
} else {
assert_eq!(
app.data.radarr_data.selected_block.blocks,
&EDIT_INDEXER_NZB_SELECTION_BLOCKS
EDIT_INDEXER_NZB_SELECTION_BLOCKS
);
}
}
@@ -346,9 +230,9 @@ mod tests {
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
IndexersHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(app.data.radarr_data.edit_indexer_modal, None);
}
@@ -365,10 +249,10 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
ActiveRadarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
@@ -377,7 +261,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteIndexer(None))
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
#[test]
@@ -392,16 +276,16 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::with(
&SUBMIT_KEY,
SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
ActiveRadarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
}
@@ -421,14 +305,14 @@ mod tests {
app.data.radarr_data.prompt_confirm = true;
IndexersHandler::with(
&ESC_KEY,
ESC_KEY,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
ActiveRadarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -436,14 +320,14 @@ mod tests {
fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) {
let mut app = App::default();
app.is_loading = is_ready;
app.data.radarr_data.indexer_test_error = Some("test result".to_owned());
app.data.radarr_data.indexer_test_errors = Some("test result".to_owned());
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::TestIndexer.into());
IndexersHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::TestIndexer, &None).handle();
IndexersHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::TestIndexer, None).handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.data.radarr_data.indexer_test_error, None);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert_eq!(app.data.radarr_data.indexer_test_errors, None);
}
#[rstest]
@@ -454,9 +338,9 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
IndexersHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
IndexersHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(app.error.text.is_empty());
}
}
@@ -471,51 +355,6 @@ mod tests {
use super::*;
#[test]
fn test_indexer_add() {
let mut app = App::default();
app
.data
.radarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.add.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AddIndexer.into()
);
}
#[test]
fn test_indexer_add_no_op_when_not_ready() {
let mut app = App::default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app
.data
.radarr_data
.indexers
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.add.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
}
#[test]
fn test_refresh_indexers_key() {
let mut app = App::default();
@@ -527,14 +366,14 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(app.should_refresh);
}
@@ -550,14 +389,14 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.refresh.key,
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.should_refresh);
}
@@ -571,20 +410,20 @@ mod tests {
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.settings.key,
DEFAULT_KEYBINDINGS.settings.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
ActiveRadarrBlock::AllIndexerSettingsPrompt.into()
);
assert_eq!(
app.data.radarr_data.selected_block.blocks,
&INDEXER_SETTINGS_SELECTION_BLOCKS
INDEXER_SETTINGS_SELECTION_BLOCKS
);
}
@@ -600,14 +439,14 @@ mod tests {
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.settings.key,
DEFAULT_KEYBINDINGS.settings.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
#[test]
@@ -620,16 +459,16 @@ mod tests {
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.test.key,
DEFAULT_KEYBINDINGS.test.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::TestIndexer.into()
ActiveRadarrBlock::TestIndexer.into()
);
}
@@ -645,14 +484,14 @@ mod tests {
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.test.key,
DEFAULT_KEYBINDINGS.test.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
#[test]
@@ -665,16 +504,16 @@ mod tests {
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.test_all.key,
DEFAULT_KEYBINDINGS.test_all.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::TestAllIndexers.into()
ActiveRadarrBlock::TestAllIndexers.into()
);
}
@@ -690,14 +529,14 @@ mod tests {
.set_items(vec![Indexer::default()]);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.test_all.key,
DEFAULT_KEYBINDINGS.test_all.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
#[test]
@@ -712,10 +551,10 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.confirm.key,
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
&ActiveRadarrBlock::DeleteIndexerPrompt,
&None,
ActiveRadarrBlock::DeleteIndexerPrompt,
None,
)
.handle();
@@ -724,7 +563,7 @@ mod tests {
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::DeleteIndexer(None))
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
}
}
@@ -793,9 +632,9 @@ mod tests {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if indexers_blocks.contains(&active_radarr_block) {
assert!(IndexersHandler::accepts(&active_radarr_block));
assert!(IndexersHandler::accepts(active_radarr_block));
} else {
assert!(!IndexersHandler::accepts(&active_radarr_block));
assert!(!IndexersHandler::accepts(active_radarr_block));
}
})
}
@@ -806,10 +645,10 @@ mod tests {
app.is_loading = true;
let handler = IndexersHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
);
assert!(!handler.is_ready());
@@ -821,10 +660,10 @@ mod tests {
app.is_loading = false;
let handler = IndexersHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
);
assert!(!handler.is_ready());
@@ -841,10 +680,10 @@ mod tests {
.set_items(vec![Indexer::default()]);
let handler = IndexersHandler::with(
&DEFAULT_KEYBINDINGS.esc.key,
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
&ActiveRadarrBlock::Indexers,
&None,
ActiveRadarrBlock::Indexers,
None,
);
assert!(handler.is_ready());
+49 -58
View File
@@ -1,16 +1,19 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
};
use crate::models::{BlockSelectionState, Scrollable};
use crate::models::servarr_models::Indexer;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
mod edit_indexer_handler;
@@ -22,43 +25,52 @@ mod test_all_indexers_handler;
mod indexers_handler_tests;
pub(super) struct IndexersHandler<'a, 'b> {
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_radarr_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
}
impl<'a, 'b> IndexersHandler<'a, 'b> {
handle_table_events!(self, indexers, self.app.data.radarr_data.indexers, Indexer);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, 'b> {
fn handle(&mut self) {
match self.active_radarr_block {
_ if EditIndexerHandler::accepts(self.active_radarr_block) => {
EditIndexerHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
let indexer_table_handling_config =
TableHandlingConfig::new(ActiveRadarrBlock::Indexers.into());
if !self.handle_indexers_table_events(indexer_table_handling_config) {
match self.active_radarr_block {
_ if EditIndexerHandler::accepts(self.active_radarr_block) => {
EditIndexerHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
}
_ if IndexerSettingsHandler::accepts(self.active_radarr_block) => {
IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
}
_ if TestAllIndexersHandler::accepts(self.active_radarr_block) => {
TestAllIndexersHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
}
_ => self.handle_key_event(),
}
_ if IndexerSettingsHandler::accepts(self.active_radarr_block) => {
IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
}
_ if TestAllIndexersHandler::accepts(self.active_radarr_block) => {
TestAllIndexersHandler::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 {
EditIndexerHandler::accepts(active_block)
|| IndexerSettingsHandler::accepts(active_block)
|| TestAllIndexersHandler::accepts(active_block)
|| INDEXERS_BLOCKS.contains(active_block)
|| INDEXERS_BLOCKS.contains(&active_block)
}
fn with(
key: &'a Key,
key: Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
active_block: ActiveRadarrBlock,
context: Option<ActiveRadarrBlock>,
) -> IndexersHandler<'a, 'b> {
IndexersHandler {
key,
@@ -68,7 +80,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
}
}
fn get_key(&self) -> &Key {
fn get_key(&self) -> Key {
self.key
}
@@ -76,32 +88,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
!self.app.is_loading && !self.app.data.radarr_data.indexers.is_empty()
}
fn handle_scroll_up(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
self.app.data.radarr_data.indexers.scroll_up();
}
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
self.app.data.radarr_data.indexers.scroll_down();
}
}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
self.app.data.radarr_data.indexers.scroll_to_top();
}
}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
self.app.data.radarr_data.indexers.scroll_to_bottom();
}
}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Indexers {
if self.active_radarr_block == ActiveRadarrBlock::Indexers {
self
.app
.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into());
@@ -140,10 +136,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
.protocol;
if protocol == "torrent" {
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
} else {
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS);
BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS);
}
}
_ => (),
@@ -158,7 +154,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
}
ActiveRadarrBlock::TestIndexer => {
self.app.pop_navigation_stack();
self.app.data.radarr_data.indexer_test_error = None;
self.app.data.radarr_data.indexer_test_errors = None;
}
_ => handle_clear_errors(self.app),
}
@@ -168,35 +164,30 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Indexers => match self.key {
_ if *key == DEFAULT_KEYBINDINGS.add.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::AddIndexer.into());
}
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
_ if *key == DEFAULT_KEYBINDINGS.test.key => {
_ if key == DEFAULT_KEYBINDINGS.test.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::TestIndexer.into());
}
_ if *key == DEFAULT_KEYBINDINGS.test_all.key => {
_ if key == DEFAULT_KEYBINDINGS.test_all.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::TestAllIndexers.into());
}
_ if *key == DEFAULT_KEYBINDINGS.settings.key => {
_ if key == DEFAULT_KEYBINDINGS.settings.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS);
BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS);
}
_ => (),
},
ActiveRadarrBlock::DeleteIndexerPrompt => {
if *key == DEFAULT_KEYBINDINGS.confirm.key {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None));

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