Skip to content
Merged
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 @@ -2,12 +2,12 @@ name: Async Benchmark

on:
push:
branches: [ main, master ]
branches: [ master ]
paths:
- '**/*async*.py'
- '.github/workflows/benchmark.yml'
pull_request:
branches: [ main, master ]
branches: [ master ]
paths:
- '**/*async*.py'
- '.github/workflows/benchmark.yml'
Expand All @@ -24,7 +24,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.14'

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
test: # This code is based on https://github.com/gaogaotiantian/viztracer/blob/master/.github/workflows/python-package.yml
strategy:
matrix:
python-version: ['3.9', '3.10', '3.13', '3.14'] # 3.9 is kept as the minimum supported version test (EOL)
python-version: ['3.10', '3.11', '3.13', '3.14']
os: [ ubuntu-latest ]
runs-on: ${{ matrix.os }}
timeout-minutes: 5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_html.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
test: # This code is based on https://github.com/gaogaotiantian/viztracer/blob/master/.github/workflows/python-package.yml
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.13']
python-version: ['3.10', '3.11', '3.13', '3.14']
os: [ ubuntu-latest ]
runs-on: ${{ matrix.os }}
timeout-minutes: 5
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
条目分类参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [2.7.5] - 2026-08-25

### Summary

本次更新修复网页端收藏夹总数误解析,并同步禁漫 APP 2.1.2 的移动端版本号。

### Fixed
- 修复收藏夹页面改版后,总数正则优先命中 CSS 尺寸、导致 `JmHtmlClient.favorite_folder().total` 错误返回 `50` 的问题。

### Changed
- 更新禁漫移动端版本号至 `2.1.2`。
- `RequestRetryAllFailException` 现在会保留并输出各域名、各次重试的原始异常,便于定位真实失败原因。

## [2.7.4] - 2026-08-09

### Summary
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
## 安装教程

> ⚠如果你没有安装过 Python,需要先前往 [Python 官网下载](https://www.python.org/downloads/) 再执行以下步骤。
>**推荐使用 Python 3.12及以上版本**
>**推荐使用 Python 3.14**

* 通过pip官方源安装(推荐,并且更新也是这个命令)

Expand Down Expand Up @@ -255,8 +255,9 @@ jmv 350234 -y

## 使用小说明

* 推荐使用 **Python 3.12+**,目前最低兼容版本为3.9。
> 注意:Python 3.9 及更早版本皆已于 2025 年彻底结束官方生命周期 (EOL),使用3.9及以下随时有可能遇到第三方库不兼容的问题。
* 推荐使用 **Python 3.14**,目前 CI 只覆盖 Python 3.10 及以上版本。
> [!NOTE]
> Python 3.9 及更早版本均已结束官方支持 (EOL),使用3.9及以下随时有可能遇到第三方库不兼容的问题。Python 3.9 仍保留安装兼容,但不再纳入 CI。

* 个人项目,文档和示例会有不及时之处,可以Issue提问。

Expand Down
7 changes: 4 additions & 3 deletions assets/readme/README-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ In addition to downloading, other JM interfaces are also implemented on demand.
## Installation Guide

> ⚠ If you have not installed Python, you must install Python before executing the following steps. [Download from Python Official Site](https://www.python.org/downloads/)
> **Version 3.12+ is recommended.**
> **Python 3.14 is recommended.**

* Install via official pip source (recommended, the update command is identical)

Expand Down Expand Up @@ -249,8 +249,9 @@ Please check the documentation homepage → [jmcomic.readthedocs.io (Chinese lan

## Prerequisites

* Version **3.12+** is recommended, with a minimum compatible version of 3.9.
> Note: Python 3.9 and earlier versions reached their End Of Life (EOL) in 2025. You may encounter third-party library incompatibilities at any time if you use version 3.9 or below.
* **Python 3.14** is recommended. CI currently only covers Python 3.10 and later.
> [!NOTE]
> Python 3.9 and earlier versions are no longer officially supported (EOL). You may encounter third-party library incompatibilities at any time if you use version 3.9 or below. Python 3.9 remains install-compatible, but is no longer included in CI.

* Since this is a personal project, the documentation/examples may occasionally be out of sync. Please feel free to open an Issue for any clarifications.

Expand Down
7 changes: 4 additions & 3 deletions assets/readme/README-jp.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
## インストール手順

> ⚠ まだPythonをインストールしていない場合は、先に [公式のPythonページからダウンロード](https://www.python.org/downloads/) してインストールをお願いします。
> **Python 3.12以上の使用を推奨します**
> **Python 3.14の使用を推奨します**

* 公式 pip ソースからインストール(推奨。アップデートもこのコマンドを使用します)

Expand Down Expand Up @@ -244,8 +244,9 @@ jmv 350234 -y

## ご利用上の注意点

* **Python 3.12以上**を推奨します。現在の最小互換バージョンは3.9です。
> 注意: Python 3.9 およびそれ以前のバージョンは2025年に完全にサポート終了 (EOL) となっており、3.9以下のバージョンを使用すると、サードパーティ製ライブラリの非互換性の問題がいつでも発生する可能性があります。
* **Python 3.14**を推奨します。現在、CI は Python 3.10 以降のみを対象としています。
> [!NOTE]
> Python 3.9 およびそれ以前のバージョンは公式サポートが終了 (EOL) しており、3.9以下のバージョンを使用すると、サードパーティ製ライブラリの非互換性の問題がいつでも発生する可能性があります。Python 3.9 はインストール互換性を維持しますが、CI の対象外です。

* 個人のプロジェクトであるため、ドキュメントやサンプルコードの更新が遅れることがあります。ご不明な点はIssueにてご質問ください。

Expand Down
7 changes: 4 additions & 3 deletions assets/readme/README-kr.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
## 설치 가이드

> ⚠ Python이 시스템에 설치되어 있지 않다면, 다음 단계를 진행하기 전에 먼저 [Python 공식 사이트](https://www.python.org/downloads/) 에서 다운로드하여 설치해주시기 바랍니다.
> **Python 3.12 이상 버전을 권장합니다**
> **Python 3.14 사용을 권장합니다**

* 공식 pip 저장소를 통한 설치 (추천. 업데이트 명령어도 동일합니다)

Expand Down Expand Up @@ -244,8 +244,9 @@ jmv 350234 -y

## 사용 팁

* **Python 3.12 이상**을 권장하며, 현재 최소 호환 버전은 3.9입니다.
> 참고: Python 3.9 및 이전 버전은 모두 2025년에 공식 지원 종료(EOL)되었으므로 3.9 이하 버전을 사용할 경우 언제든 서드파티 라이브러리 호환성 문제에 부딪힐 수 있습니다.
* **Python 3.14**를 권장하며, 현재 CI는 Python 3.10 이상만 대상으로 합니다.
> [!NOTE]
> Python 3.9 및 이전 버전은 모두 공식 지원이 종료(EOL)되었으므로 3.9 이하 버전을 사용할 경우 언제든 서드파티 라이브러리 호환성 문제에 부딪힐 수 있습니다. Python 3.9는 설치 호환성을 유지하지만 더 이상 CI 대상에는 포함되지 않습니다.

* 여유 시간에 만들어 지는 프로젝트이기에 정보나 활용 코드의 늦은 갱신이 다분합니다. 이슈(Issue)페이지로 연락주시기 바랍니다!

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

__version__ = '2.7.4'
__version__ = '2.7.5'

from .jm_task_context import *
from .api import *
Expand Down
13 changes: 12 additions & 1 deletion src/jmcomic/jm_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ async def _request_with_retry(self,
if not domain_list:
ExceptionTool.raises("无可用 API 域名列表")

retry_errors = []
for domain_index, domain in enumerate(domain_list):
url = self._build_api_url(url_path, domain)

Expand Down Expand Up @@ -289,11 +290,21 @@ async def _request_with_retry(self,
return resp
except Exception as e:
self.before_retry(e, url, retry, domain_index)
retry_errors.append({
'domain': domain,
'url': url,
'retry': retry,
'error': e,
})

# 所有域名都失败
msg = f"请求重试全部失败: [{url_path}], {domain_list}"
jm_log('req.fallback', msg)
ExceptionTool.raises(msg, {}, RequestRetryAllFailException)
ExceptionTool.raises(
msg,
{ExceptionTool.CONTEXT_KEY_RETRY_ERRORS: retry_errors},
RequestRetryAllFailException,
)

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def before_retry(self, e, url, retry, domain_index):
Expand Down
27 changes: 22 additions & 5 deletions src/jmcomic/jm_client_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def request_with_retry(self,
domain_index=0,
retry_count=0,
is_image=False,
_retry_errors=None,
**kwargs,
):
"""
Expand All @@ -83,8 +84,12 @@ def request_with_retry(self,
**kwargs,
)

if _retry_errors is None:
_retry_errors = []

if domain_index >= len(self.domain_list):
return self.fallback(request, url, domain_index, retry_count, is_image, **kwargs)
return self.fallback(request, url, domain_index, retry_count, is_image,
retry_errors=_retry_errors, **kwargs)

url_backup = url

Expand Down Expand Up @@ -120,11 +125,19 @@ def request_with_retry(self,
raise e

self.before_retry(e, kwargs, retry_count, url)
_retry_errors.append({
'domain': self.domain_list[domain_index] if url_backup.startswith('/') else None,
'url': url,
'retry': retry_count,
'error': e,
})

if retry_count < self.retry_times:
return self.request_with_retry(request, url_backup, domain_index, retry_count + 1, is_image, **kwargs)
return self.request_with_retry(request, url_backup, domain_index, retry_count + 1, is_image,
_retry_errors, **kwargs)
else:
return self.request_with_retry(request, url_backup, domain_index + 1, 0, is_image, **kwargs)
return self.request_with_retry(request, url_backup, domain_index + 1, 0, is_image,
_retry_errors, **kwargs)

# noinspection PyMethodMayBeStatic
def raise_if_resp_should_retry(self, resp, is_image):
Expand Down Expand Up @@ -212,10 +225,14 @@ def set_domain_list(self, domain_list: List[str]):
self.domain_list = domain_list

# noinspection PyUnusedLocal
def fallback(self, request, url, domain_index, retry_count, is_image, **kwargs):
def fallback(self, request, url, domain_index, retry_count, is_image, retry_errors=None, **kwargs):
msg = f"请求重试全部失败: [{url}], {self.domain_list}"
jm_log('req.fallback', msg)
ExceptionTool.raises(msg, {}, RequestRetryAllFailException)
ExceptionTool.raises(
msg,
{ExceptionTool.CONTEXT_KEY_RETRY_ERRORS: retry_errors or []},
RequestRetryAllFailException,
)

# noinspection PyMethodMayBeStatic
def append_params_to_url(self, url, params):
Expand Down
2 changes: 1 addition & 1 deletion src/jmcomic/jm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class JmMagicConstants:
APP_TOKEN_SECRET_2 = '18comicAPPContent'
APP_DATA_SECRET = '185Hcomic3PAPP7R'
API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik'
APP_VERSION = '2.0.30'
APP_VERSION = '2.1.2'


# 模块级别共用配置
Expand Down
23 changes: 23 additions & 0 deletions src/jmcomic/jm_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ def error_jmid(self) -> str:
class RequestRetryAllFailException(JmcomicException):
description = '请求重试全部失败异常'

@property
def errors(self) -> list:
"""返回每次请求失败的记录,其中 error 为原始异常对象。"""
return self.context.get(ExceptionTool.CONTEXT_KEY_RETRY_ERRORS, [])

def __str__(self):
if not self.errors:
return super().__str__()

details = []
for index, item in enumerate(self.errors, 1):
error = item['error']
location = item.get('url') or item.get('domain') or '未知地址'
retry = item.get('retry')
retry_text = '' if retry is None else f', retry={retry}'
details.append(
f' {index}. {location}{retry_text}: '
f'{type(error).__name__}: {error}'
)

return f'{super().__str__()}\n失败详情:\n' + '\n'.join(details)


class PartialDownloadFailedException(JmcomicException):
description = '部分章节或图片下载失败异常'
Expand All @@ -84,6 +106,7 @@ class ExceptionTool:
CONTEXT_KEY_RE_PATTERN = 'pattern'
CONTEXT_KEY_MISSING_JM_ID = 'missing_jm_id'
CONTEXT_KEY_DOWNLOADER = 'downloader'
CONTEXT_KEY_RETRY_ERRORS = 'retry_errors'

@classmethod
def raises(cls,
Expand Down
10 changes: 9 additions & 1 deletion src/jmcomic/jm_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,7 @@ def do_request(domain):

retry_domain_max_times: int = self.retry_config['retry_domain_max_times']
retry_rounds: int = self.retry_config['retry_rounds']
retry_errors = []
for rindex in range(retry_rounds):
domain_list = self.get_sorted_domain(client, retry_domain_max_times)
for i, domain in enumerate(domain_list):
Expand All @@ -1795,9 +1796,16 @@ def do_request(domain):
return do_request(domain)
except Exception as e:
jm_log('req.error', e)
retry_errors.append({
'domain': domain,
'url': client.of_api_url(url, domain) if url.startswith('/') else url,
'retry': rindex,
'error': e,
})
self.update_failed_count(client, domain)

return client.fallback(request, url, 0, 0, is_image, **kwargs)
return client.fallback(request, url, 0, 0, is_image,
retry_errors=retry_errors, **kwargs)

def get_sorted_domain(self, client: JmcomicClient, times):
domain_list = client.get_domain_list()
Expand Down
2 changes: 1 addition & 1 deletion src/jmcomic/jm_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ class JmPageTool:
)

# 收藏夹的收藏总数
pattern_html_favorite_total = compile(r' : (\d+)[^/]*/\D*(\d+)')
pattern_html_favorite_total = compile(r'(?:總數|总数)\s*:\s*(\d+)\s*/\s*(\d+)')

# 所有的收藏夹
pattern_html_favorite_folder_list = [
Expand Down
19 changes: 19 additions & 0 deletions tests/test_jmcomic/test_jm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,25 @@ def test_html_forum_comment_id_parsing(self):
self.assertEqual(page[1].user_id, '201')
self.assertEqual(page[1].album_id, '301')

def test_html_favorite_total_uses_labeled_count(self):
for label, expected_total in [('總數', 6), ('总数', 12)]:
with self.subTest(label=label):
html = f'''
<style>
.modal {{ width : 50px; height: 50px; }}
.modal-content {{ inset: 0 / auto 35px; }}
</style>
<div>{label} : {expected_total}\n / 600</div>
<select class="user-select" name="movefolder-fid">
<option value="0">全部</option>
</select>
'''

page = JmPageTool.parse_html_to_favorite_page(html, page_number=1)

self.assertEqual(page.total, expected_total)
self.assertEqual(page.page_number, 1)

def test_get_detail(self):
client = self.client

Expand Down
Loading
Loading