jmcomic 2.5.34__tar.gz → 2.5.35__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jmcomic-2.5.34/src/jmcomic.egg-info → jmcomic-2.5.35}/PKG-INFO +1 -1
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/__init__.py +1 -1
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/api.py +9 -3
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_client_impl.py +1 -1
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_downloader.py +74 -46
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_entity.py +8 -4
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_exception.py +9 -2
- {jmcomic-2.5.34 → jmcomic-2.5.35/src/jmcomic.egg-info}/PKG-INFO +1 -1
- {jmcomic-2.5.34 → jmcomic-2.5.35}/LICENSE +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/README.md +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/setup.cfg +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/setup.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/cl.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_client_interface.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_config.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_option.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_plugin.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic/jm_toolkit.py +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic.egg-info/SOURCES.txt +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic.egg-info/dependency_links.txt +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic.egg-info/entry_points.txt +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic.egg-info/requires.txt +0 -0
- {jmcomic-2.5.34 → jmcomic-2.5.35}/src/jmcomic.egg-info/top_level.txt +0 -0
|
@@ -48,6 +48,7 @@ def download_album(jm_album_id,
|
|
|
48
48
|
option=None,
|
|
49
49
|
downloader=None,
|
|
50
50
|
callback=None,
|
|
51
|
+
check_exception=True,
|
|
51
52
|
) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
|
|
52
53
|
"""
|
|
53
54
|
下载一个本子(album),包含其所有的章节(photo)
|
|
@@ -58,6 +59,7 @@ def download_album(jm_album_id,
|
|
|
58
59
|
:param option: 下载选项
|
|
59
60
|
:param downloader: 下载器类
|
|
60
61
|
:param callback: 返回值回调函数,可以拿到 album 和 downloader
|
|
62
|
+
:param check_exception: 是否检查异常, 如果为True,会检查downloader是否有下载异常,并上抛PartialDownloadFailedException
|
|
61
63
|
:return: 对于的本子实体类,下载器(如果是上述的批量情况,返回值为download_batch的返回值)
|
|
62
64
|
"""
|
|
63
65
|
|
|
@@ -69,14 +71,17 @@ def download_album(jm_album_id,
|
|
|
69
71
|
|
|
70
72
|
if callback is not None:
|
|
71
73
|
callback(album, dler)
|
|
72
|
-
|
|
74
|
+
if check_exception:
|
|
75
|
+
dler.raise_if_has_exception()
|
|
73
76
|
return album, dler
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
def download_photo(jm_photo_id,
|
|
77
80
|
option=None,
|
|
78
81
|
downloader=None,
|
|
79
|
-
callback=None
|
|
82
|
+
callback=None,
|
|
83
|
+
check_exception=True,
|
|
84
|
+
):
|
|
80
85
|
"""
|
|
81
86
|
下载一个章节(photo),参数同 download_album
|
|
82
87
|
"""
|
|
@@ -88,7 +93,8 @@ def download_photo(jm_photo_id,
|
|
|
88
93
|
|
|
89
94
|
if callback is not None:
|
|
90
95
|
callback(photo, dler)
|
|
91
|
-
|
|
96
|
+
if check_exception:
|
|
97
|
+
dler.raise_if_has_exception()
|
|
92
98
|
return photo, dler
|
|
93
99
|
|
|
94
100
|
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
from .jm_option import *
|
|
2
2
|
|
|
3
3
|
|
|
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
|
+
|
|
4
29
|
# noinspection PyMethodMayBeStatic
|
|
5
30
|
class DownloadCallback:
|
|
6
31
|
|
|
@@ -50,48 +75,50 @@ class JmDownloader(DownloadCallback):
|
|
|
50
75
|
|
|
51
76
|
def __init__(self, option: JmOption) -> None:
|
|
52
77
|
self.option = option
|
|
78
|
+
self.client = option.build_jm_client()
|
|
53
79
|
# 下载成功的记录dict
|
|
54
80
|
self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
|
|
55
81
|
# 下载失败的记录list
|
|
56
|
-
self.
|
|
82
|
+
self.download_failed_image: List[Tuple[JmImageDetail, BaseException]] = []
|
|
83
|
+
self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = []
|
|
57
84
|
|
|
58
85
|
def download_album(self, album_id):
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.download_by_album_detail(album, client)
|
|
86
|
+
album = self.client.get_album_detail(album_id)
|
|
87
|
+
self.download_by_album_detail(album)
|
|
62
88
|
return album
|
|
63
89
|
|
|
64
|
-
def download_by_album_detail(self, album: JmAlbumDetail
|
|
90
|
+
def download_by_album_detail(self, album: JmAlbumDetail):
|
|
65
91
|
self.before_album(album)
|
|
66
92
|
if album.skip:
|
|
67
93
|
return
|
|
68
|
-
self.
|
|
94
|
+
self.execute_on_condition(
|
|
69
95
|
iter_objs=album,
|
|
70
|
-
apply=
|
|
96
|
+
apply=self.download_by_photo_detail,
|
|
71
97
|
count_batch=self.option.decide_photo_batch_count(album)
|
|
72
98
|
)
|
|
73
99
|
self.after_album(album)
|
|
74
100
|
|
|
75
101
|
def download_photo(self, photo_id):
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
self.download_by_photo_detail(photo, client)
|
|
102
|
+
photo = self.client.get_photo_detail(photo_id)
|
|
103
|
+
self.download_by_photo_detail(photo)
|
|
79
104
|
return photo
|
|
80
105
|
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
@catch_exception
|
|
107
|
+
def download_by_photo_detail(self, photo: JmPhotoDetail):
|
|
108
|
+
self.client.check_photo(photo)
|
|
83
109
|
|
|
84
110
|
self.before_photo(photo)
|
|
85
111
|
if photo.skip:
|
|
86
112
|
return
|
|
87
|
-
self.
|
|
113
|
+
self.execute_on_condition(
|
|
88
114
|
iter_objs=photo,
|
|
89
|
-
apply=
|
|
115
|
+
apply=self.download_by_image_detail,
|
|
90
116
|
count_batch=self.option.decide_image_batch_count(photo)
|
|
91
117
|
)
|
|
92
118
|
self.after_photo(photo)
|
|
93
119
|
|
|
94
|
-
|
|
120
|
+
@catch_exception
|
|
121
|
+
def download_by_image_detail(self, image: JmImageDetail):
|
|
95
122
|
img_save_path = self.option.decide_image_filepath(image)
|
|
96
123
|
|
|
97
124
|
image.save_path = img_save_path
|
|
@@ -110,22 +137,15 @@ class JmDownloader(DownloadCallback):
|
|
|
110
137
|
if use_cache is True and image.exists:
|
|
111
138
|
return
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
)
|
|
124
145
|
|
|
125
146
|
self.after_image(image, img_save_path)
|
|
126
147
|
|
|
127
|
-
|
|
128
|
-
def execute_by_condition(self,
|
|
148
|
+
def execute_on_condition(self,
|
|
129
149
|
iter_objs: DetailEntity,
|
|
130
150
|
apply: Callable,
|
|
131
151
|
count_batch: int,
|
|
@@ -166,20 +186,6 @@ class JmDownloader(DownloadCallback):
|
|
|
166
186
|
"""
|
|
167
187
|
return detail
|
|
168
188
|
|
|
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
|
-
|
|
183
189
|
@property
|
|
184
190
|
def all_success(self) -> bool:
|
|
185
191
|
"""
|
|
@@ -189,7 +195,7 @@ class JmDownloader(DownloadCallback):
|
|
|
189
195
|
|
|
190
196
|
注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False
|
|
191
197
|
"""
|
|
192
|
-
if
|
|
198
|
+
if self.has_download_failures:
|
|
193
199
|
return False
|
|
194
200
|
|
|
195
201
|
for album, photo_dict in self.download_success_dict.items():
|
|
@@ -202,6 +208,10 @@ class JmDownloader(DownloadCallback):
|
|
|
202
208
|
|
|
203
209
|
return True
|
|
204
210
|
|
|
211
|
+
@property
|
|
212
|
+
def has_download_failures(self):
|
|
213
|
+
return len(self.download_failed_image) != 0 or len(self.download_failed_photo) != 0
|
|
214
|
+
|
|
205
215
|
# 下面是回调方法
|
|
206
216
|
|
|
207
217
|
def before_album(self, album: JmAlbumDetail):
|
|
@@ -259,6 +269,23 @@ class JmDownloader(DownloadCallback):
|
|
|
259
269
|
downloader=self,
|
|
260
270
|
)
|
|
261
271
|
|
|
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
|
+
|
|
262
289
|
# 下面是对with语法的支持
|
|
263
290
|
|
|
264
291
|
def __enter__(self):
|
|
@@ -283,7 +310,7 @@ class DoNotDownloadImage(JmDownloader):
|
|
|
283
310
|
不会下载任何图片的Downloader,用作测试
|
|
284
311
|
"""
|
|
285
312
|
|
|
286
|
-
def download_by_image_detail(self, image: JmImageDetail
|
|
313
|
+
def download_by_image_detail(self, image: JmImageDetail):
|
|
287
314
|
# ensure make dir
|
|
288
315
|
self.option.decide_image_filepath(image)
|
|
289
316
|
|
|
@@ -297,12 +324,13 @@ class JustDownloadSpecificCountImage(JmDownloader):
|
|
|
297
324
|
count_lock = Lock()
|
|
298
325
|
count = 0
|
|
299
326
|
|
|
300
|
-
|
|
327
|
+
@catch_exception
|
|
328
|
+
def download_by_image_detail(self, image: JmImageDetail):
|
|
301
329
|
# ensure make dir
|
|
302
330
|
self.option.decide_image_filepath(image)
|
|
303
331
|
|
|
304
332
|
if self.try_countdown():
|
|
305
|
-
return super().download_by_image_detail(image
|
|
333
|
+
return super().download_by_image_detail(image)
|
|
306
334
|
|
|
307
335
|
def try_countdown(self):
|
|
308
336
|
if self.count < 0:
|
|
@@ -125,10 +125,9 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
|
|
|
125
125
|
return f'[{self.id}] {self.oname}'
|
|
126
126
|
|
|
127
127
|
def __str__(self):
|
|
128
|
-
return f'{self.__class__.__name__}'
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'}'
|
|
128
|
+
return f'''{self.__class__.__name__}({self.__alias__()}-{self.id}: "{self.title}")'''
|
|
129
|
+
|
|
130
|
+
__repr__ = __str__
|
|
132
131
|
|
|
133
132
|
@classmethod
|
|
134
133
|
def __alias__(cls):
|
|
@@ -258,6 +257,11 @@ class JmImageDetail(JmBaseEntity, Downloadable):
|
|
|
258
257
|
def is_image(cls):
|
|
259
258
|
return True
|
|
260
259
|
|
|
260
|
+
def __str__(self):
|
|
261
|
+
return f'''{self.__class__.__name__}(image-[{self.download_url}])'''
|
|
262
|
+
|
|
263
|
+
__repr__ = __str__
|
|
264
|
+
|
|
261
265
|
|
|
262
266
|
class JmPhotoDetail(DetailEntity, Downloadable):
|
|
263
267
|
|
|
@@ -15,6 +15,7 @@ class JmcomicException(Exception):
|
|
|
15
15
|
def __str__(self):
|
|
16
16
|
return self.msg
|
|
17
17
|
|
|
18
|
+
|
|
18
19
|
class ResponseUnexpectedException(JmcomicException):
|
|
19
20
|
description = '响应不符合预期异常'
|
|
20
21
|
|
|
@@ -44,7 +45,6 @@ class RegularNotMatchException(JmcomicException):
|
|
|
44
45
|
|
|
45
46
|
class JsonResolveFailException(ResponseUnexpectedException):
|
|
46
47
|
description = 'Json解析异常'
|
|
47
|
-
pass
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class MissingAlbumPhotoException(ResponseUnexpectedException):
|
|
@@ -57,9 +57,15 @@ class MissingAlbumPhotoException(ResponseUnexpectedException):
|
|
|
57
57
|
|
|
58
58
|
class RequestRetryAllFailException(JmcomicException):
|
|
59
59
|
description = '请求重试全部失败异常'
|
|
60
|
-
pass
|
|
61
60
|
|
|
62
61
|
|
|
62
|
+
class PartialDownloadFailedException(JmcomicException):
|
|
63
|
+
description = '部分章节或图片下载失败异常'
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def downloader(self):
|
|
67
|
+
return self.from_context(ExceptionTool.CONTEXT_KEY_DOWNLOADER)
|
|
68
|
+
|
|
63
69
|
class ExceptionTool:
|
|
64
70
|
"""
|
|
65
71
|
抛异常的工具
|
|
@@ -71,6 +77,7 @@ class ExceptionTool:
|
|
|
71
77
|
CONTEXT_KEY_HTML = 'html'
|
|
72
78
|
CONTEXT_KEY_RE_PATTERN = 'pattern'
|
|
73
79
|
CONTEXT_KEY_MISSING_JM_ID = 'missing_jm_id'
|
|
80
|
+
CONTEXT_KEY_DOWNLOADER = 'downloader'
|
|
74
81
|
|
|
75
82
|
@classmethod
|
|
76
83
|
def raises(cls,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|