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
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.5.34'
__version__ = '2.5.35'

from .api import *
from .jm_plugin import *
Expand Down
5 changes: 4 additions & 1 deletion src/jmcomic/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def download_album(jm_album_id,
option=None,
downloader=None,
callback=None,
check_exception=True,
) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
"""
下载一个本子(album),包含其所有的章节(photo)
Expand All @@ -58,6 +59,7 @@ def download_album(jm_album_id,
:param option: 下载选项
:param downloader: 下载器类
:param callback: 返回值回调函数,可以拿到 album 和 downloader
:param check_exception: 是否检查异常, 如果为True,会检查downloader是否有下载异常,并上抛PartialDownloadFailedException
:return: 对于的本子实体类,下载器(如果是上述的批量情况,返回值为download_batch的返回值)
"""

Expand All @@ -69,7 +71,8 @@ def download_album(jm_album_id,

if callback is not None:
callback(album, dler)

if check_exception:
dler.raise_if_have_exception()
return album, dler


Expand Down
61 changes: 48 additions & 13 deletions src/jmcomic/jm_downloader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
from .jm_option import *


def catch_exception(field_name):
def deco(func):
def wrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except Exception as e:
detail: JmBaseEntity = args[1]
getattr(self, field_name).append((detail, e))
if detail.is_image():
detail: JmImageDetail
jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: {e}')

elif detail.is_photo():
detail: JmPhotoDetail
jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: {e}')

raise e

return wrapper

return deco
Comment on lines +4 to +24
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Centralized exception handling with decorator pattern is a good improvement

The new catch_exception decorator effectively centralizes the exception handling logic, making the code more maintainable and consistent. It properly captures both the detail object and the exception, and logs appropriate messages based on the object type.

However, there's a potential thread safety issue when multiple threads append to the shared lists concurrently.

Consider adding thread safety protection:

def catch_exception(field_name):
    def deco(func):
        def wrapper(self, *args, **kwargs):
            try:
                return func(self, *args, **kwargs)
            except Exception as e:
                detail: JmBaseEntity = args[1]
+               # Use thread-safe approach for appending to shared list
+               with getattr(self, '_lock', threading.RLock()):
+                   if not hasattr(self, '_lock'):
+                       setattr(self, '_lock', threading.RLock())
                getattr(self, field_name).append((detail, e))
                if detail.is_image():
                    detail: JmImageDetail
                    jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: {e}')

                elif detail.is_photo():
                    detail: JmPhotoDetail
                    jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: {e}')

                raise e

        return wrapper

    return deco

Don't forget to import threading at the top of the file:

import threading
🧰 Tools
🪛 Ruff (0.8.2)

10-10: JmBaseEntity may be undefined, or defined from star imports

(F405)


13-13: JmImageDetail may be undefined, or defined from star imports

(F405)


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

(F405)


17-17: JmPhotoDetail may be undefined, or defined from star imports

(F405)


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

(F405)



# noinspection PyMethodMayBeStatic
class DownloadCallback:

Expand Down Expand Up @@ -53,7 +76,8 @@ def __init__(self, option: JmOption) -> None:
# 下载成功的记录dict
self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
# 下载失败的记录list
self.download_failed_list: List[Tuple[JmImageDetail, BaseException]] = []
self.download_failed_image: List[Tuple[JmImageDetail, BaseException]] = []
self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = []

def download_album(self, album_id):
client = self.client_for_album(album_id)
Expand All @@ -78,6 +102,7 @@ def download_photo(self, photo_id):
self.download_by_photo_detail(photo, client)
return photo

@catch_exception('download_failed_photo')
def download_by_photo_detail(self, photo: JmPhotoDetail, client: JmcomicClient):
client.check_photo(photo)

Expand All @@ -91,6 +116,7 @@ def download_by_photo_detail(self, photo: JmPhotoDetail, client: JmcomicClient):
)
self.after_photo(photo)

@catch_exception('download_failed_image')
def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
img_save_path = self.option.decide_image_filepath(image)

Expand All @@ -110,17 +136,11 @@ def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
if use_cache is True and image.exists:
return

try:
client.download_by_image_detail(
image,
img_save_path,
decode_image=decode_image,
)
except BaseException as e:
jm_log('image.failed', f'图片下载失败: [{image.download_url}], 异常: {e}')
# 保存失败记录
self.download_failed_list.append((image, e))
raise
client.download_by_image_detail(
image,
img_save_path,
decode_image=decode_image,
)

self.after_image(image, img_save_path)

Expand Down Expand Up @@ -189,7 +209,7 @@ def all_success(self) -> bool:

注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False
"""
if len(self.download_failed_list) != 0:
if not self.is_empty_download_failed:
return False

for album, photo_dict in self.download_success_dict.items():
Expand All @@ -202,6 +222,10 @@ def all_success(self) -> bool:

return True

@property
def is_empty_download_failed(self):
return len(self.download_failed_image) == 0 and len(self.download_failed_photo) == 0

# 下面是回调方法

def before_album(self, album: JmAlbumDetail):
Expand Down Expand Up @@ -259,6 +283,17 @@ def after_image(self, image: JmImageDetail, img_save_path):
downloader=self,
)

def raise_if_have_exception(self):
if self.is_empty_download_failed:
return
ExceptionTool.raises(
f'部分下载失败: 有{len(self.download_failed_photo)}个章节下载失败, {len(self.download_failed_image)}个图片下载失败。\n' +
f'失败章节IDs: {[photo.id for photo, _ in self.download_failed_photo][:5]}{"..." if len(self.download_failed_photo) > 5 else ""}\n' +
f'失败图片URLs: {[image.download_url for image, _ in self.download_failed_image][:5]}{"..." if len(self.download_failed_image) > 5 else ""}',
{'downloader': self},
PartialDownloadFailedException,
)

# 下面是对with语法的支持

def __enter__(self):
Expand Down
7 changes: 5 additions & 2 deletions src/jmcomic/jm_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def from_context(self, key):
def __str__(self):
return self.msg


class ResponseUnexpectedException(JmcomicException):
description = '响应不符合预期异常'

Expand Down Expand Up @@ -44,7 +45,6 @@ def pattern(self):

class JsonResolveFailException(ResponseUnexpectedException):
description = 'Json解析异常'
pass


class MissingAlbumPhotoException(ResponseUnexpectedException):
Expand All @@ -57,7 +57,10 @@ def error_jmid(self) -> str:

class RequestRetryAllFailException(JmcomicException):
description = '请求重试全部失败异常'
pass


class PartialDownloadFailedException(JmcomicException):
description = '部分章节或图片下载失败异常'


class ExceptionTool:
Expand Down