jmcomic 2.3.14__tar.gz → 2.3.16__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.3.14/src/jmcomic.egg-info → jmcomic-2.3.16}/PKG-INFO +4 -4
- {jmcomic-2.3.14 → jmcomic-2.3.16}/README.md +3 -3
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/__init__.py +1 -1
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_client_impl.py +15 -8
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_config.py +35 -14
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_downloader.py +0 -4
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_option.py +67 -22
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_plugin.py +90 -37
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_toolkit.py +1 -1
- {jmcomic-2.3.14 → jmcomic-2.3.16/src/jmcomic.egg-info}/PKG-INFO +4 -4
- {jmcomic-2.3.14 → jmcomic-2.3.16}/LICENSE +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/setup.cfg +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/setup.py +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/api.py +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/cl.py +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_client_interface.py +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic/jm_entity.py +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic.egg-info/SOURCES.txt +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic.egg-info/dependency_links.txt +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic.egg-info/entry_points.txt +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic.egg-info/requires.txt +0 -0
- {jmcomic-2.3.14 → jmcomic-2.3.16}/src/jmcomic.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: jmcomic
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.16
|
|
4
4
|
Summary: Python API For JMComic (禁漫天堂)
|
|
5
5
|
Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
|
|
6
6
|
Author: hect0x7
|
|
@@ -68,7 +68,7 @@ $ jmcomic 422866
|
|
|
68
68
|
|
|
69
69
|
- GitHub Actions:网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md))
|
|
70
70
|
- 命令行:无需写Python代码,简单易用([教程:使用命令行下载禁漫本子](./assets/docs/sources/tutorial/2_command_line.md))
|
|
71
|
-
- Python
|
|
71
|
+
- Python代码:最本质、最强大的使用方式,需要你有一定的python编程基础
|
|
72
72
|
- 支持**网页端**和**移动端**两种客户端实现,可通过配置切换(**移动端不限ip兼容性好,网页端限制ip地区但效率高**)
|
|
73
73
|
- 支持**自动重试和域名切换**机制
|
|
74
74
|
- **多线程下载**(可细化到一图一线程,效率极高)
|
|
@@ -80,9 +80,9 @@ $ jmcomic 422866
|
|
|
80
80
|
- **可扩展性强**
|
|
81
81
|
|
|
82
82
|
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件**
|
|
83
|
-
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件`
|
|
83
|
+
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件` `日志主题过滤插件`
|
|
84
84
|
- 支持自定义本子/章节/图片下载前后的回调函数
|
|
85
|
-
- 支持自定义debug
|
|
85
|
+
- 支持自定义debug/logging
|
|
86
86
|
- 支持自定义类:`Downloader(负责调度)` `Option(负责配置)` `Client(负责请求)` `实体类`等
|
|
87
87
|
|
|
88
88
|
## 进阶使用
|
|
@@ -42,7 +42,7 @@ $ jmcomic 422866
|
|
|
42
42
|
|
|
43
43
|
- GitHub Actions:网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md))
|
|
44
44
|
- 命令行:无需写Python代码,简单易用([教程:使用命令行下载禁漫本子](./assets/docs/sources/tutorial/2_command_line.md))
|
|
45
|
-
- Python
|
|
45
|
+
- Python代码:最本质、最强大的使用方式,需要你有一定的python编程基础
|
|
46
46
|
- 支持**网页端**和**移动端**两种客户端实现,可通过配置切换(**移动端不限ip兼容性好,网页端限制ip地区但效率高**)
|
|
47
47
|
- 支持**自动重试和域名切换**机制
|
|
48
48
|
- **多线程下载**(可细化到一图一线程,效率极高)
|
|
@@ -54,9 +54,9 @@ $ jmcomic 422866
|
|
|
54
54
|
- **可扩展性强**
|
|
55
55
|
|
|
56
56
|
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件**
|
|
57
|
-
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件`
|
|
57
|
+
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件` `日志主题过滤插件`
|
|
58
58
|
- 支持自定义本子/章节/图片下载前后的回调函数
|
|
59
|
-
- 支持自定义debug
|
|
59
|
+
- 支持自定义debug/logging
|
|
60
60
|
- 支持自定义类:`Downloader(负责调度)` `Option(负责配置)` `Client(负责请求)` `实体类`等
|
|
61
61
|
|
|
62
62
|
## 进阶使用
|
|
@@ -493,13 +493,13 @@ class JmApiClient(AbstractJmClient):
|
|
|
493
493
|
}
|
|
494
494
|
)
|
|
495
495
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
jm_debug('api.scramble', '
|
|
502
|
-
scramble_id =
|
|
496
|
+
scramble_id = PatternTool.match_or_default(resp.text,
|
|
497
|
+
JmcomicText.pattern_html_album_scramble_id,
|
|
498
|
+
None,
|
|
499
|
+
)
|
|
500
|
+
if scramble_id is None:
|
|
501
|
+
jm_debug('api.scramble', f'未匹配到scramble_id,响应文本:{resp.text}')
|
|
502
|
+
scramble_id = str(JmModuleConfig.SCRAMBLE_220980)
|
|
503
503
|
|
|
504
504
|
return scramble_id
|
|
505
505
|
|
|
@@ -584,7 +584,7 @@ class JmApiClient(AbstractJmClient):
|
|
|
584
584
|
def get_decode(self, url, **kwargs) -> JmApiResp:
|
|
585
585
|
# set headers
|
|
586
586
|
headers, key_ts = self.headers_key_ts
|
|
587
|
-
kwargs
|
|
587
|
+
kwargs['headers'] = headers
|
|
588
588
|
|
|
589
589
|
resp = self.get(url, **kwargs)
|
|
590
590
|
return JmApiResp.wrap(resp, key_ts)
|
|
@@ -610,6 +610,13 @@ class JmApiClient(AbstractJmClient):
|
|
|
610
610
|
# 2. 是否是特殊的内容
|
|
611
611
|
# 暂无
|
|
612
612
|
|
|
613
|
+
def after_init(self):
|
|
614
|
+
# cookies = self.__class__.fetch_init_cookies(self)
|
|
615
|
+
# self.get_root_postman().get_meta_data()['cookies'] = cookies
|
|
616
|
+
|
|
617
|
+
self.get_root_postman().get_meta_data()['cookies'] = JmModuleConfig.get_cookies(self)
|
|
618
|
+
pass
|
|
619
|
+
|
|
613
620
|
|
|
614
621
|
class FutureClientProxy(JmcomicClient):
|
|
615
622
|
"""
|
|
@@ -3,7 +3,7 @@ def field_cache(*args, **kwargs):
|
|
|
3
3
|
return field_cache(*args, **kwargs)
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
6
|
+
def default_jm_debug_logging(topic: str, msg: str):
|
|
7
7
|
from common import format_ts
|
|
8
8
|
print(f'{format_ts()}:【{topic}】{msg}')
|
|
9
9
|
|
|
@@ -62,13 +62,14 @@ class JmModuleConfig:
|
|
|
62
62
|
SCRAMBLE_421926 = 421926 # 2023-02-08后改了图片切割算法
|
|
63
63
|
SCRAMBLE_CACHE = {}
|
|
64
64
|
|
|
65
|
-
# 移动端API
|
|
66
|
-
|
|
67
|
-
MAGIC_18COMICAPPCONTENT = '18comicAPPContent'
|
|
65
|
+
# 移动端API密钥
|
|
66
|
+
APP_SECRET = '18comicAPPContent'
|
|
68
67
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
# cookies,目前只在移动端使用,因为移动端请求接口须携带,但不会校验cookies的内容。
|
|
69
|
+
APP_COOKIES = None
|
|
70
|
+
|
|
71
|
+
# 移动端图片域名
|
|
72
|
+
DOMAIN_IMAGE_LIST = str_to_list('''
|
|
72
73
|
cdn-msp.jmapiproxy1.monster
|
|
73
74
|
cdn-msp2.jmapiproxy1.monster
|
|
74
75
|
cdn-msp.jmapiproxy1.cc
|
|
@@ -78,7 +79,7 @@ class JmModuleConfig:
|
|
|
78
79
|
|
|
79
80
|
''')
|
|
80
81
|
|
|
81
|
-
# API域名
|
|
82
|
+
# 移动端API域名
|
|
82
83
|
DOMAIN_API_LIST = str_to_list('''
|
|
83
84
|
www.jmapinode1.top
|
|
84
85
|
www.jmapinode2.top
|
|
@@ -88,8 +89,11 @@ class JmModuleConfig:
|
|
|
88
89
|
|
|
89
90
|
''')
|
|
90
91
|
|
|
91
|
-
#
|
|
92
|
+
# 网页端域名配置
|
|
92
93
|
# 无需配置,默认为None,需要的时候会发起请求获得
|
|
94
|
+
# 使用优先级:
|
|
95
|
+
# 1. DOMAIN_HTML_LIST
|
|
96
|
+
# 2. [DOMAIN_HTML]
|
|
93
97
|
DOMAIN_HTML = None
|
|
94
98
|
DOMAIN_HTML_LIST = None
|
|
95
99
|
|
|
@@ -106,7 +110,7 @@ class JmModuleConfig:
|
|
|
106
110
|
REGISTRY_PLUGIN = {}
|
|
107
111
|
|
|
108
112
|
# 执行debug的函数
|
|
109
|
-
debug_executor =
|
|
113
|
+
debug_executor = default_jm_debug_logging
|
|
110
114
|
# postman构造函数
|
|
111
115
|
postman_constructor = default_postman_constructor
|
|
112
116
|
# 网页正则表达式解析失败时,执行抛出异常的函数,可以替换掉用于debug
|
|
@@ -190,7 +194,7 @@ class JmModuleConfig:
|
|
|
190
194
|
postman = postman or cls.new_postman(session=True)
|
|
191
195
|
|
|
192
196
|
url = postman.with_redirect_catching().get(cls.JM_REDIRECT_URL)
|
|
193
|
-
cls.jm_debug('
|
|
197
|
+
cls.jm_debug('module.html_url', f'获取禁漫网页URL: [{cls.JM_REDIRECT_URL}] → [{url}]')
|
|
194
198
|
return url
|
|
195
199
|
|
|
196
200
|
@classmethod
|
|
@@ -211,9 +215,22 @@ class JmModuleConfig:
|
|
|
211
215
|
from .jm_toolkit import JmcomicText
|
|
212
216
|
domain_list = JmcomicText.analyse_jm_pub_html(resp.text)
|
|
213
217
|
|
|
214
|
-
cls.jm_debug('
|
|
218
|
+
cls.jm_debug('module.html_domain_all', f'获取禁漫网页全部域名: [{resp.url}] → {domain_list}')
|
|
215
219
|
return domain_list
|
|
216
220
|
|
|
221
|
+
@classmethod
|
|
222
|
+
@field_cache("APP_COOKIES")
|
|
223
|
+
def get_cookies(cls, postman=None):
|
|
224
|
+
from .jm_toolkit import JmcomicText
|
|
225
|
+
url = JmcomicText.format_url('/setting', cls.DOMAIN_API_LIST[0])
|
|
226
|
+
postman = postman or cls.new_postman()
|
|
227
|
+
|
|
228
|
+
resp = postman.get(url)
|
|
229
|
+
cookies = dict(resp.cookies)
|
|
230
|
+
|
|
231
|
+
cls.jm_debug('module.cookies', f'获取cookies: [{url}] → {cookies}')
|
|
232
|
+
return cookies
|
|
233
|
+
|
|
217
234
|
@classmethod
|
|
218
235
|
def new_html_headers(cls, domain='18comic.vip'):
|
|
219
236
|
"""
|
|
@@ -247,7 +264,7 @@ class JmModuleConfig:
|
|
|
247
264
|
key_ts = time_stamp()
|
|
248
265
|
|
|
249
266
|
import hashlib
|
|
250
|
-
token = hashlib.md5(f"{key_ts}{cls.
|
|
267
|
+
token = hashlib.md5(f"{key_ts}{cls.APP_SECRET}".encode("utf-8")).hexdigest()
|
|
251
268
|
|
|
252
269
|
return {
|
|
253
270
|
'token': token,
|
|
@@ -331,7 +348,11 @@ class JmModuleConfig:
|
|
|
331
348
|
'impl': None,
|
|
332
349
|
'retry_times': 5
|
|
333
350
|
},
|
|
334
|
-
'plugins': {
|
|
351
|
+
'plugins': {
|
|
352
|
+
# 如果插件抛出参数校验异常,只debug。(全局配置,可以被插件的局部配置覆盖)
|
|
353
|
+
# 可选值:ignore(忽略),debug(打印日志),raise(抛异常)。
|
|
354
|
+
'valid': 'debug',
|
|
355
|
+
},
|
|
335
356
|
}
|
|
336
357
|
|
|
337
358
|
@classmethod
|
|
@@ -311,12 +311,13 @@ class JmOption:
|
|
|
311
311
|
"""
|
|
312
312
|
return self.new_jm_client(**kwargs)
|
|
313
313
|
|
|
314
|
-
def new_jm_client(self, domain=None, impl=None, **kwargs) -> JmcomicClient:
|
|
314
|
+
def new_jm_client(self, domain=None, impl=None, cache=None, **kwargs) -> JmcomicClient:
|
|
315
315
|
# 所有需要用到的 self.client 配置项如下
|
|
316
316
|
postman_conf: dict = self.client.postman.src_dict # postman dsl 配置
|
|
317
|
+
meta_data: dict = postman_conf['meta_data'] # 请求元信息
|
|
317
318
|
impl: str = impl or self.client.impl # client_key
|
|
318
319
|
retry_times: int = self.client.retry_times # 重试次数
|
|
319
|
-
cache: str = self.client.cache # 启用缓存
|
|
320
|
+
cache: str = cache or self.client.cache # 启用缓存
|
|
320
321
|
|
|
321
322
|
# domain
|
|
322
323
|
def decide_domain():
|
|
@@ -335,10 +336,9 @@ class JmOption:
|
|
|
335
336
|
|
|
336
337
|
# support kwargs overwrite meta_data
|
|
337
338
|
if len(kwargs) != 0:
|
|
338
|
-
|
|
339
|
+
meta_data.update(kwargs)
|
|
339
340
|
|
|
340
341
|
# headers
|
|
341
|
-
meta_data = postman_conf['meta_data']
|
|
342
342
|
if meta_data['headers'] is None:
|
|
343
343
|
meta_data['headers'] = self.decide_postman_headers(impl, domain[0])
|
|
344
344
|
|
|
@@ -349,7 +349,8 @@ class JmOption:
|
|
|
349
349
|
clazz = JmModuleConfig.client_impl_class(impl)
|
|
350
350
|
if clazz == AbstractJmClient or not issubclass(clazz, AbstractJmClient):
|
|
351
351
|
raise NotImplementedError(clazz)
|
|
352
|
-
|
|
352
|
+
|
|
353
|
+
client: AbstractJmClient = clazz(
|
|
353
354
|
postman,
|
|
354
355
|
retry_times,
|
|
355
356
|
fallback_domain_list=decide_domain(),
|
|
@@ -371,6 +372,9 @@ class JmOption:
|
|
|
371
372
|
|
|
372
373
|
if is_client_type(JmHtmlClient):
|
|
373
374
|
# 网页端
|
|
375
|
+
domain_list = JmModuleConfig.DOMAIN_HTML_LIST
|
|
376
|
+
if domain_list is not None:
|
|
377
|
+
return domain_list
|
|
374
378
|
return [JmModuleConfig.get_html_domain()]
|
|
375
379
|
|
|
376
380
|
ExceptionTool.raises(f'没有配置域名,且是无法识别的client类型: {client_key}')
|
|
@@ -442,36 +446,77 @@ class JmOption:
|
|
|
442
446
|
|
|
443
447
|
ExceptionTool.require_true(plugin_class is not None, f'[{group}] 未注册的plugin: {key}')
|
|
444
448
|
|
|
445
|
-
self.invoke_plugin(plugin_class, kwargs, extra)
|
|
449
|
+
self.invoke_plugin(plugin_class, kwargs, extra, pinfo)
|
|
450
|
+
|
|
451
|
+
def invoke_plugin(self, plugin_class, kwargs: Any, extra: dict, pinfo: dict):
|
|
452
|
+
# 检查插件的参数类型
|
|
453
|
+
kwargs = self.fix_kwargs(kwargs)
|
|
454
|
+
# 把插件的配置数据kwargs和附加数据extra合并,extra会覆盖kwargs
|
|
455
|
+
if len(extra) != 0:
|
|
456
|
+
kwargs.update(extra)
|
|
446
457
|
|
|
447
|
-
def invoke_plugin(self, plugin_class, kwargs: Any, extra: dict):
|
|
448
458
|
# 保证 jm_plugin.py 被加载
|
|
449
|
-
from .jm_plugin import JmOptionPlugin
|
|
459
|
+
from .jm_plugin import JmOptionPlugin, PluginValidationException
|
|
450
460
|
|
|
461
|
+
plugin = plugin_class
|
|
451
462
|
plugin_class: Type[JmOptionPlugin]
|
|
452
|
-
pkey = plugin_class.plugin_key
|
|
453
463
|
|
|
454
464
|
try:
|
|
455
|
-
# 检查插件的参数类型
|
|
456
|
-
kwargs = self.fix_kwargs(kwargs)
|
|
457
|
-
# 把插件的配置数据kwargs和附加数据extra合并
|
|
458
|
-
# extra会覆盖kwargs
|
|
459
|
-
if len(extra) != 0:
|
|
460
|
-
kwargs.update(extra)
|
|
461
465
|
# 构建插件对象
|
|
462
|
-
plugin = plugin_class.build(self)
|
|
466
|
+
plugin: JmOptionPlugin = plugin_class.build(self)
|
|
467
|
+
|
|
468
|
+
jm_debug('plugin.invoke', f'调用插件: [{plugin_class.plugin_key}]')
|
|
463
469
|
# 调用插件功能
|
|
464
|
-
jm_debug('plugin.invoke', f'调用插件: [{pkey}]')
|
|
465
470
|
plugin.invoke(**kwargs)
|
|
471
|
+
|
|
472
|
+
except PluginValidationException as e:
|
|
473
|
+
# 插件抛出的参数校验异常
|
|
474
|
+
self.handle_plugin_valid_exception(e, pinfo, kwargs, plugin)
|
|
475
|
+
|
|
466
476
|
except JmcomicException as e:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
477
|
+
# 模块内部异常,通过不是插件抛出的,而是插件调用了例如Client,Client请求失败抛出的
|
|
478
|
+
self.handle_plugin_exception(e, pinfo, kwargs, plugin)
|
|
479
|
+
|
|
470
480
|
except BaseException as e:
|
|
471
|
-
|
|
472
|
-
|
|
481
|
+
# 为插件兜底,捕获其他所有异常
|
|
482
|
+
self.handle_plugin_unexpected_error(e, pinfo, kwargs, plugin)
|
|
483
|
+
|
|
484
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
485
|
+
def handle_plugin_valid_exception(self, e, pinfo: dict, kwargs: dict, plugin):
|
|
486
|
+
from .jm_plugin import PluginValidationException
|
|
487
|
+
e: PluginValidationException
|
|
488
|
+
|
|
489
|
+
mode = pinfo.get('valid', self.plugins.valid)
|
|
490
|
+
|
|
491
|
+
if mode == 'ignore':
|
|
492
|
+
# ignore
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
if mode == 'debug':
|
|
496
|
+
# debug
|
|
497
|
+
jm_debug('plugin.validation',
|
|
498
|
+
f'插件 [{e.plugin.plugin_key}] 参数校验异常:{e.msg}'
|
|
499
|
+
)
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
if mode == 'raise':
|
|
503
|
+
# raise
|
|
473
504
|
raise e
|
|
474
505
|
|
|
506
|
+
# 其他的mode可以通过继承+方法重写来扩展
|
|
507
|
+
|
|
508
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
509
|
+
def handle_plugin_unexpected_error(self, e, pinfo: dict, kwargs: dict, plugin):
|
|
510
|
+
msg = str(e)
|
|
511
|
+
jm_debug('plugin.error', f'插件 [{plugin.plugin_key}],运行遇到未捕获异常,异常信息: {msg}')
|
|
512
|
+
raise e
|
|
513
|
+
|
|
514
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
515
|
+
def handle_plugin_exception(self, e, pinfo: dict, kwargs: dict, plugin):
|
|
516
|
+
msg = str(e)
|
|
517
|
+
jm_debug('plugin.exception', f'插件 [{plugin.plugin_key}],调用失败,异常信息: {msg}')
|
|
518
|
+
raise e
|
|
519
|
+
|
|
475
520
|
# noinspection PyMethodMayBeStatic
|
|
476
521
|
def fix_kwargs(self, kwargs) -> Dict[str, Any]:
|
|
477
522
|
"""
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
from .jm_option import *
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class PluginValidationException(Exception):
|
|
9
|
+
|
|
10
|
+
def __init__(self, plugin: 'JmOptionPlugin', msg: str):
|
|
11
|
+
self.plugin = plugin
|
|
12
|
+
self.msg = msg
|
|
13
|
+
|
|
14
|
+
|
|
8
15
|
class JmOptionPlugin:
|
|
9
16
|
plugin_key: str
|
|
10
17
|
|
|
@@ -33,6 +40,15 @@ class JmOptionPlugin:
|
|
|
33
40
|
msg=msg
|
|
34
41
|
)
|
|
35
42
|
|
|
43
|
+
def require_true(self, case: Any, msg: str):
|
|
44
|
+
"""
|
|
45
|
+
独立于ExceptionTool的一套异常抛出体系
|
|
46
|
+
"""
|
|
47
|
+
if case:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
raise PluginValidationException(self, msg)
|
|
51
|
+
|
|
36
52
|
|
|
37
53
|
class JmLoginPlugin(JmOptionPlugin):
|
|
38
54
|
"""
|
|
@@ -44,8 +60,8 @@ class JmLoginPlugin(JmOptionPlugin):
|
|
|
44
60
|
username: str,
|
|
45
61
|
password: str,
|
|
46
62
|
) -> None:
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
self.require_true(username, '用户名不能为空')
|
|
64
|
+
self.require_true(password, '密码不能为空')
|
|
49
65
|
|
|
50
66
|
client = self.option.new_jm_client()
|
|
51
67
|
client.login(username, password)
|
|
@@ -233,26 +249,32 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
233
249
|
mkdir_if_not_exists(zip_dir)
|
|
234
250
|
|
|
235
251
|
# 原文件夹 -> zip文件
|
|
236
|
-
dir_zip_dict = {}
|
|
252
|
+
dir_zip_dict: Dict[str, Optional[str]] = {}
|
|
237
253
|
photo_dict = downloader.all_downloaded[album]
|
|
238
254
|
|
|
239
255
|
if level == 'album':
|
|
240
256
|
zip_path = self.get_zip_path(album, None, filename_rule, suffix, zip_dir)
|
|
241
257
|
dir_path = self.zip_album(album, photo_dict, zip_path)
|
|
242
|
-
|
|
258
|
+
if dir_path is not None:
|
|
259
|
+
# 要删除这个album文件夹
|
|
260
|
+
dir_zip_dict[dir_path] = zip_path
|
|
261
|
+
# 也要删除album下的photo文件夹
|
|
262
|
+
for d in files_of_dir(dir_path):
|
|
263
|
+
dir_zip_dict[d] = None
|
|
243
264
|
|
|
244
265
|
elif level == 'photo':
|
|
245
266
|
for photo, image_list in photo_dict.items():
|
|
246
267
|
zip_path = self.get_zip_path(None, photo, filename_rule, suffix, zip_dir)
|
|
247
268
|
dir_path = self.zip_photo(photo, image_list, zip_path)
|
|
248
|
-
|
|
269
|
+
if dir_path is not None:
|
|
270
|
+
dir_zip_dict[dir_path] = zip_path
|
|
249
271
|
|
|
250
272
|
else:
|
|
251
273
|
ExceptionTool.raises(f'Not Implemented Zip Level: {level}')
|
|
252
274
|
|
|
253
275
|
self.after_zip(dir_zip_dict)
|
|
254
276
|
|
|
255
|
-
def zip_photo(self, photo, image_list: list, zip_path: str):
|
|
277
|
+
def zip_photo(self, photo, image_list: list, zip_path: str) -> Optional[str]:
|
|
256
278
|
"""
|
|
257
279
|
压缩photo文件夹
|
|
258
280
|
:returns: photo文件夹路径
|
|
@@ -261,46 +283,54 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
261
283
|
if len(image_list) == 0 \
|
|
262
284
|
else os.path.dirname(image_list[0][0])
|
|
263
285
|
|
|
264
|
-
all_filepath = set(map(lambda t: t[0], image_list))
|
|
286
|
+
all_filepath = set(map(lambda t: self.unified_path(t[0]), image_list))
|
|
265
287
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
288
|
+
return self.do_zip(photo_dir,
|
|
289
|
+
zip_path,
|
|
290
|
+
all_filepath,
|
|
291
|
+
f'压缩章节[{photo.photo_id}]成功 → {zip_path}',
|
|
292
|
+
)
|
|
269
293
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return photo_dir
|
|
294
|
+
@staticmethod
|
|
295
|
+
def unified_path(f):
|
|
296
|
+
return fix_filepath(f, os.path.isdir(f))
|
|
275
297
|
|
|
276
|
-
def zip_album(self, album, photo_dict: dict, zip_path):
|
|
298
|
+
def zip_album(self, album, photo_dict: dict, zip_path) -> Optional[str]:
|
|
277
299
|
"""
|
|
278
300
|
压缩album文件夹
|
|
279
301
|
:returns: album文件夹路径
|
|
280
302
|
"""
|
|
281
|
-
album_dir = self.option.decide_album_dir(album)
|
|
282
303
|
all_filepath: Set[str] = set()
|
|
283
304
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
305
|
+
def addpath(f):
|
|
306
|
+
all_filepath.update(set(f))
|
|
307
|
+
|
|
308
|
+
album_dir = self.option.decide_album_dir(album)
|
|
309
|
+
# addpath(self.option.decide_image_save_dir(photo) for photo in photo_dict.keys())
|
|
310
|
+
addpath(path for ls in photo_dict.values() for path, _ in ls)
|
|
311
|
+
|
|
312
|
+
return self.do_zip(album_dir,
|
|
313
|
+
zip_path,
|
|
314
|
+
all_filepath,
|
|
315
|
+
msg=f'压缩本子[{album.album_id}]成功 → {zip_path}',
|
|
316
|
+
)
|
|
288
317
|
|
|
318
|
+
def do_zip(self, source_dir, zip_path, all_filepath, msg):
|
|
289
319
|
if len(all_filepath) == 0:
|
|
290
320
|
self.debug('无下载文件,无需压缩', 'skip')
|
|
291
|
-
return
|
|
321
|
+
return None
|
|
292
322
|
|
|
293
323
|
from common import backup_dir_to_zip
|
|
294
324
|
backup_dir_to_zip(
|
|
295
|
-
|
|
325
|
+
source_dir,
|
|
296
326
|
zip_path,
|
|
297
|
-
acceptor=lambda f: f in all_filepath
|
|
298
|
-
)
|
|
327
|
+
acceptor=lambda f: os.path.isdir(f) or self.unified_path(f) in all_filepath
|
|
328
|
+
).close()
|
|
299
329
|
|
|
300
|
-
self.debug(
|
|
301
|
-
return
|
|
330
|
+
self.debug(msg, 'finish')
|
|
331
|
+
return self.unified_path(source_dir)
|
|
302
332
|
|
|
303
|
-
def after_zip(self, dir_zip_dict: Dict[str, str]):
|
|
333
|
+
def after_zip(self, dir_zip_dict: Dict[str, Optional[str]]):
|
|
304
334
|
# 是否要删除所有原文件
|
|
305
335
|
if self.delete_original_file is True:
|
|
306
336
|
self.delete_all_files_and_empty_dir(
|
|
@@ -326,15 +356,20 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
326
356
|
删除所有文件和文件夹
|
|
327
357
|
"""
|
|
328
358
|
import os
|
|
329
|
-
for
|
|
330
|
-
for
|
|
331
|
-
for f,
|
|
359
|
+
for photo_dict in all_downloaded.values():
|
|
360
|
+
for image_list in photo_dict.values():
|
|
361
|
+
for f, _ in image_list:
|
|
362
|
+
# check not exist
|
|
363
|
+
if file_not_exists(f):
|
|
364
|
+
continue
|
|
365
|
+
|
|
332
366
|
os.remove(f)
|
|
333
367
|
self.debug(f'删除原文件: {f}', 'remove')
|
|
334
368
|
|
|
335
|
-
for d in dir_list:
|
|
336
|
-
|
|
337
|
-
|
|
369
|
+
for d in sorted(dir_list, reverse=True):
|
|
370
|
+
# check exist
|
|
371
|
+
if file_exists(d):
|
|
372
|
+
os.rmdir(d)
|
|
338
373
|
self.debug(f'删除文件夹: {d}', 'remove')
|
|
339
374
|
|
|
340
375
|
|
|
@@ -403,11 +438,29 @@ class SendQQEmailPlugin(JmOptionPlugin):
|
|
|
403
438
|
album=None,
|
|
404
439
|
downloader=None,
|
|
405
440
|
) -> None:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return
|
|
441
|
+
self.require_true(msg_from and msg_to and password, '发件人、收件人、授权码都不能为空')
|
|
442
|
+
|
|
409
443
|
from common import EmailConfig
|
|
410
444
|
econfig = EmailConfig(msg_from, msg_to, password)
|
|
411
445
|
epostman = econfig.create_email_postman()
|
|
412
446
|
epostman.send(content, title)
|
|
447
|
+
|
|
413
448
|
self.debug('Email sent successfully')
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class DebugTopicFilterPlugin(JmOptionPlugin):
|
|
452
|
+
plugin_key = 'debug_topic_filter'
|
|
453
|
+
|
|
454
|
+
def invoke(self, whitelist) -> None:
|
|
455
|
+
if whitelist is not None:
|
|
456
|
+
whitelist = set(whitelist)
|
|
457
|
+
|
|
458
|
+
old_jm_debug = JmModuleConfig.debug_executor
|
|
459
|
+
|
|
460
|
+
def new_jm_debug(topic, msg):
|
|
461
|
+
if whitelist is not None and topic not in whitelist:
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
old_jm_debug(topic, msg)
|
|
465
|
+
|
|
466
|
+
JmModuleConfig.debug_executor = new_jm_debug
|
|
@@ -489,7 +489,7 @@ class JmApiAdaptTool:
|
|
|
489
489
|
|
|
490
490
|
fields['sort'] = sort
|
|
491
491
|
import random
|
|
492
|
-
fields['data_original_domain'] = random.choice(JmModuleConfig.
|
|
492
|
+
fields['data_original_domain'] = random.choice(JmModuleConfig.DOMAIN_IMAGE_LIST)
|
|
493
493
|
|
|
494
494
|
|
|
495
495
|
class JmImageTool:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: jmcomic
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.16
|
|
4
4
|
Summary: Python API For JMComic (禁漫天堂)
|
|
5
5
|
Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
|
|
6
6
|
Author: hect0x7
|
|
@@ -68,7 +68,7 @@ $ jmcomic 422866
|
|
|
68
68
|
|
|
69
69
|
- GitHub Actions:网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md))
|
|
70
70
|
- 命令行:无需写Python代码,简单易用([教程:使用命令行下载禁漫本子](./assets/docs/sources/tutorial/2_command_line.md))
|
|
71
|
-
- Python
|
|
71
|
+
- Python代码:最本质、最强大的使用方式,需要你有一定的python编程基础
|
|
72
72
|
- 支持**网页端**和**移动端**两种客户端实现,可通过配置切换(**移动端不限ip兼容性好,网页端限制ip地区但效率高**)
|
|
73
73
|
- 支持**自动重试和域名切换**机制
|
|
74
74
|
- **多线程下载**(可细化到一图一线程,效率极高)
|
|
@@ -80,9 +80,9 @@ $ jmcomic 422866
|
|
|
80
80
|
- **可扩展性强**
|
|
81
81
|
|
|
82
82
|
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件**
|
|
83
|
-
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件`
|
|
83
|
+
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件` `日志主题过滤插件`
|
|
84
84
|
- 支持自定义本子/章节/图片下载前后的回调函数
|
|
85
|
-
- 支持自定义debug
|
|
85
|
+
- 支持自定义debug/logging
|
|
86
86
|
- 支持自定义类:`Downloader(负责调度)` `Option(负责配置)` `Client(负责请求)` `实体类`等
|
|
87
87
|
|
|
88
88
|
## 进阶使用
|
|
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
|