Skip to content
Merged
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@

- 登录
- 搜索本子(支持所有搜索项)
- 图片下载解码
- 获取本子评论(包括回评、剧透标识)
- 分类/排行榜
- 本子/章节详情
- 获取本子/章节详情
- 图片下载解码
- 个人收藏夹
- 接口加解密(APP的接口)

Expand Down Expand Up @@ -221,15 +222,16 @@ jmv 350234 -y

- **绕过Cloudflare的反爬虫**
- **实现禁漫APP接口最新的加解密算法 (1.6.3)**
- 支持**Async**和**Sync**两套 API
- 用法多样:

- GitHub
Actions:网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md))
- 命令行:无需写Python代码,简单易用([教程:使用命令行下载禁漫本子](./assets/docs/sources/tutorial/2_command_line.md))
- Python代码:最本质、最强大的使用方式,需要你有一定的python编程基础
- **支持 Async 和 Sync 两套 API**
- 支持**网页端**和**移动端**两种客户端实现,可通过配置切换(**移动端不限ip兼容性好,网页端限制ip地区但效率高**)
- 支持**自动重试和域名切换**机制
- 支持按下载任务维度结构化收集日志
- **可配置性强**

- 不配置也能使用,十分方便
Expand All @@ -238,7 +240,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
4 changes: 3 additions & 1 deletion assets/docs/sources/api/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
- JmPhotoDetail
- JmImageDetail
- JmPageContent
- JmSearchPage
- JmAlbumComment
- JmAlbumCommentPage
- JmSearchPage
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
71 changes: 70 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。
# 如果需要在批量失败时抛异常、重试或拿到更详细信息,建议自行封装一个批量下载方法。
result = download_album([123, 456, 789])
for album_id, error in result.failed.items():
print(f'本子 {album_id} 下载失败: {error}')
```


Expand Down Expand Up @@ -164,10 +170,73 @@ for aid, atitle, tag_list in page.iter_id_title_tag(): # 使用page的iter_id_t
download_album(aid_list, option)
```

## 获取本子评论

```python
from jmcomic import JmOption, JmAlbumCommentPage, JmAlbumComment

client = JmOption.default().new_jm_client(impl='api')

# 获取第一页评论
page: JmAlbumCommentPage = client.album_pagination('123456')

print('本页评论数:', len(page))
print('本页评论数(含回评):', page.comment_count)
print('本子的评论总数:', page.total)
print('总页数:', page.page_count)

# page对象可以直接遍历评论,评论类型是JmAlbumComment
comment: JmAlbumComment
for comment in page:
print('用户:', comment.nickname or comment.username)
print('内容:', comment.content)
print('是否剧透:', comment.is_spoiler)

# 回评也是评论对象
for reply in comment.replies:
print('回评用户:', reply.nickname or reply.username)
print('回评内容:', reply.content)
print('回评是否剧透:', reply.is_spoiler)

# gen 方法支持自动循环获取评论分页,直到结束
for page in client.album_pagination_gen('123456'):
print('本页主评论数:', len(page))
print('本页评论数(含回评):', page.comment_count)
print('主评论总数:', page.total)
print('总页数:', page.page_count)

for comment in page:
print('用户:', comment.nickname or comment.username)
print('内容:', comment.content)
print('是否剧透:', comment.is_spoiler)
```

## 获取收藏夹

可参考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())
```

Loading
Loading