Skip to content
Merged

Dev #508

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 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.6.10'
__version__ = '2.6.11'

from .api import *
from .jm_plugin import *
Expand Down
92 changes: 30 additions & 62 deletions src/jmcomic/jm_client_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,55 +765,17 @@ def fetch_photo_additional_field(self, photo: JmPhotoDetail, fetch_album: bool,

def setting(self) -> JmApiResp:
"""
禁漫app的setting请求,返回如下内容(resp.res_data)
{
"logo_path": "https://cdn-msp.jmapiproxy1.monster/media/logo/new_logo.png",
"main_web_host": "18-comic.work",
"img_host": "https://cdn-msp.jmapiproxy1.monster",
"base_url": "https://www.jmapinode.biz",
"is_cn": 0,
"cn_base_url": "https://www.jmapinode.biz",
"version": "1.6.0",
"test_version": "1.6.1",
"store_link": "https://play.google.com/store/apps/details?id=com.jiaohua_browser",
"ios_version": "1.6.0",
"ios_test_version": "1.6.1",
"ios_store_link": "https://18comic.vip/stray/",
"ad_cache_version": 1698140798,
"bundle_url": "https://18-comic.work/static/apk/patches1.6.0.zip",
"is_hot_update": true,
"api_banner_path": "https://cdn-msp.jmapiproxy1.monster/media/logo/channel_log.png?v=",
"version_info": "\nAPP & IOS更新\nV1.6.0\n#禁漫 APK 更新拉!!\n更新調整以下項目\n1. 系統優化\n\nV1.5.9\n1. 跳錯誤新增 重試 網頁 按鈕\n2. 圖片讀取優化\n3.
線路調整優化\n\n無法順利更新或是系統題是有風險請使用下方\n下載點2\n有問題可以到DC群反饋\nhttps://discord.gg/V74p7HM\n",
"app_shunts": [
{
"title": "圖源1",
"key": 1
},
{
"title": "圖源2",
"key": 2
},
{
"title": "圖源3",
"key": 3
},
{
"title": "圖源4",
"key": 4
}
],
"download_url": "https://18-comic.work/static/apk/1.6.0.apk",
"app_landing_page": "https://jm365.work/pXYbfA",
"float_ad": true
}
禁漫app的setting请求
"""
resp = self.req_api('/setting')

# 检查禁漫最新的版本号
setting_ver = str(resp.model_data.version)
setting_ver = str(resp.model_data.jm3_version)
# 禁漫接口的版本 > jmcomic库内置版本
if setting_ver > JmMagicConstants.APP_VERSION and JmModuleConfig.FLAG_USE_VERSION_NEWER_IF_BEHIND:
if (
JmModuleConfig.FLAG_USE_VERSION_NEWER_IF_BEHIND
and JmcomicText.compare_versions(setting_ver, JmMagicConstants.APP_VERSION) == 1
):
jm_log('api.setting', f'change APP_VERSION from [{JmMagicConstants.APP_VERSION}] to [{setting_ver}]')
JmMagicConstants.APP_VERSION = setting_ver
Comment on lines +773 to 780
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify jm3_version field usage and version comparison logic
rg -n "jm3_version|\"version\"" --type=py -C 3

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 538


🏁 Script executed:

#!/bin/bash
# Find and examine the compare_versions function implementation
rg -n "def compare_versions" --type=py -A 15

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 1020


🏁 Script executed:

#!/bin/bash
# Check for any existing error handling around version comparisons
rg -n "try.*version|except.*version" --type=py -B 2 -A 5

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Check the implementation details and see if there's any validation
# Let's see more context around compare_versions
rg -n "def compare_versions" --type=py -A 20

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 1313


🏁 Script executed:

#!/bin/bash
# Search for the model/API response definition to see how jm3_version is handled
rg -n "jm3_version|setting.*resp|api.*response" --type=py | head -20

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 358


🏁 Script executed:

#!/bin/bash
# Check if there's any try-catch around the API call itself
rg -n "req_api|resp\s*=.*setting" --type=py -B 2 -A 10 | grep -A 15 "/setting"

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 1189


🏁 Script executed:

#!/bin/bash
# Check what APP_VERSION looks like and how it's defined
rg -n "APP_VERSION\s*=" --type=py -B 2 -A 2 | head -30

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 715


🏁 Script executed:

#!/bin/bash
# Look for the API response model definition (likely in a models or api file)
fd -e py | xargs grep -l "jm3_version\|model_data" | head -5

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Check the context around the req_api call and any error handling in that method
rg -n "def req_api" --type=py -A 20 | head -40

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 2749


🏁 Script executed:

#!/bin/bash
# Find JmApiResp class definition and model_data property
rg -n "class JmApiResp|model_data" --type=py -B 2 -A 10 | head -60

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 3887


🏁 Script executed:

#!/bin/bash
# Check if there's any data validation or error handling at the calling site
sed -n '765,785p' src/jmcomic/jm_client_impl.py

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 728


🏁 Script executed:

#!/bin/bash
# Look for any usage of compare_versions to see if error handling is done elsewhere
rg -n "compare_versions" --type=py -B 3 -A 3

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 1130


Add error handling for version comparison failures.

The compare_versions() call at line 777 lacks defensive error handling. If the API response contains a malformed jm3_version field (non-numeric segments), compare_versions() will raise a ValueError when attempting to parse version parts. Wrap the comparison in a try-catch block to handle version comparison failures gracefully and prevent crashes on unexpected API data.

🧰 Tools
🪛 Ruff (0.14.10)

776-776: JmModuleConfig may be undefined, or defined from star imports

(F405)


777-777: JmcomicText may be undefined, or defined from star imports

(F405)


777-777: JmMagicConstants may be undefined, or defined from star imports

(F405)


779-779: jm_log may be undefined, or defined from star imports

(F405)


779-779: JmMagicConstants may be undefined, or defined from star imports

(F405)


780-780: JmMagicConstants may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
In @src/jmcomic/jm_client_impl.py around lines 773 - 780, Wrap the call to
JmcomicText.compare_versions that compares setting_ver (from
resp.model_data.jm3_version) and JmMagicConstants.APP_VERSION in a try/except to
catch parsing errors (e.g., ValueError); on exception log the failure with
jm_log('api.setting', ...) including the bad setting_ver and exception details,
and skip changing JmMagicConstants.APP_VERSION; keep the existing branch gated
by JmModuleConfig.FLAG_USE_VERSION_NEWER_IF_BEHIND and only perform the
assignment when compare_versions returns 1 without error.


Expand Down Expand Up @@ -959,10 +921,7 @@ def require_resp_success(cls, resp: JmApiResp, url: Optional[str] = None):

# 1. 检查是否 album_missing
# json: {'code': 200, 'data': []}
data = resp.model().data
if isinstance(data, list) and len(data) == 0:
ExceptionTool.raise_missing(resp, JmcomicText.parse_to_jm_id(url))

# 最新api已不存在这种情况,无需检查
# 2. 是否是特殊的内容
# 暂无

Expand Down Expand Up @@ -1007,7 +966,8 @@ def raise_if_resp_should_retry(self, resp, is_image):
def after_init(self):
# 自动更新禁漫API域名
if JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN:
self.update_api_domain()
new_server_list = self.fetch_latest_api_domain_for_module()
self.update_old_api_domain(new_server_list)

# 保证拥有cookies,因为移动端要求必须携带cookies,否则会直接跳转同一本子【禁漫娘】
if JmModuleConfig.FLAG_API_CLIENT_REQUIRE_COOKIES:
Expand All @@ -1032,13 +992,19 @@ def req_api_domain_server(self, url):
else:
return res_data['Server']

def update_api_domain(self):
if True is JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE:
return
def update_old_api_domain(self, new_server_list: List[str]):
if new_server_list and sorted(self.domain_list) == sorted(JmModuleConfig.DOMAIN_API_LIST):
self.domain_list = new_server_list

def fetch_latest_api_domain_for_module(self):
if JmModuleConfig.DOMAIN_API_UPDATED_LIST is not None:
return JmModuleConfig.DOMAIN_API_UPDATED_LIST

with self.client_update_domain_lock:
if True is JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE:
return
# double check
if JmModuleConfig.DOMAIN_API_UPDATED_LIST is not None:
return JmModuleConfig.DOMAIN_API_UPDATED_LIST

# 遍历多个域名服务器
for url in JmModuleConfig.API_URL_DOMAIN_SERVER_LIST:
try:
Expand All @@ -1050,18 +1016,20 @@ def update_api_domain(self):
jm_log('api.update_domain.success',
f'获取到最新的API域名,替换jmcomic内置域名:(new){new_server_list} ---→ (old){old_server_list}'
)
# 更新域名
if sorted(self.domain_list) == sorted(old_server_list):
self.domain_list = new_server_list
JmModuleConfig.DOMAIN_API_LIST = new_server_list
break
JmModuleConfig.DOMAIN_API_UPDATED_LIST = new_server_list
return new_server_list
except Exception as e:
jm_log('api.update_domain.error',
f'通过[{url}]自动更新API域名失败,仍使用jmcomic内置域名。'
f'通过[{url}]自动更新API域名失败,尝试下一个地址。'
f'可通过代码[JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN=False]关闭自动更新API域名. 异常: {e}'
)
# set done finally
JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = True
continue

# 走到这里,说明没有获取到域名更新
# 为了本方法不被重复执行,把新域名字段修改为空列表
# 空列表相当于一个done标识
JmModuleConfig.DOMAIN_API_UPDATED_LIST = []
return JmModuleConfig.DOMAIN_API_UPDATED_LIST

client_init_cookies_lock = Lock()

Expand Down
8 changes: 7 additions & 1 deletion src/jmcomic/jm_client_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,24 @@ def decoded_data(self) -> str:
def encoded_data(self) -> str:
return self.json()['data']

def require_have_data(self):
data = self.encoded_data
if isinstance(data, list) and len(data) == 0 and self.json().get('errorMsg', None):
ExceptionTool.raises_resp(f'data返回值异常: {self.text}', self)

@property
def res_data(self) -> Any:
self.require_success()
self.require_have_data()
from json import loads
return loads(self.decoded_data)

@property
def model_data(self) -> AdvancedDict:
self.require_success()
self.require_have_data()
return AdvancedDict(self.res_data)


# album-comment
class JmAlbumCommentResp(JmJsonResp):

Expand Down
5 changes: 3 additions & 2 deletions src/jmcomic/jm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class JmMagicConstants:
APP_TOKEN_SECRET_2 = '18comicAPPContent'
APP_DATA_SECRET = '185Hcomic3PAPP7R'
API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik'
APP_VERSION = '2.0.6'
APP_VERSION = '2.0.13'


# 模块级别共用配置
Expand Down Expand Up @@ -134,6 +134,8 @@ class JmModuleConfig:
www.cdnplaystation6.cc
''')

DOMAIN_API_UPDATED_LIST = None

# 获取最新移动端API域名的地址
API_URL_DOMAIN_SERVER_LIST = shuffled('''
https://rup4a04-c01.tos-ap-southeast-1.bytepluses.com/newsvr-2025.txt
Expand Down Expand Up @@ -209,7 +211,6 @@ class JmModuleConfig:
FLAG_API_CLIENT_REQUIRE_COOKIES = True
# 自动更新禁漫API域名
FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN = True
FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = None
# log开关标记
FLAG_ENABLE_JM_LOG = True
# log时解码url
Expand Down
4 changes: 2 additions & 2 deletions src/jmcomic/jm_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ def parse_bd_rule(self, album, photo, rule):
@classmethod
def parse_f_string_rule(cls, album, photo, rule: str):
properties = {}
if album:
if album is not None:
properties.update(album.get_properties_dict())
if photo:
if photo is not None:
properties.update(photo.get_properties_dict())
return rule.format(**properties)

Expand Down
16 changes: 16 additions & 0 deletions src/jmcomic/jm_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,22 @@ def get_album_cover_url(cls,
path = f'/media/albums/{cls.parse_to_jm_id(album_id)}{size}.jpg'
return cls.format_url(path, image_domain)

@classmethod
def compare_versions(cls, v1: str, v2: str) -> int:
parts1 = list(map(int, v1.split(".")))
parts2 = list(map(int, v2.split(".")))

# 补齐长度
length = max(len(parts1), len(parts2))
parts1 += [0] * (length - len(parts1))
parts2 += [0] * (length - len(parts2))

if parts1 > parts2:
return 1 # v1 大
elif parts1 < parts2:
return -1 # v2 大
else:
return 0 # 相等
Comment on lines +417 to +432
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for invalid version strings.

The compare_versions method lacks input validation and error handling. If version strings contain non-numeric parts (e.g., "1.0.0-alpha") or are malformed, int() conversion will raise a ValueError, potentially crashing the application.

Consider adding defensive error handling:

🛡️ Proposed fix with error handling
 @classmethod
 def compare_versions(cls, v1: str, v2: str) -> int:
+    try:
         parts1 = list(map(int, v1.split(".")))
         parts2 = list(map(int, v2.split(".")))
+    except (ValueError, AttributeError) as e:
+        jm_log('version.compare.error', f'Invalid version format: v1={v1}, v2={v2}, error={e}')
+        return 0  # Treat as equal if comparison fails
 
     # 补齐长度
     length = max(len(parts1), len(parts2))
     parts1 += [0] * (length - len(parts1))
     parts2 += [0] * (length - len(parts2))
 
     if parts1 > parts2:
         return 1  # v1 大
     elif parts1 < parts2:
         return -1  # v2 大
     else:
         return 0  # 相等
🤖 Prompt for AI Agents
In @src/jmcomic/jm_toolkit.py around lines 417 - 432, The compare_versions
method currently converts dot-separated parts to int without validation, which
will raise on malformed parts; update compare_versions to validate and normalize
each segment before int conversion: for v1 and v2 split on '.', for each segment
extract the leading numeric portion (e.g., via a regex like r'^\d+') and convert
that to int (treat missing numeric portion as 0 or raise a controlled
ValueError), wrap the conversion in try/except to raise a clear error message
that includes the original inputs when conversion fails, and continue using
parts1/parts2 after padding as before for the comparison logic.


# 支持dsl: #{???} -> os.getenv(???)
JmcomicText.dsl_replacer.add_dsl_and_replacer(r'\$\{(.*?)\}', JmcomicText.match_os_env)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_jmcomic/test_jm_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class MyClient(JmApiClient):
JmModuleConfig.register_client(MyClient)

self.assertListEqual(
JmModuleConfig.DOMAIN_API_LIST,
self.option.new_jm_client(domain_list=[], impl=MyClient.client_key).get_domain_list()
self.option.new_jm_client(domain_list=[], impl=MyClient.client_key).get_domain_list(),
JmModuleConfig.DOMAIN_API_UPDATED_LIST if JmModuleConfig.DOMAIN_API_UPDATED_LIST else JmModuleConfig.DOMAIN_API_LIST,
)

def test_extends_html_client(self):
Expand Down Expand Up @@ -104,7 +104,7 @@ class MyClient(JmApiClient):

JmModuleConfig.register_client(MyClient)
self.assertListEqual(
JmModuleConfig.DOMAIN_API_LIST,
self.option.new_jm_client(domain_list=[], impl=MyClient.client_key).get_domain_list(),
JmModuleConfig.DOMAIN_API_UPDATED_LIST if JmModuleConfig.DOMAIN_API_UPDATED_LIST else JmModuleConfig.DOMAIN_API_LIST,
msg='继承client,不配置域名',
)