Skip to content

Commit 9bea7ca

Browse files
authored
v2.6.19: 更新禁漫API域名和版本号; 将合并PDF/ZIP/长图的功能简化为一个 Feature 参数,方便小白使用; 增加美观日志打印; bugfix和优化文档 (#533)
1 parent b063ce5 commit 9bea7ca

18 files changed

Lines changed: 854 additions & 58 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
> **🧭 快速指路**
3131
> - [教程:使用 GitHub Actions 下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md)
3232
> - [教程:导出并下载你的禁漫收藏夹数据](./assets/docs/sources/tutorial/10_export_favorites.md)
33+
> - [教程:下载后转为 PDF / ZIP / 长图](./assets/docs/sources/tutorial/13_export_and_feature.md)
3334
> - [塔台广播:欢迎各位机长加入并贡献代码](./.github/CONTRIBUTING.md)
3435
>
3536
> **友情提示:珍爱JM,为了减轻JM的服务器压力,请不要一次性爬取太多本子,西门🙏🙏🙏**.

assets/docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ nav:
5858
- tutorial/10_export_favorites.md
5959
- tutorial/11_log_custom.md
6060
- tutorial/12_domain_strategy.md
61+
- tutorial/13_export_and_feature.md
6162

6263
plugins:
6364
- search

assets/docs/sources/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
- [快速上手(GitHub README)](https://github.com/hect0x7/JMComic-Crawler-Python/tree/master?tab=readme-ov-file#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B)
1919
- [常用类和方法演示](tutorial/0_common_usage.md)
20+
- [下载后转为 PDF / ZIP / 长图](tutorial/13_export_and_feature.md)
2021
- [option配置以及插件写法](./option_file_syntax.md)
2122

2223
## 特殊用法教程
@@ -30,6 +31,7 @@
3031

3132
- [下载过滤器机制](tutorial/5_filter.md)
3233
- [插件机制](tutorial/6_plugin.md)
34+
- [Feature机制](tutorial/13_export_and_feature.md)
3335

3436
## 自定义
3537

assets/docs/sources/option_file_syntax.md

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

225225

226-
after_album:
226+
after_album: # 钩子(插件被调用时机)
227227
- plugin: zip # 压缩文件插件
228228
kwargs:
229-
level: photo # 按照章节,一个章节一个压缩文件
230-
# level 也可以配成 album,表示一个本子对应一个压缩文件,该压缩文件会包含这个本子的所有章节
231-
232-
filename_rule: Ptitle # 压缩文件的命名规则
233-
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
234-
# filename_rule和level有对应关系
235-
# 如果level=[photo], filename_rule只能写Pxxx
236-
# 如果level=[album], filename_rule只能写Axxx
229+
# 压缩文件插件,配在不同钩子下面,效果不一样。可以选择配在 after_album 或者 after_photo 下
230+
# 配置在 after_album 下 → 整个本子合并为一个压缩文件
231+
# 配置在 after_photo 下 → 每个章节各一个压缩文件
232+
# (旧的 level 配置已废弃,如果你配置过level,比如level=photo, 请直接改用after_photo)
237233

238234
zip_dir: D:/jmcomic/zip/ # 压缩文件存放的文件夹
239-
240235
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 和Axxx
240+
# 如果配置在 after_album 下, filename_rule 只能写 Axxx,不能写 Pxxx
241241

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

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

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# 教程:下载后转为 PDF / ZIP / 长图
2+
3+
## 1. 需求场景
4+
5+
下载本子后,很多用户有进一步导出的需求:
6+
- 导出为 **PDF**:方便在电子阅读器上查看
7+
- 导出为 **ZIP**:方便传输和存档
8+
- 合并为 **长图**:方便一张图看完整个章节
9+
10+
jmcomic 内置了三个开箱即用的导出 Feature,对应这三种需求:
11+
12+
| Feature | 效果 |
13+
|---------|------|
14+
| `Feature.export_pdf` | 下载完自动导出为 PDF |
15+
| `Feature.export_zip` | 下载完自动打包为 ZIP |
16+
| `Feature.export_long_img` | 下载完自动拼接为长图 PNG |
17+
18+
19+
> 也许你知道,这些功能之前是以插件形式 (JmOptionPlugin) 存在的。
20+
>
21+
> 是的,传统方式需要在 option 配置文件中编写插件配置,门槛偏高。
22+
>
23+
> 因此,从v2.6.19起,jmcomic 引入了上述的 **Feature** 机制,尽可能简化这些最常用的功能,让小白也能用一行代码搞定导出。
24+
25+
26+
## 2. 快速上手
27+
28+
### 2.1 导出 PDF——基本用法示例
29+
30+
```python
31+
from jmcomic import download_album, Feature
32+
33+
# 只需要加一个 extra 参数,就能在下载完成后自动导出 PDF
34+
download_album('123', extra=Feature.export_pdf)
35+
36+
# 如果要传 option 参数,就是如下写法,三个参数
37+
download_album('123', option, extra=Feature.export_pdf)
38+
```
39+
40+
**效果**:在本子下载完以后,默认在**下载根目录**下生成包含所有本子图片的 PDF 文件。如果你没有自定义过option,下载根目录就是你的工作目录(即你运行python脚本或cli的目录)。如果你配置过option,会放在dir_rule.base_dir下面。
41+
42+
```text
43+
./
44+
├── [JM123]本子标题.pdf ← 整本合并为 1 个 PDF,注意pdf文件名的格式,默认包含本子禁漫车号+本子标题
45+
```
46+
47+
### 2.2 需要多种导出格式(PDF、ZIP等)——直接组合 Feature
48+
49+
`+` 号组合,同时导出多种格式:
50+
51+
```python
52+
# 下载完后同时导出 PDF 和 ZIP
53+
download_album('123', option, extra=Feature.export_pdf + Feature.export_zip)
54+
55+
# 也支持列表语法,|语法
56+
download_album('123', option, extra=[Feature.export_pdf, Feature.export_zip])
57+
download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
58+
```
59+
60+
效果同pdf,会在本子下载完以后,额外在对应的下载目录下,生成包含所有本子图片的 PDF 文件和 ZIP 文件:
61+
62+
```text
63+
./
64+
├── [JM123]本子标题.pdf ← 整本合并为 1 个 PDF
65+
├── [JM123]本子标题.zip ← 整本合并为 1 个 zip 压缩包
66+
```
67+
68+
69+
### 2.3 自定义参数
70+
71+
如果你了解插件配置,可以同样使用Feature传递插件的自定义参数,例如改变输出目录、命名规则等:
72+
73+
```python
74+
# 示例 1:指定输出目录和命名规则
75+
download_album('123', option, extra=Feature.export_pdf(
76+
# 下面是自定义参数
77+
pdf_dir='D:/my_pdfs', # PDF 保存到 D:/my_pdfs 文件夹
78+
filename_rule='Atitle', # 用本子标题作为文件名
79+
delete_original_file=True, # 合并完 PDF 后删除原图
80+
))
81+
```
82+
83+
> 💡 **小白必读:命名规则(filename_rule)的小知识**
84+
> - `A` 开头的占位符(如 `Atitle`, `Aid`)代表 **Album (本子)**
85+
> - `P` 开头的占位符(如 `Ptitle`, `Pid`)代表 **Photo (章节)**
86+
> - `download_photo` (下载单章)时,由于程序既知道当前章节,也知道它属于哪个本子,所以 **`Pxxx``Axxx` 都可以用**
87+
> - `download_album` (下载整本)时,由于是按本子合并的,程序没有具体的“当前章节”,此时 **只能用 `Axxx`,不能用 `Pxxx`**,否则会报错。
88+
89+
```python
90+
# 示例 2:全都要——ZIP 存盘 + 长图阅读
91+
combo = (
92+
Feature.export_zip(zip_dir='D:/zips')
93+
+ Feature.export_long_img(img_dir='D:/long_imgs')
94+
)
95+
download_album('123', option, extra=combo)
96+
```
97+
98+
### 2.4 download_photo 也支持
99+
100+
```python
101+
from jmcomic import download_photo, Feature
102+
103+
# 对单个章节导出
104+
download_photo('456', option, extra=Feature.export_pdf)
105+
```
106+
107+
效果:在对应的下载目录下生成以章节标题命名的 PDF:
108+
109+
```text
110+
./
111+
├── [JM{Pid}]章节标题.pdf ← 该章节导出为 1 个 PDF
112+
```
113+
114+
> 💡 **提示**:同一个 Feature,通过 `download_album``download_photo` 调用时会自动适配不同的导出行为,详见下方 [智能适配规则](#25-智能适配规则)
115+
116+
### 2.5 智能适配规则
117+
118+
内置的导出 Feature 会根据调用的 API **自动适配**参数:
119+
120+
| 调用方式 | Feature.export_pdf | Feature.export_zip | Feature.export_long_img |
121+
|-----------------|-------------------|-------------------|----------------------|
122+
| `download_album` | 整本合并为 1 个 PDF<br>`[JM本子号]本子标题.pdf` | 整本打包为 1 个 ZIP<br>`[JM本子号]本子标题.zip` | 所有章节合并为 1 张长图<br>`[JM本子号]本子标题.png` |
123+
| `download_photo` | 该章节导出为 PDF<br>`[JM章节号]章节标题.pdf` | 该章节打包为 ZIP<br>`[JM章节号]章节标题.zip` | 该章节拼接为长图<br>`[JM章节号]章节标题.png` |
124+
125+
当你显式传入参数时(如 `filename_rule='Ptitle'`),**你的配置优先**,不会被自适应覆盖。
126+
127+
> 💡 **提示**:更多可选参数(如加密密码 `encrypt`、后缀名 `suffix` 等),参考 [Plugin 插件参数大全](../option_file_syntax.md#3-option插件配置项)
128+
129+
## 3. 传统写法(YAML 插件配置)
130+
131+
如果你更习惯配置文件,仍然可以使用传统的插件配置方式:
132+
133+
```yaml
134+
# option.yml
135+
plugins:
136+
after_album: # 整本下载完以后
137+
- plugin: img2pdf # 合并pdf
138+
kwargs:
139+
pdf_dir: ./output
140+
filename_rule: Atitle
141+
- plugin: zip # 合并为压缩文件
142+
kwargs:
143+
level: album
144+
zip_dir: ./output
145+
```
146+
147+
传统写法的更多细节见 → [Plugin 插件教程](./6_plugin.md)
148+
149+
## 4. Feature 架构设计
150+
151+
### 类层次
152+
153+
```text
154+
Feature (基类)
155+
├── PluginFeature ← 封装插件调用,参数根据来源自适应
156+
└── 你的自定义 Feature ← 继承 Feature,实现任意逻辑
157+
```
158+
159+
- **Feature 基类**:通用的附加行为抽象,不绑定任何具体实现。默认在所有生命周期钩子中执行。
160+
- **PluginFeature**:Feature 的子类,专门封装 jmcomic 插件。除了调用插件之外,还会根据调用来源动态适配 `filename_rule` 参数;ZIP 的打包粒度则由插件在运行时根据上下文自动推导。
161+
162+
### 执行流程
163+
164+
Feature **自然嵌入到 downloader 的生命周期钩子**中自动触发:
165+
166+
```text
167+
api.download_album(extra=Feature.export_pdf)
168+
169+
├→ dler.add_features(pdf, 'download_album') # 注册: [(pdf, 'download_album')]
170+
171+
└→ dler.download_album(id)
172+
173+
├→ before_album(album)
174+
175+
├→ download_by_photo_detail(photo)
176+
│ ├→ before_photo(photo)
177+
│ ├→ download jmcomic images ... # 下载禁漫图片
178+
│ └→ after_photo(photo)
179+
│ └→ _invoke_features_for('after_photo')
180+
│ └→ pdf.should_invoke('after_photo', 'download_album') → False ✗ 跳过
181+
182+
└→ after_album(album)
183+
└→ _invoke_features_for('after_album')
184+
└→ pdf.should_invoke('after_album', 'download_album') → True ✓ 执行!
185+
└→ _adapt_plugin_kwargs(from, when) # 动态生成插件参数
186+
└→ option.invoke(pdf, kwargs) # 调用pdf插件,传入参数
187+
```
188+
189+
> 💡 **关键点**
190+
>
191+
> - **执行时机**`PluginFeature` 根据注册来源自动推导(`download_album``after_album``download_photo``after_photo`)。自定义 Feature 默认在所有钩子都会执行,你可以覆写 `should_invoke` 来控制。
192+
> - **参数自适应**`PluginFeature``filename_rule` 前缀(A/P)会根据来源动态适配。ZIP 的打包粒度由插件根据上下文自动推导。用户显式传入的参数不会被覆盖。
193+
194+
### 自定义 Feature
195+
196+
Feature 基类完全不绑定插件,你可以实现任意逻辑,欢迎贡献你的feature到本项目中:
197+
198+
```python
199+
from jmcomic import Feature, download_album
200+
201+
class NotifyFeature(Feature):
202+
"""下载完成后发送通知"""
203+
def invoke(self, option, **kwargs):
204+
album = kwargs.get('album')
205+
if album:
206+
print(f'下载完成通知: {album.name}')
207+
208+
# 使用
209+
download_album('123', option, extra=NotifyFeature())
210+
```
211+

assets/option/option_workflow_download.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# GitHub Actions 下载脚本配置
2+
log: pretty
3+
24
dir_rule:
35
base_dir: ${JM_DOWNLOAD_DIR}
46
rule: Bd_Aauthor_Atitle_Pindex

src/jmcomic/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
# 被依赖方 <--- 使用方
33
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
44

5-
__version__ = '2.6.18'
5+
__version__ = '2.6.19'
66

77
from .api import *
88
from .jm_plugin import *
9+
from .jm_feature import *
910

1011
# 下面进行注册组件(客户端、插件)
1112
gb = dict(filter(lambda pair: isinstance(pair[1], type), globals().items()))

src/jmcomic/api.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ def download_batch(download_api,
77
jm_id_iter: Union[Iterable, Generator],
88
option=None,
99
downloader=None,
10+
**kwargs,
1011
) -> Set[__DOWNLOAD_API_RET]:
1112
"""
1213
批量下载 album / photo
@@ -37,6 +38,7 @@ def callback(*ret):
3738
option,
3839
downloader,
3940
callback=callback,
41+
**kwargs,
4042
),
4143
wait_finish=True
4244
)
@@ -49,6 +51,7 @@ def download_album(jm_album_id,
4951
downloader=None,
5052
callback=None,
5153
check_exception=True,
54+
extra=None,
5255
) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
5356
"""
5457
下载一个本子(album),包含其所有的章节(photo)
@@ -60,13 +63,16 @@ def download_album(jm_album_id,
6063
:param downloader: 下载器类
6164
:param callback: 返回值回调函数,可以拿到 album 和 downloader
6265
:param check_exception: 是否检查异常, 如果为True,会检查downloader是否有下载异常,并上抛PartialDownloadFailedException
66+
:param extra: 下载特性(Feature),下载时动态挂载的附加行为上下文。会自动根据上下文(如 album/photo 来源)自适应参数行为。支持单个 Feature、FeatureChain、或列表
6367
:return: 对于的本子实体类,下载器(如果是上述的批量情况,返回值为download_batch的返回值)
6468
"""
6569

6670
if not isinstance(jm_album_id, (str, int)):
67-
return download_batch(download_album, jm_album_id, option, downloader)
71+
return download_batch(download_album, jm_album_id, option, downloader, extra=extra)
6872

6973
with new_downloader(option, downloader) as dler:
74+
# 注册 Feature 及来源,由 downloader 在 after_album 钩子中自动执行
75+
dler.add_features(extra, 'download_album')
7076
album = dler.download_album(jm_album_id)
7177

7278
if callback is not None:
@@ -81,14 +87,17 @@ def download_photo(jm_photo_id,
8187
downloader=None,
8288
callback=None,
8389
check_exception=True,
90+
extra=None,
8491
):
8592
"""
8693
下载一个章节(photo),参数同 download_album
8794
"""
8895
if not isinstance(jm_photo_id, (str, int)):
89-
return download_batch(download_photo, jm_photo_id, option)
96+
return download_batch(download_photo, jm_photo_id, option, downloader, extra=extra)
9097

9198
with new_downloader(option, downloader) as dler:
99+
# 注册 Feature 及来源,由 downloader 在 after_photo 钩子中自动执行
100+
dler.add_features(extra, 'download_photo')
92101
photo = dler.download_photo(jm_photo_id)
93102

94103
if callback is not None:

src/jmcomic/jm_client_impl.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,6 @@ def get_photo_detail(self,
280280
photo = self.fetch_detail_entity(photo_id, 'photo')
281281

282282
# 一并获取该章节的所处本子
283-
# todo: 可优化,获取章节所在本子,其实不需要等待章节获取完毕后。
284-
# 可以直接调用 self.get_album_detail(photo_id),会重定向返回本子的HTML
285-
# (had polished by FutureClientProxy)
286283
if fetch_album is True:
287284
photo.from_album = self.get_album_detail(photo.album_id)
288285

@@ -1205,4 +1202,4 @@ def get_photo_detail(self, photo_id, fetch_album=True, fetch_scramble_id=True) -
12051202
if scramble_id != '':
12061203
photo.scramble_id = scramble_id
12071204

1208-
return photo
1205+
return photo

0 commit comments

Comments
 (0)