jmcomic 2.5.38__tar.gz → 2.6.0__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.
Files changed (23) hide show
  1. {jmcomic-2.5.38/src/jmcomic.egg-info → jmcomic-2.6.0}/PKG-INFO +4 -3
  2. {jmcomic-2.5.38 → jmcomic-2.6.0}/README.md +3 -2
  3. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/__init__.py +1 -1
  4. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_client_impl.py +41 -0
  5. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_config.py +13 -5
  6. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_option.py +17 -14
  7. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_plugin.py +119 -138
  8. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_toolkit.py +3 -3
  9. {jmcomic-2.5.38 → jmcomic-2.6.0/src/jmcomic.egg-info}/PKG-INFO +4 -3
  10. {jmcomic-2.5.38 → jmcomic-2.6.0}/LICENSE +0 -0
  11. {jmcomic-2.5.38 → jmcomic-2.6.0}/setup.cfg +0 -0
  12. {jmcomic-2.5.38 → jmcomic-2.6.0}/setup.py +0 -0
  13. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/api.py +0 -0
  14. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/cl.py +0 -0
  15. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_client_interface.py +0 -0
  16. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_downloader.py +0 -0
  17. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_entity.py +0 -0
  18. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic/jm_exception.py +0 -0
  19. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic.egg-info/SOURCES.txt +0 -0
  20. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic.egg-info/dependency_links.txt +0 -0
  21. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic.egg-info/entry_points.txt +0 -0
  22. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic.egg-info/requires.txt +0 -0
  23. {jmcomic-2.5.38 → jmcomic-2.6.0}/src/jmcomic.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmcomic
3
- Version: 2.5.38
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
 
@@ -157,10 +157,11 @@ jmcomic 123
157
157
  - `下载特定后缀图片插件`
158
158
  - `发送QQ邮件插件`
159
159
  - `自动使用浏览器cookies插件`
160
- - `jpg图片合成为一个pdf插件`
161
160
  - `导出收藏夹为csv文件插件`
162
161
  - `合并所有图片为pdf文件插件`
163
- - `合并所有图片为长图插件`
162
+ - `合并所有图片为长图png插件`
163
+ - `重复文件检测删除插件`
164
+ - `网页观看本地章节插件`
164
165
 
165
166
  ## 使用小说明
166
167
 
@@ -2,7 +2,7 @@
2
2
  # 被依赖方 <--- 使用方
3
3
  # config <--- entity <--- toolkit <--- client <--- option <--- downloader
4
4
 
5
- __version__ = '2.5.38'
5
+ __version__ = '2.6.0'
6
6
 
7
7
  from .api import *
8
8
  from .jm_plugin import *
@@ -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):
@@ -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
- APP_VERSION = '1.7.9'
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.cdnblackmyth.club
132
- www.cdnmhws.cc
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', 'chrome110')
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': 'chrome110',
427
+ 'impersonate': 'chrome',
420
428
  'headers': None,
421
429
  'proxies': None,
422
430
  }
@@ -70,12 +70,12 @@ class DirRule:
70
70
  album: JmAlbumDetail,
71
71
  photo: JmPhotoDetail,
72
72
  ) -> str:
73
- return self._build_path_from_rules(album, photo)
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._build_path_from_rules(album, None, True)
76
+ return self.apply_rule_to_path(album, None, True)
77
77
 
78
- def _build_path_from_rules(self, album, photo, only_album_rules=False) -> str:
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), is_dir=True)
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
- return rule_dsl.split('/')
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 '_' in rule_dsl:
146
- return rule_dsl.split('_')
148
+ if rule_list[0] != self.RULE_BASE_DIR:
149
+ rule_list.insert(0, self.RULE_BASE_DIR)
147
150
 
148
- ExceptionTool.raises(f'不支持的rule配置: "{rule_dsl}"')
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 apply_rule_directly(cls, album, photo, rule: str) -> str:
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
@@ -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.get_zip_path(album, None, filename_rule, suffix, zip_dir)
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.get_zip_path(photo.from_album, photo, filename_rule, suffix, zip_dir)
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
- from common import backup_dir_to_zip
334
- backup_dir_to_zip(photo_dir, zip_path)
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
- import zipfile
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
- def get_zip_path(self, album, photo, filename_rule, suffix, zip_dir):
415
+ @classmethod
416
+ def generate_random_str(cls, random_length) -> str:
376
417
  """
377
- 计算zip文件的路径
418
+ 自动生成随机字符密码,长度由randomlength指定
378
419
  """
379
- filename = DirRule.apply_rule_directly(album, photo, filename_rule)
380
- from os.path import join
381
- return join(
382
- zip_dir,
383
- filename + fix_suffix(suffix),
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
- pdf_dir = self.ensure_make_pdf_dir(pdf_dir)
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
- filename = DirRule.apply_rule_directly(album, photo, filename_rule)
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:
@@ -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'<h2 class="book-name" id="book-name"[^>]*?>([\s\S]*?)</h2>')
28
- pattern_html_album_episode_list = compile(r'data-album="(\d+)"[^>]*>\s*?<li.*?>\s*?第(\d+)[话話]([\s\S]*?)<[\s\S]*?>')
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'作者: *<span itemprop="author" data-type="author">([\s\S]*?)</span>'),
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.5.38
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
 
File without changes
File without changes
File without changes
File without changes
File without changes