jmcomic 0.0.2__py3-none-any.whl
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/__init__.py +29 -0
- jmcomic/api.py +131 -0
- jmcomic/cl.py +121 -0
- jmcomic/jm_client_impl.py +1217 -0
- jmcomic/jm_client_interface.py +609 -0
- jmcomic/jm_config.py +505 -0
- jmcomic/jm_downloader.py +350 -0
- jmcomic/jm_entity.py +695 -0
- jmcomic/jm_exception.py +191 -0
- jmcomic/jm_option.py +647 -0
- jmcomic/jm_plugin.py +1203 -0
- jmcomic/jm_toolkit.py +937 -0
- jmcomic-0.0.2.dist-info/METADATA +229 -0
- jmcomic-0.0.2.dist-info/RECORD +18 -0
- jmcomic-0.0.2.dist-info/WHEEL +5 -0
- jmcomic-0.0.2.dist-info/entry_points.txt +2 -0
- jmcomic-0.0.2.dist-info/licenses/LICENSE +21 -0
- jmcomic-0.0.2.dist-info/top_level.txt +1 -0
jmcomic/jm_downloader.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from .jm_option import *
|
|
2
|
+
|
|
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
|
+
|
|
29
|
+
# noinspection PyMethodMayBeStatic
|
|
30
|
+
class DownloadCallback:
|
|
31
|
+
|
|
32
|
+
def before_album(self, album: JmAlbumDetail):
|
|
33
|
+
jm_log('album.before',
|
|
34
|
+
f'本子获取成功: [{album.id}], '
|
|
35
|
+
f'作者: [{album.author}], '
|
|
36
|
+
f'章节数: [{len(album)}], '
|
|
37
|
+
f'总页数: [{album.page_count}], '
|
|
38
|
+
f'标题: [{album.name}], '
|
|
39
|
+
f'关键词: {album.tags}'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def after_album(self, album: JmAlbumDetail):
|
|
43
|
+
jm_log('album.after', f'本子下载完成: [{album.id}]')
|
|
44
|
+
|
|
45
|
+
def before_photo(self, photo: JmPhotoDetail):
|
|
46
|
+
jm_log('photo.before',
|
|
47
|
+
f'开始下载章节: {photo.id} ({photo.album_id}[{photo.index}/{len(photo.from_album)}]), '
|
|
48
|
+
f'标题: [{photo.name}], '
|
|
49
|
+
f'图片数为[{len(photo)}]'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def after_photo(self, photo: JmPhotoDetail):
|
|
53
|
+
jm_log('photo.after',
|
|
54
|
+
f'章节下载完成: [{photo.id}] ({photo.album_id}[{photo.index}/{len(photo.from_album)}])')
|
|
55
|
+
|
|
56
|
+
def before_image(self, image: JmImageDetail, img_save_path):
|
|
57
|
+
if image.exists:
|
|
58
|
+
jm_log('image.before',
|
|
59
|
+
f'图片已存在: {image.tag} ← [{img_save_path}]'
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
jm_log('image.before',
|
|
63
|
+
f'图片准备下载: {image.tag}, [{image.img_url}] → [{img_save_path}]'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def after_image(self, image: JmImageDetail, img_save_path):
|
|
67
|
+
jm_log('image.after',
|
|
68
|
+
f'图片下载完成: {image.tag}, [{image.img_url}] → [{img_save_path}]')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class JmDownloader(DownloadCallback):
|
|
72
|
+
"""
|
|
73
|
+
JmDownloader = JmOption + 调度逻辑
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, option: JmOption) -> None:
|
|
77
|
+
self.option = option
|
|
78
|
+
self.client = option.build_jm_client()
|
|
79
|
+
# 下载成功的记录dict
|
|
80
|
+
self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
|
|
81
|
+
# 下载失败的记录list
|
|
82
|
+
self.download_failed_image: List[Tuple[JmImageDetail, BaseException]] = []
|
|
83
|
+
self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = []
|
|
84
|
+
|
|
85
|
+
def download_album(self, album_id):
|
|
86
|
+
album = self.client.get_album_detail(album_id)
|
|
87
|
+
self.download_by_album_detail(album)
|
|
88
|
+
return album
|
|
89
|
+
|
|
90
|
+
def download_by_album_detail(self, album: JmAlbumDetail):
|
|
91
|
+
self.before_album(album)
|
|
92
|
+
if album.skip:
|
|
93
|
+
return
|
|
94
|
+
self.execute_on_condition(
|
|
95
|
+
iter_objs=album,
|
|
96
|
+
apply=self.download_by_photo_detail,
|
|
97
|
+
count_batch=self.option.decide_photo_batch_count(album)
|
|
98
|
+
)
|
|
99
|
+
self.after_album(album)
|
|
100
|
+
|
|
101
|
+
def download_photo(self, photo_id):
|
|
102
|
+
photo = self.client.get_photo_detail(photo_id)
|
|
103
|
+
self.download_by_photo_detail(photo)
|
|
104
|
+
return photo
|
|
105
|
+
|
|
106
|
+
@catch_exception
|
|
107
|
+
def download_by_photo_detail(self, photo: JmPhotoDetail):
|
|
108
|
+
self.client.check_photo(photo)
|
|
109
|
+
|
|
110
|
+
self.before_photo(photo)
|
|
111
|
+
if photo.skip:
|
|
112
|
+
return
|
|
113
|
+
self.execute_on_condition(
|
|
114
|
+
iter_objs=photo,
|
|
115
|
+
apply=self.download_by_image_detail,
|
|
116
|
+
count_batch=self.option.decide_image_batch_count(photo)
|
|
117
|
+
)
|
|
118
|
+
self.after_photo(photo)
|
|
119
|
+
|
|
120
|
+
@catch_exception
|
|
121
|
+
def download_by_image_detail(self, image: JmImageDetail):
|
|
122
|
+
img_save_path = self.option.decide_image_filepath(image)
|
|
123
|
+
|
|
124
|
+
image.save_path = img_save_path
|
|
125
|
+
image.exists = file_exists(img_save_path)
|
|
126
|
+
|
|
127
|
+
self.before_image(image, img_save_path)
|
|
128
|
+
|
|
129
|
+
if image.skip:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# let option decide use_cache and decode_image
|
|
133
|
+
use_cache = self.option.decide_download_cache(image)
|
|
134
|
+
decode_image = self.option.decide_download_image_decode(image)
|
|
135
|
+
|
|
136
|
+
# skip download
|
|
137
|
+
if use_cache is True and image.exists:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
self.client.download_by_image_detail(
|
|
141
|
+
image,
|
|
142
|
+
img_save_path,
|
|
143
|
+
decode_image=decode_image,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.after_image(image, img_save_path)
|
|
147
|
+
|
|
148
|
+
def execute_on_condition(self,
|
|
149
|
+
iter_objs: DetailEntity,
|
|
150
|
+
apply: Callable,
|
|
151
|
+
count_batch: int,
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
调度本子/章节的下载
|
|
155
|
+
"""
|
|
156
|
+
iter_objs = self.do_filter(iter_objs)
|
|
157
|
+
count_real = len(iter_objs)
|
|
158
|
+
|
|
159
|
+
if count_real == 0:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if count_batch >= count_real:
|
|
163
|
+
# 一个图/章节 对应 一个线程
|
|
164
|
+
multi_thread_launcher(
|
|
165
|
+
iter_objs=iter_objs,
|
|
166
|
+
apply_each_obj_func=apply,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
# 创建batch个线程的线程池
|
|
170
|
+
thread_pool_executor(
|
|
171
|
+
iter_objs=iter_objs,
|
|
172
|
+
apply_each_obj_func=apply,
|
|
173
|
+
max_workers=count_batch,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# noinspection PyMethodMayBeStatic
|
|
177
|
+
def do_filter(self, detail: DetailEntity):
|
|
178
|
+
"""
|
|
179
|
+
该方法可用于过滤本子/章节,默认不会做过滤。
|
|
180
|
+
例如:
|
|
181
|
+
只想下载 本子的最新一章,返回 [album[-1]]
|
|
182
|
+
只想下载 章节的前10张图片,返回 [photo[:10]]
|
|
183
|
+
|
|
184
|
+
:param detail: 可能是本子或者章节,需要自行使用 isinstance / detail.is_xxx 判断
|
|
185
|
+
:returns: 只想要下载的 本子的章节 或 章节的图片
|
|
186
|
+
"""
|
|
187
|
+
return detail
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def all_success(self) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
是否成功下载了全部图片
|
|
193
|
+
|
|
194
|
+
该属性需要等到downloader的全部download_xxx方法完成后才有意义。
|
|
195
|
+
|
|
196
|
+
注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False
|
|
197
|
+
"""
|
|
198
|
+
if self.has_download_failures:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
for album, photo_dict in self.download_success_dict.items():
|
|
202
|
+
if len(album) != len(photo_dict):
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
for photo, image_list in photo_dict.items():
|
|
206
|
+
if len(photo) != len(image_list):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
return True
|
|
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
|
+
|
|
215
|
+
# 下面是回调方法
|
|
216
|
+
|
|
217
|
+
def before_album(self, album: JmAlbumDetail):
|
|
218
|
+
super().before_album(album)
|
|
219
|
+
self.download_success_dict.setdefault(album, {})
|
|
220
|
+
self.option.call_all_plugin(
|
|
221
|
+
'before_album',
|
|
222
|
+
album=album,
|
|
223
|
+
downloader=self,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def after_album(self, album: JmAlbumDetail):
|
|
227
|
+
super().after_album(album)
|
|
228
|
+
self.option.call_all_plugin(
|
|
229
|
+
'after_album',
|
|
230
|
+
album=album,
|
|
231
|
+
downloader=self,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def before_photo(self, photo: JmPhotoDetail):
|
|
235
|
+
super().before_photo(photo)
|
|
236
|
+
self.download_success_dict.setdefault(photo.from_album, {})
|
|
237
|
+
self.download_success_dict[photo.from_album].setdefault(photo, [])
|
|
238
|
+
self.option.call_all_plugin(
|
|
239
|
+
'before_photo',
|
|
240
|
+
photo=photo,
|
|
241
|
+
downloader=self,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def after_photo(self, photo: JmPhotoDetail):
|
|
245
|
+
super().after_photo(photo)
|
|
246
|
+
self.option.call_all_plugin(
|
|
247
|
+
'after_photo',
|
|
248
|
+
photo=photo,
|
|
249
|
+
downloader=self,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def before_image(self, image: JmImageDetail, img_save_path):
|
|
253
|
+
super().before_image(image, img_save_path)
|
|
254
|
+
self.option.call_all_plugin(
|
|
255
|
+
'before_image',
|
|
256
|
+
image=image,
|
|
257
|
+
downloader=self,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def after_image(self, image: JmImageDetail, img_save_path):
|
|
261
|
+
super().after_image(image, img_save_path)
|
|
262
|
+
photo = image.from_photo
|
|
263
|
+
album = photo.from_album
|
|
264
|
+
|
|
265
|
+
self.download_success_dict.get(album).get(photo).append((img_save_path, image))
|
|
266
|
+
self.option.call_all_plugin(
|
|
267
|
+
'after_image',
|
|
268
|
+
image=image,
|
|
269
|
+
downloader=self,
|
|
270
|
+
)
|
|
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
|
+
|
|
289
|
+
# 下面是对with语法的支持
|
|
290
|
+
|
|
291
|
+
def __enter__(self):
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
295
|
+
if exc_type is not None:
|
|
296
|
+
jm_log('dler.exception',
|
|
297
|
+
f'{self.__class__.__name__} Exit with exception: {exc_type, str(exc_val)}'
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def use(cls, *args, **kwargs):
|
|
302
|
+
"""
|
|
303
|
+
让本类替换JmModuleConfig.CLASS_DOWNLOADER
|
|
304
|
+
"""
|
|
305
|
+
JmModuleConfig.CLASS_DOWNLOADER = cls
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class DoNotDownloadImage(JmDownloader):
|
|
309
|
+
"""
|
|
310
|
+
不会下载任何图片的Downloader,用作测试
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
def download_by_image_detail(self, image: JmImageDetail):
|
|
314
|
+
# ensure make dir
|
|
315
|
+
self.option.decide_image_filepath(image)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class JustDownloadSpecificCountImage(JmDownloader):
|
|
319
|
+
"""
|
|
320
|
+
只下载特定数量图片的Downloader,用作测试
|
|
321
|
+
"""
|
|
322
|
+
from threading import Lock
|
|
323
|
+
|
|
324
|
+
count_lock = Lock()
|
|
325
|
+
count = 0
|
|
326
|
+
|
|
327
|
+
@catch_exception
|
|
328
|
+
def download_by_image_detail(self, image: JmImageDetail):
|
|
329
|
+
# ensure make dir
|
|
330
|
+
self.option.decide_image_filepath(image)
|
|
331
|
+
|
|
332
|
+
if self.try_countdown():
|
|
333
|
+
return super().download_by_image_detail(image)
|
|
334
|
+
|
|
335
|
+
def try_countdown(self):
|
|
336
|
+
if self.count < 0:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
with self.count_lock:
|
|
340
|
+
if self.count < 0:
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
self.count -= 1
|
|
344
|
+
|
|
345
|
+
return self.count >= 0
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def use(cls, count):
|
|
349
|
+
cls.count = count
|
|
350
|
+
super().use()
|