diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 93308a353..df2750cf8 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -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 * diff --git a/src/jmcomic/api.py b/src/jmcomic/api.py index 5f657a90b..aed4f120e 100644 --- a/src/jmcomic/api.py +++ b/src/jmcomic/api.py @@ -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) @@ -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的返回值) """ @@ -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 diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 40df64ece..07f7d0c41 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -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 + + # noinspection PyMethodMayBeStatic class DownloadCallback: @@ -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) @@ -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) @@ -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) @@ -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) @@ -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(): @@ -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): @@ -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): diff --git a/src/jmcomic/jm_exception.py b/src/jmcomic/jm_exception.py index a60402988..388f183fa 100644 --- a/src/jmcomic/jm_exception.py +++ b/src/jmcomic/jm_exception.py @@ -15,6 +15,7 @@ def from_context(self, key): def __str__(self): return self.msg + class ResponseUnexpectedException(JmcomicException): description = '响应不符合预期异常' @@ -44,7 +45,6 @@ def pattern(self): class JsonResolveFailException(ResponseUnexpectedException): description = 'Json解析异常' - pass class MissingAlbumPhotoException(ResponseUnexpectedException): @@ -57,7 +57,10 @@ def error_jmid(self) -> str: class RequestRetryAllFailException(JmcomicException): description = '请求重试全部失败异常' - pass + + +class PartialDownloadFailedException(JmcomicException): + description = '部分章节或图片下载失败异常' class ExceptionTool: