Skip to content

Commit 09233da

Browse files
committed
fix pr review
1 parent 26a830d commit 09233da

6 files changed

Lines changed: 107 additions & 124 deletions

File tree

assets/docs/sources/option_file_syntax.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -223,34 +223,28 @@ plugins:
223223
rule: '{Atitle}/{Aid}_cover.jpg'
224224

225225

226-
after_album:
226+
after_album: # 钩子(插件被调用时机)
227227
- plugin: zip # 压缩文件插件
228228
kwargs:
229-
# ⚠ level 参数已在 v2.6.19 废弃,打包粒度由插件所在的钩子自动推导:
230-
# 配置在 after_album 下 → 整本合并为一个压缩文件
231-
# 配置在 after_photo 下 → 每个章节各一个压缩文件
232-
# 旧配置会自动等价迁移,无需手动修改配置文件。
233-
# 迁移示例:
234-
# 旧:after_album + level: photo → 等价于:after_photo(不写 level)
235-
# 旧:after_album + level: album → 等价于:after_album(不写 level)
236-
237-
filename_rule: Ptitle # 压缩文件的命名规则
238-
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
239-
# filename_rule和所在钩子有对应关系
240-
# 如果配置在 after_photo 下, filename_rule只能写Pxxx
241-
# 如果配置在 after_album 下, filename_rule只能写Axxx
229+
# 压缩文件插件,配在不同钩子下面,效果不一样。可以选择配在 after_album 或者 after_photo 下
230+
# 配置在 after_album 下 → 整个本子合并为一个压缩文件
231+
# 配置在 after_photo 下 → 每个章节各一个压缩文件
232+
# (旧的 level 配置已废弃,如果你配置过level,比如level=photo, 请直接改用after_photo)
242233

243234
zip_dir: D:/jmcomic/zip/ # 压缩文件存放的文件夹
244-
245235
suffix: zip #压缩包后缀名,默认值为zip,可以指定为zip或者7z
236+
filename_rule: Atitle # 压缩文件的命名规则
237+
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
238+
# filename_rule和所在钩子有对应关系
239+
# 如果配置在 after_photo 下, filename_rule只能写 Pxxx
240+
# 如果配置在 after_album 下, filename_rule只能写 Axxx
246241

247-
# v2.6.0 以后,zip插件也支持dir_rule配置项,可以替代旧版本的zip_dir和filename_rule
242+
# zip插件也支持dir_rule配置项,可以替代旧版本的zip_dir和filename_rule
248243
# 请注意⚠ 使用此配置项会使filename_rule,zip_dir,suffix三个配置项无效,与这三个配置项同时存在时仅会使用dir_rule
249244
# 示例如下:
250245
# dir_rule: # 新配置项,可取代旧的zip_dir和filename_rule
251246
# base_dir: D:/jmcomic-zip
252247
# rule: 'Bd / {Atitle} / [{Pid}]-{Ptitle}.zip' # 设置压缩文件夹规则,中间Atitle表示创建一层文件夹,名称是本子标题。[{Pid}]-{Ptitle}.zip 表示压缩文件的命名规则(需显式写出后缀名)
253-
# 使用此方法指定压缩包存储路径则无需和所在钩子对应
254248

255249
delete_original_file: true # 压缩成功后,删除所有原文件和文件夹
256250

assets/docs/sources/tutorial/13_export_and_feature.md

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
- 导出为 **ZIP**:方便传输和存档
88
- 合并为 **长图**:方便一张图看完整个章节
99

10-
jmcomic 一直通过内置插件(`img2pdf``zip``long_img`)支持这些功能,但传统方式需要在 YAML 配置文件中编写插件配置,门槛偏高。
11-
12-
从最新版本起,jmcomic 引入了 **Feature(特性)** 机制——一套通用的**下载附加行为系统**,让你用一行代码搞定导出。Feature 不仅能调用插件,还能封装任意自定义逻辑(通知、清理等),并且会根据调用方式自动选择最合理的配置。
13-
14-
内置了三个开箱即用的导出 Feature:
10+
jmcomic 内置了三个开箱即用的导出 Feature,对应这三种需求:
1511

1612
| Feature | 效果 |
1713
|---------|------|
1814
| `Feature.export_pdf` | 下载完自动导出为 PDF |
1915
| `Feature.export_zip` | 下载完自动打包为 ZIP |
2016
| `Feature.export_long_img` | 下载完自动拼接为长图 PNG |
2117

18+
19+
> 也许你知道,这些功能之前是以插件形式 (JmOptionPlugin) 存在的。
20+
>
21+
> 是的,传统方式需要在 option 配置文件中编写插件配置,门槛偏高。
22+
>
23+
> 因此,从v2.6.19起,jmcomic 引入了上述的 **Feature** 机制,尽可能简化这些最常用的功能,让小白也能用一行代码搞定导出。
24+
25+
2226
## 2. 快速上手
2327

2428
### 2.1 导出 PDF——基本用法示例
@@ -27,35 +31,49 @@ jmcomic 一直通过内置插件(`img2pdf`、`zip`、`long_img`)支持这些
2731
from jmcomic import download_album, Feature
2832

2933
# 只需要加一个 extra 参数,就能在下载完成后自动导出 PDF
34+
download_album('123', extra=Feature.export_pdf)
35+
36+
# 如果要传 option 参数,就是如下写法,三个参数
3037
download_album('123', option, extra=Feature.export_pdf)
3138
```
3239

33-
效果:在**当前工作目录**下生成以本子标题命名的 PDF 文件:
40+
**效果**:在本子下载完以后,额外在**当前工作目录**下生成包含所有本子图片的 PDF 文件:
3441

3542
```
3643
./
37-
├── [本子标题].pdf ← 整本合并为 1 个 PDF
44+
├── [JM123]本子标题.pdf ← 整本合并为 1 个 PDF,注意pdf文件名的格式,默认包含本子禁漫车号+本子标题
3845
```
3946

40-
### 2.2 需要多种导出格式(PDF、ZIP)——直接组合 Feature
47+
### 2.2 需要多种导出格式(PDF、ZIP等)——直接组合 Feature
4148

4249
`+` 号组合,同时导出多种格式:
4350

4451
```python
4552
# 下载完后同时导出 PDF 和 ZIP
4653
download_album('123', option, extra=Feature.export_pdf + Feature.export_zip)
4754

48-
# 也支持列表语法
55+
# 也支持列表语法,|语法
4956
download_album('123', option, extra=[Feature.export_pdf, Feature.export_zip])
57+
download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
5058
```
5159

60+
效果同pdf,会在本子下载完以后,额外在当前工作目录下,生成包含所有本子图片的 PDF 文件和 ZIP 文件:
61+
62+
```
63+
./
64+
├── [JM123]本子标题.pdf ← 整本合并为 1 个 PDF
65+
├── [JM123]本子标题.zip ← 整本合并为 1 个 zip 压缩包
66+
```
67+
68+
5269
### 2.3 自定义参数
5370

54-
像调用函数一样传入自定义参数,可以改变输出目录、命名规则等:
71+
如果你了解插件配置,可以同样使用Feature传递插件的自定义参数,例如改变输出目录、命名规则等:
5572

5673
```python
5774
# 示例 1:指定输出目录和命名规则
5875
download_album('123', option, extra=Feature.export_pdf(
76+
# 下面是自定义参数
5977
pdf_dir='D:/my_pdfs', # PDF 保存到 D:/my_pdfs 文件夹
6078
filename_rule='Ptitle', # 用章节标题作为文件名
6179
delete_original_file=True, # 合并完 PDF 后删除原图
@@ -89,10 +107,10 @@ download_photo('456', option, extra=Feature.export_pdf)
89107
90108
### 2.5 智能适配规则
91109

92-
内置的导出 Feature 会根据调用的 API **自动适配**参数(命名规则、打包粒度等)
110+
内置的导出 Feature 会根据调用的 API **自动适配**参数:
93111

94-
| 调用方式 | Feature.export_pdf | Feature.export_zip | Feature.export_long_img |
95-
|---------|-------------------|-------------------|----------------------|
112+
| 调用方式 | Feature.export_pdf | Feature.export_zip | Feature.export_long_img |
113+
|-----------------|-------------------|-------------------|----------------------|
96114
| `download_album` | 整本合并为 1 个 PDF<br>`[本子标题].pdf` | 整本打包为 1 个 ZIP<br>`[本子标题].zip` | 所有章节合并为 1 张长图<br>`[本子ID].png` |
97115
| `download_photo` | 该章节导出为 PDF<br>`[章节标题].pdf` | 该章节打包为 ZIP<br>`[章节标题].zip` | 该章节拼接为长图<br>`[章节ID].png` |
98116

@@ -107,12 +125,12 @@ download_photo('456', option, extra=Feature.export_pdf)
107125
```yaml
108126
# option.yml
109127
plugins:
110-
after_album:
111-
- plugin: img2pdf
128+
after_album: # 整本下载完以后
129+
- plugin: img2pdf # 合并pdf
112130
kwargs:
113131
pdf_dir: ./output
114132
filename_rule: Atitle
115-
- plugin: zip
133+
- plugin: zip # 合并为压缩文件
116134
kwargs:
117135
level: album
118136
zip_dir: ./output
@@ -148,16 +166,16 @@ api.download_album(extra=Feature.export_pdf)
148166
149167
├→ download_by_photo_detail(photo)
150168
│ ├→ before_photo(photo)
151-
│ ├→ download images ...
169+
│ ├→ download jmcomic images ... # 下载禁漫图片
152170
│ └→ after_photo(photo)
153171
│ └→ _invoke_features_for('after_photo')
154172
│ └→ pdf.should_invoke('after_photo', 'download_album') → False ✗ 跳过
155173
156174
└→ after_album(album)
157175
└→ _invoke_features_for('after_album')
158176
└→ pdf.should_invoke('after_album', 'download_album') → True ✓ 执行!
159-
└→ _adapt_kwargs('download_album')
160-
# Atitle 不变, Ptitle→Atitle, Pid→Aid, level→album
177+
└→ _adapt_plugin_kwargs(from, when) # 动态生成插件参数
178+
└→ option.invoke(pdf, kwargs) # 调用pdf插件,传入参数
161179
```
162180

163181
> 💡 **关键点**
@@ -167,32 +185,19 @@ api.download_album(extra=Feature.export_pdf)
167185
168186
### 自定义 Feature
169187

170-
Feature 基类完全不绑定插件,你可以实现任意逻辑:
188+
Feature 基类完全不绑定插件,你可以实现任意逻辑,欢迎贡献你的feature到本项目中
171189

172190
```python
173191
from jmcomic import Feature, download_album
174192

175193
class NotifyFeature(Feature):
176194
"""下载完成后发送通知"""
177-
def invoke(self, option, **context):
178-
album = context.get('album')
195+
def invoke(self, option, **kwargs):
196+
album = kwargs.get('album')
179197
if album:
180198
print(f'下载完成通知: {album.name}')
181199

182200
# 使用
183201
download_album('123', option, extra=NotifyFeature())
184202
```
185203

186-
### 自定义 PluginFeature
187-
188-
如果你注册了自定义插件,也可以创建对应的 PluginFeature:
189-
190-
```python
191-
from jmcomic import PluginFeature, Feature
192-
193-
# 假设你注册了一个 plugin_key 为 'my_export' 的插件
194-
Feature.my_export = PluginFeature('my_export', output_dir='./my_output')
195-
196-
# 使用
197-
download_album('123', option, extra=Feature.my_export)
198-
```

src/jmcomic/jm_downloader.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,16 +299,16 @@ def add_features(self, features, feature_from: str):
299299
else:
300300
ExceptionTool.raises(f'不支持的 extra 类型: {type(features)},请传入 Feature / FeatureChain / list / None')
301301

302-
def _invoke_features_for(self, when: str, **context):
302+
def _invoke_features_for(self, when: str, **kwargs):
303303
"""
304304
在指定钩子(when)中触发匹配的 Feature。
305305
306306
:param when: 当前钩子名,如 'after_album', 'after_photo'
307-
:param context: album, photo, downloader 等上下文
307+
:param kwargs: album, photo, downloader 等上下文
308308
"""
309309
for feature, feature_from in self._feature_list:
310-
if feature.should_invoke(when, feature_from):
311-
feature.invoke(self.option, feature_from=feature_from, **context)
310+
if feature.should_invoke(feature_from, when):
311+
feature.invoke(self.option, feature_from=feature_from, when=when, **kwargs)
312312

313313
def raise_if_has_exception(self):
314314
if not self.has_download_failures:

src/jmcomic/jm_feature.py

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
"""
2-
该文件存放的是 Feature(下载特性)机制
2+
该文件存放的是 Feature 机制
33
4-
Feature 用于在下载生命周期中挂载上下文相关的动态附加行为,
5-
例如下载完成后自适应导出为 PDF、ZIP 或长图等。
6-
它不仅是插件的封装,更能根据调用来源(整本/单章)智能调整执行策略。
4+
Feature 用于封装复杂、高级的功能特性,例如pdf导出插件,以前用户需要知道插件名称,调用时机,option插件参数等等,使用feature相当于包办了这些。
75
86
用法:
97
from jmcomic import download_album, Feature
108
119
# 最简单
1210
download_album(id, option, extra=Feature.export_pdf)
1311
14-
# 带参数
12+
# 带自定义参数
1513
download_album(id, option, extra=Feature.export_pdf(pdf_dir='./output'))
1614
1715
# 多个 Feature(列表 / 运算符均可)
1816
download_album(id, option, extra=[Feature.export_pdf, Feature.export_zip])
1917
download_album(id, option, extra=Feature.export_pdf + Feature.export_zip)
2018
"""
19+
from typing import LiteralString
2120

2221
from .jm_plugin import *
2322

@@ -36,23 +35,25 @@ class Feature:
3635
export_zip: 'PluginFeature'
3736
export_long_img: 'PluginFeature'
3837

39-
def should_invoke(self, when: str, feature_from: str) -> bool:
38+
def should_invoke(self, feature_from: str, when: str) -> bool:
4039
"""
4140
判断在当前钩子(when)下,根据来源(feature_from),是否应该执行。
4241
默认返回 True(任何钩子都执行)。子类可覆写来限制执行时机。
4342
44-
:param when: 当前触发的钩子名称,如 'after_album', 'after_photo'
4543
:param feature_from: Feature 的注册来源,如 'download_album', 'download_photo'
44+
:param when: 当前触发的钩子名称,如 'after_album', 'after_photo'
4645
:returns: 是否应该执行
4746
"""
4847
return True
4948

50-
def invoke(self, option, **context):
49+
def invoke(self, option: JmOption, feature_from: str, when: str, **kwargs):
5150
"""
5251
执行此 Feature。子类需实现该方法。
5352
5453
:param option: 当前的 JmOption
55-
:param context: album, photo, downloader, feature_from 等上下文
54+
:param feature_from 注册来源,如 'download_album', 'download_photo'
55+
:param when: 钩子回调时机,如 'after_album', 'after_photo'
56+
:param kwargs: album, photo, downloader 等回调参数
5657
"""
5758
raise NotImplementedError
5859

@@ -83,9 +84,9 @@ def __init__(self, plugin_key, **kwargs):
8384
# 用户通过 __call__ 显式传入的参数名,这些参数不会被 _adapt_kwargs 动态适配
8485
self._user_keys: set = set()
8586

86-
def should_invoke(self, when: str, feature_from: str) -> bool:
87+
def should_invoke(self, feature_from: str, when: str) -> bool:
8788
"""
88-
根据注册来源推导执行时机
89+
默认根据注册来源推导执行时机
8990
download_album → after_album, download_photo → after_photo
9091
"""
9192
if feature_from == 'download_album':
@@ -100,52 +101,34 @@ def __call__(self, **kwargs):
100101
new_kwargs.update(kwargs)
101102
new_instance = PluginFeature(self.plugin_key, **new_kwargs)
102103
# 记录用户显式传入的参数名,这些参数不被动态适配
103-
new_instance._user_keys = set(kwargs.keys())
104+
new_instance._user_0keys = set(kwargs.keys())
104105
return new_instance
105106

106-
def invoke(self, option, feature_from=None, **context):
107+
def invoke(self, option: JmOption, feature_from: str, when: str, **extra):
107108
"""
108109
执行此 Feature 对应的插件。
109110
根据 feature_from 动态适配 filename_rule 等参数。
110111
"""
111-
pclass = JmModuleConfig.REGISTRY_PLUGIN.get(self.plugin_key)
112-
if pclass is None:
113-
ExceptionTool.raises(f'PluginFeature 引用了未注册的插件: {self.plugin_key}')
112+
pclass: type = JmModuleConfig.REGISTRY_PLUGIN.get(self.plugin_key)
113+
ExceptionTool.require_true(pclass is not None, f'PluginFeature 引用了未注册的插件: {self.plugin_key}, from {feature_from}, when {when}')
114114

115115
# 根据 feature_from 动态适配参数
116-
adapted = self._adapt_kwargs(feature_from)
117-
merged_kwargs = {**adapted, **context}
116+
plugin_kwargs: dict = self._adapt_plugin_kwargs(feature_from, when)
118117

119118
option.invoke_plugin(
120119
pclass=pclass,
121-
kwargs=merged_kwargs,
122-
extra={},
123-
pinfo={'plugin': self.plugin_key, 'kwargs': adapted},
120+
kwargs=plugin_kwargs,
121+
extra=extra,
122+
pinfo={'plugin': self.plugin_key, 'kwargs': plugin_kwargs},
124123
)
125124

126-
def _adapt_kwargs(self, feature_from):
125+
def _adapt_plugin_kwargs(self, feature_from: str, when: str) -> dict:
127126
"""
128-
根据 feature_from 动态适配参数:
129-
- filename_rule 前缀:download_album → A前缀,download_photo → P前缀
130-
131-
注意:用户通过 __call__ 显式传入的参数(记录在 _user_keys 中)不会被适配。
127+
根据feature_from和when动态确定以下插件参数:
128+
filename_rule
132129
"""
133130
kwargs = self.kwargs.copy()
134-
135-
if feature_from == 'download_album':
136-
# album 模式:P前缀规则 → A前缀规则
137-
if 'filename_rule' not in self._user_keys and 'filename_rule' in kwargs:
138-
rule = kwargs['filename_rule']
139-
if rule and rule[0] == 'P':
140-
kwargs['filename_rule'] = 'A' + rule[1:]
141-
142-
elif feature_from == 'download_photo':
143-
# photo 模式:A前缀规则 → P前缀规则
144-
if 'filename_rule' not in self._user_keys and 'filename_rule' in kwargs:
145-
rule = kwargs['filename_rule']
146-
if rule and rule[0] == 'A':
147-
kwargs['filename_rule'] = 'P' + rule[1:]
148-
131+
kwargs.setdefault('filename_rule', '[JM{Aid}]{Atitle}' if feature_from == 'download_album' else '[JM{Pid}]{Ptitle}')
149132
return kwargs
150133

151134
def __repr__(self):
@@ -184,6 +167,6 @@ def __repr__(self):
184167
# 预定义特性(用插件类的 plugin_key 引用,附带默认参数)
185168
# filename_rule 会根据 feature_from 在 invoke 时动态适配 A/P 前缀
186169
# zip 的打包粒度由插件根据上下文(album/photo)自动推导,无需 level 参数
187-
Feature.export_pdf = PluginFeature(Img2pdfPlugin.plugin_key, pdf_dir='./', filename_rule='Atitle')
188-
Feature.export_zip = PluginFeature(ZipPlugin.plugin_key, zip_dir='./', filename_rule='Ptitle')
189-
Feature.export_long_img = PluginFeature(LongImgPlugin.plugin_key, img_dir='./', filename_rule='Pid')
170+
Feature.export_pdf = PluginFeature(Img2pdfPlugin.plugin_key, pdf_dir='./')
171+
Feature.export_zip = PluginFeature(ZipPlugin.plugin_key, zip_dir='./')
172+
Feature.export_long_img = PluginFeature(LongImgPlugin.plugin_key, img_dir='./')

0 commit comments

Comments
 (0)