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.
- {mikancli-0.2.1 → mikancli-0.2.2}/PKG-INFO +8 -1
- {mikancli-0.2.1 → mikancli-0.2.2}/README.en.md +6 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/README.md +49 -44
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/input_parsing.py +2 -2
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/prompts.py +4 -1
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/search_flow.py +10 -6
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/display.py +1 -1
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/PKG-INFO +8 -1
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/SOURCES.txt +2 -1
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/requires.txt +1 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/pyproject.toml +2 -1
- {mikancli-0.2.1 → mikancli-0.2.2}/tests/test_display.py +1 -0
- mikancli-0.2.2/tests/test_search_prompt.py +35 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/__init__.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/__main__.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/__init__.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/entrypoint.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/qbittorrent_flow.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/cli/save_path_flow.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/config.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/__init__.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/models.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/normalize.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/core/rules.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/i18n.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/__init__.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/mikan.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/mikan_parsers.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/mikan_urls.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/qbittorrent.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli/integrations/qbittorrent_client.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/dependency_links.txt +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/entry_points.txt +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/mikancli.egg-info/top_level.txt +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/setup.cfg +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/tests/test_cli_json.py +0 -0
- {mikancli-0.2.1 → mikancli-0.2.2}/tests/test_feed_preview.py +0 -0
- {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.
|
|
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
|
+

|
|
50
|
+
|
|
51
|
+

|
|
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
|
+

|
|
25
|
+
|
|
26
|
+

|
|
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
|
|
5
|
+
MikanCli 是一个 Python 命令行工具,可以在 Mikanani.me 上搜索番剧/动画,选定对应的 Bangumi 条目和字幕组 RSS 订阅,并据此生成 qBittorrent 的 RSS 下载规则。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
它既提供引导式交互流程,也支持用于脚本或检查的 JSON 预览模式。目前交互式流程是功能最完整的模式。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- 按动画标题或关键词搜索 Mikan
|
|
12
12
|
- 从匹配的 Bangumi 结果和字幕组 RSS 订阅中进行选择
|
|
13
13
|
- 在确认订阅前分页预览最近的 RSS 条目
|
|
14
14
|
- 使用 Rich 显示规则摘要、订阅预览和状态消息,并在网络请求期间显示加载指示器
|
|
15
15
|
- 使用包含和排除过滤条件生成 qBittorrent RSS 规则
|
|
16
|
-
-
|
|
16
|
+
- 按最低集数过滤动画发布标题
|
|
17
17
|
- 选择并保存默认下载文件夹
|
|
18
|
-
-
|
|
18
|
+
- 通过命令行配置 qBittorrent WebUI 访问
|
|
19
19
|
- 将 RSS 订阅和自动下载规则提交到 qBittorrent,并验证 qBittorrent 已保存这些内容
|
|
20
|
-
- 以 JSON
|
|
20
|
+
- 以 JSON 形式输出规则草稿,不提交任何内容
|
|
21
|
+
|
|
22
|
+
## 界面预览
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+

|
|
21
27
|
|
|
22
28
|
## 环境要求
|
|
23
29
|
|
|
24
30
|
- Python 3.10 或更新版本
|
|
25
|
-
- `pipx
|
|
31
|
+
- `pipx`,用于将 MikanCli 安装为独立命令行应用
|
|
26
32
|
- qBittorrent,如果你希望 MikanCli 自动提交 RSS 订阅和规则
|
|
27
33
|
|
|
28
34
|
## 快速开始 | 安装
|
|
29
35
|
|
|
30
|
-
推荐使用 `pipx` 安装 MikanCli
|
|
31
|
-
|
|
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
|
-
|
|
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`
|
|
75
|
+
依赖项(包括用于输出的 Rich 和用于交互提示的 InquirerPy)声明在 `pyproject.toml` 中,由 `pip` 或 `pipx` 安装。MikanCli 运行时不会安装依赖包。
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
如果已有可编辑模式的 `pip` 安装,在拉取依赖变更后,需要在原环境中重新运行 `python -m pip install -e .`。通过 `pipx` 安装的应用应使用 `pipx` 更新,以确保依赖仍安装在应用自己的环境中。
|
|
72
78
|
|
|
73
79
|
## 终端输出
|
|
74
80
|
|
|
75
|
-
Rich
|
|
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
|
|
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.
|
|
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
|
-
|
|
119
|
+
在需要输入文本的交互提示中,可输入 `exit` 或 `quit` 退出;菜单中也会提供退出选项。
|
|
114
120
|
|
|
115
121
|
## qBittorrent 设置
|
|
116
122
|
|
|
117
|
-
在 MikanCli
|
|
123
|
+
在 MikanCli 提交订阅或规则之前,需要先启用 qBittorrent WebUI:
|
|
118
124
|
|
|
119
125
|
1. 打开 qBittorrent 设置
|
|
120
126
|
2. 启用 WebUI 或远程控制
|
|
121
|
-
3. 确认 WebUI
|
|
127
|
+
3. 确认 WebUI 地址、用户名和密码。地址为空通常表示 `http://localhost:[port]`
|
|
122
128
|
4. 运行 `mikancli --setup-qbittorrent`
|
|
123
129
|
|
|
124
130
|
设置说明:
|
|
125
131
|
|
|
126
|
-
- URL 直接按 Enter
|
|
132
|
+
- URL 直接按 Enter 将使用 `http://localhost:8080`
|
|
127
133
|
- 输入 `localhost:8080` 会自动规范化为 `http://localhost:8080`
|
|
128
|
-
- 如果 qBittorrent WebUI 允许 localhost
|
|
129
|
-
- 如果 qBittorrent 拒绝连接,请重新检查 qBittorrent
|
|
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
|
|
152
|
+
qBittorrent 密码会保存在配置文件中,以便 MikanCli 后续运行时提交规则。在共享机器上请妥善保护该文件。
|
|
147
153
|
|
|
148
154
|
## 语言支持
|
|
149
155
|
|
|
150
|
-
MikanCli
|
|
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
|
|
200
|
+
- `--save-path PATH`:为生成的 qBittorrent 规则使用该基础下载文件夹
|
|
201
|
+
- `--json`:以 JSON 形式输出规则草稿。此模式不会提交到 qBittorrent
|
|
197
202
|
- `--setup-qbittorrent`:配置并验证 qBittorrent WebUI 设置
|
|
198
203
|
- `--language {en,zh-CN}`:为本次运行选择 CLI 语言
|
|
199
|
-
- `--version
|
|
204
|
+
- `--version`:输出已安装的 CLI 版本
|
|
200
205
|
|
|
201
206
|
## 最低集数过滤
|
|
202
207
|
|
|
203
|
-
当 RSS
|
|
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
|
|
221
|
+
MikanCli 会将最低集数转换为 qBittorrent 规则 `mustContain` 字段中的正则表达式,并启用 `useRegex`。普通的 `--include` 和 `--exclude` 值仍按字面词或短语处理;只有标题中包含所有 include 值,qBittorrent 才会视为匹配。
|
|
217
222
|
|
|
218
|
-
提交最低集数规则时,MikanCli 会检查 qBittorrent 的 RSS 条目保留上限。如果 qBittorrent
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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="
|
|
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.
|
|
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
|
+

|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
46
53
|
## Requirements
|
|
47
54
|
|
|
48
55
|
- Python 3.10 or newer
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mikancli"
|
|
7
|
-
version = "0.2.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|