From 879cf04627bf8b045d860ae4fe9134b3d4ed1cd4 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:00:53 +0800 Subject: [PATCH 1/9] =?UTF-8?q?v2.5.35:=20=E5=AE=8C=E5=96=84=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E4=B8=8A=E6=8A=9B=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/__init__.py | 2 +- src/jmcomic/api.py | 5 +++- src/jmcomic/jm_downloader.py | 55 +++++++++++++++++++++++++++--------- src/jmcomic/jm_exception.py | 7 +++-- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 93308a35..df2750cf 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 5f657a90..aed4f120 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 40df64ec..15391f70 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -53,7 +53,31 @@ 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]] = [] + + @staticmethod + def catch_exception(field_name): + def deco(func): + def wrapper(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except Exception as e: + getattr(self, field_name).append(e) + detail: JmBaseEntity = args[1] + 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 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 len(self.download_failed_image) != 0: return False for album, photo_dict in self.download_success_dict.items(): @@ -259,6 +279,15 @@ def after_image(self, image: JmImageDetail, img_save_path): downloader=self, ) + def raise_if_have_exception(self): + if len(self.download_failed_image) == 0 and len(self.download_success_dict) == 0: + return + ExceptionTool.raises( + f'部分下载失败: 有{len(self.download_failed_photo)}个章节下载失败, {len(self.download_failed_image)}个图片下载失败', + {'downloader': self}, + PartialDownloadFailedException, + ) + # 下面是对with语法的支持 def __enter__(self): diff --git a/src/jmcomic/jm_exception.py b/src/jmcomic/jm_exception.py index a6040298..388f183f 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: From 129a866a1c0b3245ca2dc4c083c517d396c83f16 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:16:43 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E4=B8=8A=E6=8A=9B=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_downloader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 15391f70..062d27d1 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -209,7 +209,7 @@ def all_success(self) -> bool: 注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False """ - if len(self.download_failed_image) != 0: + if not self.is_empty_download_failed: return False for album, photo_dict in self.download_success_dict.items(): @@ -222,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): @@ -280,7 +284,7 @@ def after_image(self, image: JmImageDetail, img_save_path): ) def raise_if_have_exception(self): - if len(self.download_failed_image) == 0 and len(self.download_success_dict) == 0: + if self.is_empty_download_failed: return ExceptionTool.raises( f'部分下载失败: 有{len(self.download_failed_photo)}个章节下载失败, {len(self.download_failed_image)}个图片下载失败', From 783bf4cff743f84ca35489e521661da4249c171d Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:18:08 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E4=B8=8A=E6=8A=9B=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_downloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 062d27d1..14582541 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -63,8 +63,8 @@ def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except Exception as e: - getattr(self, field_name).append(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}') From 09471dd3279c2d3679fe7f6b3e45f5ca3217e642 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:20:22 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E4=B8=8A=E6=8A=9B=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_downloader.py | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 14582541..59adbaaf 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: @@ -56,29 +79,6 @@ def __init__(self, option: JmOption) -> None: self.download_failed_image: List[Tuple[JmImageDetail, BaseException]] = [] self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = [] - @staticmethod - 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 - def download_album(self, album_id): client = self.client_for_album(album_id) album = client.get_album_detail(album_id) From b02994d79f5961371bca86c72a612254a1eecdd7 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:07:53 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E4=B8=8A=E6=8A=9B=E6=9C=BA=E5=88=B6=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_downloader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 59adbaaf..07f7d0c4 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -287,7 +287,9 @@ 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)}个图片下载失败', + 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, ) From e99af0c1ccb91d4b7c7115f32f1c9d0704bf5609 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Wed, 9 Apr 2025 00:20:05 +0800 Subject: [PATCH 6/9] =?UTF-8?q?v2.5.35:=20=E4=BC=98=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=B8=8B=E8=BD=BD=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/api.py | 2 +- src/jmcomic/jm_client_impl.py | 2 +- src/jmcomic/jm_downloader.py | 113 ++++++++++++++++------------------ src/jmcomic/jm_entity.py | 24 +++++--- 4 files changed, 68 insertions(+), 73 deletions(-) diff --git a/src/jmcomic/api.py b/src/jmcomic/api.py index aed4f120..e79b1c54 100644 --- a/src/jmcomic/api.py +++ b/src/jmcomic/api.py @@ -72,7 +72,7 @@ def download_album(jm_album_id, if callback is not None: callback(album, dler) if check_exception: - dler.raise_if_have_exception() + dler.raise_if_has_exception() return album, dler diff --git a/src/jmcomic/jm_client_impl.py b/src/jmcomic/jm_client_impl.py index f06bb784..a9ac3463 100644 --- a/src/jmcomic/jm_client_impl.py +++ b/src/jmcomic/jm_client_impl.py @@ -566,7 +566,7 @@ def check_special_text(cls, resp): cls.raise_request_error( resp, - f'{reason}' + f'{reason}({content})' + (f': {url}' if url is not None else '') ) diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 07f7d0c4..450bd2b7 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -1,27 +1,27 @@ 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}') +def catch_exception(func): + def wrapper(self, *args, **kwargs): + self: JmDownloader + try: + return func(self, *args, **kwargs) + except Exception as e: + detail: JmBaseEntity = args[0] + if detail.is_image(): + detail: JmImageDetail + jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: [{e}]') + self.download_failed_image.append((detail, e)) - elif detail.is_photo(): - detail: JmPhotoDetail - jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: {e}') + elif detail.is_photo(): + detail: JmPhotoDetail + jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: [{e}]') + self.download_failed_photo.append((detail, e)) - raise e + raise e - return wrapper - - return deco + wrapper.__name__ = func.__name__ + return wrapper # noinspection PyMethodMayBeStatic @@ -73,6 +73,7 @@ class JmDownloader(DownloadCallback): def __init__(self, option: JmOption) -> None: self.option = option + self.client = option.build_jm_client() # 下载成功的记录dict self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {} # 下载失败的记录list @@ -80,44 +81,42 @@ def __init__(self, option: JmOption) -> None: self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = [] def download_album(self, album_id): - client = self.client_for_album(album_id) - album = client.get_album_detail(album_id) - self.download_by_album_detail(album, client) + album = self.client.get_album_detail(album_id) + self.download_by_album_detail(album) return album - def download_by_album_detail(self, album: JmAlbumDetail, client: JmcomicClient): + def download_by_album_detail(self, album: JmAlbumDetail): self.before_album(album) if album.skip: return - self.execute_by_condition( + self.execute_on_condition( iter_objs=album, - apply=lambda photo: self.download_by_photo_detail(photo, client), + apply=self.download_by_photo_detail, count_batch=self.option.decide_photo_batch_count(album) ) self.after_album(album) def download_photo(self, photo_id): - client = self.client_for_photo(photo_id) - photo = client.get_photo_detail(photo_id) - self.download_by_photo_detail(photo, client) + photo = self.client.get_photo_detail(photo_id) + self.download_by_photo_detail(photo) return photo - @catch_exception('download_failed_photo') - def download_by_photo_detail(self, photo: JmPhotoDetail, client: JmcomicClient): - client.check_photo(photo) + @catch_exception + def download_by_photo_detail(self, photo: JmPhotoDetail): + self.client.check_photo(photo) self.before_photo(photo) if photo.skip: return - self.execute_by_condition( + self.execute_on_condition( iter_objs=photo, - apply=lambda image: self.download_by_image_detail(image, client), + apply=self.download_by_image_detail, count_batch=self.option.decide_image_batch_count(photo) ) self.after_photo(photo) - @catch_exception('download_failed_image') - def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient): + @catch_exception + def download_by_image_detail(self, image: JmImageDetail): img_save_path = self.option.decide_image_filepath(image) image.save_path = img_save_path @@ -136,7 +135,7 @@ def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient): if use_cache is True and image.exists: return - client.download_by_image_detail( + self.client.download_by_image_detail( image, img_save_path, decode_image=decode_image, @@ -144,8 +143,7 @@ def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient): self.after_image(image, img_save_path) - # noinspection PyMethodMayBeStatic - def execute_by_condition(self, + def execute_on_condition(self, iter_objs: DetailEntity, apply: Callable, count_batch: int, @@ -186,20 +184,6 @@ def do_filter(self, detail: DetailEntity): """ return detail - # noinspection PyUnusedLocal - def client_for_album(self, jm_album_id) -> JmcomicClient: - """ - 默认情况下,所有的JmDownloader共用一个JmcomicClient - """ - return self.option.build_jm_client() - - # noinspection PyUnusedLocal - def client_for_photo(self, jm_photo_id) -> JmcomicClient: - """ - 默认情况下,所有的JmDownloader共用一个JmcomicClient - """ - return self.option.build_jm_client() - @property def all_success(self) -> bool: """ @@ -209,7 +193,7 @@ def all_success(self) -> bool: 注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False """ - if not self.is_empty_download_failed: + if not self.has_no_download_failed_exception: return False for album, photo_dict in self.download_success_dict.items(): @@ -223,7 +207,7 @@ def all_success(self) -> bool: return True @property - def is_empty_download_failed(self): + def has_no_download_failed_exception(self): return len(self.download_failed_image) == 0 and len(self.download_failed_photo) == 0 # 下面是回调方法 @@ -283,13 +267,19 @@ def after_image(self, image: JmImageDetail, img_save_path): downloader=self, ) - def raise_if_have_exception(self): - if self.is_empty_download_failed: + def raise_if_has_exception(self): + if self.has_no_download_failed_exception: return + msg_ls = ['部分下载失败', '', ''] + + if len(self.download_failed_photo) != 0: + msg_ls[1] = f'共{len(self.download_failed_photo)}个章节下载失败: {self.download_failed_photo}' + + if len(self.download_failed_image) != 0: + msg_ls[2] = f'共{len(self.download_failed_image)}个图片下载失败: {self.download_failed_image}' + 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 ""}', + '\n'.join(msg_ls), {'downloader': self}, PartialDownloadFailedException, ) @@ -318,7 +308,7 @@ class DoNotDownloadImage(JmDownloader): 不会下载任何图片的Downloader,用作测试 """ - def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient): + def download_by_image_detail(self, image: JmImageDetail): # ensure make dir self.option.decide_image_filepath(image) @@ -332,12 +322,13 @@ class JustDownloadSpecificCountImage(JmDownloader): count_lock = Lock() count = 0 - def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient): + @catch_exception + def download_by_image_detail(self, image: JmImageDetail): # ensure make dir self.option.decide_image_filepath(image) if self.try_countdown(): - return super().download_by_image_detail(image, client) + return super().download_by_image_detail(image) def try_countdown(self): if self.count < 0: diff --git a/src/jmcomic/jm_entity.py b/src/jmcomic/jm_entity.py index 16d99d7e..3e8ad823 100644 --- a/src/jmcomic/jm_entity.py +++ b/src/jmcomic/jm_entity.py @@ -35,6 +35,13 @@ def is_album(cls): def is_page(cls): return False + @classmethod + def __alias__(cls): + # "JmAlbumDetail" -> "album" (本子) + # "JmPhotoDetail" -> "photo" (章节) + cls_name = cls.__name__ + return cls_name[cls_name.index("m") + 1: cls_name.rfind("Detail")].lower() + class IndexedEntity: def getindex(self, index: int): @@ -125,17 +132,9 @@ def idoname(self): return f'[{self.id}] {self.oname}' def __str__(self): - return f'{self.__class__.__name__}' \ - '{' \ - f'{self.id}: {self.title}' \ - '}' + return f'''{self.__class__.__name__}({self.__alias__()}-{self.id}: "{self.title}")''' - @classmethod - def __alias__(cls): - # "JmAlbumDetail" -> "album" (本子) - # "JmPhotoDetail" -> "photo" (章节) - cls_name = cls.__name__ - return cls_name[cls_name.index("m") + 1: cls_name.rfind("Detail")].lower() + __repr__ = __str__ @classmethod def get_dirname(cls, detail: 'DetailEntity', ref: str) -> str: @@ -258,6 +257,11 @@ def tag(self) -> str: def is_image(cls): return True + def __str__(self): + return f'''{self.__class__.__name__}({self.__alias__()}-[{self.download_url}])''' + + __repr__ = __str__ + class JmPhotoDetail(DetailEntity, Downloadable): From 58cf648ddfef52f1b3cc5fc22b7e829b40dcbcaf Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Wed, 9 Apr 2025 00:52:20 +0800 Subject: [PATCH 7/9] =?UTF-8?q?v2.5.35:=20=E4=BC=98=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=B8=8B=E8=BD=BD=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/sources/tutorial/0_common_usage.md | 21 +++++++++++++++---- src/jmcomic/api.py | 7 +++++-- src/jmcomic/jm_downloader.py | 8 +++---- src/jmcomic/jm_entity.py | 16 +++++++------- src/jmcomic/jm_exception.py | 4 ++++ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/assets/docs/sources/tutorial/0_common_usage.md b/assets/docs/sources/tutorial/0_common_usage.md index 614dd3cb..7ca8472e 100644 --- a/assets/docs/sources/tutorial/0_common_usage.md +++ b/assets/docs/sources/tutorial/0_common_usage.md @@ -82,25 +82,38 @@ from jmcomic import * # 客户端 client = JmOption.default().new_jm_client() -# 捕获jmcomic可能出现的异常 +# 捕获获取本子/章节详情时可能出现的异常 try: # 请求本子实体类 album: JmAlbumDetail = client.get_album_detail('427413') except MissingAlbumPhotoException as e: print(f'id={e.error_jmid}的本子不存在') - + except JsonResolveFailException as e: print(f'解析json失败') # 响应对象 resp = e.resp print(f'resp.text: {resp.text}, resp.status_code: {resp.status_code}') - + except RequestRetryAllFailException as e: print(f'请求失败,重试次数耗尽') - + except JmcomicException as e: # 捕获所有异常,用作兜底 print(f'jmcomic遇到异常: {e}') + +# 多线程下载时,可能出现非当前线程下载失败,抛出异常, +# 而JmDownloader有对应字段记录了这些线程发生的异常 +# 使用check_exception=True参数可以使downloader主动检查是否存在下载异常 +# 如果有,则当前线程会主动上抛一个PartialDownloadFailedException异常 +# 该参数主要用于主动检查部分下载失败的情况, +# 因为非当前线程抛出的异常(比如下载章节的线程和下载图片的线程),这些线程如果抛出异常, +# 当前线程是感知不到的,try-catch下载方法download_album不能捕获到其他线程发生的异常。 +try: + album, downloader = download_album(123, check_exception=True) +except PartialDownloadFailedException as e: + downloader: JmDownloader = e.downloader + print(f'下载出现部分失败, 下载失败的章节: {downloader.download_failed_photo}, 下载失败的图片: {downloader.download_failed_image}') ``` diff --git a/src/jmcomic/api.py b/src/jmcomic/api.py index e79b1c54..47141450 100644 --- a/src/jmcomic/api.py +++ b/src/jmcomic/api.py @@ -79,7 +79,9 @@ def download_album(jm_album_id, def download_photo(jm_photo_id, option=None, downloader=None, - callback=None): + callback=None, + check_exception=True, + ): """ 下载一个章节(photo),参数同 download_album """ @@ -91,7 +93,8 @@ def download_photo(jm_photo_id, if callback is not None: callback(photo, dler) - + if check_exception: + dler.raise_if_has_exception() return photo, dler diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index 450bd2b7..f819ecf4 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -193,7 +193,7 @@ def all_success(self) -> bool: 注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False """ - if not self.has_no_download_failed_exception: + if self.has_download_failures: return False for album, photo_dict in self.download_success_dict.items(): @@ -207,8 +207,8 @@ def all_success(self) -> bool: return True @property - def has_no_download_failed_exception(self): - return len(self.download_failed_image) == 0 and len(self.download_failed_photo) == 0 + def has_download_failures(self): + return len(self.download_failed_image) != 0 or len(self.download_failed_photo) != 0 # 下面是回调方法 @@ -268,7 +268,7 @@ def after_image(self, image: JmImageDetail, img_save_path): ) def raise_if_has_exception(self): - if self.has_no_download_failed_exception: + if not self.has_download_failures: return msg_ls = ['部分下载失败', '', ''] diff --git a/src/jmcomic/jm_entity.py b/src/jmcomic/jm_entity.py index 3e8ad823..4d799b84 100644 --- a/src/jmcomic/jm_entity.py +++ b/src/jmcomic/jm_entity.py @@ -35,13 +35,6 @@ def is_album(cls): def is_page(cls): return False - @classmethod - def __alias__(cls): - # "JmAlbumDetail" -> "album" (本子) - # "JmPhotoDetail" -> "photo" (章节) - cls_name = cls.__name__ - return cls_name[cls_name.index("m") + 1: cls_name.rfind("Detail")].lower() - class IndexedEntity: def getindex(self, index: int): @@ -136,6 +129,13 @@ def __str__(self): __repr__ = __str__ + @classmethod + def __alias__(cls): + # "JmAlbumDetail" -> "album" (本子) + # "JmPhotoDetail" -> "photo" (章节) + cls_name = cls.__name__ + return cls_name[cls_name.index("m") + 1: cls_name.rfind("Detail")].lower() + @classmethod def get_dirname(cls, detail: 'DetailEntity', ref: str) -> str: """ @@ -258,7 +258,7 @@ def is_image(cls): return True def __str__(self): - return f'''{self.__class__.__name__}({self.__alias__()}-[{self.download_url}])''' + return f'''{self.__class__.__name__}(image-[{self.download_url}])''' __repr__ = __str__ diff --git a/src/jmcomic/jm_exception.py b/src/jmcomic/jm_exception.py index 388f183f..8305277b 100644 --- a/src/jmcomic/jm_exception.py +++ b/src/jmcomic/jm_exception.py @@ -62,6 +62,9 @@ class RequestRetryAllFailException(JmcomicException): class PartialDownloadFailedException(JmcomicException): description = '部分章节或图片下载失败异常' + @property + def downloader(self): + return self.from_context(ExceptionTool.CONTEXT_KEY_DOWNLOADER) class ExceptionTool: """ @@ -74,6 +77,7 @@ class ExceptionTool: CONTEXT_KEY_HTML = 'html' CONTEXT_KEY_RE_PATTERN = 'pattern' CONTEXT_KEY_MISSING_JM_ID = 'missing_jm_id' + CONTEXT_KEY_DOWNLOADER = 'downloader' @classmethod def raises(cls, From 9bc9a00cb9f382b950bb4ccc0dc130eb72e8543f Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:02:55 +0800 Subject: [PATCH 8/9] =?UTF-8?q?v2.5.35:=20=E4=BC=98=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=B8=8B=E8=BD=BD=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_downloader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jmcomic/jm_downloader.py b/src/jmcomic/jm_downloader.py index f819ecf4..1518454a 100644 --- a/src/jmcomic/jm_downloader.py +++ b/src/jmcomic/jm_downloader.py @@ -2,6 +2,9 @@ def catch_exception(func): + from functools import wraps + + @wraps(func) def wrapper(self, *args, **kwargs): self: JmDownloader try: @@ -20,7 +23,6 @@ def wrapper(self, *args, **kwargs): raise e - wrapper.__name__ = func.__name__ return wrapper From 99dde9a4446e910ee538031c89cb76d5bce2e8be Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:15:35 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E6=B5=8B=E8=AF=95case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_jmcomic/test_jm_api.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_jmcomic/test_jm_api.py b/tests/test_jmcomic/test_jm_api.py index 226d5aad..ef401185 100644 --- a/tests/test_jmcomic/test_jm_api.py +++ b/tests/test_jmcomic/test_jm_api.py @@ -76,3 +76,24 @@ def run_func_async(func): print(e) raise AssertionError(exception_list) + + def test_partial_exception(self): + class TestDownloader(JmDownloader): + @catch_exception + def download_by_image_detail(self, image: JmImageDetail): + raise Exception('test_partial_exception') + + @catch_exception + def download_by_photo_detail(self, photo: JmPhotoDetail): + if photo.index != 2: + raise Exception('test_partial_exception') + return super().download_by_photo_detail(photo) + + self.assertRaises( + PartialDownloadFailedException, + lambda: download_album(182150, downloader=TestDownloader, check_exception=True) + ) + self.assertRaises( + PartialDownloadFailedException, + lambda: download_photo(182151, downloader=TestDownloader, check_exception=True) + )