jmcomic 2.5.38__py3-none-any.whl → 2.6.0__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 +1 -1
- jmcomic/jm_client_impl.py +41 -0
- jmcomic/jm_config.py +13 -5
- jmcomic/jm_option.py +17 -14
- jmcomic/jm_plugin.py +119 -138
- jmcomic/jm_toolkit.py +3 -3
- {jmcomic-2.5.38.dist-info → jmcomic-2.6.0.dist-info}/METADATA +4 -3
- jmcomic-2.6.0.dist-info/RECORD +18 -0
- {jmcomic-2.5.38.dist-info → jmcomic-2.6.0.dist-info}/WHEEL +1 -1
- jmcomic-2.5.38.dist-info/RECORD +0 -18
- {jmcomic-2.5.38.dist-info → jmcomic-2.6.0.dist-info}/entry_points.txt +0 -0
- {jmcomic-2.5.38.dist-info → jmcomic-2.6.0.dist-info}/licenses/LICENSE +0 -0
- {jmcomic-2.5.38.dist-info → jmcomic-2.6.0.dist-info}/top_level.txt +0 -0
jmcomic/__init__.py
CHANGED
jmcomic/jm_client_impl.py
CHANGED
|
@@ -1005,10 +1005,51 @@ class JmApiClient(AbstractJmClient):
|
|
|
1005
1005
|
ExceptionTool.raises_resp(f'响应无数据!request_url=[{url}]', resp)
|
|
1006
1006
|
|
|
1007
1007
|
def after_init(self):
|
|
1008
|
+
# 自动更新禁漫API域名
|
|
1009
|
+
if JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN:
|
|
1010
|
+
self.update_api_domain()
|
|
1011
|
+
|
|
1008
1012
|
# 保证拥有cookies,因为移动端要求必须携带cookies,否则会直接跳转同一本子【禁漫娘】
|
|
1009
1013
|
if JmModuleConfig.FLAG_API_CLIENT_REQUIRE_COOKIES:
|
|
1010
1014
|
self.ensure_have_cookies()
|
|
1011
1015
|
|
|
1016
|
+
client_update_domain_lock = Lock()
|
|
1017
|
+
|
|
1018
|
+
def update_api_domain(self):
|
|
1019
|
+
if True is JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE:
|
|
1020
|
+
return
|
|
1021
|
+
|
|
1022
|
+
with self.client_update_domain_lock:
|
|
1023
|
+
if True is JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE:
|
|
1024
|
+
return
|
|
1025
|
+
try:
|
|
1026
|
+
# 获取域名列表
|
|
1027
|
+
resp = self.postman.get(JmModuleConfig.API_URL_DOMAIN_SERVER)
|
|
1028
|
+
res_json = JmCryptoTool.decode_resp_data(resp.text, '', JmMagicConstants.API_DOMAIN_SERVER_SECRET)
|
|
1029
|
+
res_data = json_loads(res_json)
|
|
1030
|
+
|
|
1031
|
+
# 检查返回值
|
|
1032
|
+
if not res_data.get('Server', None):
|
|
1033
|
+
jm_log('api.update_domain.empty',
|
|
1034
|
+
f'获取禁漫最新API域名失败, 返回值: {res_json}')
|
|
1035
|
+
return
|
|
1036
|
+
new_server_list: list[str] = res_data['Server']
|
|
1037
|
+
old_server_list = JmModuleConfig.DOMAIN_API_LIST
|
|
1038
|
+
jm_log('api.update_domain.success',
|
|
1039
|
+
f'获取到最新的API域名,替换jmcomic内置域名:(new){new_server_list} ---→ (old){old_server_list}'
|
|
1040
|
+
)
|
|
1041
|
+
# 更新域名
|
|
1042
|
+
if self.domain_list is old_server_list:
|
|
1043
|
+
self.domain_list = new_server_list
|
|
1044
|
+
JmModuleConfig.DOMAIN_API_LIST = new_server_list
|
|
1045
|
+
except Exception as e:
|
|
1046
|
+
jm_log('api.update_domain.error',
|
|
1047
|
+
f'自动更新API域名失败,仍使用jmcomic内置域名。'
|
|
1048
|
+
f'可通过代码[JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN=False]关闭自动更新API域名. 异常: {e}'
|
|
1049
|
+
)
|
|
1050
|
+
finally:
|
|
1051
|
+
JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = True
|
|
1052
|
+
|
|
1012
1053
|
client_init_cookies_lock = Lock()
|
|
1013
1054
|
|
|
1014
1055
|
def ensure_have_cookies(self):
|
jmcomic/jm_config.py
CHANGED
|
@@ -76,7 +76,8 @@ class JmMagicConstants:
|
|
|
76
76
|
APP_TOKEN_SECRET = '18comicAPP'
|
|
77
77
|
APP_TOKEN_SECRET_2 = '18comicAPPContent'
|
|
78
78
|
APP_DATA_SECRET = '185Hcomic3PAPP7R'
|
|
79
|
-
|
|
79
|
+
API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik'
|
|
80
|
+
APP_VERSION = '1.8.0'
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
# 模块级别共用配置
|
|
@@ -128,11 +129,15 @@ class JmModuleConfig:
|
|
|
128
129
|
# 移动端API域名
|
|
129
130
|
DOMAIN_API_LIST = shuffled('''
|
|
130
131
|
www.cdnmhwscc.vip
|
|
131
|
-
www.
|
|
132
|
-
www.
|
|
132
|
+
www.cdnplaystation6.club
|
|
133
|
+
www.cdnplaystation6.org
|
|
133
134
|
www.cdnuc.vip
|
|
135
|
+
www.cdn-mspjmapiproxy.xyz
|
|
134
136
|
''')
|
|
135
137
|
|
|
138
|
+
# 获取最新移动端API域名的地址
|
|
139
|
+
API_URL_DOMAIN_SERVER = f'{PROT}jmappc01-1308024008.cos.ap-guangzhou.myqcloud.com/server-2024.txt'
|
|
140
|
+
|
|
136
141
|
APP_HEADERS_TEMPLATE = {
|
|
137
142
|
'Accept-Encoding': 'gzip, deflate',
|
|
138
143
|
'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, '
|
|
@@ -200,6 +205,9 @@ class JmModuleConfig:
|
|
|
200
205
|
FLAG_USE_FIX_TIMESTAMP = True
|
|
201
206
|
# 移动端Client初始化cookies
|
|
202
207
|
FLAG_API_CLIENT_REQUIRE_COOKIES = True
|
|
208
|
+
# 自动更新禁漫API域名
|
|
209
|
+
FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN = True
|
|
210
|
+
FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = None
|
|
203
211
|
# log开关标记
|
|
204
212
|
FLAG_ENABLE_JM_LOG = True
|
|
205
213
|
# log时解码url
|
|
@@ -380,7 +388,7 @@ class JmModuleConfig:
|
|
|
380
388
|
|
|
381
389
|
@classmethod
|
|
382
390
|
def new_postman(cls, session=False, **kwargs):
|
|
383
|
-
kwargs.setdefault('impersonate', '
|
|
391
|
+
kwargs.setdefault('impersonate', 'chrome')
|
|
384
392
|
kwargs.setdefault('headers', JmModuleConfig.new_html_headers())
|
|
385
393
|
kwargs.setdefault('proxies', JmModuleConfig.DEFAULT_PROXIES)
|
|
386
394
|
|
|
@@ -416,7 +424,7 @@ class JmModuleConfig:
|
|
|
416
424
|
'postman': {
|
|
417
425
|
'type': 'curl_cffi',
|
|
418
426
|
'meta_data': {
|
|
419
|
-
'impersonate': '
|
|
427
|
+
'impersonate': 'chrome',
|
|
420
428
|
'headers': None,
|
|
421
429
|
'proxies': None,
|
|
422
430
|
}
|
jmcomic/jm_option.py
CHANGED
|
@@ -70,12 +70,12 @@ class DirRule:
|
|
|
70
70
|
album: JmAlbumDetail,
|
|
71
71
|
photo: JmPhotoDetail,
|
|
72
72
|
) -> str:
|
|
73
|
-
return self.
|
|
73
|
+
return self.apply_rule_to_path(album, photo)
|
|
74
74
|
|
|
75
75
|
def decide_album_root_dir(self, album: JmAlbumDetail) -> str:
|
|
76
|
-
return self.
|
|
76
|
+
return self.apply_rule_to_path(album, None, True)
|
|
77
77
|
|
|
78
|
-
def
|
|
78
|
+
def apply_rule_to_path(self, album, photo, only_album_rules=False) -> str:
|
|
79
79
|
path_ls = []
|
|
80
80
|
for rule, parser in self.parser_list:
|
|
81
81
|
if only_album_rules and not (rule == self.RULE_BASE_DIR or rule.startswith('A')):
|
|
@@ -92,7 +92,7 @@ class DirRule:
|
|
|
92
92
|
|
|
93
93
|
path_ls.append(path)
|
|
94
94
|
|
|
95
|
-
return fix_filepath('/'.join(path_ls)
|
|
95
|
+
return fix_filepath('/'.join(path_ls))
|
|
96
96
|
|
|
97
97
|
def get_rule_parser_list(self, rule_dsl: str):
|
|
98
98
|
"""
|
|
@@ -103,7 +103,6 @@ class DirRule:
|
|
|
103
103
|
parser_list: list = []
|
|
104
104
|
|
|
105
105
|
for rule in rule_list:
|
|
106
|
-
rule = rule.strip()
|
|
107
106
|
if rule == self.RULE_BASE_DIR:
|
|
108
107
|
parser_list.append((rule, self.parse_bd_rule))
|
|
109
108
|
continue
|
|
@@ -135,17 +134,21 @@ class DirRule:
|
|
|
135
134
|
return str(DetailEntity.get_dirname(detail, rule[1:]))
|
|
136
135
|
|
|
137
136
|
# noinspection PyMethodMayBeStatic
|
|
138
|
-
def split_rule_dsl(self, rule_dsl: str):
|
|
139
|
-
if rule_dsl == self.RULE_BASE_DIR:
|
|
140
|
-
return [rule_dsl]
|
|
141
|
-
|
|
137
|
+
def split_rule_dsl(self, rule_dsl: str) -> list[str]:
|
|
142
138
|
if '/' in rule_dsl:
|
|
143
|
-
|
|
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()
|
|
144
147
|
|
|
145
|
-
if
|
|
146
|
-
|
|
148
|
+
if rule_list[0] != self.RULE_BASE_DIR:
|
|
149
|
+
rule_list.insert(0, self.RULE_BASE_DIR)
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
return rule_list
|
|
149
152
|
|
|
150
153
|
@classmethod
|
|
151
154
|
def get_rule_parser(cls, rule: str):
|
|
@@ -158,7 +161,7 @@ class DirRule:
|
|
|
158
161
|
ExceptionTool.raises(f'不支持的rule配置: "{rule}"')
|
|
159
162
|
|
|
160
163
|
@classmethod
|
|
161
|
-
def
|
|
164
|
+
def apply_rule_to_filename(cls, album, photo, rule: str) -> str:
|
|
162
165
|
if album is None:
|
|
163
166
|
album = photo.from_album
|
|
164
167
|
# noinspection PyArgumentList
|
jmcomic/jm_plugin.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
该文件存放的是option插件
|
|
3
3
|
"""
|
|
4
|
-
import os.path
|
|
5
4
|
|
|
6
5
|
from .jm_option import *
|
|
7
6
|
|
|
@@ -57,11 +56,12 @@ class JmOptionPlugin:
|
|
|
57
56
|
|
|
58
57
|
raise PluginValidationException(self, msg)
|
|
59
58
|
|
|
60
|
-
def warning_lib_not_install(self, lib: str):
|
|
59
|
+
def warning_lib_not_install(self, lib: str, throw=False):
|
|
61
60
|
msg = (f'插件`{self.plugin_key}`依赖库: {lib},请先安装{lib}再使用。'
|
|
62
61
|
f'安装命令: [pip install {lib}]')
|
|
63
62
|
import warnings
|
|
64
63
|
warnings.warn(msg)
|
|
64
|
+
self.require_param(throw, msg)
|
|
65
65
|
|
|
66
66
|
def execute_deletion(self, paths: List[str]):
|
|
67
67
|
"""
|
|
@@ -76,6 +76,9 @@ class JmOptionPlugin:
|
|
|
76
76
|
continue
|
|
77
77
|
|
|
78
78
|
if os.path.isdir(p):
|
|
79
|
+
if os.listdir(p):
|
|
80
|
+
self.log(f'文件夹中存在非本次下载的文件,请手动删除文件夹内的文件: {p}', 'remove.ignore')
|
|
81
|
+
continue
|
|
79
82
|
os.rmdir(p)
|
|
80
83
|
self.log(f'删除文件夹: {p}', 'remove')
|
|
81
84
|
else:
|
|
@@ -104,6 +107,33 @@ class JmOptionPlugin:
|
|
|
104
107
|
def wait_until_finish(self):
|
|
105
108
|
pass
|
|
106
109
|
|
|
110
|
+
# noinspection PyMethodMayBeStatic
|
|
111
|
+
def decide_filepath(self,
|
|
112
|
+
album: Optional[JmAlbumDetail],
|
|
113
|
+
photo: Optional[JmPhotoDetail],
|
|
114
|
+
filename_rule: str, suffix: str, base_dir: Optional[str],
|
|
115
|
+
dir_rule_dict: Optional[dict]
|
|
116
|
+
):
|
|
117
|
+
"""
|
|
118
|
+
根据规则计算一个文件的全路径
|
|
119
|
+
|
|
120
|
+
参数 dir_rule_dict 优先级最高,
|
|
121
|
+
如果 dir_rule_dict 不为空,优先用 dir_rule_dict
|
|
122
|
+
否则使用 base_dir + filename_rule + suffix
|
|
123
|
+
"""
|
|
124
|
+
filepath: str
|
|
125
|
+
base_dir: str
|
|
126
|
+
if dir_rule_dict is not None:
|
|
127
|
+
dir_rule = DirRule(**dir_rule_dict)
|
|
128
|
+
filepath = dir_rule.apply_rule_to_path(album, photo)
|
|
129
|
+
base_dir = os.path.dirname(filepath)
|
|
130
|
+
else:
|
|
131
|
+
base_dir = base_dir or os.getcwd()
|
|
132
|
+
filepath = os.path.join(base_dir, DirRule.apply_rule_to_filename(album, photo, filename_rule) + fix_suffix(suffix))
|
|
133
|
+
|
|
134
|
+
mkdir_if_not_exists(base_dir)
|
|
135
|
+
return filepath
|
|
136
|
+
|
|
107
137
|
|
|
108
138
|
class JmLoginPlugin(JmOptionPlugin):
|
|
109
139
|
"""
|
|
@@ -274,6 +304,11 @@ class FindUpdatePlugin(JmOptionPlugin):
|
|
|
274
304
|
|
|
275
305
|
|
|
276
306
|
class ZipPlugin(JmOptionPlugin):
|
|
307
|
+
"""
|
|
308
|
+
感谢zip加密功能的贡献者:
|
|
309
|
+
- AXIS5 a.k.a AXIS5Hacker (https://github.com/hect0x7/JMComic-Crawler-Python/pull/375)
|
|
310
|
+
"""
|
|
311
|
+
|
|
277
312
|
plugin_key = 'zip'
|
|
278
313
|
|
|
279
314
|
# noinspection PyAttributeOutsideInit
|
|
@@ -285,7 +320,9 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
285
320
|
level='photo',
|
|
286
321
|
filename_rule='Ptitle',
|
|
287
322
|
suffix='zip',
|
|
288
|
-
zip_dir='./'
|
|
323
|
+
zip_dir='./',
|
|
324
|
+
dir_rule=None,
|
|
325
|
+
encrypt=None,
|
|
289
326
|
) -> None:
|
|
290
327
|
|
|
291
328
|
from .jm_downloader import JmDownloader
|
|
@@ -302,19 +339,20 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
302
339
|
photo_dict = self.get_downloaded_photo(downloader, album, photo)
|
|
303
340
|
|
|
304
341
|
if level == 'album':
|
|
305
|
-
zip_path = self.
|
|
306
|
-
self.zip_album(album, photo_dict, zip_path, path_to_delete)
|
|
342
|
+
zip_path = self.decide_filepath(album, None, filename_rule, suffix, zip_dir, dir_rule)
|
|
343
|
+
self.zip_album(album, photo_dict, zip_path, path_to_delete, encrypt)
|
|
307
344
|
|
|
308
345
|
elif level == 'photo':
|
|
309
346
|
for photo, image_list in photo_dict.items():
|
|
310
|
-
zip_path = self.
|
|
311
|
-
self.zip_photo(photo, image_list, zip_path, path_to_delete)
|
|
347
|
+
zip_path = self.decide_filepath(photo.from_album, photo, filename_rule, suffix, zip_dir, dir_rule)
|
|
348
|
+
self.zip_photo(photo, image_list, zip_path, path_to_delete, encrypt)
|
|
312
349
|
|
|
313
350
|
else:
|
|
314
351
|
ExceptionTool.raises(f'Not Implemented Zip Level: {level}')
|
|
315
352
|
|
|
316
353
|
self.after_zip(path_to_delete)
|
|
317
354
|
|
|
355
|
+
# noinspection PyMethodMayBeStatic
|
|
318
356
|
def get_downloaded_photo(self, downloader, album, photo):
|
|
319
357
|
return (
|
|
320
358
|
downloader.download_success_dict[album]
|
|
@@ -322,7 +360,7 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
322
360
|
else downloader.download_success_dict[photo.from_album] # after_photo
|
|
323
361
|
)
|
|
324
362
|
|
|
325
|
-
def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete):
|
|
363
|
+
def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete, encrypt_dict):
|
|
326
364
|
"""
|
|
327
365
|
压缩photo文件夹
|
|
328
366
|
"""
|
|
@@ -330,8 +368,11 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
330
368
|
if len(image_list) == 0 \
|
|
331
369
|
else os.path.dirname(image_list[0][0])
|
|
332
370
|
|
|
333
|
-
|
|
334
|
-
|
|
371
|
+
with self.open_zip_file(zip_path, encrypt_dict) as f:
|
|
372
|
+
for file in files_of_dir(photo_dir):
|
|
373
|
+
abspath = os.path.join(photo_dir, file)
|
|
374
|
+
relpath = os.path.relpath(abspath, photo_dir)
|
|
375
|
+
f.write(abspath, relpath)
|
|
335
376
|
|
|
336
377
|
self.log(f'压缩章节[{photo.photo_id}]成功 → {zip_path}', 'finish')
|
|
337
378
|
path_to_delete.append(self.unified_path(photo_dir))
|
|
@@ -340,14 +381,13 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
340
381
|
def unified_path(f):
|
|
341
382
|
return fix_filepath(f, os.path.isdir(f))
|
|
342
383
|
|
|
343
|
-
def zip_album(self, album, photo_dict: dict, zip_path, path_to_delete):
|
|
384
|
+
def zip_album(self, album, photo_dict: dict, zip_path, path_to_delete, encrypt_dict):
|
|
344
385
|
"""
|
|
345
386
|
压缩album文件夹
|
|
346
387
|
"""
|
|
347
388
|
|
|
348
389
|
album_dir = self.option.dir_rule.decide_album_root_dir(album)
|
|
349
|
-
|
|
350
|
-
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as f:
|
|
390
|
+
with self.open_zip_file(zip_path, encrypt_dict) as f:
|
|
351
391
|
for photo in photo_dict.keys():
|
|
352
392
|
# 定位到章节所在文件夹
|
|
353
393
|
photo_dir = self.unified_path(self.option.decide_image_save_dir(photo))
|
|
@@ -372,16 +412,65 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
372
412
|
self.execute_deletion(dirs)
|
|
373
413
|
|
|
374
414
|
# noinspection PyMethodMayBeStatic
|
|
375
|
-
|
|
415
|
+
@classmethod
|
|
416
|
+
def generate_random_str(cls, random_length) -> str:
|
|
376
417
|
"""
|
|
377
|
-
|
|
418
|
+
自动生成随机字符密码,长度由randomlength指定
|
|
378
419
|
"""
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
420
|
+
import random
|
|
421
|
+
|
|
422
|
+
random_str = ''
|
|
423
|
+
base_str = r'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
|
424
|
+
base_length = len(base_str) - 1
|
|
425
|
+
for _ in range(random_length):
|
|
426
|
+
random_str += base_str[random.randint(0, base_length)]
|
|
427
|
+
return random_str
|
|
428
|
+
|
|
429
|
+
def open_zip_file(self, zip_path: str, encrypt_dict: Optional[dict]):
|
|
430
|
+
if encrypt_dict is None:
|
|
431
|
+
import zipfile
|
|
432
|
+
return zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
|
433
|
+
|
|
434
|
+
password, is_random = self.decide_password(encrypt_dict, zip_path)
|
|
435
|
+
if encrypt_dict.get('impl', '') == '7z':
|
|
436
|
+
try:
|
|
437
|
+
# noinspection PyUnresolvedReferences
|
|
438
|
+
import py7zr
|
|
439
|
+
except ImportError:
|
|
440
|
+
self.warning_lib_not_install('py7zr', True)
|
|
441
|
+
|
|
442
|
+
# noinspection PyUnboundLocalVariable
|
|
443
|
+
filters = [{'id': py7zr.FILTER_COPY}]
|
|
444
|
+
return py7zr.SevenZipFile(zip_path, mode='w', password=password, filters=filters, header_encryption=True)
|
|
445
|
+
else:
|
|
446
|
+
try:
|
|
447
|
+
# noinspection PyUnresolvedReferences
|
|
448
|
+
import pyzipper
|
|
449
|
+
except ImportError:
|
|
450
|
+
self.warning_lib_not_install('pyzipper', True)
|
|
451
|
+
|
|
452
|
+
# noinspection PyUnboundLocalVariable
|
|
453
|
+
aes_zip_file = pyzipper.AESZipFile(zip_path, "w", pyzipper.ZIP_DEFLATED)
|
|
454
|
+
aes_zip_file.setencryption(pyzipper.WZ_AES, nbits=128)
|
|
455
|
+
password_bytes = str.encode(password)
|
|
456
|
+
aes_zip_file.setpassword(password_bytes)
|
|
457
|
+
if is_random:
|
|
458
|
+
aes_zip_file.comment = password_bytes
|
|
459
|
+
return aes_zip_file
|
|
460
|
+
|
|
461
|
+
def decide_password(self, encrypt_dict: dict, zip_path: str):
|
|
462
|
+
encrypt_type = encrypt_dict.get('type', '')
|
|
463
|
+
is_random = False
|
|
464
|
+
|
|
465
|
+
if encrypt_type == 'random':
|
|
466
|
+
is_random = True
|
|
467
|
+
password = self.generate_random_str(48)
|
|
468
|
+
self.log(f'生成随机密码: [{password}] → [{zip_path}]', 'encrypt')
|
|
469
|
+
else:
|
|
470
|
+
password = str(encrypt_dict['password'])
|
|
471
|
+
self.log(f'使用指定密码: [{password}] → [{zip_path}]', 'encrypt')
|
|
472
|
+
|
|
473
|
+
return password, is_random
|
|
385
474
|
|
|
386
475
|
|
|
387
476
|
class ClientProxyPlugin(JmOptionPlugin):
|
|
@@ -649,95 +738,6 @@ class FavoriteFolderExportPlugin(JmOptionPlugin):
|
|
|
649
738
|
self.execute_multi_line_cmd(cmd_list)
|
|
650
739
|
|
|
651
740
|
|
|
652
|
-
class ConvertJpgToPdfPlugin(JmOptionPlugin):
|
|
653
|
-
plugin_key = 'j2p'
|
|
654
|
-
|
|
655
|
-
def check_image_suffix_is_valid(self, std_suffix):
|
|
656
|
-
"""
|
|
657
|
-
检查option配置的图片后缀转换,目前限制使用Magick时只能搭配jpg
|
|
658
|
-
暂不探究Magick是否支持更多图片格式
|
|
659
|
-
"""
|
|
660
|
-
cur_suffix: Optional[str] = self.option.download.image.suffix
|
|
661
|
-
|
|
662
|
-
ExceptionTool.require_true(
|
|
663
|
-
cur_suffix is not None and cur_suffix.endswith(std_suffix),
|
|
664
|
-
'请把图片的后缀转换配置为jpg,不然无法使用Magick!'
|
|
665
|
-
f'(当前配置是[{cur_suffix}])\n'
|
|
666
|
-
f'配置模板如下: \n'
|
|
667
|
-
f'```\n'
|
|
668
|
-
f'download:\n'
|
|
669
|
-
f' image:\n'
|
|
670
|
-
f' suffix: {std_suffix} # 当前配置是{cur_suffix}\n'
|
|
671
|
-
f'```'
|
|
672
|
-
)
|
|
673
|
-
|
|
674
|
-
def invoke(self,
|
|
675
|
-
photo: JmPhotoDetail,
|
|
676
|
-
downloader=None,
|
|
677
|
-
pdf_dir=None,
|
|
678
|
-
filename_rule='Pid',
|
|
679
|
-
quality=100,
|
|
680
|
-
delete_original_file=False,
|
|
681
|
-
override_cmd=None,
|
|
682
|
-
override_jpg=None,
|
|
683
|
-
**kwargs,
|
|
684
|
-
):
|
|
685
|
-
self.delete_original_file = delete_original_file
|
|
686
|
-
|
|
687
|
-
# 检查图片后缀配置
|
|
688
|
-
suffix = override_jpg or '.jpg'
|
|
689
|
-
self.check_image_suffix_is_valid(suffix)
|
|
690
|
-
|
|
691
|
-
# 处理文件夹配置
|
|
692
|
-
filename = DirRule.apply_rule_directly(None, photo, filename_rule)
|
|
693
|
-
photo_dir = self.option.decide_image_save_dir(photo)
|
|
694
|
-
|
|
695
|
-
# 处理生成的pdf文件的路径
|
|
696
|
-
if pdf_dir is None:
|
|
697
|
-
pdf_dir = photo_dir
|
|
698
|
-
else:
|
|
699
|
-
pdf_dir = fix_filepath(pdf_dir, True)
|
|
700
|
-
mkdir_if_not_exists(pdf_dir)
|
|
701
|
-
|
|
702
|
-
pdf_filepath = os.path.join(pdf_dir, f'{filename}.pdf')
|
|
703
|
-
|
|
704
|
-
# 生成命令
|
|
705
|
-
def generate_cmd():
|
|
706
|
-
return (
|
|
707
|
-
override_cmd or
|
|
708
|
-
'magick convert -quality {quality} "{photo_dir}*{suffix}" "{pdf_filepath}"'
|
|
709
|
-
).format(
|
|
710
|
-
quality=quality,
|
|
711
|
-
photo_dir=photo_dir,
|
|
712
|
-
suffix=suffix,
|
|
713
|
-
pdf_filepath=pdf_filepath,
|
|
714
|
-
)
|
|
715
|
-
|
|
716
|
-
cmd = generate_cmd()
|
|
717
|
-
self.log(f'Execute Command: [{cmd}]')
|
|
718
|
-
code = self.execute_cmd(cmd)
|
|
719
|
-
|
|
720
|
-
ExceptionTool.require_true(
|
|
721
|
-
code == 0,
|
|
722
|
-
'jpg图片合并为pdf失败!'
|
|
723
|
-
'请确认你是否安装了magick,安装网站: [https://www.imagemagick.org/]',
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
self.log(f'Convert Successfully: JM{photo.id} → {pdf_filepath}')
|
|
727
|
-
|
|
728
|
-
if downloader is not None:
|
|
729
|
-
from .jm_downloader import JmDownloader
|
|
730
|
-
downloader: JmDownloader
|
|
731
|
-
|
|
732
|
-
paths = [
|
|
733
|
-
path
|
|
734
|
-
for path, image in downloader.download_success_dict[photo.from_album][photo]
|
|
735
|
-
]
|
|
736
|
-
|
|
737
|
-
paths.append(self.option.decide_image_save_dir(photo, ensure_exists=False))
|
|
738
|
-
self.execute_deletion(paths)
|
|
739
|
-
|
|
740
|
-
|
|
741
741
|
class Img2pdfPlugin(JmOptionPlugin):
|
|
742
742
|
plugin_key = 'img2pdf'
|
|
743
743
|
|
|
@@ -747,6 +747,7 @@ class Img2pdfPlugin(JmOptionPlugin):
|
|
|
747
747
|
downloader=None,
|
|
748
748
|
pdf_dir=None,
|
|
749
749
|
filename_rule='Pid',
|
|
750
|
+
dir_rule=None,
|
|
750
751
|
delete_original_file=False,
|
|
751
752
|
**kwargs,
|
|
752
753
|
):
|
|
@@ -762,13 +763,7 @@ class Img2pdfPlugin(JmOptionPlugin):
|
|
|
762
763
|
self.delete_original_file = delete_original_file
|
|
763
764
|
|
|
764
765
|
# 处理生成的pdf文件的路径
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
# 处理pdf文件名
|
|
768
|
-
filename = DirRule.apply_rule_directly(album, photo, filename_rule)
|
|
769
|
-
|
|
770
|
-
# pdf路径
|
|
771
|
-
pdf_filepath = os.path.join(pdf_dir, f'{filename}.pdf')
|
|
766
|
+
pdf_filepath = self.decide_filepath(album, photo, filename_rule, 'pdf', pdf_dir, dir_rule)
|
|
772
767
|
|
|
773
768
|
# 调用 img2pdf 把 photo_dir 下的所有图片转为pdf
|
|
774
769
|
img_path_ls, img_dir_ls = self.write_img_2_pdf(pdf_filepath, album, photo)
|
|
@@ -794,18 +789,14 @@ class Img2pdfPlugin(JmOptionPlugin):
|
|
|
794
789
|
continue
|
|
795
790
|
img_path_ls += imgs
|
|
796
791
|
|
|
792
|
+
if len(img_path_ls) == 0:
|
|
793
|
+
self.log(f'所有文件夹都不存在图片,无法生成pdf:{img_dir_ls}', 'error')
|
|
794
|
+
|
|
797
795
|
with open(pdf_filepath, 'wb') as f:
|
|
798
796
|
f.write(img2pdf.convert(img_path_ls))
|
|
799
797
|
|
|
800
798
|
return img_path_ls, img_dir_ls
|
|
801
799
|
|
|
802
|
-
@staticmethod
|
|
803
|
-
def ensure_make_pdf_dir(pdf_dir: str):
|
|
804
|
-
pdf_dir = pdf_dir or os.getcwd()
|
|
805
|
-
pdf_dir = fix_filepath(pdf_dir, True)
|
|
806
|
-
mkdir_if_not_exists(pdf_dir)
|
|
807
|
-
return pdf_dir
|
|
808
|
-
|
|
809
800
|
|
|
810
801
|
class LongImgPlugin(JmOptionPlugin):
|
|
811
802
|
plugin_key = 'long_img'
|
|
@@ -817,6 +808,7 @@ class LongImgPlugin(JmOptionPlugin):
|
|
|
817
808
|
img_dir=None,
|
|
818
809
|
filename_rule='Pid',
|
|
819
810
|
delete_original_file=False,
|
|
811
|
+
dir_rule=None,
|
|
820
812
|
**kwargs,
|
|
821
813
|
):
|
|
822
814
|
if photo is None and album is None:
|
|
@@ -830,14 +822,8 @@ class LongImgPlugin(JmOptionPlugin):
|
|
|
830
822
|
|
|
831
823
|
self.delete_original_file = delete_original_file
|
|
832
824
|
|
|
833
|
-
# 处理文件夹配置
|
|
834
|
-
img_dir = self.get_img_dir(img_dir)
|
|
835
|
-
|
|
836
825
|
# 处理生成的长图文件的路径
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
# 长图路径
|
|
840
|
-
long_img_path = os.path.join(img_dir, f'{filename}.png')
|
|
826
|
+
long_img_path = self.decide_filepath(album, photo, filename_rule, 'png', img_dir, dir_rule)
|
|
841
827
|
|
|
842
828
|
# 调用 PIL 把 photo_dir 下的所有图片合并为长图
|
|
843
829
|
img_path_ls = self.write_img_2_long_img(long_img_path, album, photo)
|
|
@@ -856,7 +842,7 @@ class LongImgPlugin(JmOptionPlugin):
|
|
|
856
842
|
img_dir_items = [self.option.decide_image_save_dir(photo) for photo in album]
|
|
857
843
|
|
|
858
844
|
img_paths = itertools.chain(*map(files_of_dir, img_dir_items))
|
|
859
|
-
img_paths = filter(lambda x: not x.startswith('.'), img_paths) # 过滤系统文件
|
|
845
|
+
img_paths = list(filter(lambda x: not x.startswith('.'), img_paths)) # 过滤系统文件
|
|
860
846
|
|
|
861
847
|
images = self.open_images(img_paths)
|
|
862
848
|
|
|
@@ -895,12 +881,6 @@ class LongImgPlugin(JmOptionPlugin):
|
|
|
895
881
|
self.log(f"Failed to open image {img_path}: {e}", 'error')
|
|
896
882
|
return images
|
|
897
883
|
|
|
898
|
-
@staticmethod
|
|
899
|
-
def get_img_dir(img_dir: Optional[str]) -> str:
|
|
900
|
-
img_dir = fix_filepath(img_dir or os.getcwd())
|
|
901
|
-
mkdir_if_not_exists(img_dir)
|
|
902
|
-
return img_dir
|
|
903
|
-
|
|
904
884
|
|
|
905
885
|
class JmServerPlugin(JmOptionPlugin):
|
|
906
886
|
plugin_key = 'jm_server'
|
|
@@ -960,6 +940,7 @@ class JmServerPlugin(JmOptionPlugin):
|
|
|
960
940
|
# 服务器的代码位于一个独立库:plugin_jm_server,需要独立安装
|
|
961
941
|
# 源代码仓库:https://github.com/hect0x7/plugin-jm-server
|
|
962
942
|
try:
|
|
943
|
+
# noinspection PyUnresolvedReferences
|
|
963
944
|
import plugin_jm_server
|
|
964
945
|
self.log(f'当前使用plugin_jm_server版本: {plugin_jm_server.__version__}')
|
|
965
946
|
except ImportError:
|
jmcomic/jm_toolkit.py
CHANGED
|
@@ -24,8 +24,8 @@ class JmcomicText:
|
|
|
24
24
|
|
|
25
25
|
pattern_html_album_album_id = compile(r'<span class="number">.*?:JM(\d+)</span>')
|
|
26
26
|
pattern_html_album_scramble_id = compile(r'var scramble_id = (\d+);')
|
|
27
|
-
pattern_html_album_name = compile(r'
|
|
28
|
-
pattern_html_album_episode_list = compile(r'data-album="(\d+)"[^>]
|
|
27
|
+
pattern_html_album_name = compile(r'id="book-name"[^>]*?>([\s\S]*?)<')
|
|
28
|
+
pattern_html_album_episode_list = compile(r'data-album="(\d+)"[^>]*>[\s\S]*?第(\d+)[话話]([\s\S]*?)<[\s\S]*?>')
|
|
29
29
|
pattern_html_album_page_count = compile(r'<span class="pagecount">.*?:(\d+)</span>')
|
|
30
30
|
pattern_html_album_pub_date = compile(r'>上架日期 : (.*?)</span>')
|
|
31
31
|
pattern_html_album_update_date = compile(r'>更新日期 : (.*?)</span>')
|
|
@@ -47,7 +47,7 @@ class JmcomicText:
|
|
|
47
47
|
]
|
|
48
48
|
# 作者
|
|
49
49
|
pattern_html_album_authors = [
|
|
50
|
-
compile(r'
|
|
50
|
+
compile(r'<span itemprop="author" data-type="author">([\s\S]*?)</span>'),
|
|
51
51
|
pattern_html_tag_a,
|
|
52
52
|
]
|
|
53
53
|
# 點擊喜歡
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jmcomic
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Python API For JMComic (禁漫天堂)
|
|
5
5
|
Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
|
|
6
6
|
Author: hect0x7
|
|
@@ -197,10 +197,11 @@ jmcomic 123
|
|
|
197
197
|
- `下载特定后缀图片插件`
|
|
198
198
|
- `发送QQ邮件插件`
|
|
199
199
|
- `自动使用浏览器cookies插件`
|
|
200
|
-
- `jpg图片合成为一个pdf插件`
|
|
201
200
|
- `导出收藏夹为csv文件插件`
|
|
202
201
|
- `合并所有图片为pdf文件插件`
|
|
203
|
-
-
|
|
202
|
+
- `合并所有图片为长图png插件`
|
|
203
|
+
- `重复文件检测删除插件`
|
|
204
|
+
- `网页观看本地章节插件`
|
|
204
205
|
|
|
205
206
|
## 使用小说明
|
|
206
207
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
jmcomic/__init__.py,sha256=vA2i9PVvDB_Ea_fk-0frPv_mjXqUMS5QOsftQOBGj74,902
|
|
2
|
+
jmcomic/api.py,sha256=ZduhXDmh4lg1dkXHs7UTAaPpYNO7kcdPCDH5JJT9KSI,4253
|
|
3
|
+
jmcomic/cl.py,sha256=PBSh0JndNFZw3B7WJPj5Y8SeFdKzHE00jIwYo9An-K0,3475
|
|
4
|
+
jmcomic/jm_client_impl.py,sha256=nzKOSGg0mCno9OWfcIPoF2SsOd_GQY_0nFUaMrMMJW8,41627
|
|
5
|
+
jmcomic/jm_client_interface.py,sha256=Bld80cYwBwIqcMSJK09g5q8U_UX3IWnQJxuRJQwmIOk,19082
|
|
6
|
+
jmcomic/jm_config.py,sha256=2airaTXejmeSoow5cUBgHsPmmFA4m8MlqIHmT8OvWWA,17147
|
|
7
|
+
jmcomic/jm_downloader.py,sha256=6w9D7eoL2M85-ScH745-OZBvaC0uwGkHi6awTUzq-3E,11036
|
|
8
|
+
jmcomic/jm_entity.py,sha256=x-3yUvM74C_Gof20mJw2SmZq7sSSsGNg0Cp47oZ-dFg,19968
|
|
9
|
+
jmcomic/jm_exception.py,sha256=x3KGMLlQS2zi1GX7z5G58zJN2EwLkI4mAURkxZYjEvA,5055
|
|
10
|
+
jmcomic/jm_option.py,sha256=GijXHBDXNUt4oh_6th7TsT2gj4tBJjPLQfwPPNHyTlU,21247
|
|
11
|
+
jmcomic/jm_plugin.py,sha256=M-f7xty_90d3mSYmzrXSpYdua-KEBQVzGcWip1OA5gc,40345
|
|
12
|
+
jmcomic/jm_toolkit.py,sha256=zYwBny0Uqe1SXn2ZoLJfss4aWZq5KzTSeBKK7mQf810,29128
|
|
13
|
+
jmcomic-2.6.0.dist-info/licenses/LICENSE,sha256=kz4coTxZxuGxisK3W00tjK57Zh3RcMGq-EnbXrK7-xA,1064
|
|
14
|
+
jmcomic-2.6.0.dist-info/METADATA,sha256=aeRjLf2ds6Z465Q7EiA2F1lH_Eiy83BwQ_sl_vH1tUA,8199
|
|
15
|
+
jmcomic-2.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
jmcomic-2.6.0.dist-info/entry_points.txt,sha256=tRbQltaGSBjejI0c9jYt-4SXQMd5nSDHcMvHmuTy4ow,44
|
|
17
|
+
jmcomic-2.6.0.dist-info/top_level.txt,sha256=puvVMFYJqIbd6NOTMEvOyugMTT8woBfSQyxEBan3zY4,8
|
|
18
|
+
jmcomic-2.6.0.dist-info/RECORD,,
|
jmcomic-2.5.38.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
jmcomic/__init__.py,sha256=y1Ks64A8KjMmgD_7AQCGKpyRjU6iIFox28YgBz03_P4,903
|
|
2
|
-
jmcomic/api.py,sha256=ZduhXDmh4lg1dkXHs7UTAaPpYNO7kcdPCDH5JJT9KSI,4253
|
|
3
|
-
jmcomic/cl.py,sha256=PBSh0JndNFZw3B7WJPj5Y8SeFdKzHE00jIwYo9An-K0,3475
|
|
4
|
-
jmcomic/jm_client_impl.py,sha256=m8-e_TA3vIXdUkXJcuJ0uzek4GM48y4QvBRXrT6JqUc,39633
|
|
5
|
-
jmcomic/jm_client_interface.py,sha256=Bld80cYwBwIqcMSJK09g5q8U_UX3IWnQJxuRJQwmIOk,19082
|
|
6
|
-
jmcomic/jm_config.py,sha256=p7g2FQMVXBnejz1sSzM15YAtJ5f91fZZeNMqFeYum5I,16772
|
|
7
|
-
jmcomic/jm_downloader.py,sha256=6w9D7eoL2M85-ScH745-OZBvaC0uwGkHi6awTUzq-3E,11036
|
|
8
|
-
jmcomic/jm_entity.py,sha256=x-3yUvM74C_Gof20mJw2SmZq7sSSsGNg0Cp47oZ-dFg,19968
|
|
9
|
-
jmcomic/jm_exception.py,sha256=x3KGMLlQS2zi1GX7z5G58zJN2EwLkI4mAURkxZYjEvA,5055
|
|
10
|
-
jmcomic/jm_option.py,sha256=6hwnm4UPnQPujsA3Gkt17krnJfUF435b-272hVLqlTk,21167
|
|
11
|
-
jmcomic/jm_plugin.py,sha256=1sSPbo2ieXW-asmXapJ0ZaB1Lx5co7SS5dfbYchJ1X4,39914
|
|
12
|
-
jmcomic/jm_toolkit.py,sha256=YdzCq2QwO4NjKZickjJWVLvksmxzOf_qlcGUAua6HFs,29172
|
|
13
|
-
jmcomic-2.5.38.dist-info/licenses/LICENSE,sha256=kz4coTxZxuGxisK3W00tjK57Zh3RcMGq-EnbXrK7-xA,1064
|
|
14
|
-
jmcomic-2.5.38.dist-info/METADATA,sha256=FAnf3tUCnAmgzk91jG_o8EsN9qpccupVe7E8OyvkQgM,8161
|
|
15
|
-
jmcomic-2.5.38.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
|
16
|
-
jmcomic-2.5.38.dist-info/entry_points.txt,sha256=tRbQltaGSBjejI0c9jYt-4SXQMd5nSDHcMvHmuTy4ow,44
|
|
17
|
-
jmcomic-2.5.38.dist-info/top_level.txt,sha256=puvVMFYJqIbd6NOTMEvOyugMTT8woBfSQyxEBan3zY4,8
|
|
18
|
-
jmcomic-2.5.38.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|