Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
> **🧭 快速指路**
> - [教程:使用 GitHub Actions 下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md)
> - [教程:导出并下载你的禁漫收藏夹数据](./assets/docs/sources/tutorial/10_export_favorites.md)
> - [教程:下载后转为 PDF / ZIP / 长图](./assets/docs/sources/tutorial/13_export_and_feature.md)
> - [塔台广播:欢迎各位机长加入并贡献代码](./.github/CONTRIBUTING.md)
>
> **友情提示:珍爱JM,为了减轻JM的服务器压力,请不要一次性爬取太多本子,西门🙏🙏🙏**.
Expand Down
1 change: 1 addition & 0 deletions assets/docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ nav:
- tutorial/10_export_favorites.md
- tutorial/11_log_custom.md
- tutorial/12_domain_strategy.md
- tutorial/13_export_and_feature.md

plugins:
- search
Expand Down
2 changes: 2 additions & 0 deletions assets/docs/sources/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

- [快速上手(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)
- [常用类和方法演示](tutorial/0_common_usage.md)
- [下载同时转 PDF/ZIP/长图](tutorial/13_export_and_feature.md)
- [option配置以及插件写法](./option_file_syntax.md)

## 特殊用法教程
Expand All @@ -30,6 +31,7 @@

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

## 自定义

Expand Down
23 changes: 11 additions & 12 deletions assets/docs/sources/option_file_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,29 +223,28 @@ plugins:
rule: '{Atitle}/{Aid}_cover.jpg'


after_album:
after_album: # 钩子(插件被调用时机)
- plugin: zip # 压缩文件插件
kwargs:
level: photo # 按照章节,一个章节一个压缩文件
# level 也可以配成 album,表示一个本子对应一个压缩文件,该压缩文件会包含这个本子的所有章节

filename_rule: Ptitle # 压缩文件的命名规则
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
# filename_rule和level有对应关系
# 如果level=[photo], filename_rule只能写Pxxx
# 如果level=[album], filename_rule只能写Axxx
# 压缩文件插件,配在不同钩子下面,效果不一样。可以选择配在 after_album 或者 after_photo 下
# 配置在 after_album 下 → 整个本子合并为一个压缩文件
# 配置在 after_photo 下 → 每个章节各一个压缩文件
# (旧的 level 配置已废弃,如果你配置过level,比如level=photo, 请直接改用after_photo)

zip_dir: D:/jmcomic/zip/ # 压缩文件存放的文件夹

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

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

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

Expand Down
203 changes: 203 additions & 0 deletions assets/docs/sources/tutorial/13_export_and_feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Feature 机制——下载附加行为

## 1. 需求场景

下载本子后,很多用户有进一步导出的需求:
- 导出为 **PDF**:方便在电子阅读器上查看
- 导出为 **ZIP**:方便传输和存档
- 合并为 **长图**:方便一张图看完整个章节

jmcomic 内置了三个开箱即用的导出 Feature,对应这三种需求:

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


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


## 2. 快速上手

### 2.1 导出 PDF——基本用法示例

```python
from jmcomic import download_album, Feature

# 只需要加一个 extra 参数,就能在下载完成后自动导出 PDF
download_album('123', extra=Feature.export_pdf)

# 如果要传 option 参数,就是如下写法,三个参数
download_album('123', option, extra=Feature.export_pdf)
```

**效果**:在本子下载完以后,额外在**当前工作目录**下生成包含所有本子图片的 PDF 文件:

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

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

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

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

# 也支持列表语法,|语法
download_album('123', option, extra=[Feature.export_pdf, Feature.export_zip])
download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

效果同pdf,会在本子下载完以后,额外在当前工作目录下,生成包含所有本子图片的 PDF 文件和 ZIP 文件:

```
./
├── [JM123]本子标题.pdf ← 整本合并为 1 个 PDF
├── [JM123]本子标题.zip ← 整本合并为 1 个 zip 压缩包
```


### 2.3 自定义参数

如果你了解插件配置,可以同样使用Feature传递插件的自定义参数,例如改变输出目录、命名规则等:

```python
# 示例 1:指定输出目录和命名规则
download_album('123', option, extra=Feature.export_pdf(
# 下面是自定义参数
pdf_dir='D:/my_pdfs', # PDF 保存到 D:/my_pdfs 文件夹
filename_rule='Ptitle', # 用章节标题作为文件名
delete_original_file=True, # 合并完 PDF 后删除原图
))
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# 示例 2:全都要——ZIP 存盘 + 长图阅读
combo = (
Feature.export_zip(zip_dir='D:/zips')
+ Feature.export_long_img(img_dir='D:/long_imgs')
)
download_album('123', option, extra=combo)
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### 2.4 download_photo 也支持

```python
from jmcomic import download_photo, Feature

# 对单个章节导出
download_photo('456', option, extra=Feature.export_pdf)
```

效果:在当前工作目录下生成以章节标题命名的 PDF:

```
./
├── [章节标题].pdf ← 该章节导出为 1 个 PDF
```

Comment thread
coderabbitai[bot] marked this conversation as resolved.
> 💡 **提示**:同一个 Feature,通过 `download_album` 和 `download_photo` 调用时会自动适配不同的导出行为,详见下方 [智能适配规则](#25-智能适配规则)。

### 2.5 智能适配规则

内置的导出 Feature 会根据调用的 API **自动适配**参数:

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

当你显式传入参数时(如 `filename_rule='Ptitle'`),**你的配置优先**,不会被自适应覆盖。

> 💡 **提示**:更多可选参数(如加密密码 `encrypt`、后缀名 `suffix` 等),参考 [Plugin 插件参数大全](./6_plugin.md#参数)。

## 3. 传统写法(YAML 插件配置)

如果你更习惯配置文件,仍然可以使用传统的插件配置方式:

```yaml
# option.yml
plugins:
after_album: # 整本下载完以后
- plugin: img2pdf # 合并pdf
kwargs:
pdf_dir: ./output
filename_rule: Atitle
- plugin: zip # 合并为压缩文件
kwargs:
level: album
zip_dir: ./output
```

传统写法的更多细节见 → [Plugin 插件教程](./6_plugin.md)

## 4. Feature 架构设计

### 类层次

```
Feature (基类)
├── PluginFeature ← 封装插件调用,参数根据来源自适应
└── 你的自定义 Feature ← 继承 Feature,实现任意逻辑
```

- **Feature 基类**:通用的附加行为抽象,不绑定任何具体实现。默认在所有生命周期钩子中执行。
- **PluginFeature**:Feature 的子类,专门封装 jmcomic 插件。除了调用插件之外,还会根据调用来源动态适配 `filename_rule`、`level` 等参数。

### 执行流程

Feature **自然嵌入到 downloader 的生命周期钩子**中自动触发:

```
api.download_album(extra=Feature.export_pdf)
├→ dler.add_features(pdf, 'download_album') # 注册: [(pdf, 'download_album')]
└→ dler.download_album(id)
├→ before_album(album)
├→ download_by_photo_detail(photo)
│ ├→ before_photo(photo)
│ ├→ download jmcomic images ... # 下载禁漫图片
│ └→ after_photo(photo)
│ └→ _invoke_features_for('after_photo')
│ └→ pdf.should_invoke('after_photo', 'download_album') → False ✗ 跳过
└→ after_album(album)
└→ _invoke_features_for('after_album')
└→ pdf.should_invoke('after_album', 'download_album') → True ✓ 执行!
└→ _adapt_plugin_kwargs(from, when) # 动态生成插件参数
└→ option.invoke(pdf, kwargs) # 调用pdf插件,传入参数
```

> 💡 **关键点**:
>
> - **执行时机**:`PluginFeature` 根据注册来源自动推导(`download_album` → `after_album`,`download_photo` → `after_photo`)。自定义 Feature 默认在所有钩子都会执行,你可以覆写 `should_invoke` 来控制。
> - **参数自适应**:`PluginFeature` 的 `filename_rule` 前缀(A/P)和 `level`(album/photo)会根据来源动态适配。用户显式传入的参数不会被覆盖。

### 自定义 Feature

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

```python
from jmcomic import Feature, download_album

class NotifyFeature(Feature):
"""下载完成后发送通知"""
def invoke(self, option, **kwargs):
album = kwargs.get('album')
if album:
print(f'下载完成通知: {album.name}')

# 使用
download_album('123', option, extra=NotifyFeature())
```

3 changes: 2 additions & 1 deletion src/jmcomic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
# 被依赖方 <--- 使用方
# config <--- entity <--- toolkit <--- client <--- option <--- downloader

__version__ = '2.6.18'
__version__ = '2.6.19'

from .api import *
from .jm_plugin import *
from .jm_feature import *

# 下面进行注册组件(客户端、插件)
gb = dict(filter(lambda pair: isinstance(pair[1], type), globals().items()))
Expand Down
13 changes: 11 additions & 2 deletions src/jmcomic/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def download_batch(download_api,
jm_id_iter: Union[Iterable, Generator],
option=None,
downloader=None,
**kwargs,
) -> Set[__DOWNLOAD_API_RET]:
"""
批量下载 album / photo
Expand Down Expand Up @@ -37,6 +38,7 @@ def callback(*ret):
option,
downloader,
callback=callback,
**kwargs,
),
wait_finish=True
)
Expand All @@ -49,6 +51,7 @@ def download_album(jm_album_id,
downloader=None,
callback=None,
check_exception=True,
extra=None,
) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
"""
下载一个本子(album),包含其所有的章节(photo)
Expand All @@ -60,13 +63,16 @@ def download_album(jm_album_id,
:param downloader: 下载器类
:param callback: 返回值回调函数,可以拿到 album 和 downloader
:param check_exception: 是否检查异常, 如果为True,会检查downloader是否有下载异常,并上抛PartialDownloadFailedException
:param extra: 下载特性(Feature),下载时动态挂载的附加行为上下文。会自动根据上下文(如 album/photo 来源)自适应参数行为。支持单个 Feature、FeatureChain、或列表
:return: 对于的本子实体类,下载器(如果是上述的批量情况,返回值为download_batch的返回值)
"""

if not isinstance(jm_album_id, (str, int)):
return download_batch(download_album, jm_album_id, option, downloader)
return download_batch(download_album, jm_album_id, option, downloader, extra=extra)

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

if callback is not None:
Expand All @@ -81,14 +87,17 @@ def download_photo(jm_photo_id,
downloader=None,
callback=None,
check_exception=True,
extra=None,
):
"""
下载一个章节(photo),参数同 download_album
"""
if not isinstance(jm_photo_id, (str, int)):
return download_batch(download_photo, jm_photo_id, option)
return download_batch(download_photo, jm_photo_id, option, downloader, extra=extra)

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

if callback is not None:
Expand Down
Loading
Loading