mikancli 0.2.1__tar.gz → 0.2.2__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.2.1 → mikancli-0.2.2}/PKG-INFO +8 -1
  2. {mikancli-0.2.1 → mikancli-0.2.2}/README.en.md +6 -0
  3. {mikancli-0.2.1 → mikancli-0.2.2}/README.md +49 -44
  4. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/input_parsing.py +2 -2
  5. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/prompts.py +4 -1
  6. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/search_flow.py +10 -6
  7. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/display.py +1 -1
  8. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/PKG-INFO +8 -1
  9. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/SOURCES.txt +2 -1
  10. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/requires.txt +1 -0
  11. {mikancli-0.2.1 → mikancli-0.2.2}/pyproject.toml +2 -1
  12. {mikancli-0.2.1 → mikancli-0.2.2}/tests/test_display.py +1 -0
  13. mikancli-0.2.2/tests/test_search_prompt.py +35 -0
  14. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/__init__.py +0 -0
  15. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/__main__.py +0 -0
  16. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/__init__.py +0 -0
  17. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/entrypoint.py +0 -0
  18. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/qbittorrent_flow.py +0 -0
  19. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/save_path_flow.py +0 -0
  20. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/config.py +0 -0
  21. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/__init__.py +0 -0
  22. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/models.py +0 -0
  23. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/normalize.py +0 -0
  24. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/rules.py +0 -0
  25. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/i18n.py +0 -0
  26. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/__init__.py +0 -0
  27. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/mikan.py +0 -0
  28. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/mikan_parsers.py +0 -0
  29. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/mikan_urls.py +0 -0
  30. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/qbittorrent.py +0 -0
  31. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/qbittorrent_client.py +0 -0
  32. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/dependency_links.txt +0 -0
  33. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/entry_points.txt +0 -0
  34. {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/top_level.txt +0 -0
  35. {mikancli-0.2.1 → mikancli-0.2.2}/setup.cfg +0 -0
  36. {mikancli-0.2.1 → mikancli-0.2.2}/tests/test_cli_json.py +0 -0
  37. {mikancli-0.2.1 → mikancli-0.2.2}/tests/test_feed_preview.py +0 -0
  38. {mikancli-0.2.1 → mikancli-0.2.2}/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.2.1
3
+ Version: 0.2.2
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,6 +20,7 @@ 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: prompt-toolkit<4,>=3.0.52
23
24
  Requires-Dist: rich>=14.0
24
25
 
25
26
  # MikanCli
@@ -43,6 +44,12 @@ It supports both a guided interactive flow and a JSON preview mode for scripting
43
44
  - Submit RSS feeds and auto-download rules to qBittorrent, then verify that qBittorrent saved them
44
45
  - Print rule drafts as JSON without submitting anything
45
46
 
47
+ ## Screenshots
48
+
49
+ ![Search input (demo data)](docs/images/search-input.png)
50
+
51
+ ![RSS feed preview (demo data)](docs/images/feed-preview.png)
52
+
46
53
  ## Requirements
47
54
 
48
55
  - Python 3.10 or newer
@@ -19,6 +19,12 @@ It supports both a guided interactive flow and a JSON preview mode for scripting
19
19
  - Submit RSS feeds and auto-download rules to qBittorrent, then verify that qBittorrent saved them
20
20
  - Print rule drafts as JSON without submitting anything
21
21
 
22
+ ## Screenshots
23
+
24
+ ![Search input (demo data)](docs/images/search-input.png)
25
+
26
+ ![RSS feed preview (demo data)](docs/images/feed-preview.png)
27
+
22
28
  ## Requirements
23
29
 
24
30
  - Python 3.10 or newer
@@ -2,40 +2,46 @@
2
2
 
3
3
  语言:[简体中文](README.md) | [English](README.en.md)
4
4
 
5
- MikanCli 是一个 Python 命令行工具,用于在 Mikanani.me 上查找番剧/动画,选择正确的 Bangumi 和字幕组 RSS 订阅,并把该选择转换成 qBittorrent RSS 下载规则。
5
+ MikanCli 是一个 Python 命令行工具,可以在 Mikanani.me 上搜索番剧/动画,选定对应的 Bangumi 条目和字幕组 RSS 订阅,并据此生成 qBittorrent 的 RSS 下载规则。
6
6
 
7
- 它支持引导式交互流程,也支持用于脚本或检查的 JSON 预览模式。目前交互式流程是最完整的模式。
7
+ 它既提供引导式交互流程,也支持用于脚本或检查的 JSON 预览模式。目前交互式流程是功能最完整的模式。
8
8
 
9
9
  ## 功能
10
10
 
11
- - 根据动画标题或关键词搜索 Mikan
11
+ - 按动画标题或关键词搜索 Mikan
12
12
  - 从匹配的 Bangumi 结果和字幕组 RSS 订阅中进行选择
13
13
  - 在确认订阅前分页预览最近的 RSS 条目
14
14
  - 使用 Rich 显示规则摘要、订阅预览和状态消息,并在网络请求期间显示加载指示器
15
15
  - 使用包含和排除过滤条件生成 qBittorrent RSS 规则
16
- - 过滤达到某个最低集数的动画标题格式
16
+ - 按最低集数过滤动画发布标题
17
17
  - 选择并保存默认下载文件夹
18
- - 从命令行配置 qBittorrent WebUI 访问
18
+ - 通过命令行配置 qBittorrent WebUI 访问
19
19
  - 将 RSS 订阅和自动下载规则提交到 qBittorrent,并验证 qBittorrent 已保存这些内容
20
- - 以 JSON 形式打印规则草稿,不提交任何内容
20
+ - 以 JSON 形式输出规则草稿,不提交任何内容
21
+
22
+ ## 界面预览
23
+
24
+ ![搜索输入框(演示数据)](docs/images/search-input.png)
25
+
26
+ ![RSS 订阅预览(演示数据)](docs/images/feed-preview.png)
21
27
 
22
28
  ## 环境要求
23
29
 
24
30
  - Python 3.10 或更新版本
25
- - `pipx`,用于把 MikanCli 安装为独立命令行应用
31
+ - `pipx`,用于将 MikanCli 安装为独立命令行应用
26
32
  - qBittorrent,如果你希望 MikanCli 自动提交 RSS 订阅和规则
27
33
 
28
34
  ## 快速开始 | 安装
29
35
 
30
- 推荐使用 `pipx` 安装 MikanCli,这样你可以在任何终端中直接使用它。
31
- 如果你已经安装了 `pipx`,可以跳过下面两条命令:
36
+ 推荐使用 `pipx` 安装 MikanCli,这样可以在任意终端中直接使用。
37
+ 如果已经安装 `pipx`,可以跳过以下两条命令:
32
38
 
33
39
  ```bash
34
40
  python -m pip install --user pipx # 为当前用户安装 pipx
35
41
  python -m pipx ensurepath # 将 pipx 可执行文件目录添加到 PATH
36
42
  ```
37
43
 
38
- 运行 `pipx ensurepath` 后,重新打开一个终端,然后执行:
44
+ 运行 `pipx ensurepath` 后,请重新打开一个终端,然后执行:
39
45
 
40
46
  ```bash
41
47
  pipx install mikancli
@@ -43,7 +49,7 @@ pipx install mikancli
43
49
 
44
50
  ## 使用方法
45
51
 
46
- 安装完成后,运行下面的命令并按照菜单操作:
52
+ 安装完成后,运行以下命令,并按菜单提示操作:
47
53
 
48
54
  ```bash
49
55
  mikancli
@@ -59,22 +65,22 @@ cd MikanCli
59
65
  python -m pipx install -e .
60
66
  ```
61
67
 
62
- 用于开发时,也可以使用可编辑的 `pip` 安装:
68
+ 开发时也可以使用可编辑模式的 `pip` 安装:
63
69
 
64
70
  ```bash
65
71
  python -m pip install -e .
66
72
  python -m mikancli
67
73
  ```
68
74
 
69
- 依赖项(包括用于输出的 Rich 和用于交互提示的 InquirerPy)声明在 `pyproject.toml` 中,并由 `pip` 或 `pipx` 安装。MikanCli 不会在运行时安装依赖包。
75
+ 依赖项(包括用于输出的 Rich 和用于交互提示的 InquirerPy)声明在 `pyproject.toml` 中,由 `pip` 或 `pipx` 安装。MikanCli 运行时不会安装依赖包。
70
76
 
71
- 已有可编辑 `pip` 安装在拉取依赖变更后,需要在原环境中重新运行 `python -m pip install -e .`。通过 `pipx` 安装的应用应使用 `pipx` 更新,让依赖仍安装在应用自己的环境中。
77
+ 如果已有可编辑模式的 `pip` 安装,在拉取依赖变更后,需要在原环境中重新运行 `python -m pip install -e .`。通过 `pipx` 安装的应用应使用 `pipx` 更新,以确保依赖仍安装在应用自己的环境中。
72
78
 
73
79
  ## 终端输出
74
80
 
75
- Rich 用于显示规则摘要、订阅预览和反馈消息,InquirerPy 负责菜单和文本输入。发布标题中的方括号会原样显示,较长的标题、URL 和路径会根据终端宽度自动换行。
81
+ Rich 负责显示规则摘要、订阅预览和反馈消息,InquirerPy 负责菜单和文本输入。发布标题中的方括号会原样显示;较长的标题、URL 和路径会按终端宽度自动换行。
76
82
 
77
- 交互式终端会在网络请求期间显示加载指示器,并在提示输入前或请求失败后停止。输出被重定向时,会使用静态状态消息,订阅预览保留纯文本布局。
83
+ 交互式终端会在网络请求期间显示加载指示器,并在提示输入前或请求失败后停止。输出被重定向时,会改用静态状态消息,订阅预览仍保留纯文本布局。
78
84
 
79
85
  在 PowerShell 中禁用 Rich 颜色:
80
86
 
@@ -85,48 +91,48 @@ mikancli
85
91
 
86
92
  使用 `Remove-Item Env:NO_COLOR` 移除此设置。粗体、暗色样式和加载动画可能仍会显示;菜单样式由 InquirerPy 单独管理。
87
93
 
88
- `--json` 草稿模式只输出一个 JSON 文档,不包含 Rich 格式或进度消息。
94
+ `--json` 草稿模式只输出一个 JSON 文档,不使用 Rich 格式,也不显示进度消息。
89
95
 
90
96
  ## 引导式流程
91
97
 
92
- 不带参数运行 `mikancli` 时,第一个菜单会让你选择:
98
+ 不带参数运行 `mikancli` 时,首先会看到菜单,可选择:
93
99
 
94
100
  - 搜索动画
95
101
  - 修改 qBittorrent 配置
96
102
  - 切换语言
97
103
  - 退出 MikanCli
98
104
 
99
- 搜索流程会依次:
105
+ 搜索流程依次为:
100
106
 
101
- 1. 询问动画标题或关键词
107
+ 1. 输入动画标题或关键词
102
108
  2. 在 Mikan 上搜索匹配的 Bangumi 条目
103
- 3. 让你选择正确的 Bangumi 条目
109
+ 3. 选择正确的 Bangumi 条目
104
110
  4. 从所选 Bangumi 页面获取字幕组 RSS 订阅
105
- 5. 让你选择字幕组
111
+ 5. 选择字幕组
106
112
  6. 预览最近的 RSS 订阅条目
107
- 7. 询问包含、排除和最低集数过滤条件
108
- 8. 询问下载保存位置
113
+ 7. 设置包含、排除和最低集数过滤条件
114
+ 8. 选择下载保存位置
109
115
  9. 生成规则草稿
110
116
  10. 将订阅和规则提交到 qBittorrent
111
- 11. 通过 qBittorrent WebUI API 验证提交的订阅和规则
117
+ 11. 通过 qBittorrent WebUI API 验证订阅和规则已保存
112
118
 
113
- 在需要输入文本的交互式提示中,可以输入 `exit` 或 `quit` 退出;菜单中也会包含退出选项。
119
+ 在需要输入文本的交互提示中,可输入 `exit` 或 `quit` 退出;菜单中也会提供退出选项。
114
120
 
115
121
  ## qBittorrent 设置
116
122
 
117
- 在 MikanCli 可以提交订阅或规则之前,需要先启用 qBittorrent WebUI:
123
+ 在 MikanCli 提交订阅或规则之前,需要先启用 qBittorrent WebUI:
118
124
 
119
125
  1. 打开 qBittorrent 设置
120
126
  2. 启用 WebUI 或远程控制
121
- 3. 确认 WebUI 地址、用户名和密码。如果地址为空,通常表示它是 `http://localhost:[port]`
127
+ 3. 确认 WebUI 地址、用户名和密码。地址为空通常表示 `http://localhost:[port]`
122
128
  4. 运行 `mikancli --setup-qbittorrent`
123
129
 
124
130
  设置说明:
125
131
 
126
- - URL 直接按 Enter 会使用 `http://localhost:8080`
132
+ - URL 直接按 Enter 将使用 `http://localhost:8080`
127
133
  - 输入 `localhost:8080` 会自动规范化为 `http://localhost:8080`
128
- - 如果 qBittorrent WebUI 允许 localhost 绕过认证,用户名和密码可以留空
129
- - 如果 qBittorrent 拒绝连接,请重新检查 qBittorrent 设置里的 WebUI 端口和凭据
134
+ - 如果 qBittorrent WebUI 允许 localhost 免认证访问,用户名和密码可以留空
135
+ - 如果 qBittorrent 拒绝连接,请重新检查 qBittorrent 设置中的 WebUI 端口和凭据
130
136
 
131
137
  ## 配置
132
138
 
@@ -134,20 +140,20 @@ MikanCli 将配置保存在 JSON 文件中:
134
140
 
135
141
  - Windows:`%APPDATA%\Roaming\MikanCli\config.json`
136
142
 
137
- 保存的设置可以包括:
143
+ 保存的设置可包括:
138
144
 
139
145
  - 默认下载文件夹
140
146
  - 语言偏好
141
147
  - qBittorrent WebUI URL
142
148
  - qBittorrent 用户名和密码
143
149
  - qBittorrent 分类
144
- - qBittorrent 是否应将匹配的种子以暂停状态添加
150
+ - qBittorrent 是否以暂停状态添加匹配的种子
145
151
 
146
- qBittorrent 密码会保存在配置文件中,这样 MikanCli 后续运行时才能提交规则。共享机器上请保护好这个文件。
152
+ qBittorrent 密码会保存在配置文件中,以便 MikanCli 后续运行时提交规则。在共享机器上请妥善保护该文件。
147
153
 
148
154
  ## 语言支持
149
155
 
150
- MikanCli 支持英文和简体中文的用户界面文本,包括交互式菜单、提示、设置说明、摘要和帮助文本。命令名称、选项名称、JSON 字段名、Mikan 标题、字幕组名称、URL 和 qBittorrent API 数据保持稳定。
156
+ MikanCli 的面向用户 CLI 文本支持英文和简体中文,包括交互菜单、提示、设置说明、摘要和帮助文本。命令名、选项名、JSON 字段名、Mikan 标题、字幕组名称、URL 和 qBittorrent API 数据保持固定,不随语言变化。
151
157
 
152
158
  语言选择优先级如下:
153
159
 
@@ -156,8 +162,7 @@ MikanCli 支持英文和简体中文的用户界面文本,包括交互式菜
156
162
  3. 已保存的配置值
157
163
  4. 默认回退到英文
158
164
 
159
- 交互式用户始终可以从启动菜单切换语言。选中的语言会保存到配置文件,供以后运行使用。
160
-
165
+ 交互式用户可随时从启动菜单切换语言。所选语言会保存到配置文件,供后续运行使用。
161
166
 
162
167
  ## 项目结构
163
168
 
@@ -192,15 +197,15 @@ usage: mikancli [-h] [--include INCLUDE] [--exclude EXCLUDE]
192
197
  - `--include VALUE`:要求已接受的发布标题中包含某个词或短语。可重复传入多个值
193
198
  - `--exclude VALUE`:拒绝标题中包含某个词或短语的发布。可重复传入多个值
194
199
  - `--min-episode NUMBER`:只接受达到此集数或更高集数的动画发布标题
195
- - `--save-path PATH`:为生成的 qBittorrent 规则使用这个基础下载文件夹
196
- - `--json`:以 JSON 形式打印规则草稿。此模式不会提交到 qBittorrent
200
+ - `--save-path PATH`:为生成的 qBittorrent 规则使用该基础下载文件夹
201
+ - `--json`:以 JSON 形式输出规则草稿。此模式不会提交到 qBittorrent
197
202
  - `--setup-qbittorrent`:配置并验证 qBittorrent WebUI 设置
198
203
  - `--language {en,zh-CN}`:为本次运行选择 CLI 语言
199
- - `--version`:打印已安装的 CLI 版本
204
+ - `--version`:输出已安装的 CLI 版本
200
205
 
201
206
  ## 最低集数过滤
202
207
 
203
- 当 RSS 条目标题使用常见的动画格式,并在集数前有短横线或使用方括号集数时,可以使用 `--min-episode`:
208
+ 当 RSS 条目标题采用常见动画格式,在集数前使用短横线或用方括号标注集数时,可以使用 `--min-episode`:
204
209
 
205
210
  ```text
206
211
  [SubsPlease] One Piece - 1126 (1080p)
@@ -213,10 +218,10 @@ usage: mikancli [-h] [--include INCLUDE] [--exclude EXCLUDE]
213
218
  mikancli "one piece" --include SubsPlease --min-episode 1126
214
219
  ```
215
220
 
216
- MikanCli 会把最低集数转换成 qBittorrent 规则 `mustContain` 字段中的正则表达式,并启用 `useRegex`。普通的 `--include` 和 `--exclude` 值仍然按字面词或短语处理,并且每个包含值都必须出现在标题中,qBittorrent 才会认为它匹配。
221
+ MikanCli 会将最低集数转换为 qBittorrent 规则 `mustContain` 字段中的正则表达式,并启用 `useRegex`。普通的 `--include` 和 `--exclude` 值仍按字面词或短语处理;只有标题中包含所有 include 值,qBittorrent 才会视为匹配。
217
222
 
218
- 提交最低集数规则时,MikanCli 会检查 qBittorrent 的 RSS 条目保留上限。如果 qBittorrent 每个订阅保留少于 500 个条目,MikanCli 会询问是否增加这个设置,让 qBittorrent 能看到足够多的 Mikan 订阅条目来匹配较新的集数。
223
+ 提交最低集数规则时,MikanCli 会检查 qBittorrent 的 RSS 条目保留上限。如果 qBittorrent 每个订阅保留的条目少于 500 条,MikanCli 会询问是否调高该设置,以便 qBittorrent 能看到足够多的 Mikan 订阅条目来匹配较新的集数。
219
224
 
220
225
  ## 发布
221
226
 
222
- 仓库包含 GitHub Actions 工作流 `.github/workflows/publish.yml`。当 `main` 分支中 `pyproject.toml` 的包版本发生变化时,该工作流会发布到 PyPI。
227
+ 仓库包含 GitHub Actions 工作流 `.github/workflows/publish.yml`。当 `main` 分支中 `pyproject.toml` 的包版本发生变化时,该工作流会发布到 PyPI。
@@ -6,9 +6,9 @@ from mikancli.display import print_message
6
6
  from mikancli.i18n import t
7
7
 
8
8
 
9
- def prompt_required_text(prompt: str) -> str:
9
+ def prompt_required_text(prompt: str, *, boxed: bool = False) -> str:
10
10
  while True:
11
- entered = collapse_spaces(prompt_text(prompt, allow_exit=True))
11
+ entered = collapse_spaces(prompt_text(prompt, allow_exit=True, boxed=boxed))
12
12
  if entered:
13
13
  return entered
14
14
  print_message(t("common.value_required"), style="warning")
@@ -106,7 +106,7 @@ def select_option(
106
106
  return selected
107
107
 
108
108
 
109
- def prompt_text(message: str, *, default: str | None = None, allow_exit: bool = False) -> str:
109
+ def prompt_text(message: str, *, default: str | None = None, allow_exit: bool = False, boxed: bool = False) -> str:
110
110
 
111
111
  inquirer = get_inquirer()
112
112
 
@@ -115,6 +115,9 @@ def prompt_text(message: str, *, default: str | None = None, allow_exit: bool =
115
115
  default=default or "",
116
116
  style=get_prompt_style(),
117
117
  )
118
+ if boxed and console.is_terminal:
119
+ # InquirerPy wraps PromptSession; its native frame preserves input editing.
120
+ prompt._session.show_frame = True
118
121
  entered = collapse_spaces(prompt.execute())
119
122
  if allow_exit and entered.casefold() in EXIT_TEXT_VALUES:
120
123
  raise ExitRequested()
@@ -37,6 +37,10 @@ def search_prompt(*, retry: bool = False) -> str:
37
37
  return t("search.prompt_retry" if retry else "search.prompt")
38
38
 
39
39
 
40
+ def prompt_search_keyword(*, retry: bool = False) -> str:
41
+ return prompt_required_text(search_prompt(retry=retry), boxed=True)
42
+
43
+
40
44
  def select_candidate_or_search_again(candidates: tuple[MikanBangumi, ...], *, keyword: str) -> MikanBangumi | str:
41
45
 
42
46
  print_section(t("ui.search_results"), keyword)
@@ -179,7 +183,7 @@ def run_interactive_selection(*, initial_keyword: str | None) -> tuple[MikanBang
179
183
 
180
184
  keyword = collapse_spaces(initial_keyword or "")
181
185
  if not keyword:
182
- keyword = prompt_required_text(search_prompt())
186
+ keyword = prompt_search_keyword()
183
187
 
184
188
  while True:
185
189
  try:
@@ -187,17 +191,17 @@ def run_interactive_selection(*, initial_keyword: str | None) -> tuple[MikanBang
187
191
  candidates = search_mikan_bangumi(keyword)
188
192
  except MikanLookupError as exc:
189
193
  print_message(str(exc), style="error")
190
- keyword = prompt_required_text(search_prompt(retry=True))
194
+ keyword = prompt_search_keyword(retry=True)
191
195
  continue
192
196
 
193
197
  if not candidates:
194
198
  print_message(t("search.no_results", keyword=keyword), style="warning")
195
- keyword = prompt_required_text(search_prompt(retry=True))
199
+ keyword = prompt_search_keyword(retry=True)
196
200
  continue
197
201
 
198
202
  selected_candidate = select_candidate_or_search_again(candidates, keyword=keyword)
199
203
  if selected_candidate == SEARCH_AGAIN:
200
- keyword = prompt_required_text(search_prompt(retry=True))
204
+ keyword = prompt_search_keyword(retry=True)
201
205
  continue
202
206
 
203
207
  bangumi = selected_candidate
@@ -222,7 +226,7 @@ def run_interactive_selection(*, initial_keyword: str | None) -> tuple[MikanBang
222
226
  if selected_subgroup == BACK_TO_CANDIDATES:
223
227
  break
224
228
  if selected_subgroup == SEARCH_AGAIN:
225
- keyword = prompt_required_text(search_prompt(retry=True))
229
+ keyword = prompt_search_keyword(retry=True)
226
230
  break
227
231
 
228
232
  subgroup = selected_subgroup
@@ -238,7 +242,7 @@ def run_interactive_selection(*, initial_keyword: str | None) -> tuple[MikanBang
238
242
  if decision == BACK_TO_SUBGROUPS:
239
243
  continue
240
244
  if decision == REJECT_SUBGROUP:
241
- keyword = prompt_required_text(search_prompt(retry=True))
245
+ keyword = prompt_search_keyword(retry=True)
242
246
  break
243
247
 
244
248
  return bangumi, subgroup
@@ -46,7 +46,7 @@ def print_section(title: str, *paragraphs: str) -> None:
46
46
  def loading(message: str) -> AbstractContextManager[object]:
47
47
  """Animate terminal requests, or print one static message when redirected."""
48
48
  if console.is_terminal and console.is_interactive:
49
- return console.status(Text(message), spinner="line", spinner_style=PRIMARY_COLOR)
49
+ return console.status(Text(message), spinner="dots", spinner_style=PRIMARY_COLOR)
50
50
  print_message(message)
51
51
  return nullcontext()
52
52
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mikancli
3
- Version: 0.2.1
3
+ Version: 0.2.2
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,6 +20,7 @@ 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: prompt-toolkit<4,>=3.0.52
23
24
  Requires-Dist: rich>=14.0
24
25
 
25
26
  # MikanCli
@@ -43,6 +44,12 @@ It supports both a guided interactive flow and a JSON preview mode for scripting
43
44
  - Submit RSS feeds and auto-download rules to qBittorrent, then verify that qBittorrent saved them
44
45
  - Print rule drafts as JSON without submitting anything
45
46
 
47
+ ## Screenshots
48
+
49
+ ![Search input (demo data)](docs/images/search-input.png)
50
+
51
+ ![RSS feed preview (demo data)](docs/images/feed-preview.png)
52
+
46
53
  ## Requirements
47
54
 
48
55
  - Python 3.10 or newer
@@ -32,4 +32,5 @@ mikancli/integrations/qbittorrent_client.py
32
32
  tests/test_cli_json.py
33
33
  tests/test_display.py
34
34
  tests/test_feed_preview.py
35
- tests/test_qbittorrent_episode_filter.py
35
+ tests/test_qbittorrent_episode_filter.py
36
+ tests/test_search_prompt.py
@@ -1,2 +1,3 @@
1
1
  InquirerPy>=0.3
2
+ prompt-toolkit<4,>=3.0.52
2
3
  rich>=14.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mikancli"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "CLI foundation for building qBittorrent RSS rules from anime search input."
9
9
  readme = "README.en.md"
10
10
  requires-python = ">=3.10"
@@ -27,6 +27,7 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  "InquirerPy>=0.3",
30
+ "prompt-toolkit>=3.0.52,<4",
30
31
  "rich>=14.0",
31
32
  ]
32
33
 
@@ -40,6 +40,7 @@ class DisplayTests(TestCase):
40
40
  raise failure
41
41
  if terminal and interactive:
42
42
  target.status.assert_called_once()
43
+ self.assertEqual(target.status.call_args.kwargs["spinner"], "dots")
43
44
  self.assertEqual(target.status.call_args.args[0].plain, literal)
44
45
  target.status.return_value.__exit__.assert_called_once_with(ValueError, failure, ANY)
45
46
  message.assert_not_called()
@@ -0,0 +1,35 @@
1
+ from unittest import TestCase
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from InquirerPy import inquirer
5
+ from prompt_toolkit.application import create_app_session
6
+ from prompt_toolkit.input import DummyInput
7
+ from prompt_toolkit.output import DummyOutput
8
+
9
+ from mikancli.cli import input_parsing, prompts, search_flow
10
+
11
+
12
+ class SearchPromptTests(TestCase):
13
+ def test_search_frame_survives_empty_retry_and_exit(self) -> None:
14
+ answers = iter(["", " One Piece ", "quit", "other", "redirected"])
15
+ frames = []
16
+
17
+ def execute(widget):
18
+ frames.append(widget._session.show_frame)
19
+ return next(answers)
20
+
21
+ terminal = MagicMock(is_terminal=True)
22
+ with (
23
+ create_app_session(input=DummyInput(), output=DummyOutput()),
24
+ patch.object(inquirer.text, "execute", autospec=True, side_effect=execute),
25
+ patch.object(prompts, "console", terminal),
26
+ patch.object(input_parsing, "print_message") as warning,
27
+ ):
28
+ self.assertEqual(search_flow.prompt_search_keyword(), "One Piece")
29
+ with self.assertRaises(prompts.ExitRequested):
30
+ search_flow.prompt_search_keyword(retry=True)
31
+ self.assertEqual(prompts.prompt_text("Other"), "other")
32
+ terminal.is_terminal = False
33
+ self.assertEqual(search_flow.prompt_search_keyword(), "redirected")
34
+ self.assertEqual(frames, [True, True, True, False, False])
35
+ warning.assert_called_once()
File without changes
File without changes
File without changes
File without changes
File without changes