Skip to content

Commit ebf0ea6

Browse files
authored
v2.5.35: 优化部分下载失败时异常处理机制,更新文档 (#412)
1 parent e4584d0 commit ebf0ea6

File tree

8 files changed

+140
-61
lines changed

8 files changed

+140
-61
lines changed

assets/docs/sources/tutorial/0_common_usage.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,38 @@ from jmcomic import *
8282
# 客户端
8383
client = JmOption.default().new_jm_client()
8484

85-
# 捕获jmcomic可能出现的异常
85+
# 捕获获取本子/章节详情时可能出现的异常
8686
try:
8787
# 请求本子实体类
8888
album: JmAlbumDetail = client.get_album_detail('427413')
8989
except MissingAlbumPhotoException as e:
9090
print(f'id={e.error_jmid}的本子不存在')
91-
91+
9292
except JsonResolveFailException as e:
9393
print(f'解析json失败')
9494
# 响应对象
9595
resp = e.resp
9696
print(f'resp.text: {resp.text}, resp.status_code: {resp.status_code}')
97-
97+
9898
except RequestRetryAllFailException as e:
9999
print(f'请求失败,重试次数耗尽')
100-
100+
101101
except JmcomicException as e:
102102
# 捕获所有异常,用作兜底
103103
print(f'jmcomic遇到异常: {e}')
104+
105+
# 多线程下载时,可能出现非当前线程下载失败,抛出异常,
106+
# 而JmDownloader有对应字段记录了这些线程发生的异常
107+
# 使用check_exception=True参数可以使downloader主动检查是否存在下载异常
108+
# 如果有,则当前线程会主动上抛一个PartialDownloadFailedException异常
109+
# 该参数主要用于主动检查部分下载失败的情况,
110+
# 因为非当前线程抛出的异常(比如下载章节的线程和下载图片的线程),这些线程如果抛出异常,
111+
# 当前线程是感知不到的,try-catch下载方法download_album不能捕获到其他线程发生的异常。
112+
try:
113+
album, downloader = download_album(123, check_exception=True)
114+
except PartialDownloadFailedException as e:
115+
downloader: JmDownloader = e.downloader
116+
print(f'下载出现部分失败, 下载失败的章节: {downloader.download_failed_photo}, 下载失败的图片: {downloader.download_failed_image}')
104117
```
105118

106119

src/jmcomic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# 被依赖方 <--- 使用方
33
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
44

5-
__version__ = '2.5.34'
5+
__version__ = '2.5.35'
66

77
from .api import *
88
from .jm_plugin import *

src/jmcomic/api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def download_album(jm_album_id,
4848
option=None,
4949
downloader=None,
5050
callback=None,
51+
check_exception=True,
5152
) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
5253
"""
5354
下载一个本子(album),包含其所有的章节(photo)
@@ -58,6 +59,7 @@ def download_album(jm_album_id,
5859
:param option: 下载选项
5960
:param downloader: 下载器类
6061
:param callback: 返回值回调函数,可以拿到 album 和 downloader
62+
:param check_exception: 是否检查异常, 如果为True,会检查downloader是否有下载异常,并上抛PartialDownloadFailedException
6163
:return: 对于的本子实体类,下载器(如果是上述的批量情况,返回值为download_batch的返回值)
6264
"""
6365

@@ -69,14 +71,17 @@ def download_album(jm_album_id,
6971

7072
if callback is not None:
7173
callback(album, dler)
72-
74+
if check_exception:
75+
dler.raise_if_has_exception()
7376
return album, dler
7477

7578

7679
def download_photo(jm_photo_id,
7780
option=None,
7881
downloader=None,
79-
callback=None):
82+
callback=None,
83+
check_exception=True,
84+
):
8085
"""
8186
下载一个章节(photo),参数同 download_album
8287
"""
@@ -88,7 +93,8 @@ def download_photo(jm_photo_id,
8893

8994
if callback is not None:
9095
callback(photo, dler)
91-
96+
if check_exception:
97+
dler.raise_if_has_exception()
9298
return photo, dler
9399

94100

src/jmcomic/jm_client_impl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ def check_special_text(cls, resp):
566566

567567
cls.raise_request_error(
568568
resp,
569-
f'{reason}'
569+
f'{reason}({content})'
570570
+ (f': {url}' if url is not None else '')
571571
)
572572

src/jmcomic/jm_downloader.py

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
from .jm_option import *
22

33

4+
def catch_exception(func):
5+
from functools import wraps
6+
7+
@wraps(func)
8+
def wrapper(self, *args, **kwargs):
9+
self: JmDownloader
10+
try:
11+
return func(self, *args, **kwargs)
12+
except Exception as e:
13+
detail: JmBaseEntity = args[0]
14+
if detail.is_image():
15+
detail: JmImageDetail
16+
jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: [{e}]')
17+
self.download_failed_image.append((detail, e))
18+
19+
elif detail.is_photo():
20+
detail: JmPhotoDetail
21+
jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: [{e}]')
22+
self.download_failed_photo.append((detail, e))
23+
24+
raise e
25+
26+
return wrapper
27+
28+
429
# noinspection PyMethodMayBeStatic
530
class DownloadCallback:
631

@@ -50,48 +75,50 @@ class JmDownloader(DownloadCallback):
5075

5176
def __init__(self, option: JmOption) -> None:
5277
self.option = option
78+
self.client = option.build_jm_client()
5379
# 下载成功的记录dict
5480
self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
5581
# 下载失败的记录list
56-
self.download_failed_list: List[Tuple[JmImageDetail, BaseException]] = []
82+
self.download_failed_image: List[Tuple[JmImageDetail, BaseException]] = []
83+
self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = []
5784

5885
def download_album(self, album_id):
59-
client = self.client_for_album(album_id)
60-
album = client.get_album_detail(album_id)
61-
self.download_by_album_detail(album, client)
86+
album = self.client.get_album_detail(album_id)
87+
self.download_by_album_detail(album)
6288
return album
6389

64-
def download_by_album_detail(self, album: JmAlbumDetail, client: JmcomicClient):
90+
def download_by_album_detail(self, album: JmAlbumDetail):
6591
self.before_album(album)
6692
if album.skip:
6793
return
68-
self.execute_by_condition(
94+
self.execute_on_condition(
6995
iter_objs=album,
70-
apply=lambda photo: self.download_by_photo_detail(photo, client),
96+
apply=self.download_by_photo_detail,
7197
count_batch=self.option.decide_photo_batch_count(album)
7298
)
7399
self.after_album(album)
74100

75101
def download_photo(self, photo_id):
76-
client = self.client_for_photo(photo_id)
77-
photo = client.get_photo_detail(photo_id)
78-
self.download_by_photo_detail(photo, client)
102+
photo = self.client.get_photo_detail(photo_id)
103+
self.download_by_photo_detail(photo)
79104
return photo
80105

81-
def download_by_photo_detail(self, photo: JmPhotoDetail, client: JmcomicClient):
82-
client.check_photo(photo)
106+
@catch_exception
107+
def download_by_photo_detail(self, photo: JmPhotoDetail):
108+
self.client.check_photo(photo)
83109

84110
self.before_photo(photo)
85111
if photo.skip:
86112
return
87-
self.execute_by_condition(
113+
self.execute_on_condition(
88114
iter_objs=photo,
89-
apply=lambda image: self.download_by_image_detail(image, client),
115+
apply=self.download_by_image_detail,
90116
count_batch=self.option.decide_image_batch_count(photo)
91117
)
92118
self.after_photo(photo)
93119

94-
def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
120+
@catch_exception
121+
def download_by_image_detail(self, image: JmImageDetail):
95122
img_save_path = self.option.decide_image_filepath(image)
96123

97124
image.save_path = img_save_path
@@ -110,22 +137,15 @@ def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
110137
if use_cache is True and image.exists:
111138
return
112139

113-
try:
114-
client.download_by_image_detail(
115-
image,
116-
img_save_path,
117-
decode_image=decode_image,
118-
)
119-
except BaseException as e:
120-
jm_log('image.failed', f'图片下载失败: [{image.download_url}], 异常: {e}')
121-
# 保存失败记录
122-
self.download_failed_list.append((image, e))
123-
raise
140+
self.client.download_by_image_detail(
141+
image,
142+
img_save_path,
143+
decode_image=decode_image,
144+
)
124145

125146
self.after_image(image, img_save_path)
126147

127-
# noinspection PyMethodMayBeStatic
128-
def execute_by_condition(self,
148+
def execute_on_condition(self,
129149
iter_objs: DetailEntity,
130150
apply: Callable,
131151
count_batch: int,
@@ -166,20 +186,6 @@ def do_filter(self, detail: DetailEntity):
166186
"""
167187
return detail
168188

169-
# noinspection PyUnusedLocal
170-
def client_for_album(self, jm_album_id) -> JmcomicClient:
171-
"""
172-
默认情况下,所有的JmDownloader共用一个JmcomicClient
173-
"""
174-
return self.option.build_jm_client()
175-
176-
# noinspection PyUnusedLocal
177-
def client_for_photo(self, jm_photo_id) -> JmcomicClient:
178-
"""
179-
默认情况下,所有的JmDownloader共用一个JmcomicClient
180-
"""
181-
return self.option.build_jm_client()
182-
183189
@property
184190
def all_success(self) -> bool:
185191
"""
@@ -189,7 +195,7 @@ def all_success(self) -> bool:
189195
190196
注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False
191197
"""
192-
if len(self.download_failed_list) != 0:
198+
if self.has_download_failures:
193199
return False
194200

195201
for album, photo_dict in self.download_success_dict.items():
@@ -202,6 +208,10 @@ def all_success(self) -> bool:
202208

203209
return True
204210

211+
@property
212+
def has_download_failures(self):
213+
return len(self.download_failed_image) != 0 or len(self.download_failed_photo) != 0
214+
205215
# 下面是回调方法
206216

207217
def before_album(self, album: JmAlbumDetail):
@@ -259,6 +269,23 @@ def after_image(self, image: JmImageDetail, img_save_path):
259269
downloader=self,
260270
)
261271

272+
def raise_if_has_exception(self):
273+
if not self.has_download_failures:
274+
return
275+
msg_ls = ['部分下载失败', '', '']
276+
277+
if len(self.download_failed_photo) != 0:
278+
msg_ls[1] = f'共{len(self.download_failed_photo)}个章节下载失败: {self.download_failed_photo}'
279+
280+
if len(self.download_failed_image) != 0:
281+
msg_ls[2] = f'共{len(self.download_failed_image)}个图片下载失败: {self.download_failed_image}'
282+
283+
ExceptionTool.raises(
284+
'\n'.join(msg_ls),
285+
{'downloader': self},
286+
PartialDownloadFailedException,
287+
)
288+
262289
# 下面是对with语法的支持
263290

264291
def __enter__(self):
@@ -283,7 +310,7 @@ class DoNotDownloadImage(JmDownloader):
283310
不会下载任何图片的Downloader,用作测试
284311
"""
285312

286-
def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
313+
def download_by_image_detail(self, image: JmImageDetail):
287314
# ensure make dir
288315
self.option.decide_image_filepath(image)
289316

@@ -297,12 +324,13 @@ class JustDownloadSpecificCountImage(JmDownloader):
297324
count_lock = Lock()
298325
count = 0
299326

300-
def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
327+
@catch_exception
328+
def download_by_image_detail(self, image: JmImageDetail):
301329
# ensure make dir
302330
self.option.decide_image_filepath(image)
303331

304332
if self.try_countdown():
305-
return super().download_by_image_detail(image, client)
333+
return super().download_by_image_detail(image)
306334

307335
def try_countdown(self):
308336
if self.count < 0:

src/jmcomic/jm_entity.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,9 @@ def idoname(self):
125125
return f'[{self.id}] {self.oname}'
126126

127127
def __str__(self):
128-
return f'{self.__class__.__name__}' \
129-
'{' \
130-
f'{self.id}: {self.title}' \
131-
'}'
128+
return f'''{self.__class__.__name__}({self.__alias__()}-{self.id}: "{self.title}")'''
129+
130+
__repr__ = __str__
132131

133132
@classmethod
134133
def __alias__(cls):
@@ -258,6 +257,11 @@ def tag(self) -> str:
258257
def is_image(cls):
259258
return True
260259

260+
def __str__(self):
261+
return f'''{self.__class__.__name__}(image-[{self.download_url}])'''
262+
263+
__repr__ = __str__
264+
261265

262266
class JmPhotoDetail(DetailEntity, Downloadable):
263267

src/jmcomic/jm_exception.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def from_context(self, key):
1515
def __str__(self):
1616
return self.msg
1717

18+
1819
class ResponseUnexpectedException(JmcomicException):
1920
description = '响应不符合预期异常'
2021

@@ -44,7 +45,6 @@ def pattern(self):
4445

4546
class JsonResolveFailException(ResponseUnexpectedException):
4647
description = 'Json解析异常'
47-
pass
4848

4949

5050
class MissingAlbumPhotoException(ResponseUnexpectedException):
@@ -57,9 +57,15 @@ def error_jmid(self) -> str:
5757

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

6261

62+
class PartialDownloadFailedException(JmcomicException):
63+
description = '部分章节或图片下载失败异常'
64+
65+
@property
66+
def downloader(self):
67+
return self.from_context(ExceptionTool.CONTEXT_KEY_DOWNLOADER)
68+
6369
class ExceptionTool:
6470
"""
6571
抛异常的工具
@@ -71,6 +77,7 @@ class ExceptionTool:
7177
CONTEXT_KEY_HTML = 'html'
7278
CONTEXT_KEY_RE_PATTERN = 'pattern'
7379
CONTEXT_KEY_MISSING_JM_ID = 'missing_jm_id'
80+
CONTEXT_KEY_DOWNLOADER = 'downloader'
7481

7582
@classmethod
7683
def raises(cls,

0 commit comments

Comments
 (0)