mikancli 0.1.8__tar.gz → 0.2.1__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 (39) hide show
  1. {mikancli-0.1.8 → mikancli-0.2.1}/PKG-INFO +25 -4
  2. mikancli-0.1.8/README.md → mikancli-0.2.1/README.en.md +23 -3
  3. mikancli-0.2.1/README.md +222 -0
  4. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/__init__.py +1 -1
  5. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/entrypoint.py +11 -6
  6. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/input_parsing.py +3 -2
  7. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/prompts.py +17 -4
  8. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/qbittorrent_flow.py +37 -34
  9. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/save_path_flow.py +4 -3
  10. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/search_flow.py +18 -16
  11. mikancli-0.2.1/mikancli/display.py +207 -0
  12. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/i18n.py +18 -0
  13. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli.egg-info/PKG-INFO +25 -4
  14. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli.egg-info/SOURCES.txt +3 -0
  15. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli.egg-info/requires.txt +1 -0
  16. {mikancli-0.1.8 → mikancli-0.2.1}/pyproject.toml +3 -2
  17. mikancli-0.2.1/tests/test_cli_json.py +30 -0
  18. mikancli-0.2.1/tests/test_display.py +84 -0
  19. mikancli-0.2.1/tests/test_feed_preview.py +79 -0
  20. mikancli-0.1.8/mikancli/display.py +0 -135
  21. mikancli-0.1.8/tests/test_feed_preview.py +0 -38
  22. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/__main__.py +0 -0
  23. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/cli/__init__.py +0 -0
  24. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/config.py +0 -0
  25. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/core/__init__.py +0 -0
  26. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/core/models.py +0 -0
  27. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/core/normalize.py +0 -0
  28. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/core/rules.py +0 -0
  29. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/integrations/__init__.py +0 -0
  30. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/integrations/mikan.py +0 -0
  31. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/integrations/mikan_parsers.py +0 -0
  32. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/integrations/mikan_urls.py +0 -0
  33. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/integrations/qbittorrent.py +0 -0
  34. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli/integrations/qbittorrent_client.py +0 -0
  35. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli.egg-info/dependency_links.txt +0 -0
  36. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli.egg-info/entry_points.txt +0 -0
  37. {mikancli-0.1.8 → mikancli-0.2.1}/mikancli.egg-info/top_level.txt +0 -0
  38. {mikancli-0.1.8 → mikancli-0.2.1}/setup.cfg +0 -0
  39. {mikancli-0.1.8 → mikancli-0.2.1}/tests/test_qbittorrent_episode_filter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mikancli
3
- Version: 0.1.8
3
+ Version: 0.2.1
4
4
  Summary: CLI foundation for building qBittorrent RSS rules from anime search input.
5
5
  Author: metails
6
6
  Project-URL: Repository, https://github.com/JianHua-Deng/MikanCli
@@ -20,10 +20,11 @@ Classifier: Topic :: Utilities
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  Requires-Dist: InquirerPy>=0.3
23
+ Requires-Dist: rich>=14.0
23
24
 
24
25
  # MikanCli
25
26
 
26
- Language: [English](README.md) | [简体中文](README.zh-CN.md)
27
+ Language: [简体中文](README.md) | [English](README.en.md)
27
28
 
28
29
  MikanCli is a Python command-line tool for finding bangumi/anime on Mikanani.me, choosing the correct Bangumi and subgroup RSS feed, and turning that selection into a qBittorrent RSS download rule.
29
30
 
@@ -34,6 +35,7 @@ It supports both a guided interactive flow and a JSON preview mode for scripting
34
35
  - Search Mikan by anime title or keyword
35
36
  - Choose from matching Bangumi results and subgroup-specific RSS feeds
36
37
  - Preview recent RSS feed items with pagination before confirming a feed
38
+ - Read Rich-formatted rule summaries, feed previews, and status messages, with loading indicators for network requests
37
39
  - Build qBittorrent RSS rules with include and exclude filters
38
40
  - Filter anime-style episode numbers at or above a minimum episode
39
41
  - Choose and save a default download folder
@@ -88,7 +90,26 @@ python -m pip install -e .
88
90
  python -m mikancli
89
91
  ```
90
92
 
91
- Dependencies are declared in `pyproject.toml` and installed by `pip` or `pipx`. MikanCli does not install packages at runtime.
93
+ Dependencies, including Rich for output and InquirerPy for prompts, are declared in `pyproject.toml` and installed by `pip` or `pipx`. MikanCli does not install packages at runtime.
94
+
95
+ After pulling dependency changes into an existing editable `pip` installation, rerun `python -m pip install -e .` in that environment. Update `pipx` installations through `pipx` so dependencies stay in the app's environment.
96
+
97
+ ## Terminal Output
98
+
99
+ Rich formats rule summaries, feed previews, and feedback. InquirerPy handles menus and text input. Release titles containing brackets are displayed literally, and long titles, URLs, and paths wrap to fit the terminal.
100
+
101
+ Interactive terminals show loading indicators during network requests. Indicators stop before prompts and after failures. Redirected output uses static status messages, and feed previews retain their plain-text layout.
102
+
103
+ To disable Rich colors in PowerShell:
104
+
105
+ ```powershell
106
+ $env:NO_COLOR = "1"
107
+ mikancli
108
+ ```
109
+
110
+ Remove that setting with `Remove-Item Env:NO_COLOR`. Bold or dim text and loading animations may remain; InquirerPy manages menu styling separately.
111
+
112
+ The `--json` draft mode prints one JSON document without Rich formatting or progress messages.
92
113
 
93
114
  ## Guided Flow
94
115
 
@@ -169,7 +190,7 @@ mikancli/
169
190
  core/ dataclasses, normalization, and rule-building logic
170
191
  integrations/ Mikan and qBittorrent adapters
171
192
  config.py user config and folder selection helpers
172
- display.py text summaries and feed previews
193
+ display.py Rich summaries, feed previews, feedback, and loading indicators
173
194
  ```
174
195
 
175
196
  The console command is declared in `pyproject.toml`:
@@ -1,6 +1,6 @@
1
1
  # MikanCli
2
2
 
3
- Language: [English](README.md) | [简体中文](README.zh-CN.md)
3
+ Language: [简体中文](README.md) | [English](README.en.md)
4
4
 
5
5
  MikanCli is a Python command-line tool for finding bangumi/anime on Mikanani.me, choosing the correct Bangumi and subgroup RSS feed, and turning that selection into a qBittorrent RSS download rule.
6
6
 
@@ -11,6 +11,7 @@ It supports both a guided interactive flow and a JSON preview mode for scripting
11
11
  - Search Mikan by anime title or keyword
12
12
  - Choose from matching Bangumi results and subgroup-specific RSS feeds
13
13
  - Preview recent RSS feed items with pagination before confirming a feed
14
+ - Read Rich-formatted rule summaries, feed previews, and status messages, with loading indicators for network requests
14
15
  - Build qBittorrent RSS rules with include and exclude filters
15
16
  - Filter anime-style episode numbers at or above a minimum episode
16
17
  - Choose and save a default download folder
@@ -65,7 +66,26 @@ python -m pip install -e .
65
66
  python -m mikancli
66
67
  ```
67
68
 
68
- Dependencies are declared in `pyproject.toml` and installed by `pip` or `pipx`. MikanCli does not install packages at runtime.
69
+ Dependencies, including Rich for output and InquirerPy for prompts, are declared in `pyproject.toml` and installed by `pip` or `pipx`. MikanCli does not install packages at runtime.
70
+
71
+ After pulling dependency changes into an existing editable `pip` installation, rerun `python -m pip install -e .` in that environment. Update `pipx` installations through `pipx` so dependencies stay in the app's environment.
72
+
73
+ ## Terminal Output
74
+
75
+ Rich formats rule summaries, feed previews, and feedback. InquirerPy handles menus and text input. Release titles containing brackets are displayed literally, and long titles, URLs, and paths wrap to fit the terminal.
76
+
77
+ Interactive terminals show loading indicators during network requests. Indicators stop before prompts and after failures. Redirected output uses static status messages, and feed previews retain their plain-text layout.
78
+
79
+ To disable Rich colors in PowerShell:
80
+
81
+ ```powershell
82
+ $env:NO_COLOR = "1"
83
+ mikancli
84
+ ```
85
+
86
+ Remove that setting with `Remove-Item Env:NO_COLOR`. Bold or dim text and loading animations may remain; InquirerPy manages menu styling separately.
87
+
88
+ The `--json` draft mode prints one JSON document without Rich formatting or progress messages.
69
89
 
70
90
  ## Guided Flow
71
91
 
@@ -146,7 +166,7 @@ mikancli/
146
166
  core/ dataclasses, normalization, and rule-building logic
147
167
  integrations/ Mikan and qBittorrent adapters
148
168
  config.py user config and folder selection helpers
149
- display.py text summaries and feed previews
169
+ display.py Rich summaries, feed previews, feedback, and loading indicators
150
170
  ```
151
171
 
152
172
  The console command is declared in `pyproject.toml`:
@@ -0,0 +1,222 @@
1
+ # MikanCli
2
+
3
+ 语言:[简体中文](README.md) | [English](README.en.md)
4
+
5
+ MikanCli 是一个 Python 命令行工具,用于在 Mikanani.me 上查找番剧/动画,选择正确的 Bangumi 和字幕组 RSS 订阅,并把该选择转换成 qBittorrent RSS 下载规则。
6
+
7
+ 它支持引导式交互流程,也支持用于脚本或检查的 JSON 预览模式。目前交互式流程是最完整的模式。
8
+
9
+ ## 功能
10
+
11
+ - 根据动画标题或关键词搜索 Mikan
12
+ - 从匹配的 Bangumi 结果和字幕组 RSS 订阅中进行选择
13
+ - 在确认订阅前分页预览最近的 RSS 条目
14
+ - 使用 Rich 显示规则摘要、订阅预览和状态消息,并在网络请求期间显示加载指示器
15
+ - 使用包含和排除过滤条件生成 qBittorrent RSS 规则
16
+ - 过滤达到某个最低集数的动画标题格式
17
+ - 选择并保存默认下载文件夹
18
+ - 从命令行配置 qBittorrent WebUI 访问
19
+ - 将 RSS 订阅和自动下载规则提交到 qBittorrent,并验证 qBittorrent 已保存这些内容
20
+ - 以 JSON 形式打印规则草稿,不提交任何内容
21
+
22
+ ## 环境要求
23
+
24
+ - Python 3.10 或更新版本
25
+ - `pipx`,用于把 MikanCli 安装为独立命令行应用
26
+ - qBittorrent,如果你希望 MikanCli 自动提交 RSS 订阅和规则
27
+
28
+ ## 快速开始 | 安装
29
+
30
+ 推荐使用 `pipx` 安装 MikanCli,这样你可以在任何终端中直接使用它。
31
+ 如果你已经安装了 `pipx`,可以跳过下面两条命令:
32
+
33
+ ```bash
34
+ python -m pip install --user pipx # 为当前用户安装 pipx
35
+ python -m pipx ensurepath # 将 pipx 可执行文件目录添加到 PATH
36
+ ```
37
+
38
+ 运行 `pipx ensurepath` 后,重新打开一个终端,然后执行:
39
+
40
+ ```bash
41
+ pipx install mikancli
42
+ ```
43
+
44
+ ## 使用方法
45
+
46
+ 安装完成后,运行下面的命令并按照菜单操作:
47
+
48
+ ```bash
49
+ mikancli
50
+ ```
51
+
52
+ ## 通过克隆仓库安装
53
+
54
+ 从本地克隆安装:
55
+
56
+ ```bash
57
+ git clone https://github.com/JianHua-Deng/MikanCli.git
58
+ cd MikanCli
59
+ python -m pipx install -e .
60
+ ```
61
+
62
+ 用于开发时,也可以使用可编辑的 `pip` 安装:
63
+
64
+ ```bash
65
+ python -m pip install -e .
66
+ python -m mikancli
67
+ ```
68
+
69
+ 依赖项(包括用于输出的 Rich 和用于交互提示的 InquirerPy)声明在 `pyproject.toml` 中,并由 `pip` 或 `pipx` 安装。MikanCli 不会在运行时安装依赖包。
70
+
71
+ 已有可编辑 `pip` 安装在拉取依赖变更后,需要在原环境中重新运行 `python -m pip install -e .`。通过 `pipx` 安装的应用应使用 `pipx` 更新,让依赖仍安装在应用自己的环境中。
72
+
73
+ ## 终端输出
74
+
75
+ Rich 用于显示规则摘要、订阅预览和反馈消息,InquirerPy 负责菜单和文本输入。发布标题中的方括号会原样显示,较长的标题、URL 和路径会根据终端宽度自动换行。
76
+
77
+ 交互式终端会在网络请求期间显示加载指示器,并在提示输入前或请求失败后停止。输出被重定向时,会使用静态状态消息,订阅预览保留纯文本布局。
78
+
79
+ 在 PowerShell 中禁用 Rich 颜色:
80
+
81
+ ```powershell
82
+ $env:NO_COLOR = "1"
83
+ mikancli
84
+ ```
85
+
86
+ 使用 `Remove-Item Env:NO_COLOR` 移除此设置。粗体、暗色样式和加载动画可能仍会显示;菜单样式由 InquirerPy 单独管理。
87
+
88
+ `--json` 草稿模式只输出一个 JSON 文档,不包含 Rich 格式或进度消息。
89
+
90
+ ## 引导式流程
91
+
92
+ 不带参数运行 `mikancli` 时,第一个菜单会让你选择:
93
+
94
+ - 搜索动画
95
+ - 修改 qBittorrent 配置
96
+ - 切换语言
97
+ - 退出 MikanCli
98
+
99
+ 搜索流程会依次:
100
+
101
+ 1. 询问动画标题或关键词
102
+ 2. 在 Mikan 上搜索匹配的 Bangumi 条目
103
+ 3. 让你选择正确的 Bangumi 条目
104
+ 4. 从所选 Bangumi 页面获取字幕组 RSS 订阅
105
+ 5. 让你选择字幕组
106
+ 6. 预览最近的 RSS 订阅条目
107
+ 7. 询问包含、排除和最低集数过滤条件
108
+ 8. 询问下载保存位置
109
+ 9. 生成规则草稿
110
+ 10. 将订阅和规则提交到 qBittorrent
111
+ 11. 通过 qBittorrent WebUI API 验证提交的订阅和规则
112
+
113
+ 在需要输入文本的交互式提示中,可以输入 `exit` 或 `quit` 退出;菜单中也会包含退出选项。
114
+
115
+ ## qBittorrent 设置
116
+
117
+ 在 MikanCli 可以提交订阅或规则之前,需要先启用 qBittorrent WebUI:
118
+
119
+ 1. 打开 qBittorrent 设置
120
+ 2. 启用 WebUI 或远程控制
121
+ 3. 确认 WebUI 地址、用户名和密码。如果地址为空,通常表示它是 `http://localhost:[port]`
122
+ 4. 运行 `mikancli --setup-qbittorrent`
123
+
124
+ 设置说明:
125
+
126
+ - URL 直接按 Enter 会使用 `http://localhost:8080`
127
+ - 输入 `localhost:8080` 会自动规范化为 `http://localhost:8080`
128
+ - 如果 qBittorrent WebUI 允许 localhost 绕过认证,用户名和密码可以留空
129
+ - 如果 qBittorrent 拒绝连接,请重新检查 qBittorrent 设置里的 WebUI 端口和凭据
130
+
131
+ ## 配置
132
+
133
+ MikanCli 将配置保存在 JSON 文件中:
134
+
135
+ - Windows:`%APPDATA%\Roaming\MikanCli\config.json`
136
+
137
+ 保存的设置可以包括:
138
+
139
+ - 默认下载文件夹
140
+ - 语言偏好
141
+ - qBittorrent WebUI URL
142
+ - qBittorrent 用户名和密码
143
+ - qBittorrent 分类
144
+ - qBittorrent 是否应将匹配的种子以暂停状态添加
145
+
146
+ qBittorrent 密码会保存在配置文件中,这样 MikanCli 后续运行时才能提交规则。共享机器上请保护好这个文件。
147
+
148
+ ## 语言支持
149
+
150
+ MikanCli 支持英文和简体中文的用户界面文本,包括交互式菜单、提示、设置说明、摘要和帮助文本。命令名称、选项名称、JSON 字段名、Mikan 标题、字幕组名称、URL 和 qBittorrent API 数据保持稳定。
151
+
152
+ 语言选择优先级如下:
153
+
154
+ 1. `--language en` 或 `--language zh-CN`
155
+ 2. `MIKANCLI_LANG=en` 或 `MIKANCLI_LANG=zh-CN`
156
+ 3. 已保存的配置值
157
+ 4. 默认回退到英文
158
+
159
+ 交互式用户始终可以从启动菜单切换语言。选中的语言会保存到配置文件,供以后运行使用。
160
+
161
+
162
+ ## 项目结构
163
+
164
+ ```text
165
+ mikancli/
166
+ cli/ CLI 入口、提示和交互流程
167
+ core/ 数据类、规范化和规则生成逻辑
168
+ integrations/ Mikan 和 qBittorrent 适配器
169
+ config.py 用户配置和文件夹选择辅助函数
170
+ display.py Rich 摘要、订阅预览、反馈和加载指示器
171
+ ```
172
+
173
+ 控制台命令在 `pyproject.toml` 中声明:
174
+
175
+ ```toml
176
+ [project.scripts]
177
+ mikancli = "mikancli.cli.entrypoint:main"
178
+ ```
179
+
180
+ ## 命令用法
181
+
182
+ ```text
183
+ usage: mikancli [-h] [--include INCLUDE] [--exclude EXCLUDE]
184
+ [--min-episode MIN_EPISODE] [--save-path SAVE_PATH] [--json]
185
+ [--setup-qbittorrent] [--language {en,zh-CN}] [--version]
186
+ [keyword]
187
+ ```
188
+
189
+ 选项:
190
+
191
+ - `keyword`:动画标题或搜索短语
192
+ - `--include VALUE`:要求已接受的发布标题中包含某个词或短语。可重复传入多个值
193
+ - `--exclude VALUE`:拒绝标题中包含某个词或短语的发布。可重复传入多个值
194
+ - `--min-episode NUMBER`:只接受达到此集数或更高集数的动画发布标题
195
+ - `--save-path PATH`:为生成的 qBittorrent 规则使用这个基础下载文件夹
196
+ - `--json`:以 JSON 形式打印规则草稿。此模式不会提交到 qBittorrent
197
+ - `--setup-qbittorrent`:配置并验证 qBittorrent WebUI 设置
198
+ - `--language {en,zh-CN}`:为本次运行选择 CLI 语言
199
+ - `--version`:打印已安装的 CLI 版本
200
+
201
+ ## 最低集数过滤
202
+
203
+ 当 RSS 条目标题使用常见的动画格式,并在集数前有短横线或使用方括号集数时,可以使用 `--min-episode`:
204
+
205
+ ```text
206
+ [SubsPlease] One Piece - 1126 (1080p)
207
+ [Skymoon-Raws][One Piece][1126][ViuTV][WEB-RIP][CHT][SRT][1080p][MKV]
208
+ ```
209
+
210
+ 例如:
211
+
212
+ ```bash
213
+ mikancli "one piece" --include SubsPlease --min-episode 1126
214
+ ```
215
+
216
+ MikanCli 会把最低集数转换成 qBittorrent 规则 `mustContain` 字段中的正则表达式,并启用 `useRegex`。普通的 `--include` 和 `--exclude` 值仍然按字面词或短语处理,并且每个包含值都必须出现在标题中,qBittorrent 才会认为它匹配。
217
+
218
+ 提交最低集数规则时,MikanCli 会检查 qBittorrent 的 RSS 条目保留上限。如果 qBittorrent 每个订阅保留少于 500 个条目,MikanCli 会询问是否增加这个设置,让 qBittorrent 能看到足够多的 Mikan 订阅条目来匹配较新的集数。
219
+
220
+ ## 发布
221
+
222
+ 仓库包含 GitHub Actions 工作流 `.github/workflows/publish.yml`。当 `main` 分支中 `pyproject.toml` 的包版本发生变化时,该工作流会发布到 PyPI。
@@ -4,4 +4,4 @@ __all__ = [
4
4
  "__version__",
5
5
  ]
6
6
 
7
- __version__ = "0.1.8"
7
+ __version__ = "0.2.0"
@@ -31,7 +31,7 @@ from mikancli.config import get_config_path, load_config, save_config
31
31
  from mikancli.core.models import AppConfig, RuleDraft, SearchRequest
32
32
  from mikancli.core.normalize import collapse_spaces
33
33
  from mikancli.core.rules import build_rule_draft
34
- from mikancli.display import print_text_summary
34
+ from mikancli.display import print_message, print_section, print_text_summary
35
35
  from mikancli.i18n import (
36
36
  LANGUAGE_LABELS,
37
37
  SUPPORTED_LANGUAGES,
@@ -143,6 +143,7 @@ def resolve_startup_language(requested_language: str | None, config: AppConfig)
143
143
 
144
144
 
145
145
  def prompt_startup_action() -> str:
146
+ print_section("MikanCli")
146
147
  return select_option(
147
148
  t("startup.choose_action"),
148
149
  [
@@ -156,6 +157,7 @@ def prompt_startup_action() -> str:
156
157
 
157
158
 
158
159
  def run_language_configuration_flow(config: AppConfig, config_path: Path) -> AppConfig:
160
+ print_section(t("startup.language"))
159
161
  selected_language = select_option(
160
162
  t("language.choose"),
161
163
  [(language, LANGUAGE_LABELS[language]) for language in SUPPORTED_LANGUAGES],
@@ -165,7 +167,7 @@ def run_language_configuration_flow(config: AppConfig, config_path: Path) -> App
165
167
  set_language(selected_language)
166
168
  updated_config = replace(config, language=selected_language)
167
169
  save_config(config_path, updated_config)
168
- print(t("language.saved", language=LANGUAGE_LABELS[selected_language]))
170
+ print_message(t("language.saved", language=LANGUAGE_LABELS[selected_language]), style="success")
169
171
  return updated_config
170
172
 
171
173
 
@@ -194,6 +196,8 @@ def build_interactive_draft(args: argparse.Namespace, *, config: AppConfig,confi
194
196
 
195
197
  bangumi, subgroup = run_interactive_selection(initial_keyword=args.keyword)
196
198
 
199
+ if not args.include or not args.exclude or args.min_episode is None:
200
+ print_section(t("ui.filters"))
197
201
  include_words = tuple(args.include) or prompt_word_list(
198
202
  t("filters.include_prompt")
199
203
  )
@@ -206,6 +210,7 @@ def build_interactive_draft(args: argparse.Namespace, *, config: AppConfig,confi
206
210
  t("filters.min_episode_prompt"),
207
211
  field_name=t("filters.min_episode_field"),
208
212
  )
213
+ print_section(t("ui.save_path"))
209
214
  save_path = resolve_save_path(
210
215
  args.save_path,
211
216
  config,
@@ -247,7 +252,7 @@ def main(argv: list[str] | None = None) -> int:
247
252
  try:
248
253
  return setup_qbittorrent(config, config_path)
249
254
  except ExitRequested:
250
- print(t("common.exited"))
255
+ print_message(t("common.exited"))
251
256
  return 0
252
257
 
253
258
  if args.json:
@@ -307,7 +312,7 @@ def main(argv: list[str] | None = None) -> int:
307
312
  config_path=config_path,
308
313
  )
309
314
  except ExitRequested:
310
- print(t("common.exited"))
315
+ print_message(t("common.exited"))
311
316
  return 0
312
317
 
313
318
  summary_exit_code = print_text_summary(draft)
@@ -316,7 +321,7 @@ def main(argv: list[str] | None = None) -> int:
316
321
  try:
317
322
  submission_exit_code = prompt_to_submit_rule_to_qbittorrent(config, draft)
318
323
  if submission_exit_code == QBITTORRENT_NOT_CONFIGURED:
319
- print(t("qb.submit.not_configured"))
324
+ print_message(t("qb.submit.not_configured"), style="warning")
320
325
  setup_exit_code = prompt_for_qbittorrent_setup_if_needed(config, config_path)
321
326
  if setup_exit_code not in {
322
327
  QBITTORRENT_SETUP_SUCCESS,
@@ -331,7 +336,7 @@ def main(argv: list[str] | None = None) -> int:
331
336
  continue
332
337
 
333
338
  except ExitRequested:
334
- print(t("common.exited"))
339
+ print_message(t("common.exited"))
335
340
  return 0
336
341
 
337
342
  if has_startup_menu and submission_exit_code == QBITTORRENT_SUBMISSION_SKIPPED:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from mikancli.cli.prompts import prompt_text
4
4
  from mikancli.core.normalize import collapse_spaces
5
+ from mikancli.display import print_message
5
6
  from mikancli.i18n import t
6
7
 
7
8
 
@@ -10,7 +11,7 @@ def prompt_required_text(prompt: str) -> str:
10
11
  entered = collapse_spaces(prompt_text(prompt, allow_exit=True))
11
12
  if entered:
12
13
  return entered
13
- print(t("common.value_required"))
14
+ print_message(t("common.value_required"), style="warning")
14
15
 
15
16
 
16
17
  def parse_word_list(value: str) -> tuple[str, ...]:
@@ -62,4 +63,4 @@ def prompt_optional_positive_int(prompt: str, *, field_name: str) -> int | None:
62
63
  try:
63
64
  return parse_optional_positive_int(entered, field_name=field_name)
64
65
  except ValueError as exc:
65
- print(exc)
66
+ print_message(str(exc), style="warning")
@@ -4,12 +4,12 @@ from collections.abc import Iterable
4
4
  from typing import Any, TypeVar
5
5
 
6
6
  from mikancli.core.normalize import collapse_spaces
7
+ from mikancli.display import PRIMARY_COLOR, console
7
8
  from mikancli.i18n import t
8
9
 
9
10
  T = TypeVar("T")
10
11
  EXIT_OPTION = "__exit_cli__"
11
12
  EXIT_TEXT_VALUES = {"exit", "quit"}
12
- PROMPT_SEPARATOR = "----------------------------------------"
13
13
  MENU_SEPARATOR_LABEL = ""
14
14
 
15
15
 
@@ -39,8 +39,18 @@ def get_separator_class():
39
39
  return Separator
40
40
 
41
41
 
42
+ def get_prompt_style():
43
+ from InquirerPy import get_style
44
+
45
+ return get_style({
46
+ "questionmark": PRIMARY_COLOR, "question": "bold",
47
+ "answermark": PRIMARY_COLOR, "answer": PRIMARY_COLOR, "input": PRIMARY_COLOR,
48
+ "pointer": f"bold {PRIMARY_COLOR}", "fuzzy_border": PRIMARY_COLOR,
49
+ }, style_override=False)
50
+
51
+
42
52
  def prepare_prompt_message(message: str) -> str:
43
- print(PROMPT_SEPARATOR)
53
+ console.print()
44
54
  return message.strip("\n")
45
55
 
46
56
 
@@ -86,6 +96,8 @@ def select_option(
86
96
  pointer=">",
87
97
  instruction=t("common.use_arrow_keys"),
88
98
  max_height="70%",
99
+ style=get_prompt_style(),
100
+ border=len(options) > 2,
89
101
  cycle=True,
90
102
  )
91
103
  selected = prompt.execute()
@@ -101,6 +113,7 @@ def prompt_text(message: str, *, default: str | None = None, allow_exit: bool =
101
113
  prompt = inquirer.text(
102
114
  message=prepare_prompt_message(message),
103
115
  default=default or "",
116
+ style=get_prompt_style(),
104
117
  )
105
118
  entered = collapse_spaces(prompt.execute())
106
119
  if allow_exit and entered.casefold() in EXIT_TEXT_VALUES:
@@ -113,9 +126,9 @@ def prompt_password(message: str, *, allow_exit: bool = False) -> str:
113
126
  inquirer = get_inquirer()
114
127
  prompt_factory = getattr(inquirer, "secret", None)
115
128
  if prompt_factory is not None:
116
- prompt = prompt_factory(message=prepare_prompt_message(message))
129
+ prompt = prompt_factory(message=prepare_prompt_message(message), style=get_prompt_style())
117
130
  else: # pragma: no cover - fallback for alternate InquirerPy versions
118
- prompt = inquirer.text(message=prepare_prompt_message(message), secret=True)
131
+ prompt = inquirer.text(message=prepare_prompt_message(message), secret=True, style=get_prompt_style())
119
132
 
120
133
  entered = prompt.execute()
121
134
  if allow_exit and entered.strip().casefold() in EXIT_TEXT_VALUES:
@@ -6,6 +6,7 @@ from mikancli.cli.prompts import confirm_choice, prompt_password, prompt_text
6
6
  from mikancli.config import save_config
7
7
  from mikancli.core.models import AppConfig, QBittorrentSettings, RuleDraft
8
8
  from mikancli.core.normalize import collapse_spaces, sanitize_folder_name
9
+ from mikancli.display import loading, print_message, print_section
9
10
  from mikancli.i18n import t
10
11
  from mikancli.integrations.qbittorrent import (
11
12
  MIN_RSS_ARTICLES_FOR_EPISODE_FILTER,
@@ -27,15 +28,10 @@ QBITTORRENT_ERROR = 3
27
28
 
28
29
  def setup_qbittorrent(config: AppConfig, config_path: Path) -> int:
29
30
  """Prompt for qBittorrent WebUI settings, verify the connection, and save them on success. Returns QBITTORRENT_SETUP_SUCCESS when verification succeeds and QBITTORRENT_ERROR when qBittorrent rejects or cannot be reached."""
30
- print()
31
- print(t("qb.setup.title"))
32
- print(t("qb.setup.step1"))
33
- print(t("qb.setup.step2"))
34
- print(t("qb.setup.step3"))
35
- print(t("qb.setup.step4"))
36
- print(t("qb.setup.step5"))
37
- print("------------------------------------------")
38
- print()
31
+ print_section(
32
+ t("qb.setup.title").strip("- "),
33
+ "\n".join(t(f"qb.setup.step{step}") for step in range(1, 6)),
34
+ )
39
35
 
40
36
  default_url = config.qbittorrent_url or "http://localhost:8080"
41
37
  entered_url = (
@@ -49,7 +45,7 @@ def setup_qbittorrent(config: AppConfig, config_path: Path) -> int:
49
45
  or "http://localhost:8080"
50
46
  )
51
47
 
52
- print(t("qb.setup.auth_hint"))
48
+ print_message(t("qb.setup.auth_hint"))
53
49
  username = (
54
50
  collapse_spaces(
55
51
  prompt_text(
@@ -72,10 +68,10 @@ def setup_qbittorrent(config: AppConfig, config_path: Path) -> int:
72
68
  )
73
69
 
74
70
  try:
75
- print(t("qb.setup.verifying"))
76
- version = check_connection(settings)
71
+ with loading(t("qb.setup.verifying")):
72
+ version = check_connection(settings)
77
73
  except QBittorrentError as exc:
78
- print(str(exc))
74
+ print_message(str(exc), style="error")
79
75
  return QBITTORRENT_ERROR
80
76
 
81
77
  save_config(
@@ -90,7 +86,7 @@ def setup_qbittorrent(config: AppConfig, config_path: Path) -> int:
90
86
  qbittorrent_add_paused=config.qbittorrent_add_paused,
91
87
  ),
92
88
  )
93
- print(t("qb.setup.verified", version=version))
89
+ print_message(t("qb.setup.verified", version=version), style="success")
94
90
  return QBITTORRENT_SETUP_SUCCESS
95
91
 
96
92
 
@@ -160,7 +156,8 @@ def prompt_to_raise_rss_article_limit_if_needed(
160
156
  if draft.min_episode is None:
161
157
  return
162
158
 
163
- current_limit = get_rss_max_articles_per_feed(settings)
159
+ with loading(t("qb.submit.checking")):
160
+ current_limit = get_rss_max_articles_per_feed(settings)
164
161
  if (
165
162
  current_limit is None
166
163
  or current_limit >= MIN_RSS_ARTICLES_FOR_EPISODE_FILTER
@@ -177,23 +174,26 @@ def prompt_to_raise_rss_article_limit_if_needed(
177
174
  allow_exit=True,
178
175
  )
179
176
  if not should_update:
180
- print(
177
+ print_message(
181
178
  t(
182
179
  "qb.submit.rss_limit_skipped",
183
180
  current=current_limit,
184
- )
181
+ ),
182
+ style="warning",
185
183
  )
186
184
  return
187
185
 
188
- set_rss_max_articles_per_feed(
189
- settings,
190
- MIN_RSS_ARTICLES_FOR_EPISODE_FILTER,
191
- )
192
- print(
186
+ with loading(t("qb.submit.updating_limit")):
187
+ set_rss_max_articles_per_feed(
188
+ settings,
189
+ MIN_RSS_ARTICLES_FOR_EPISODE_FILTER,
190
+ )
191
+ print_message(
193
192
  t(
194
193
  "qb.submit.rss_limit_updated",
195
194
  minimum=MIN_RSS_ARTICLES_FOR_EPISODE_FILTER,
196
- )
195
+ ),
196
+ style="success",
197
197
  )
198
198
 
199
199
 
@@ -203,6 +203,7 @@ def prompt_to_submit_rule_to_qbittorrent(config: AppConfig, draft: RuleDraft,) -
203
203
  if not config.qbittorrent_url:
204
204
  return QBITTORRENT_NOT_CONFIGURED
205
205
 
206
+ print_section("qBittorrent")
206
207
  should_submit = confirm_choice(
207
208
  t("qb.submit.continue"),
208
209
  default=True,
@@ -218,7 +219,9 @@ def prompt_to_submit_rule_to_qbittorrent(config: AppConfig, draft: RuleDraft,) -
218
219
  )
219
220
 
220
221
  try:
221
- if qbittorrent_rule_exists(settings, draft.rule_name):
222
+ with loading(t("qb.submit.checking")):
223
+ rule_exists = qbittorrent_rule_exists(settings, draft.rule_name)
224
+ if rule_exists:
222
225
  should_replace = confirm_choice(
223
226
  t("qb.submit.replace_existing", rule_name=draft.rule_name),
224
227
  default=False,
@@ -229,17 +232,17 @@ def prompt_to_submit_rule_to_qbittorrent(config: AppConfig, draft: RuleDraft,) -
229
232
 
230
233
  prompt_to_raise_rss_article_limit_if_needed(settings, draft)
231
234
  feed_path = prompt_for_rss_feed_name(draft)
232
- print(t("qb.submit.submitting"))
233
- submit_rule_draft(
234
- settings,
235
- draft,
236
- add_paused=config.qbittorrent_add_paused,
237
- assigned_category=config.qbittorrent_category,
238
- feed_path=feed_path,
239
- )
235
+ with loading(t("qb.submit.submitting")):
236
+ submit_rule_draft(
237
+ settings,
238
+ draft,
239
+ add_paused=config.qbittorrent_add_paused,
240
+ assigned_category=config.qbittorrent_category,
241
+ feed_path=feed_path,
242
+ )
240
243
  except QBittorrentError as exc:
241
- print(t("qb.submit.failed", error=str(exc)))
244
+ print_message(t("qb.submit.failed", error=str(exc)), style="error")
242
245
  return QBITTORRENT_ERROR
243
246
 
244
- print(t("qb.submit.success"))
247
+ print_message(t("qb.submit.success"), style="success")
245
248
  return QBITTORRENT_SETUP_SUCCESS