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_option.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
from .jm_client_impl import *
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CacheRegistry:
|
|
5
|
+
REGISTRY = {}
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def level_option(cls, option, _client):
|
|
9
|
+
registry = cls.REGISTRY
|
|
10
|
+
registry.setdefault(option, {})
|
|
11
|
+
return registry[option]
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def level_client(cls, _option, client):
|
|
15
|
+
registry = cls.REGISTRY
|
|
16
|
+
registry.setdefault(client, {})
|
|
17
|
+
return registry[client]
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def enable_client_cache_on_condition(cls,
|
|
21
|
+
option: 'JmOption',
|
|
22
|
+
client: JmcomicClient,
|
|
23
|
+
cache: Union[None, bool, str, Callable],
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
cache parameter
|
|
27
|
+
|
|
28
|
+
if None: no cache
|
|
29
|
+
|
|
30
|
+
if bool:
|
|
31
|
+
true: level_option
|
|
32
|
+
|
|
33
|
+
false: no cache
|
|
34
|
+
|
|
35
|
+
if str:
|
|
36
|
+
(invoke corresponding Cache class method)
|
|
37
|
+
|
|
38
|
+
:param option: JmOption
|
|
39
|
+
:param client: JmcomicClient
|
|
40
|
+
:param cache: config dsl
|
|
41
|
+
"""
|
|
42
|
+
if cache is None:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
elif isinstance(cache, bool):
|
|
46
|
+
if cache is False:
|
|
47
|
+
return
|
|
48
|
+
else:
|
|
49
|
+
cache = cls.level_option
|
|
50
|
+
|
|
51
|
+
elif isinstance(cache, str):
|
|
52
|
+
func = getattr(cls, cache, None)
|
|
53
|
+
ExceptionTool.require_true(func is not None, f'未实现的cache配置名: {cache}')
|
|
54
|
+
cache = func
|
|
55
|
+
|
|
56
|
+
cache: Callable
|
|
57
|
+
client.set_cache_dict(cache(option, client))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DirRule:
|
|
61
|
+
RULE_BASE_DIR = 'Bd'
|
|
62
|
+
|
|
63
|
+
def __init__(self, rule: str, base_dir=None):
|
|
64
|
+
base_dir = JmcomicText.parse_to_abspath(base_dir)
|
|
65
|
+
self.base_dir = base_dir
|
|
66
|
+
self.rule_dsl = rule
|
|
67
|
+
self.parser_list: List[Tuple[str, Callable]] = self.get_rule_parser_list(rule)
|
|
68
|
+
|
|
69
|
+
def decide_image_save_dir(self,
|
|
70
|
+
album: JmAlbumDetail,
|
|
71
|
+
photo: JmPhotoDetail,
|
|
72
|
+
) -> str:
|
|
73
|
+
return self.apply_rule_to_path(album, photo)
|
|
74
|
+
|
|
75
|
+
def decide_album_root_dir(self, album: JmAlbumDetail) -> str:
|
|
76
|
+
return self.apply_rule_to_path(album, None, True)
|
|
77
|
+
|
|
78
|
+
def apply_rule_to_path(self, album, photo, only_album_rules=False) -> str:
|
|
79
|
+
path_ls = []
|
|
80
|
+
for rule, parser in self.parser_list:
|
|
81
|
+
if only_album_rules and not (rule == self.RULE_BASE_DIR or rule.startswith('A')):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
path = parser(album, photo, rule)
|
|
86
|
+
except BaseException as e:
|
|
87
|
+
# noinspection PyUnboundLocalVariable
|
|
88
|
+
jm_log('dir_rule', f'路径规则"{rule}"的解析出错: {e}, album={album}, photo={photo}')
|
|
89
|
+
raise e
|
|
90
|
+
if parser != self.parse_bd_rule:
|
|
91
|
+
path = fix_windir_name(str(path)).strip()
|
|
92
|
+
|
|
93
|
+
path_ls.append(path)
|
|
94
|
+
|
|
95
|
+
return fix_filepath('/'.join(path_ls))
|
|
96
|
+
|
|
97
|
+
def get_rule_parser_list(self, rule_dsl: str):
|
|
98
|
+
"""
|
|
99
|
+
解析下载路径dsl,得到一个路径规则解析列表
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
rule_list = self.split_rule_dsl(rule_dsl)
|
|
103
|
+
parser_list: list = []
|
|
104
|
+
|
|
105
|
+
for rule in rule_list:
|
|
106
|
+
if rule == self.RULE_BASE_DIR:
|
|
107
|
+
parser_list.append((rule, self.parse_bd_rule))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
parser = self.get_rule_parser(rule)
|
|
111
|
+
if parser is None:
|
|
112
|
+
ExceptionTool.raises(f'不支持的dsl: "{rule}" in "{rule_dsl}"')
|
|
113
|
+
|
|
114
|
+
parser_list.append((rule, parser))
|
|
115
|
+
|
|
116
|
+
return parser_list
|
|
117
|
+
|
|
118
|
+
# noinspection PyUnusedLocal
|
|
119
|
+
def parse_bd_rule(self, album, photo, rule):
|
|
120
|
+
return self.base_dir
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def parse_f_string_rule(cls, album, photo, rule: str):
|
|
124
|
+
properties = {}
|
|
125
|
+
if album:
|
|
126
|
+
properties.update(album.get_properties_dict())
|
|
127
|
+
if photo:
|
|
128
|
+
properties.update(photo.get_properties_dict())
|
|
129
|
+
return rule.format(**properties)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def parse_detail_rule(cls, album, photo, rule: str):
|
|
133
|
+
detail = album if rule.startswith('A') else photo
|
|
134
|
+
return str(DetailEntity.get_dirname(detail, rule[1:]))
|
|
135
|
+
|
|
136
|
+
# noinspection PyMethodMayBeStatic
|
|
137
|
+
def split_rule_dsl(self, rule_dsl: str) -> List[str]:
|
|
138
|
+
if '/' in rule_dsl:
|
|
139
|
+
rule_list = rule_dsl.split('/')
|
|
140
|
+
elif '_' in rule_dsl:
|
|
141
|
+
rule_list = rule_dsl.split('_')
|
|
142
|
+
else:
|
|
143
|
+
rule_list = [rule_dsl]
|
|
144
|
+
|
|
145
|
+
for i, e in enumerate(rule_list):
|
|
146
|
+
rule_list[i] = e.strip()
|
|
147
|
+
|
|
148
|
+
if rule_list[0] != self.RULE_BASE_DIR:
|
|
149
|
+
rule_list.insert(0, self.RULE_BASE_DIR)
|
|
150
|
+
|
|
151
|
+
return rule_list
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def get_rule_parser(cls, rule: str):
|
|
155
|
+
if '{' in rule:
|
|
156
|
+
return cls.parse_f_string_rule
|
|
157
|
+
|
|
158
|
+
if rule.startswith(('A', 'P')):
|
|
159
|
+
return cls.parse_detail_rule
|
|
160
|
+
|
|
161
|
+
ExceptionTool.raises(f'不支持的rule配置: "{rule}"')
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def apply_rule_to_filename(cls, album, photo, rule: str) -> str:
|
|
165
|
+
if album is None:
|
|
166
|
+
album = photo.from_album
|
|
167
|
+
# noinspection PyArgumentList
|
|
168
|
+
return fix_windir_name(cls.get_rule_parser(rule)(album, photo, rule)).strip()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class JmOption:
|
|
172
|
+
|
|
173
|
+
def __init__(self,
|
|
174
|
+
dir_rule: Dict,
|
|
175
|
+
download: Dict,
|
|
176
|
+
client: Dict,
|
|
177
|
+
plugins: Dict,
|
|
178
|
+
filepath=None,
|
|
179
|
+
call_after_init_plugin=True,
|
|
180
|
+
):
|
|
181
|
+
# 路径规则配置
|
|
182
|
+
self.dir_rule = DirRule(**dir_rule)
|
|
183
|
+
# 客户端配置
|
|
184
|
+
self.client = AdvancedDict(client)
|
|
185
|
+
# 下载配置
|
|
186
|
+
self.download = AdvancedDict(download)
|
|
187
|
+
# 插件配置
|
|
188
|
+
self.plugins = AdvancedDict(plugins)
|
|
189
|
+
# 其他配置
|
|
190
|
+
self.filepath = filepath
|
|
191
|
+
|
|
192
|
+
# 需要主线程等待完成的插件
|
|
193
|
+
self.need_wait_plugins = []
|
|
194
|
+
|
|
195
|
+
if call_after_init_plugin:
|
|
196
|
+
self.call_all_plugin('after_init', safe=True)
|
|
197
|
+
|
|
198
|
+
def copy_option(self):
|
|
199
|
+
return self.__class__(
|
|
200
|
+
dir_rule={
|
|
201
|
+
'rule': self.dir_rule.rule_dsl,
|
|
202
|
+
'base_dir': self.dir_rule.base_dir,
|
|
203
|
+
},
|
|
204
|
+
download=self.download.src_dict,
|
|
205
|
+
client=self.client.src_dict,
|
|
206
|
+
plugins=self.plugins.src_dict,
|
|
207
|
+
filepath=self.filepath,
|
|
208
|
+
call_after_init_plugin=False
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
下面是decide系列方法,为了支持重写和增加程序动态性。
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
# noinspection PyUnusedLocal
|
|
216
|
+
def decide_image_batch_count(self, photo: JmPhotoDetail):
|
|
217
|
+
return self.download.threading.image
|
|
218
|
+
|
|
219
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
220
|
+
def decide_photo_batch_count(self, album: JmAlbumDetail):
|
|
221
|
+
return self.download.threading.photo
|
|
222
|
+
|
|
223
|
+
# noinspection PyMethodMayBeStatic
|
|
224
|
+
def decide_image_filename(self, image: JmImageDetail) -> str:
|
|
225
|
+
"""
|
|
226
|
+
返回图片的文件名,不包含后缀
|
|
227
|
+
默认返回禁漫的图片文件名,例如:00001 (.jpg)
|
|
228
|
+
"""
|
|
229
|
+
return image.filename_without_suffix
|
|
230
|
+
|
|
231
|
+
def decide_image_suffix(self, image: JmImageDetail) -> str:
|
|
232
|
+
"""
|
|
233
|
+
返回图片的后缀,如果返回的后缀和原后缀不一致,则会进行图片格式转换
|
|
234
|
+
"""
|
|
235
|
+
# 动图则使用原后缀
|
|
236
|
+
if image.is_gif:
|
|
237
|
+
return image.img_file_suffix
|
|
238
|
+
|
|
239
|
+
# 非动图,以配置为先
|
|
240
|
+
return self.download.image.suffix or image.img_file_suffix
|
|
241
|
+
|
|
242
|
+
def decide_image_save_dir(self, photo, ensure_exists=True) -> str:
|
|
243
|
+
# 使用 self.dir_rule 决定 save_dir
|
|
244
|
+
save_dir = self.dir_rule.decide_image_save_dir(
|
|
245
|
+
photo.from_album,
|
|
246
|
+
photo
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if ensure_exists:
|
|
250
|
+
save_dir = JmcomicText.try_mkdir(save_dir)
|
|
251
|
+
|
|
252
|
+
return save_dir
|
|
253
|
+
|
|
254
|
+
def decide_image_filepath(self, image: JmImageDetail, consider_custom_suffix=True) -> str:
|
|
255
|
+
# 以此决定保存文件夹、后缀、不包含后缀的文件名
|
|
256
|
+
save_dir = self.decide_image_save_dir(image.from_photo)
|
|
257
|
+
suffix = self.decide_image_suffix(image) if consider_custom_suffix else image.img_file_suffix
|
|
258
|
+
return os.path.join(save_dir, fix_windir_name(self.decide_image_filename(image)) + suffix)
|
|
259
|
+
|
|
260
|
+
def decide_download_cache(self, _image: JmImageDetail) -> bool:
|
|
261
|
+
return self.download.cache
|
|
262
|
+
|
|
263
|
+
def decide_download_image_decode(self, image: JmImageDetail) -> bool:
|
|
264
|
+
# .gif file needn't be decoded
|
|
265
|
+
if image.is_gif:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
return self.download.image.decode
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
下面是创建对象相关方法
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def default_dict(cls) -> Dict:
|
|
276
|
+
return JmModuleConfig.option_default_dict()
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def default(cls) -> 'JmOption':
|
|
280
|
+
"""
|
|
281
|
+
使用默认的 JmOption
|
|
282
|
+
"""
|
|
283
|
+
return cls.construct({})
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def construct(cls, origdic: Dict, cover_default=True) -> 'JmOption':
|
|
287
|
+
dic = cls.merge_default_dict(origdic) if cover_default else origdic
|
|
288
|
+
|
|
289
|
+
# log
|
|
290
|
+
log = dic.pop('log', True)
|
|
291
|
+
if log is False:
|
|
292
|
+
disable_jm_log()
|
|
293
|
+
|
|
294
|
+
# version
|
|
295
|
+
version = dic.pop('version', None)
|
|
296
|
+
# noinspection PyTypeChecker
|
|
297
|
+
if version is not None and float(version) >= float(JmModuleConfig.JM_OPTION_VER):
|
|
298
|
+
# 版本号更高,跳过兼容代码
|
|
299
|
+
return cls(**dic)
|
|
300
|
+
|
|
301
|
+
# 旧版本option,做兼容
|
|
302
|
+
cls.compatible_with_old_versions(dic)
|
|
303
|
+
|
|
304
|
+
return cls(**dic)
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def compatible_with_old_versions(cls, dic):
|
|
308
|
+
"""
|
|
309
|
+
兼容旧的option版本
|
|
310
|
+
"""
|
|
311
|
+
# 1: 并发配置项
|
|
312
|
+
dt: dict = dic['download']['threading']
|
|
313
|
+
if 'batch_count' in dt:
|
|
314
|
+
batch_count = dt.pop('batch_count')
|
|
315
|
+
dt['image'] = batch_count
|
|
316
|
+
|
|
317
|
+
# 2: 插件配置项 plugin -> plugins
|
|
318
|
+
if 'plugin' in dic:
|
|
319
|
+
dic['plugins'] = dic.pop('plugin')
|
|
320
|
+
|
|
321
|
+
def deconstruct(self) -> Dict:
|
|
322
|
+
return {
|
|
323
|
+
'version': JmModuleConfig.JM_OPTION_VER,
|
|
324
|
+
'log': JmModuleConfig.FLAG_ENABLE_JM_LOG,
|
|
325
|
+
'dir_rule': {
|
|
326
|
+
'rule': self.dir_rule.rule_dsl,
|
|
327
|
+
'base_dir': self.dir_rule.base_dir,
|
|
328
|
+
},
|
|
329
|
+
'download': self.download.src_dict,
|
|
330
|
+
'client': self.client.src_dict,
|
|
331
|
+
'plugins': self.plugins.src_dict
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
下面是文件IO方法
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def from_file(cls, filepath: str) -> 'JmOption':
|
|
340
|
+
dic: dict = PackerUtil.unpack(filepath)[0]
|
|
341
|
+
dic.setdefault('filepath', filepath)
|
|
342
|
+
return cls.construct(dic)
|
|
343
|
+
|
|
344
|
+
def to_file(self, filepath=None):
|
|
345
|
+
if filepath is None:
|
|
346
|
+
filepath = self.filepath
|
|
347
|
+
|
|
348
|
+
ExceptionTool.require_true(filepath is not None, "未指定JmOption的保存路径")
|
|
349
|
+
|
|
350
|
+
PackerUtil.pack(self.deconstruct(), filepath)
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
下面是创建客户端的相关方法
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
@field_cache()
|
|
357
|
+
def build_jm_client(self, **kwargs):
|
|
358
|
+
"""
|
|
359
|
+
该方法会首次调用会创建JmcomicClient对象,
|
|
360
|
+
然后保存在self中,
|
|
361
|
+
多次调用`不会`创建新的JmcomicClient对象
|
|
362
|
+
"""
|
|
363
|
+
return self.new_jm_client(**kwargs)
|
|
364
|
+
|
|
365
|
+
def new_jm_client(self, domain_list=None, impl=None, cache=None, **kwargs) -> Union[JmHtmlClient, JmApiClient]:
|
|
366
|
+
"""
|
|
367
|
+
创建新的Client(客户端),不同Client之间的元数据不共享
|
|
368
|
+
"""
|
|
369
|
+
from copy import deepcopy
|
|
370
|
+
|
|
371
|
+
# 所有需要用到的 self.client 配置项如下
|
|
372
|
+
postman_conf: dict = deepcopy(self.client.postman.src_dict) # postman dsl 配置
|
|
373
|
+
|
|
374
|
+
meta_data: dict = postman_conf['meta_data'] # 元数据
|
|
375
|
+
|
|
376
|
+
retry_times: int = self.client.retry_times # 重试次数
|
|
377
|
+
|
|
378
|
+
cache: str = cache if cache is not None else self.client.cache # 启用缓存
|
|
379
|
+
|
|
380
|
+
impl: str = impl or self.client.impl # client_key
|
|
381
|
+
|
|
382
|
+
if isinstance(impl, type):
|
|
383
|
+
# eg: impl = JmHtmlClient
|
|
384
|
+
# noinspection PyUnresolvedReferences
|
|
385
|
+
impl = impl.client_key
|
|
386
|
+
|
|
387
|
+
# start construct client
|
|
388
|
+
|
|
389
|
+
# domain
|
|
390
|
+
def decide_domain_list():
|
|
391
|
+
nonlocal domain_list
|
|
392
|
+
|
|
393
|
+
if domain_list is None:
|
|
394
|
+
domain_list = self.client.domain
|
|
395
|
+
|
|
396
|
+
if not isinstance(domain_list, (list, str)):
|
|
397
|
+
# dict
|
|
398
|
+
domain_list = domain_list.get(impl, [])
|
|
399
|
+
|
|
400
|
+
if isinstance(domain_list, str):
|
|
401
|
+
# multi-lines text
|
|
402
|
+
domain_list = str_to_list(domain_list)
|
|
403
|
+
|
|
404
|
+
# list or str
|
|
405
|
+
if len(domain_list) == 0:
|
|
406
|
+
domain_list = self.decide_client_domain(impl)
|
|
407
|
+
|
|
408
|
+
return domain_list
|
|
409
|
+
|
|
410
|
+
# support kwargs overwrite meta_data
|
|
411
|
+
if len(kwargs) != 0:
|
|
412
|
+
meta_data.update(kwargs)
|
|
413
|
+
|
|
414
|
+
# postman
|
|
415
|
+
postman = Postmans.create(data=postman_conf)
|
|
416
|
+
|
|
417
|
+
# client
|
|
418
|
+
clazz = JmModuleConfig.client_impl_class(impl)
|
|
419
|
+
if clazz == AbstractJmClient or not issubclass(clazz, AbstractJmClient):
|
|
420
|
+
raise NotImplementedError(clazz)
|
|
421
|
+
|
|
422
|
+
client: AbstractJmClient = clazz(
|
|
423
|
+
postman=postman,
|
|
424
|
+
domain_list=decide_domain_list(),
|
|
425
|
+
retry_times=retry_times,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# enable cache
|
|
429
|
+
CacheRegistry.enable_client_cache_on_condition(self, client, cache)
|
|
430
|
+
|
|
431
|
+
# noinspection PyTypeChecker
|
|
432
|
+
return client
|
|
433
|
+
|
|
434
|
+
def update_cookies(self, cookies: dict):
|
|
435
|
+
metadata: dict = self.client.postman.meta_data.src_dict
|
|
436
|
+
orig_cookies: Optional[Dict] = metadata.get('cookies', None)
|
|
437
|
+
if orig_cookies is None:
|
|
438
|
+
metadata['cookies'] = cookies
|
|
439
|
+
else:
|
|
440
|
+
orig_cookies.update(cookies)
|
|
441
|
+
metadata['cookies'] = orig_cookies
|
|
442
|
+
|
|
443
|
+
# noinspection PyMethodMayBeStatic,PyTypeChecker
|
|
444
|
+
def decide_client_domain(self, client_key: str) -> List[str]:
|
|
445
|
+
def is_client_type(ctype) -> bool:
|
|
446
|
+
return self.client_key_is_given_type(client_key, ctype)
|
|
447
|
+
|
|
448
|
+
if is_client_type(JmApiClient):
|
|
449
|
+
# 移动端
|
|
450
|
+
return JmModuleConfig.DOMAIN_API_LIST
|
|
451
|
+
|
|
452
|
+
if is_client_type(JmHtmlClient):
|
|
453
|
+
# 网页端
|
|
454
|
+
domain_list = JmModuleConfig.DOMAIN_HTML_LIST
|
|
455
|
+
if domain_list is not None:
|
|
456
|
+
return domain_list
|
|
457
|
+
return [JmModuleConfig.get_html_domain()]
|
|
458
|
+
|
|
459
|
+
ExceptionTool.raises(f'没有配置域名,且是无法识别的client类型: {client_key}')
|
|
460
|
+
|
|
461
|
+
@classmethod
|
|
462
|
+
def client_key_is_given_type(cls, client_key, ctype: Type[JmcomicClient]):
|
|
463
|
+
if client_key == ctype.client_key:
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
clazz = JmModuleConfig.client_impl_class(client_key)
|
|
467
|
+
if issubclass(clazz, ctype):
|
|
468
|
+
return True
|
|
469
|
+
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
@classmethod
|
|
473
|
+
def merge_default_dict(cls, user_dict, default_dict=None):
|
|
474
|
+
"""
|
|
475
|
+
深度合并两个字典
|
|
476
|
+
"""
|
|
477
|
+
if default_dict is None:
|
|
478
|
+
default_dict = cls.default_dict()
|
|
479
|
+
|
|
480
|
+
for key, value in user_dict.items():
|
|
481
|
+
if isinstance(value, dict) and isinstance(default_dict.get(key), dict):
|
|
482
|
+
default_dict[key] = cls.merge_default_dict(value, default_dict[key])
|
|
483
|
+
else:
|
|
484
|
+
default_dict[key] = value
|
|
485
|
+
return default_dict
|
|
486
|
+
|
|
487
|
+
# 下面的方法提供面向对象的调用风格
|
|
488
|
+
|
|
489
|
+
def download_album(self,
|
|
490
|
+
album_id,
|
|
491
|
+
downloader=None,
|
|
492
|
+
callback=None,
|
|
493
|
+
):
|
|
494
|
+
from .api import download_album
|
|
495
|
+
download_album(album_id, self, downloader, callback)
|
|
496
|
+
|
|
497
|
+
def download_photo(self,
|
|
498
|
+
photo_id,
|
|
499
|
+
downloader=None,
|
|
500
|
+
callback=None
|
|
501
|
+
):
|
|
502
|
+
from .api import download_photo
|
|
503
|
+
download_photo(photo_id, self, downloader, callback)
|
|
504
|
+
|
|
505
|
+
# 下面的方法为调用插件提供支持
|
|
506
|
+
|
|
507
|
+
def call_all_plugin(self, group: str, safe=True, **extra):
|
|
508
|
+
plugin_list: List[dict] = self.plugins.get(group, [])
|
|
509
|
+
if plugin_list is None or len(plugin_list) == 0:
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
# 保证 jm_plugin.py 被加载
|
|
513
|
+
from .jm_plugin import JmOptionPlugin
|
|
514
|
+
|
|
515
|
+
plugin_registry = JmModuleConfig.REGISTRY_PLUGIN
|
|
516
|
+
for pinfo in plugin_list:
|
|
517
|
+
key, kwargs = pinfo['plugin'], pinfo.get('kwargs', None) # kwargs为None
|
|
518
|
+
pclass: Optional[Type[JmOptionPlugin]] = plugin_registry.get(key, None)
|
|
519
|
+
|
|
520
|
+
ExceptionTool.require_true(pclass is not None, f'[{group}] 未注册的plugin: {key}')
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
self.invoke_plugin(pclass, kwargs, extra, pinfo)
|
|
524
|
+
except BaseException as e:
|
|
525
|
+
if safe is True:
|
|
526
|
+
traceback_print_exec()
|
|
527
|
+
else:
|
|
528
|
+
raise e
|
|
529
|
+
|
|
530
|
+
def invoke_plugin(self, pclass, kwargs: Optional[Dict], extra: dict, pinfo: dict):
|
|
531
|
+
# 检查插件的参数类型
|
|
532
|
+
kwargs = self.fix_kwargs(kwargs)
|
|
533
|
+
# 把插件的配置数据kwargs和附加数据extra合并,extra会覆盖kwargs
|
|
534
|
+
if len(extra) != 0:
|
|
535
|
+
kwargs.update(extra)
|
|
536
|
+
|
|
537
|
+
# 保证 jm_plugin.py 被加载
|
|
538
|
+
from .jm_plugin import JmOptionPlugin, PluginValidationException
|
|
539
|
+
|
|
540
|
+
pclass: Type[JmOptionPlugin]
|
|
541
|
+
plugin: Optional[JmOptionPlugin] = None
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
# 构建插件对象
|
|
545
|
+
plugin: JmOptionPlugin = pclass.build(self)
|
|
546
|
+
|
|
547
|
+
# 设置日志开关
|
|
548
|
+
if pinfo.get('log', True) is not True:
|
|
549
|
+
plugin.log_enable = False
|
|
550
|
+
|
|
551
|
+
jm_log('plugin.invoke', f'调用插件: [{pclass.plugin_key}]')
|
|
552
|
+
|
|
553
|
+
# 调用插件功能
|
|
554
|
+
plugin.invoke(**kwargs)
|
|
555
|
+
|
|
556
|
+
except PluginValidationException as e:
|
|
557
|
+
# 插件抛出的参数校验异常
|
|
558
|
+
self.handle_plugin_valid_exception(e, pinfo, kwargs, plugin, pclass)
|
|
559
|
+
|
|
560
|
+
except JmcomicException as e:
|
|
561
|
+
# 模块内部异常,通过不是插件抛出的,而是插件调用了例如Client,Client请求失败抛出的
|
|
562
|
+
self.handle_plugin_jmcomic_exception(e, pinfo, kwargs, plugin, pclass)
|
|
563
|
+
|
|
564
|
+
except BaseException as e:
|
|
565
|
+
# 为插件兜底,捕获其他所有异常
|
|
566
|
+
self.handle_plugin_unexpected_error(e, pinfo, kwargs, plugin, pclass)
|
|
567
|
+
|
|
568
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
569
|
+
def handle_plugin_valid_exception(self, e, pinfo: dict, kwargs: dict, _plugin, _pclass):
|
|
570
|
+
from .jm_plugin import PluginValidationException
|
|
571
|
+
e: PluginValidationException
|
|
572
|
+
|
|
573
|
+
mode = pinfo.get('valid', self.plugins.valid)
|
|
574
|
+
|
|
575
|
+
if mode == 'ignore':
|
|
576
|
+
# ignore
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
if mode == 'log':
|
|
580
|
+
# log
|
|
581
|
+
jm_log('plugin.validation',
|
|
582
|
+
f'插件 [{e.plugin.plugin_key}] 参数校验异常:{e.msg}'
|
|
583
|
+
)
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
if mode == 'raise':
|
|
587
|
+
# raise
|
|
588
|
+
raise e
|
|
589
|
+
|
|
590
|
+
# 其他的mode可以通过继承+方法重写来扩展
|
|
591
|
+
|
|
592
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
593
|
+
def handle_plugin_unexpected_error(self, e, pinfo: dict, kwargs: dict, _plugin, pclass):
|
|
594
|
+
msg = str(e)
|
|
595
|
+
jm_log('plugin.error', f'插件 [{pclass.plugin_key}],运行遇到未捕获异常,异常信息: [{msg}]')
|
|
596
|
+
raise e
|
|
597
|
+
|
|
598
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
599
|
+
def handle_plugin_jmcomic_exception(self, e, pinfo: dict, kwargs: dict, _plugin, pclass):
|
|
600
|
+
msg = str(e)
|
|
601
|
+
jm_log('plugin.exception', f'插件 [{pclass.plugin_key}] 调用失败,异常信息: [{msg}]')
|
|
602
|
+
raise e
|
|
603
|
+
|
|
604
|
+
# noinspection PyMethodMayBeStatic
|
|
605
|
+
def fix_kwargs(self, kwargs: Optional[Dict]) -> Dict[str, Any]:
|
|
606
|
+
"""
|
|
607
|
+
kwargs将来要传给方法参数,这要求kwargs的key是str类型,
|
|
608
|
+
该方法检查kwargs的key的类型,如果不是str,尝试转为str,不行则抛异常。
|
|
609
|
+
"""
|
|
610
|
+
if kwargs is None:
|
|
611
|
+
kwargs = {}
|
|
612
|
+
else:
|
|
613
|
+
ExceptionTool.require_true(
|
|
614
|
+
isinstance(kwargs, dict),
|
|
615
|
+
f'插件的kwargs参数必须为dict类型,而不能是类型: {type(kwargs)}'
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
kwargs: dict
|
|
619
|
+
new_kwargs: Dict[str, Any] = {}
|
|
620
|
+
|
|
621
|
+
for k, v in kwargs.items():
|
|
622
|
+
if isinstance(v, str):
|
|
623
|
+
newv = JmcomicText.parse_dsl_text(v)
|
|
624
|
+
v = newv
|
|
625
|
+
|
|
626
|
+
if isinstance(k, str):
|
|
627
|
+
new_kwargs[k] = v
|
|
628
|
+
continue
|
|
629
|
+
|
|
630
|
+
if isinstance(k, (int, float)):
|
|
631
|
+
newk = str(k)
|
|
632
|
+
jm_log('plugin.kwargs', f'插件参数类型转换: {k} ({type(k)}) -> {newk} ({type(newk)})')
|
|
633
|
+
new_kwargs[newk] = v
|
|
634
|
+
continue
|
|
635
|
+
|
|
636
|
+
ExceptionTool.raises(
|
|
637
|
+
f'插件kwargs参数类型有误,'
|
|
638
|
+
f'字段: {k},预期类型为str,实际类型为{type(k)}'
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
return new_kwargs
|
|
642
|
+
|
|
643
|
+
def wait_all_plugins_finish(self):
|
|
644
|
+
from .jm_plugin import JmOptionPlugin
|
|
645
|
+
for plugin in self.need_wait_plugins:
|
|
646
|
+
plugin: JmOptionPlugin
|
|
647
|
+
plugin.wait_until_finish()
|