Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ jmv 350234 -y
- **可扩展性强**

- 支持自定义本子/章节/图片下载前后的回调函数
- 支持自定义本子/章节/图片下载前后事件的回调函数
Comment thread
hect0x7 marked this conversation as resolved.
- 支持自定义类:`Downloader(负责调度)` `Option(负责配置)` `Client(负责请求)` `实体类`等
- 支持自定义日志、异常监听器
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件,目前核心内置插件有**:
Expand Down
2 changes: 1 addition & 1 deletion assets/docs/sources/api/command-line.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# command-line

::: jmcomic.cl
::: jmcomic.cli
options:
members:
- JmcomicUI
10 changes: 5 additions & 5 deletions assets/docs/sources/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@
</tr>
<tr>
<td align="center">
<a href="https://github.com/hect0x7/plugin-jm-server">
<a href="https://github.com/hect0x7/jm-view-server">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/hect0x7/hect0x7/output/profile/pin-plugin-jm-server-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/hect0x7/hect0x7/output/profile/pin-plugin-jm-server.svg" />
<img alt="Repo Card" src="https://raw.githubusercontent.com/hect0x7/hect0x7/output/profile/pin-plugin-jm-server.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/hect0x7/hect0x7/output/profile/pin-jm-view-server-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/hect0x7/hect0x7/output/profile/pin-jm-view-server.svg" />
<img alt="Repo Card" src="https://raw.githubusercontent.com/hect0x7/hect0x7/output/profile/pin-jm-view-server.svg" />
</picture>
</a>
</td>
Expand All @@ -83,4 +83,4 @@
</a>
</td>
</tr>
</table>
</table>
10 changes: 5 additions & 5 deletions assets/docs/sources/option_file_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ plugins:

# v2.5.0 引入的插件
# 可以启动一个服务器,可以在浏览器上查看本子
# 基于flask框架,需要安装额外库: [pip install plugin_jm_server]
# 源码:https://github.com/hect0x7/plugin-jm-server
# 基于flask框架,需要安装额外库: [pip install jm-view-server]
# 源码:https://github.com/hect0x7/jm-view-server
- plugin: jm_server
kwargs:
password: '3333' # 服务器访问密码
Expand Down Expand Up @@ -227,10 +227,10 @@ plugins:
rule: '{Atitle}/{Aid}_cover.jpg'


after_album: # 钩子(插件被调用时机
after_album: # 事件(插件被调用的时机
- plugin: zip # 压缩文件插件
kwargs:
# 压缩文件插件,配在不同钩子下面,效果不一样。可以选择配在 after_album 或者 after_photo 下
# 压缩文件插件,配在不同事件下面,效果不一样。可以选择配在 after_album 或者 after_photo 下
# 配置在 after_album 下 → 整个本子合并为一个压缩文件
# 配置在 after_photo 下 → 每个章节各一个压缩文件
# (旧的 level 配置已废弃,如果你配置过level,比如level=photo, 请直接改用after_photo)
Expand All @@ -239,7 +239,7 @@ plugins:
suffix: zip #压缩包后缀名,默认值为zip,可以指定为zip或者7z
filename_rule: Atitle # 压缩文件的命名规则
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
# filename_rule和所在钩子有对应关系
# filename_rule和所在事件有对应关系
# 如果配置在 after_photo 下, filename_rule 可以写 Pxxx 和Axxx
# 如果配置在 after_album 下, filename_rule 只能写 Axxx,不能写 Pxxx

Expand Down
30 changes: 29 additions & 1 deletion assets/docs/sources/tutorial/0_common_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,20 @@ except JmcomicException as e:
# 而JmDownloader有对应字段记录了这些线程发生的异常
# 使用check_exception=True参数可以使downloader主动检查是否存在下载异常
# 如果有,则当前线程会主动上抛一个PartialDownloadFailedException异常
# 该参数主要用于主动检查部分下载失败的情况,
# 该参数主要用于主动检查部分下载失败的情况,(仅对单个本子/章节 ID 生效,传入多个 ID 时不生效。多个 ID 的场景见下)
# 因为非当前线程抛出的异常(比如下载章节的线程和下载图片的线程),这些线程如果抛出异常,
# 当前线程是感知不到的,try-catch下载方法download_album不能捕获到其他线程发生的异常。
try:
album, downloader = download_album(123, check_exception=True)
except PartialDownloadFailedException as e:
downloader: JmDownloader = e.downloader
print(f'下载出现部分失败, 下载失败的章节: {downloader.download_failed_photo}, 下载失败的图片: {downloader.download_failed_image}')

# 多 ID 下载不会因为某一项失败而中断,请检查 BatchResult.failed。
# 如果需要在批量失败时抛异常或重试,建议自行封装 download_batch。
result = download_album([123, 456, 789])
for album_id, error in result.failed.items():
print(f'本子 {album_id} 下载失败: {error}')
```


Expand Down Expand Up @@ -168,6 +174,28 @@ download_album(aid_list, option)

可参考discussions: https://github.com/hect0x7/JMComic-Crawler-Python/discussions/235

### 一键导出全部收藏夹

下面的代码不会下载图片,只会把帐号中的全部收藏夹导出为 CSV,并生成 `favorites.zip`:

```python
from jmcomic import JmOption, FavoriteFolderExportPlugin

USERNAME = '你的禁漫帐号'
PASSWORD = '你的禁漫密码'

option = JmOption.default()
option.build_jm_client().login(USERNAME, PASSWORD)

FavoriteFolderExportPlugin(option).invoke(
save_dir='./',
zip_enable=True,
zip_filepath='./favorites.zip',
)
```

### 获取并遍历收藏夹

```python
from jmcomic import *

Expand Down
103 changes: 97 additions & 6 deletions assets/docs/sources/tutorial/11_log_custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,45 @@ from jmcomic import disable_jm_log
disable_jm_log()
```

## 2. 日志过滤,只保留特定topic
## 2. 日志过滤,只保留特定 topic

使用插件配置
最简单的方式是使用内置插件配置:

```yaml
log: true

plugins:
after_init:
- plugin: log_topic_filter # 日志topic过滤插件
- plugin: log_topic_filter # 日志 topic 过滤插件
kwargs:
whitelist: [ # 只保留api和html,这两个是Client发请求时会打的日志topic
whitelist: [ # 只保留 api 和 html,这两个是 Client 发请求时会打印的日志 topic
'api',
'html',
]
```

这个插件底层使用标准 `logging.Filter`。jmcomic 将每条日志的 topic 放在 `LogRecord.topic` 中,需要通过代码自定义时可以直接使用 logging API:

```python
import logging

from jmcomic import jm_logger


class TopicFilter(logging.Filter):
def __init__(self, whitelist):
super().__init__()
self.whitelist = set(whitelist)

def filter(self, record):
return getattr(record, 'topic', None) in self.whitelist


jm_logger.addFilter(TopicFilter({'api', 'html'}))
```

Filter 加在 `jm_logger` 上会作用于它的所有 Handler;如果只想过滤某个输出目标,也可以将 Filter 加到对应的 Handler 上。

## 3. 屏蔽插件的日志

给插件配置加上一个`log`配置项即可
Expand All @@ -52,7 +74,76 @@ plugins:
whitelist: [ api, ]
```

## 4. 深度自定义:两类不同的拦截手段
## 4. 并发下载的日志上下文

场景:当你在同时运行多个下载任务时,你希望区分哪些日志属于哪些任务,或者你希望结构化展示下载任务 对应的 下载日志。

首先,你需要设定一个任务id,这一步是在定义你的**任务维度**。下载一个本子还是多个本子都可以算作一个任务。

```python
from jmcomic import download_album, jm_task_context

with jm_task_context(task_id='task-1'): # 设置这个下载本子123的任务id为 task-1
download_album(123)

with jm_task_context(task_id='task-1'): # 设置这三个本子的任务id为 task-1
download_album([123, 456, 789])
```

设置了task_id以后,整个 download_album 内的日志打印都会具有这个task_id标识。

> jmcomic 使用 [ContextVar](https://docs.python.org/3/library/contextvars.html) 把上下文传播到库内部创建的下载线程或async task里。你设置的task_id其实就是上下文里的一个字段

最直接的效果是,默认终端日志会自动显示任务 ID:

```text
[2026-01-01 12:00:00] [MainThread]:[task_id=task-1; album=123] 【album.after】本子下载完成: [123]
```

接下来你可以实现更高级的功能,比如收集任务对应的日志:

给 `jm_logger` 添加一个 Handler。每当 jmcomic 产生一条日志,Handler 都可以从 `record.jm_task_context` 中取出它所属的 `task_id`,从而按任务收集日志。完整代码如下:

```python
import logging
from collections import defaultdict

from jmcomic import jm_logger, download_album, jm_task_context

task_logs_dict = defaultdict(list) # 收集任务日志,任务id -> 日志列表


class TaskLogHandler(logging.Handler):
def emit(self, record):
context: dict = getattr(record, 'jm_task_context', None) or {} # 通过 jm_task_context 字段取出任务上下文
task_id = context.get('task_id') # 任务上下文里的task_id,就是你上面自定义的 task-1
if task_id is not None:
task_logs_dict[task_id].append(self.format(record)) # 收集日志


handler = TaskLogHandler()
handler.setFormatter(jm_logger.handlers[0].formatter) # 复用jmcomic默认 Handler 的日志格式,你也可以自定义日志格式
Comment thread
hect0x7 marked this conversation as resolved.
jm_logger.addHandler(handler)

# 自定义handler后,再正常使用下载方法
task_id = 'task-1'
with jm_task_context(task_id=task_id): # 任务id
download_album(123)
```

任务上下文里,常用字段如下:

| 字段 | 类型 | 谁来设置 | 含义 | 示例 |
| --- |--------| --- |-----------------------------------------------------------------------| --- |
| `task_id` | 你传入的类型;本例需可哈希,建议使用 `str` | 由你通过 `jm_task_context` 设置 | 你的一次下载任务标识 | `task-1` |
| `download_type` | `str` | jmcomic 自动设置 | 你使用的download入口函数类型,例如 download_album -> album | `album` / `photo` |
| `jm_id` | `str` | jmcomic 自动设置 | 你使用的download入口函数的入参,例如 download_album(123) -> 123。传入多个 ID 时,每个 ID 都在各自隔离的上下文中记录对应的 `jm_id`,并继承同一个 `task_id`。 | `123` |

> 默认终端日志仅在 `task_id` 有值时显示任务上下文,并一同显示 `download_type` 和 `jm_id`。未设置 `task_id` 时,上下文仍会正常传递,但不会显示在默认日志中。

你也可以放入其他对象到任务上下文里,比如放入一个局部queue用来收集日志。在 jm_task_context 方法里传入即可 `jm_task_context(**fields)`

## 5. 深度自定义:两类不同的拦截手段

根据你的需求复杂度,你可以选择以下方式:

Expand All @@ -64,4 +155,4 @@ plugins:

适用于:需要完全重塑日志的分发逻辑,或者将日志直接桥接到不符合标准 logging 协议的第三方系统。

详细参考文档:[模块自定义](./4_module_custom.md#自定义log)
代码示例:[模块自定义-自定义log](./4_module_custom.md#自定义log)
7 changes: 3 additions & 4 deletions assets/docs/sources/tutorial/13_export_and_feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,12 @@ Feature (基类)
└── 你的自定义 Feature ← 继承 Feature,实现任意逻辑
```

- **Feature 基类**:通用的附加行为抽象,不绑定任何具体实现。默认在所有生命周期钩子中执行
- **Feature 基类**:通用的附加行为抽象,不绑定任何具体实现。默认在所有生命周期事件中执行
- **PluginFeature**:Feature 的子类,专门封装 jmcomic 插件。除了调用插件之外,还会根据调用来源动态适配 `filename_rule` 参数;ZIP 的打包粒度则由插件在运行时根据上下文自动推导。

### 执行流程

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

```text
api.download_album(extra=Feature.export_pdf)
Expand All @@ -187,7 +187,7 @@ api.download_album(extra=Feature.export_pdf)

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

### 自定义 Feature
Expand All @@ -207,4 +207,3 @@ class NotifyFeature(Feature):
# 使用
download_album('123', option, extra=NotifyFeature())
```

16 changes: 9 additions & 7 deletions assets/docs/sources/tutorial/4_module_custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@



## 自定义下载前后的回调函数
## 自定义下载事件的回调函数

```python
def custom_download_callback():
def custom_download_event_callback():
"""
该函数演示自定义下载时的回调函数
该函数演示如何自定义下载事件的回调函数。
公开下载 API 不接收 callback 参数;需要响应下载事件时,请自定义 Downloader。
"""

# jmcomic的下载功能由 JmModuleConfig.CLASS_DOWNLOADER 这个类来负责执行
# 这个类默认是 JmDownloader,继承了DownloadCallback
# 你可以写一个自定义类,继承JmDownloader,覆盖属于DownloadCallback的方法,来实现自定义回调
# 这个类默认是 JmDownloader,继承了 DownloadCallback
# DownloadCallback 定义了 Downloader 内部各类下载事件的回调方法
# 你可以继承 JmDownloader 并覆盖相应方法来响应事件
class MyDownloader(JmDownloader):
# 覆盖 album 下载完成后的回调
# 覆盖 album 下载完成事件的回调函数
def after_album(self, album: JmAlbumDetail):
print(f'album下载完毕: {album}')
pass
Comment thread
hect0x7 marked this conversation as resolved.
Expand Down Expand Up @@ -192,4 +194,4 @@ def custom_exception_listener():
# 注册监听器/回调
# 这个异常类(或者这个异常的子类)的实例将要被raise前,你的listener方法会被调用
JmModuleConfig.register_exception_listener(etype, listener)
```
```
4 changes: 2 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ Pillow
psutil
pycryptodome
requests
plugin_jm_server
jm-view-server
zhconv
img2pdf
img2pdf
3 changes: 2 additions & 1 deletion src/jmcomic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# 被依赖方 <--- 使用方
# config <--- entity <--- toolkit <--- client <--- option <--- downloader

__version__ = '2.7.2'
__version__ = '2.7.3'

from .jm_task_context import *
Comment thread
hect0x7 marked this conversation as resolved.
from .api import *
from .jm_plugin import *
from .jm_feature import *
Expand Down
Loading
Loading