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.
@@ -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()