mikancli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. mikancli-0.1.0/PKG-INFO +198 -0
  2. mikancli-0.1.0/README.md +175 -0
  3. mikancli-0.1.0/mikancli/__init__.py +7 -0
  4. mikancli-0.1.0/mikancli/__main__.py +5 -0
  5. mikancli-0.1.0/mikancli/cli/__init__.py +3 -0
  6. mikancli-0.1.0/mikancli/cli/entrypoint.py +215 -0
  7. mikancli-0.1.0/mikancli/cli/input_parsing.py +40 -0
  8. mikancli-0.1.0/mikancli/cli/prompts.py +145 -0
  9. mikancli-0.1.0/mikancli/cli/qbittorrent_flow.py +169 -0
  10. mikancli-0.1.0/mikancli/cli/save_path_flow.py +122 -0
  11. mikancli-0.1.0/mikancli/cli/search_flow.py +203 -0
  12. mikancli-0.1.0/mikancli/config.py +160 -0
  13. mikancli-0.1.0/mikancli/core/__init__.py +1 -0
  14. mikancli-0.1.0/mikancli/core/models.py +74 -0
  15. mikancli-0.1.0/mikancli/core/normalize.py +32 -0
  16. mikancli-0.1.0/mikancli/core/rules.py +52 -0
  17. mikancli-0.1.0/mikancli/display.py +89 -0
  18. mikancli-0.1.0/mikancli/integrations/__init__.py +1 -0
  19. mikancli-0.1.0/mikancli/integrations/mikan.py +72 -0
  20. mikancli-0.1.0/mikancli/integrations/mikan_parsers.py +282 -0
  21. mikancli-0.1.0/mikancli/integrations/mikan_urls.py +35 -0
  22. mikancli-0.1.0/mikancli/integrations/qbittorrent.py +156 -0
  23. mikancli-0.1.0/mikancli/integrations/qbittorrent_client.py +243 -0
  24. mikancli-0.1.0/mikancli.egg-info/PKG-INFO +198 -0
  25. mikancli-0.1.0/mikancli.egg-info/SOURCES.txt +36 -0
  26. mikancli-0.1.0/mikancli.egg-info/dependency_links.txt +1 -0
  27. mikancli-0.1.0/mikancli.egg-info/entry_points.txt +2 -0
  28. mikancli-0.1.0/mikancli.egg-info/requires.txt +1 -0
  29. mikancli-0.1.0/mikancli.egg-info/top_level.txt +1 -0
  30. mikancli-0.1.0/pyproject.toml +40 -0
  31. mikancli-0.1.0/setup.cfg +4 -0
  32. mikancli-0.1.0/tests/test_cli.py +739 -0
  33. mikancli-0.1.0/tests/test_config.py +279 -0
  34. mikancli-0.1.0/tests/test_mikan.py +143 -0
  35. mikancli-0.1.0/tests/test_normalize.py +15 -0
  36. mikancli-0.1.0/tests/test_prompts.py +221 -0
  37. mikancli-0.1.0/tests/test_qbittorrent.py +446 -0
  38. mikancli-0.1.0/tests/test_rules.py +44 -0
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: mikancli
3
+ Version: 0.1.0
4
+ Summary: CLI foundation for building qBittorrent RSS rules from anime search input.
5
+ Author: metails
6
+ Project-URL: Repository, https://github.com/JianHua-Deng/MikanCli
7
+ Project-URL: Issues, https://github.com/JianHua-Deng/MikanCli/issues
8
+ Keywords: anime,rss,qbittorrent,cli,mikan
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: InquirerPy>=0.3
23
+
24
+ # MikanCli
25
+
26
+ MikanCli is a Python CLI for turning an anime search keyword into a qBittorrent RSS feed and download-rule setup workflow.
27
+
28
+ ## Current increment
29
+
30
+ The repository currently contains the first small slice of the project:
31
+
32
+ - a Python CLI scaffold
33
+ - keyword normalization
34
+ - Mikan Bangumi search, subgroup discovery, and subgroup RSS feed resolution
35
+ - draft qBittorrent rule construction and interactive qBittorrent submission
36
+ - focused tests for the pure logic
37
+
38
+ This increment now searches Mikan, lets you select the Bangumi and subgroup,
39
+ resolves the subgroup-specific RSS feed, and prints that feed alongside the
40
+ draft rule details. In interactive search mode, MikanCli can now submit the
41
+ RSS feed and auto-download rule to qBittorrent after you confirm the draft.
42
+ After submission, it reads qBittorrent back to verify that the feed and rule
43
+ were created. qBittorrent WebUI setup verification is also available.
44
+
45
+ Internally, the project is split by responsibility:
46
+
47
+ - `mikancli/cli/` contains CLI entrypoint, prompts, and interactive navigation
48
+ - `mikancli/core/` contains models, normalization helpers, and rule-building logic
49
+ - `mikancli/integrations/` contains external service adapters such as Mikan
50
+ - `config.py` and `display.py` stay at the package root as shared support modules
51
+
52
+ ## Install from source
53
+
54
+ Clone the repository, then install it from the project folder:
55
+
56
+ ```bash
57
+ git clone <repo-url>
58
+ cd MikanCli
59
+ python -m pip install -e .
60
+ ```
61
+
62
+ This installs MikanCli in editable mode, installs the dependencies declared in
63
+ `pyproject.toml`, and registers the `mikancli` command. After that, you can run
64
+ MikanCli from any terminal location:
65
+
66
+ ```bash
67
+ mikancli
68
+ ```
69
+
70
+ You can also run the module directly from the project folder after installing
71
+ the local environment:
72
+
73
+ ```bash
74
+ python -m mikancli
75
+ ```
76
+
77
+ Dependencies are installed by `pip` when the package is installed. MikanCli does
78
+ not install packages at runtime.
79
+
80
+ ## Usage
81
+
82
+ Search directly from the command line:
83
+
84
+ ```bash
85
+ mikancli "solo leveling" --include HEVC --exclude 720p
86
+ ```
87
+
88
+ You can also run it with no extra arguments and let the script guide you:
89
+
90
+ ```bash
91
+ mikancli
92
+ ```
93
+
94
+ To set up qBittorrent WebUI access for future increments:
95
+
96
+ ```bash
97
+ mikancli --setup-qbittorrent
98
+ ```
99
+
100
+ When you run `mikancli` with no extra arguments, the first menu now lets you choose between:
101
+
102
+ - `Search anime`
103
+ - `Modify qBittorrent configurations`
104
+
105
+ If you choose `Search anime` and qBittorrent is not configured yet, MikanCli can still guide you into qBittorrent setup before continuing.
106
+
107
+ If `--save-path` is omitted, MikanCli first checks for a saved default in the
108
+ user config file. On Windows, the default config location is
109
+ `%APPDATA%\MikanCli\config.json`.
110
+ In a normal interactive run, the guided prompts now use `InquirerPy`, so list selections stay in place instead of printing a new block of text on every key press.
111
+ Every interactive menu now includes an `Exit MikanCli` option, and text prompts accept `exit` or `quit` to stop the tool cleanly.
112
+ The first search prompt now says that explicitly, so the quit path is visible before any lookup starts.
113
+
114
+ - use the saved default folder
115
+ - use the system Downloads folder
116
+ - browse for a folder
117
+ - type a folder path manually
118
+
119
+ If the chosen folder is not already the saved default, MikanCli asks whether it should be saved for future runs.
120
+
121
+ Current guided flow:
122
+
123
+ - ask for anime keyword if you did not type one
124
+ - search Mikan for matching Bangumi entries
125
+ - let you choose the correct Mikan entry when more than one match is found
126
+ - fetch subgroup entries from the selected Bangumi page
127
+ - let you choose the correct subgroup RSS feed when more than one subgroup is found
128
+ - show the subgroup RSS contents before confirmation
129
+ - let you confirm, go back to subgroup selection, or search again
130
+ - optionally ask for include words
131
+ - optionally ask for exclude words
132
+ - let you choose the base download folder from a menu
133
+ - ask for the content folder name inside that base folder, defaulting to the selected Bangumi title
134
+ - allow quitting cleanly from any interactive menu or prompt
135
+ - print the resolved Mikan page URL, subgroup, and subgroup RSS feed URL with the draft rule summary
136
+ - ask whether to submit the RSS feed and download rule to qBittorrent when WebUI access is configured
137
+ - verify submitted qBittorrent feeds and rules by reading them back from the WebUI API
138
+
139
+ ## qBittorrent setup
140
+
141
+ Before MikanCli can talk to qBittorrent, do a one-time setup inside qBittorrent:
142
+
143
+ 1. Open qBittorrent settings.
144
+ 2. Enable WebUI / remote control.
145
+ 3. Check the WebUI address, username, and password there.
146
+
147
+ Then run:
148
+
149
+ ```bash
150
+ mikancli --setup-qbittorrent
151
+ ```
152
+
153
+ Setup notes:
154
+
155
+ - if you press Enter for the URL, MikanCli uses `http://localhost:8080`
156
+ - you can enter `localhost:8080` and MikanCli will normalize it automatically
157
+ - username and password may be left blank for the first test if your local qBittorrent allows it
158
+
159
+ Troubleshooting:
160
+
161
+ - if MikanCli says it could not reach qBittorrent, WebUI may be disabled or using a different port
162
+ - if MikanCli says the credentials were rejected, re-check the WebUI username/password in qBittorrent settings
163
+
164
+ ## Packaging and release
165
+
166
+ MikanCli is structured as an installable Python CLI package. The console command
167
+ is declared in `pyproject.toml`:
168
+
169
+ ```toml
170
+ [project.scripts]
171
+ mikancli = "mikancli.cli.entrypoint:main"
172
+ ```
173
+
174
+ Current local install flow:
175
+
176
+ ```bash
177
+ python -m pip install -e .
178
+ mikancli
179
+ ```
180
+
181
+ Planned public install flow after publishing to PyPI:
182
+
183
+ ```bash
184
+ python -m pip install mikancli
185
+ mikancli
186
+ ```
187
+
188
+ Recommended CLI-app install flow after publishing:
189
+
190
+ ```bash
191
+ pipx install mikancli
192
+ mikancli
193
+ ```
194
+
195
+ Remaining packaging work:
196
+
197
+ - add a release workflow for building and publishing distributions
198
+ - later consider a Windows executable for non-technical users
@@ -0,0 +1,175 @@
1
+ # MikanCli
2
+
3
+ MikanCli is a Python CLI for turning an anime search keyword into a qBittorrent RSS feed and download-rule setup workflow.
4
+
5
+ ## Current increment
6
+
7
+ The repository currently contains the first small slice of the project:
8
+
9
+ - a Python CLI scaffold
10
+ - keyword normalization
11
+ - Mikan Bangumi search, subgroup discovery, and subgroup RSS feed resolution
12
+ - draft qBittorrent rule construction and interactive qBittorrent submission
13
+ - focused tests for the pure logic
14
+
15
+ This increment now searches Mikan, lets you select the Bangumi and subgroup,
16
+ resolves the subgroup-specific RSS feed, and prints that feed alongside the
17
+ draft rule details. In interactive search mode, MikanCli can now submit the
18
+ RSS feed and auto-download rule to qBittorrent after you confirm the draft.
19
+ After submission, it reads qBittorrent back to verify that the feed and rule
20
+ were created. qBittorrent WebUI setup verification is also available.
21
+
22
+ Internally, the project is split by responsibility:
23
+
24
+ - `mikancli/cli/` contains CLI entrypoint, prompts, and interactive navigation
25
+ - `mikancli/core/` contains models, normalization helpers, and rule-building logic
26
+ - `mikancli/integrations/` contains external service adapters such as Mikan
27
+ - `config.py` and `display.py` stay at the package root as shared support modules
28
+
29
+ ## Install from source
30
+
31
+ Clone the repository, then install it from the project folder:
32
+
33
+ ```bash
34
+ git clone <repo-url>
35
+ cd MikanCli
36
+ python -m pip install -e .
37
+ ```
38
+
39
+ This installs MikanCli in editable mode, installs the dependencies declared in
40
+ `pyproject.toml`, and registers the `mikancli` command. After that, you can run
41
+ MikanCli from any terminal location:
42
+
43
+ ```bash
44
+ mikancli
45
+ ```
46
+
47
+ You can also run the module directly from the project folder after installing
48
+ the local environment:
49
+
50
+ ```bash
51
+ python -m mikancli
52
+ ```
53
+
54
+ Dependencies are installed by `pip` when the package is installed. MikanCli does
55
+ not install packages at runtime.
56
+
57
+ ## Usage
58
+
59
+ Search directly from the command line:
60
+
61
+ ```bash
62
+ mikancli "solo leveling" --include HEVC --exclude 720p
63
+ ```
64
+
65
+ You can also run it with no extra arguments and let the script guide you:
66
+
67
+ ```bash
68
+ mikancli
69
+ ```
70
+
71
+ To set up qBittorrent WebUI access for future increments:
72
+
73
+ ```bash
74
+ mikancli --setup-qbittorrent
75
+ ```
76
+
77
+ When you run `mikancli` with no extra arguments, the first menu now lets you choose between:
78
+
79
+ - `Search anime`
80
+ - `Modify qBittorrent configurations`
81
+
82
+ If you choose `Search anime` and qBittorrent is not configured yet, MikanCli can still guide you into qBittorrent setup before continuing.
83
+
84
+ If `--save-path` is omitted, MikanCli first checks for a saved default in the
85
+ user config file. On Windows, the default config location is
86
+ `%APPDATA%\MikanCli\config.json`.
87
+ In a normal interactive run, the guided prompts now use `InquirerPy`, so list selections stay in place instead of printing a new block of text on every key press.
88
+ Every interactive menu now includes an `Exit MikanCli` option, and text prompts accept `exit` or `quit` to stop the tool cleanly.
89
+ The first search prompt now says that explicitly, so the quit path is visible before any lookup starts.
90
+
91
+ - use the saved default folder
92
+ - use the system Downloads folder
93
+ - browse for a folder
94
+ - type a folder path manually
95
+
96
+ If the chosen folder is not already the saved default, MikanCli asks whether it should be saved for future runs.
97
+
98
+ Current guided flow:
99
+
100
+ - ask for anime keyword if you did not type one
101
+ - search Mikan for matching Bangumi entries
102
+ - let you choose the correct Mikan entry when more than one match is found
103
+ - fetch subgroup entries from the selected Bangumi page
104
+ - let you choose the correct subgroup RSS feed when more than one subgroup is found
105
+ - show the subgroup RSS contents before confirmation
106
+ - let you confirm, go back to subgroup selection, or search again
107
+ - optionally ask for include words
108
+ - optionally ask for exclude words
109
+ - let you choose the base download folder from a menu
110
+ - ask for the content folder name inside that base folder, defaulting to the selected Bangumi title
111
+ - allow quitting cleanly from any interactive menu or prompt
112
+ - print the resolved Mikan page URL, subgroup, and subgroup RSS feed URL with the draft rule summary
113
+ - ask whether to submit the RSS feed and download rule to qBittorrent when WebUI access is configured
114
+ - verify submitted qBittorrent feeds and rules by reading them back from the WebUI API
115
+
116
+ ## qBittorrent setup
117
+
118
+ Before MikanCli can talk to qBittorrent, do a one-time setup inside qBittorrent:
119
+
120
+ 1. Open qBittorrent settings.
121
+ 2. Enable WebUI / remote control.
122
+ 3. Check the WebUI address, username, and password there.
123
+
124
+ Then run:
125
+
126
+ ```bash
127
+ mikancli --setup-qbittorrent
128
+ ```
129
+
130
+ Setup notes:
131
+
132
+ - if you press Enter for the URL, MikanCli uses `http://localhost:8080`
133
+ - you can enter `localhost:8080` and MikanCli will normalize it automatically
134
+ - username and password may be left blank for the first test if your local qBittorrent allows it
135
+
136
+ Troubleshooting:
137
+
138
+ - if MikanCli says it could not reach qBittorrent, WebUI may be disabled or using a different port
139
+ - if MikanCli says the credentials were rejected, re-check the WebUI username/password in qBittorrent settings
140
+
141
+ ## Packaging and release
142
+
143
+ MikanCli is structured as an installable Python CLI package. The console command
144
+ is declared in `pyproject.toml`:
145
+
146
+ ```toml
147
+ [project.scripts]
148
+ mikancli = "mikancli.cli.entrypoint:main"
149
+ ```
150
+
151
+ Current local install flow:
152
+
153
+ ```bash
154
+ python -m pip install -e .
155
+ mikancli
156
+ ```
157
+
158
+ Planned public install flow after publishing to PyPI:
159
+
160
+ ```bash
161
+ python -m pip install mikancli
162
+ mikancli
163
+ ```
164
+
165
+ Recommended CLI-app install flow after publishing:
166
+
167
+ ```bash
168
+ pipx install mikancli
169
+ mikancli
170
+ ```
171
+
172
+ Remaining packaging work:
173
+
174
+ - add a release workflow for building and publishing distributions
175
+ - later consider a Windows executable for non-technical users
@@ -0,0 +1,7 @@
1
+ """MikanCli package."""
2
+
3
+ __all__ = [
4
+ "__version__",
5
+ ]
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from mikancli.cli.entrypoint import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,3 @@
1
+ from mikancli.cli.entrypoint import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from mikancli.cli.input_parsing import prompt_word_list
8
+ from mikancli.cli.prompts import ExitRequested, select_option
9
+ from mikancli.cli.qbittorrent_flow import (
10
+ QBITTORRENT_SUBMISSION_SKIPPED,
11
+ prompt_for_qbittorrent_setup_if_needed,
12
+ prompt_to_submit_rule_to_qbittorrent,
13
+ run_qbittorrent_configuration_flow,
14
+ setup_qbittorrent,
15
+ )
16
+ from mikancli.cli.save_path_flow import (
17
+ build_content_save_path,
18
+ prompt_for_content_folder_name,
19
+ resolve_save_path,
20
+ )
21
+ from mikancli.cli.search_flow import resolve_mikan_selection, run_interactive_selection
22
+ from mikancli.config import get_config_path, load_config
23
+ from mikancli.core.models import AppConfig, RuleDraft, SearchRequest
24
+ from mikancli.core.normalize import collapse_spaces
25
+ from mikancli.core.rules import build_rule_draft
26
+ from mikancli.display import print_text_summary
27
+
28
+ STARTUP_ACTION_SEARCH = "search"
29
+ STARTUP_ACTION_QBITTORRENT = "qbittorrent"
30
+
31
+
32
+ def build_parser() -> argparse.ArgumentParser:
33
+ """Create the command-line parser for MikanCli flags and positional arguments. Returns a configured ArgumentParser; it does not parse arguments by itself."""
34
+ parser = argparse.ArgumentParser(
35
+ prog="mikancli",
36
+ description=(
37
+ "Search Mikan for an anime, inspect subgroup RSS contents, and preview "
38
+ "or submit qBittorrent RSS rule inputs."
39
+ ),
40
+ )
41
+ parser.add_argument("keyword", nargs="?", help="Anime title or search phrase.")
42
+ parser.add_argument(
43
+ "--include",
44
+ action="append",
45
+ default=[],
46
+ help="Word that must appear in accepted releases. Repeat for multiple values.",
47
+ )
48
+ parser.add_argument(
49
+ "--exclude",
50
+ action="append",
51
+ default=[],
52
+ help="Word that must not appear in accepted releases. Repeat for multiple values.",
53
+ )
54
+ parser.add_argument(
55
+ "--save-path",
56
+ help="Optional save path to attach to the qBittorrent rule.",
57
+ )
58
+ parser.add_argument(
59
+ "--json",
60
+ action="store_true",
61
+ help="Print the draft as JSON.",
62
+ )
63
+ parser.add_argument(
64
+ "--setup-qbittorrent",
65
+ action="store_true",
66
+ help="Configure and verify qBittorrent WebUI access.",
67
+ )
68
+ return parser
69
+
70
+
71
+ def _prompt_startup_action() -> str:
72
+ return select_option(
73
+ "Choose what you want to do",
74
+ [
75
+ (STARTUP_ACTION_SEARCH, "Search anime"),
76
+ (STARTUP_ACTION_QBITTORRENT, "Modify qBittorrent configurations"),
77
+ ],
78
+ default=STARTUP_ACTION_SEARCH,
79
+ allow_exit=True,
80
+ )
81
+
82
+
83
+ def build_request_from_args(args: argparse.Namespace, *, config: AppConfig, config_path: Path) -> SearchRequest:
84
+ """Convert parsed JSON-mode CLI arguments into a SearchRequest. Returns the cleaned request or raises ValueError when the required keyword is missing."""
85
+
86
+ if not args.keyword:
87
+ raise ValueError("keyword is required when using --json")
88
+
89
+ save_path = resolve_save_path(
90
+ args.save_path,
91
+ config,
92
+ prompt_for_default=False,
93
+ config_path=config_path,
94
+ )
95
+
96
+ return SearchRequest(
97
+ keyword=collapse_spaces(args.keyword),
98
+ include_words=tuple(args.include),
99
+ exclude_words=tuple(args.exclude),
100
+ save_path=save_path,
101
+ )
102
+
103
+
104
+ def _build_interactive_draft(args: argparse.Namespace, *, config: AppConfig,config_path: Path) -> RuleDraft:
105
+
106
+ bangumi, subgroup = run_interactive_selection(initial_keyword=args.keyword)
107
+
108
+ include_words = tuple(args.include) or prompt_word_list(
109
+ "Enter include words separated by commas, or press Enter to skip: "
110
+ )
111
+ exclude_words = tuple(args.exclude) or prompt_word_list(
112
+ "Enter exclude words separated by commas, or press Enter to skip: "
113
+ )
114
+ save_path = resolve_save_path(
115
+ args.save_path,
116
+ config,
117
+ prompt_for_default=True,
118
+ config_path=config_path,
119
+ )
120
+ content_folder_name = prompt_for_content_folder_name(bangumi.title)
121
+ final_save_path = build_content_save_path(save_path, content_folder_name)
122
+
123
+ request = SearchRequest(
124
+ keyword=bangumi.title,
125
+ include_words=include_words,
126
+ exclude_words=exclude_words,
127
+ save_path=final_save_path,
128
+ )
129
+ return build_rule_draft(
130
+ request,
131
+ bangumi=bangumi,
132
+ subgroup=subgroup,
133
+ notes=("Review the draft before submitting it to qBittorrent.",),
134
+ )
135
+
136
+
137
+ def main(argv: list[str] | None = None) -> int:
138
+
139
+ parser = build_parser()
140
+ args = parser.parse_args(argv)
141
+ config_path = get_config_path()
142
+ config = load_config(config_path)
143
+
144
+ if args.setup_qbittorrent:
145
+ try:
146
+ return setup_qbittorrent(config, config_path)
147
+ except ExitRequested:
148
+ print("Exited MikanCli.")
149
+ return 0
150
+
151
+ if args.json:
152
+ try:
153
+ request = build_request_from_args(
154
+ args,
155
+ config=config,
156
+ config_path=config_path,
157
+ )
158
+ except ValueError as exc:
159
+ parser.error(str(exc))
160
+
161
+ bangumi, subgroup, lookup_notes = resolve_mikan_selection(request)
162
+ draft = build_rule_draft(
163
+ request,
164
+ bangumi=bangumi,
165
+ subgroup=subgroup,
166
+ notes=lookup_notes,
167
+ )
168
+ print(json.dumps(draft.to_dict(), ensure_ascii=False, indent=2))
169
+ return 0
170
+
171
+ has_startup_menu = args.keyword is None
172
+
173
+ while True:
174
+ try:
175
+ if has_startup_menu:
176
+ while True:
177
+ startup_action = _prompt_startup_action()
178
+ if startup_action == STARTUP_ACTION_QBITTORRENT:
179
+ setup_exit_code = run_qbittorrent_configuration_flow(config, config_path)
180
+ if setup_exit_code != 0:
181
+ return setup_exit_code
182
+ config = load_config(config_path)
183
+ continue
184
+ break
185
+
186
+ setup_exit_code = prompt_for_qbittorrent_setup_if_needed(
187
+ config,
188
+ config_path,
189
+ )
190
+ if setup_exit_code != 0:
191
+ return setup_exit_code
192
+ config = load_config(config_path)
193
+ draft = _build_interactive_draft(
194
+ args,
195
+ config=config,
196
+ config_path=config_path,
197
+ )
198
+ except ExitRequested:
199
+ print("Exited MikanCli.")
200
+ return 0
201
+
202
+ summary_exit_code = print_text_summary(draft)
203
+ if summary_exit_code != 0:
204
+ return summary_exit_code
205
+ try:
206
+ submission_exit_code = prompt_to_submit_rule_to_qbittorrent(config, draft)
207
+ except ExitRequested:
208
+ print("Exited MikanCli.")
209
+ return 0
210
+
211
+ if has_startup_menu and submission_exit_code == QBITTORRENT_SUBMISSION_SKIPPED:
212
+ config = load_config(config_path)
213
+ continue
214
+
215
+ return 0 if submission_exit_code == QBITTORRENT_SUBMISSION_SKIPPED else submission_exit_code
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from mikancli.cli.prompts import prompt_text
4
+ from mikancli.core.normalize import collapse_spaces
5
+
6
+
7
+ def prompt_required_text(prompt: str) -> str:
8
+ while True:
9
+ entered = collapse_spaces(prompt_text(prompt, allow_exit=True))
10
+ if entered:
11
+ return entered
12
+ print("A value is required.")
13
+
14
+
15
+ def parse_word_list(value: str) -> tuple[str, ...]:
16
+ """Parse a comma-separated word list while dropping blanks and duplicates. Example: parse_word_list("HEVC, 1080p, HEVC") returns ("HEVC", "1080p")."""
17
+ words: list[str] = []
18
+ seen: set[str] = set()
19
+
20
+ for raw_part in value.split(","):
21
+ cleaned = collapse_spaces(raw_part)
22
+ if not cleaned:
23
+ continue
24
+
25
+ marker = cleaned.casefold()
26
+ if marker in seen:
27
+ continue
28
+
29
+ seen.add(marker)
30
+ words.append(cleaned)
31
+
32
+ return tuple(words)
33
+
34
+
35
+ def prompt_word_list(prompt: str) -> tuple[str, ...]:
36
+ entered = collapse_spaces(prompt_text(prompt, allow_exit=True))
37
+ if not entered:
38
+ return ()
39
+
40
+ return parse_word_list(entered)