Skip to content
Closed
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
6 changes: 3 additions & 3 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
timeout-minutes: 15

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: '3.14'

Expand All @@ -49,7 +49,7 @@ jobs:

- name: Upload Benchmark Report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: benchmark-report
path: PERFORMANCE_REPORT.md
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/close_specific_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Close PR
run: |
Expand All @@ -36,4 +36,4 @@ jobs:
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body '${{ env.comment }}'
gh pr close ${{ github.event.pull_request.number }} --repo ${{ github.repository }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6 changes: 3 additions & 3 deletions .github/workflows/download.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ jobs:
UPLOAD_NAME: 'Click me to download'

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
mv "../$ZIP_NAME" .

- name: 上传结果
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ env.UPLOAD_NAME }}
path: ${{ env.JM_DOWNLOAD_DIR }}/${{ env.ZIP_NAME }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/download_dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ jobs:
JM_DOWNLOAD_DIR: /home/runner/work/jmcomic/download/

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

Expand Down Expand Up @@ -153,7 +153,7 @@ jobs:
mv "../$ZIP_NAME" .

- name: 上传结果
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ env.UPLOAD_NAME }}
path: ${{ env.JM_DOWNLOAD_DIR }}/${{ env.ZIP_NAME }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/export_favorites.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ jobs:
ZIP_FP: /home/runner/work/jmcomic/download/export.7z

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

Expand All @@ -71,7 +71,7 @@ jobs:
python workflow_export_favorites.py

- name: 上传结果
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: '导出的收藏夹'
path: ${{ env.ZIP_FP }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

Expand All @@ -31,7 +31,7 @@ jobs:
python -m build

- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release_auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
contents: write
if: startsWith(github.event.head_commit.message, 'v')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Python 3.11
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.11"

Expand All @@ -33,7 +33,7 @@ jobs:
python -m build

- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_html.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
条目分类参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [Unreleased]

### Added
- 新增 `JmSimpleRuntime`、`JmSyncRuntime` 与 `JmAsyncRuntime`;裸同步 Downloader 的局部调度使用单池 Runtime,顶层同步 API 或自定义同步调度可复用 `id/photo/image` 三层线程池,异步下载可复用 `blocking` 线程池。
- 新增 `DownloadControl` 和 `DownloadCancelledException`,支持通过任务上下文协作式取消同步与异步下载。

### Changed
- Python 3.9 保留安装兼容,但不再纳入 CI。
- 下载调度统一使用标准库 Executor;顶层 API 和裸同步 Downloader 的临时调度显式关闭自己创建的 Runtime,`jm_task_context` 只传播字段;外部 Runtime 和 Executor 仍由调用方关闭。
- 顶层下载会把 Runtime 和 Option 作为公开字段直接放入任务上下文;Runtime 不依赖 Context 或 Option,未配置的层级由实际调用点传入默认 worker 数。

## [2.7.5] - 2026-08-25

### Summary
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/12_domain_strategy.md
- tutorial/13_export_and_feature.md
- tutorial/14_async_usage.md
- tutorial/16_shared_executors.md
- tutorial/15_download_progress.md

plugins:
Expand Down
44 changes: 44 additions & 0 deletions assets/docs/sources/api/download.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,47 @@
options:
members:
- JmAsyncDownloader

::: jmcomic.jm_downloader
options:
members:
- BaseDownloader
- JmDownloader

## 下载 Runtime 与取消控制

::: jmcomic.jm_runtime
options:
members:
- JmRuntime
- JmSimpleRuntime
- JmSyncRuntime
- JmAsyncRuntime

::: jmcomic.jm_exception
options:
members:
- DownloadCancelledException

::: jmcomic.jm_task_context
options:
members:
- DownloadControl
- jm_task_context
- bind_jm_task_context
- get_jm_task_context
- get_current_control
- get_jm_runtime
- get_current_option

同步顶层 API 默认创建 `JmSyncRuntime`,并在调用结束时显式关闭。需要让多个顶层调用复用线程池时,先创建 `JmSyncRuntime`,通过 `jm_task_context(runtime=runtime)` 传播,并在任务完成后显式调用 `runtime.close()`;异步调用改用 `JmAsyncRuntime`。裸同步 Downloader 没有 Runtime 时,会为每次局部调度创建并关闭只有一个线程池的 `JmSimpleRuntime`。完整示例见[复用下载 Runtime](../tutorial/16_shared_executors.md)。

Runtime 只负责 Executor 的配置、调度和生命周期。`JmSimpleRuntime.multi_thread_launcher()` 不要求下载层级;`JmSyncRuntime` 的 launcher 使用 `id/photo/image` 层级。未显式配置容量时,由下载调用点把 Option 或 Downloader 已解析出的默认 worker 数传给 Runtime。

使用 `get_jm_runtime()` 可以读取当前任务激活的 Runtime;没有激活 Runtime 时返回 `None`。

Runtime 和 Option 以公开字段 `runtime`、`option` 直接保存在 `JM_TASK_CONTEXT` 中。`get_jm_task_context()` 返回包含这两个字段的完整副本;`get_jm_runtime()` 和 `get_current_option()` 是读取它们的便捷方法。自定义日志处理器也可以从 LogRecord 的任务上下文中访问这两个公开字段,默认文本日志不会展开对象内容。

`jm_task_context` 只负责字段传播与作用域恢复,不会关闭 Runtime。谁创建 Runtime,谁显式调用 `runtime.close()`;Runtime 只会关闭自身创建的 Executor,不会关闭调用方注入的 Executor。

取消检查统一由 `BaseDownloader.raise_if_cancelled()` 这个 `classmethod` 执行。自定义 Downloader 可以重写它来调整检查策略;Client 和顶层 API 不直接读取取消状态。
56 changes: 56 additions & 0 deletions assets/docs/sources/tutorial/0_common_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,62 @@ print('是否全部成功:', batch_result.all_succeeded)

下载单个 ID 时,请求本子失败会直接抛出异常;如果只有部分章节或图片失败,会在任务结束后汇总抛出 `PartialDownloadFailedException`,此时不会返回 `DownloadResult`。批量下载则继续执行其他任务,并把失败项集中放进 `batch_result.failed`。

### 取消下载

下载开始后,可以从按钮、定时器或其他线程调用 `DownloadControl.cancel()` 请求停止。JMComic 收到请求后不会再开始下载新的章节和图片,但会先把当前正在处理的图片完整保存,避免留下损坏文件;随后下载会抛出 `DownloadCancelledException`,调用方可以据此提示用户任务已经取消。

下面的同步示例启动一个下载线程,并在两秒后从主线程请求取消:

```python
from threading import Thread
from time import sleep

from jmcomic import DownloadCancelledException, DownloadControl, download_album, jm_task_context

control = DownloadControl()

def run_download():
try:
# ContextVar 不会自动进入用户创建的新线程,
# 因此 context 必须在实际调用下载的线程内建立。
with jm_task_context(control=control):
download_album('123')
except DownloadCancelledException as e:
print('取消原因:', e.reason)

thread = Thread(target=run_download)
thread.start()

# GUI 按钮、请求处理器或其他线程调用
sleep(2)
control.cancel('用户取消')
thread.join()
```

异步 API 使用完全相同的 `DownloadControl` 和 context。`asyncio.create_task()` 会自动复制当前 Context,因此 Task 会看到同一个 `control`。`DownloadControl` 的协作式取消路径不会调用 `asyncio.Task.cancel()`;如果要保留“当前图片完整写入后再停止”的保证,只调用 `control.cancel()`。如果调用方直接取消外层异步批量 Task,批量 API 会取消并等待其内部子 Task 完成清理,再透传 `CancelledError`;这条外部取消路径不保证当前图片完整写入。

```python
import asyncio

from jmcomic import DownloadCancelledException, DownloadControl, download_album_async, jm_task_context

async def main():
control = DownloadControl()
with jm_task_context(control=control):
task = asyncio.create_task(download_album_async('123'))

await asyncio.sleep(2)
control.cancel('用户取消')
try:
await task
except DownloadCancelledException as e:
print('取消原因:', e.reason)

asyncio.run(main())
```

取消不会删除已经完整写入的图片。同步 HTTP 请求以及已经运行的解密或写盘不能被强制中断,因此取消会在当前不可中断操作完成或超时后生效。取消后不会执行整章、整本完成回调及 PDF/ZIP 等导出 Feature。

### 速查表

| 你的需求 | 推荐写法 |
Expand Down
9 changes: 7 additions & 2 deletions assets/docs/sources/tutorial/14_async_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ import jmcomic

async def main():
# 异步下载单个本子
album, downloader = await jmcomic.download_album_async('438696')
result = await jmcomic.download_album_async('438696')
album = result.detail
downloader = result.downloader

# 返回的 downloader 已释放网络连接和线程池,只用于读取下载结果
# 返回时该 Downloader 的网络 client 已关闭;如果传入共享 blocking
# 执行器,它仍由创建它的调用方管理
print(downloader.download_failed_image)

# 异步下载单章节
Expand Down Expand Up @@ -52,6 +55,8 @@ async def main():
asyncio.run(main())
```

异步下载的网络 I/O 仍由 event loop 和 Semaphore 控制;解密、PIL、写盘及同步 hook 使用标准线程池。每个 `JmAsyncDownloader` 仍独立创建并关闭自己的 client、session 和并发控制;需要显式复用线程池时,请参阅[复用下载 Runtime](16_shared_executors.md)。

## 3. 异步获取实体类,并发请求

### 💡 关于 async with 和自动初始化
Expand Down
Loading
Loading