From af3ead1edf4cfef41dacd271e831842121f73fed Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 7 Jun 2025 00:31:00 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6=E7=94=9F=E6=88=90=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E8=A7=84=E5=88=99=20(#437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_config.py | 4 +- src/jmcomic/jm_option.py | 31 +++---- src/jmcomic/jm_plugin.py | 175 ++++++++++----------------------------- 3 files changed, 61 insertions(+), 149 deletions(-) diff --git a/src/jmcomic/jm_config.py b/src/jmcomic/jm_config.py index 44cd46465..3e473bac5 100644 --- a/src/jmcomic/jm_config.py +++ b/src/jmcomic/jm_config.py @@ -380,7 +380,7 @@ def disable_jm_log(cls): @classmethod def new_postman(cls, session=False, **kwargs): - kwargs.setdefault('impersonate', 'chrome110') + kwargs.setdefault('impersonate', 'chrome') kwargs.setdefault('headers', JmModuleConfig.new_html_headers()) kwargs.setdefault('proxies', JmModuleConfig.DEFAULT_PROXIES) @@ -416,7 +416,7 @@ def new_postman(cls, session=False, **kwargs): 'postman': { 'type': 'curl_cffi', 'meta_data': { - 'impersonate': 'chrome110', + 'impersonate': 'chrome', 'headers': None, 'proxies': None, } diff --git a/src/jmcomic/jm_option.py b/src/jmcomic/jm_option.py index 1b182d7ca..c7e12c1cf 100644 --- a/src/jmcomic/jm_option.py +++ b/src/jmcomic/jm_option.py @@ -70,12 +70,12 @@ def decide_image_save_dir(self, album: JmAlbumDetail, photo: JmPhotoDetail, ) -> str: - return self._build_path_from_rules(album, photo) + return self.apply_rule_to_path(album, photo) def decide_album_root_dir(self, album: JmAlbumDetail) -> str: - return self._build_path_from_rules(album, None, True) + return self.apply_rule_to_path(album, None, True) - def _build_path_from_rules(self, album, photo, only_album_rules=False) -> str: + def apply_rule_to_path(self, album, photo, only_album_rules=False) -> str: path_ls = [] for rule, parser in self.parser_list: if only_album_rules and not (rule == self.RULE_BASE_DIR or rule.startswith('A')): @@ -92,7 +92,7 @@ def _build_path_from_rules(self, album, photo, only_album_rules=False) -> str: path_ls.append(path) - return fix_filepath('/'.join(path_ls), is_dir=True) + return fix_filepath('/'.join(path_ls)) def get_rule_parser_list(self, rule_dsl: str): """ @@ -103,7 +103,6 @@ def get_rule_parser_list(self, rule_dsl: str): parser_list: list = [] for rule in rule_list: - rule = rule.strip() if rule == self.RULE_BASE_DIR: parser_list.append((rule, self.parse_bd_rule)) continue @@ -135,17 +134,21 @@ def parse_detail_rule(cls, album, photo, rule: str): return str(DetailEntity.get_dirname(detail, rule[1:])) # noinspection PyMethodMayBeStatic - def split_rule_dsl(self, rule_dsl: str): - if rule_dsl == self.RULE_BASE_DIR: - return [rule_dsl] - + def split_rule_dsl(self, rule_dsl: str) -> list[str]: if '/' in rule_dsl: - return rule_dsl.split('/') + rule_list = rule_dsl.split('/') + elif '_' in rule_dsl: + rule_list = rule_dsl.split('_') + else: + rule_list = [rule_dsl] + + for i, e in enumerate(rule_list): + rule_list[i] = e.strip() - if '_' in rule_dsl: - return rule_dsl.split('_') + if rule_list[0] != self.RULE_BASE_DIR: + rule_list.insert(0, self.RULE_BASE_DIR) - ExceptionTool.raises(f'不支持的rule配置: "{rule_dsl}"') + return rule_list @classmethod def get_rule_parser(cls, rule: str): @@ -158,7 +161,7 @@ def get_rule_parser(cls, rule: str): ExceptionTool.raises(f'不支持的rule配置: "{rule}"') @classmethod - def apply_rule_directly(cls, album, photo, rule: str) -> str: + def apply_rule_to_filename(cls, album, photo, rule: str) -> str: if album is None: album = photo.from_album # noinspection PyArgumentList diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 64df45b11..7efcea761 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -1,7 +1,6 @@ """ 该文件存放的是option插件 """ -import os.path from .jm_option import * @@ -76,6 +75,9 @@ def execute_deletion(self, paths: List[str]): continue if os.path.isdir(p): + if os.listdir(p): + self.log(f'文件夹中存在非本次下载的文件,请手动删除文件夹内的文件: {p}', 'remove.ignore') + continue os.rmdir(p) self.log(f'删除文件夹: {p}', 'remove') else: @@ -104,6 +106,33 @@ def leave_wait_list(self): def wait_until_finish(self): pass + # noinspection PyMethodMayBeStatic + def decide_filepath(self, + album: Optional[JmAlbumDetail], + photo: Optional[JmPhotoDetail], + filename_rule: str, suffix: str, base_dir: Optional[str], + dir_rule_dict: Optional[dict] + ): + """ + 根据规则计算一个文件的全路径 + + 参数 dir_rule_dict 优先级最高, + 如果 dir_rule_dict 不为空,优先用 dir_rule_dict + 否则使用 base_dir + filename_rule + suffix + """ + filepath: str + base_dir: str + if dir_rule_dict is not None: + dir_rule = DirRule(**dir_rule_dict) + filepath = dir_rule.apply_rule_to_path(album, photo) + base_dir = os.path.dirname(filepath) + else: + base_dir = base_dir or os.getcwd() + filepath = os.path.join(base_dir, DirRule.apply_rule_to_filename(album, photo, filename_rule) + fix_suffix(suffix)) + + mkdir_if_not_exists(base_dir) + return filepath + class JmLoginPlugin(JmOptionPlugin): """ @@ -285,7 +314,8 @@ def invoke(self, level='photo', filename_rule='Ptitle', suffix='zip', - zip_dir='./' + zip_dir='./', + dir_rule=None, ) -> None: from .jm_downloader import JmDownloader @@ -302,12 +332,12 @@ def invoke(self, photo_dict = self.get_downloaded_photo(downloader, album, photo) if level == 'album': - zip_path = self.get_zip_path(album, None, filename_rule, suffix, zip_dir) + zip_path = self.decide_filepath(album, None, filename_rule, suffix, zip_dir, dir_rule) self.zip_album(album, photo_dict, zip_path, path_to_delete) elif level == 'photo': for photo, image_list in photo_dict.items(): - zip_path = self.get_zip_path(photo.from_album, photo, filename_rule, suffix, zip_dir) + zip_path = self.decide_filepath(photo.from_album, photo, filename_rule, suffix, zip_dir, dir_rule) self.zip_photo(photo, image_list, zip_path, path_to_delete) else: @@ -371,18 +401,6 @@ def after_zip(self, path_to_delete: List[str]): self.execute_deletion(image_paths) self.execute_deletion(dirs) - # noinspection PyMethodMayBeStatic - def get_zip_path(self, album, photo, filename_rule, suffix, zip_dir): - """ - 计算zip文件的路径 - """ - filename = DirRule.apply_rule_directly(album, photo, filename_rule) - from os.path import join - return join( - zip_dir, - filename + fix_suffix(suffix), - ) - class ClientProxyPlugin(JmOptionPlugin): plugin_key = 'client_proxy' @@ -649,95 +667,6 @@ def zip_with_password(self): self.execute_multi_line_cmd(cmd_list) -class ConvertJpgToPdfPlugin(JmOptionPlugin): - plugin_key = 'j2p' - - def check_image_suffix_is_valid(self, std_suffix): - """ - 检查option配置的图片后缀转换,目前限制使用Magick时只能搭配jpg - 暂不探究Magick是否支持更多图片格式 - """ - cur_suffix: Optional[str] = self.option.download.image.suffix - - ExceptionTool.require_true( - cur_suffix is not None and cur_suffix.endswith(std_suffix), - '请把图片的后缀转换配置为jpg,不然无法使用Magick!' - f'(当前配置是[{cur_suffix}])\n' - f'配置模板如下: \n' - f'```\n' - f'download:\n' - f' image:\n' - f' suffix: {std_suffix} # 当前配置是{cur_suffix}\n' - f'```' - ) - - def invoke(self, - photo: JmPhotoDetail, - downloader=None, - pdf_dir=None, - filename_rule='Pid', - quality=100, - delete_original_file=False, - override_cmd=None, - override_jpg=None, - **kwargs, - ): - self.delete_original_file = delete_original_file - - # 检查图片后缀配置 - suffix = override_jpg or '.jpg' - self.check_image_suffix_is_valid(suffix) - - # 处理文件夹配置 - filename = DirRule.apply_rule_directly(None, photo, filename_rule) - photo_dir = self.option.decide_image_save_dir(photo) - - # 处理生成的pdf文件的路径 - if pdf_dir is None: - pdf_dir = photo_dir - else: - pdf_dir = fix_filepath(pdf_dir, True) - mkdir_if_not_exists(pdf_dir) - - pdf_filepath = os.path.join(pdf_dir, f'{filename}.pdf') - - # 生成命令 - def generate_cmd(): - return ( - override_cmd or - 'magick convert -quality {quality} "{photo_dir}*{suffix}" "{pdf_filepath}"' - ).format( - quality=quality, - photo_dir=photo_dir, - suffix=suffix, - pdf_filepath=pdf_filepath, - ) - - cmd = generate_cmd() - self.log(f'Execute Command: [{cmd}]') - code = self.execute_cmd(cmd) - - ExceptionTool.require_true( - code == 0, - 'jpg图片合并为pdf失败!' - '请确认你是否安装了magick,安装网站: [https://www.imagemagick.org/]', - ) - - self.log(f'Convert Successfully: JM{photo.id} → {pdf_filepath}') - - if downloader is not None: - from .jm_downloader import JmDownloader - downloader: JmDownloader - - paths = [ - path - for path, image in downloader.download_success_dict[photo.from_album][photo] - ] - - paths.append(self.option.decide_image_save_dir(photo, ensure_exists=False)) - self.execute_deletion(paths) - - class Img2pdfPlugin(JmOptionPlugin): plugin_key = 'img2pdf' @@ -747,6 +676,7 @@ def invoke(self, downloader=None, pdf_dir=None, filename_rule='Pid', + dir_rule=None, delete_original_file=False, **kwargs, ): @@ -762,13 +692,7 @@ def invoke(self, self.delete_original_file = delete_original_file # 处理生成的pdf文件的路径 - pdf_dir = self.ensure_make_pdf_dir(pdf_dir) - - # 处理pdf文件名 - filename = DirRule.apply_rule_directly(album, photo, filename_rule) - - # pdf路径 - pdf_filepath = os.path.join(pdf_dir, f'{filename}.pdf') + pdf_filepath = self.decide_filepath(album, photo, filename_rule, 'pdf', pdf_dir, dir_rule) # 调用 img2pdf 把 photo_dir 下的所有图片转为pdf img_path_ls, img_dir_ls = self.write_img_2_pdf(pdf_filepath, album, photo) @@ -794,18 +718,14 @@ def write_img_2_pdf(self, pdf_filepath, album: JmAlbumDetail, photo: JmPhotoDeta continue img_path_ls += imgs + if len(img_path_ls) == 0: + self.log(f'所有文件夹都不存在图片,无法生成pdf:{img_dir_ls}', 'error') + with open(pdf_filepath, 'wb') as f: f.write(img2pdf.convert(img_path_ls)) return img_path_ls, img_dir_ls - @staticmethod - def ensure_make_pdf_dir(pdf_dir: str): - pdf_dir = pdf_dir or os.getcwd() - pdf_dir = fix_filepath(pdf_dir, True) - mkdir_if_not_exists(pdf_dir) - return pdf_dir - class LongImgPlugin(JmOptionPlugin): plugin_key = 'long_img' @@ -817,6 +737,7 @@ def invoke(self, img_dir=None, filename_rule='Pid', delete_original_file=False, + dir_rule=None, **kwargs, ): if photo is None and album is None: @@ -830,14 +751,8 @@ def invoke(self, self.delete_original_file = delete_original_file - # 处理文件夹配置 - img_dir = self.get_img_dir(img_dir) - # 处理生成的长图文件的路径 - filename = DirRule.apply_rule_directly(album, photo, filename_rule) - - # 长图路径 - long_img_path = os.path.join(img_dir, f'{filename}.png') + long_img_path = self.decide_filepath(album, photo, filename_rule, 'png', img_dir, dir_rule) # 调用 PIL 把 photo_dir 下的所有图片合并为长图 img_path_ls = self.write_img_2_long_img(long_img_path, album, photo) @@ -856,7 +771,7 @@ def write_img_2_long_img(self, long_img_path, album: JmAlbumDetail, photo: JmPho img_dir_items = [self.option.decide_image_save_dir(photo) for photo in album] img_paths = itertools.chain(*map(files_of_dir, img_dir_items)) - img_paths = filter(lambda x: not x.startswith('.'), img_paths) # 过滤系统文件 + img_paths = list(filter(lambda x: not x.startswith('.'), img_paths)) # 过滤系统文件 images = self.open_images(img_paths) @@ -895,12 +810,6 @@ def open_images(self, img_paths: List[str]): self.log(f"Failed to open image {img_path}: {e}", 'error') return images - @staticmethod - def get_img_dir(img_dir: Optional[str]) -> str: - img_dir = fix_filepath(img_dir or os.getcwd()) - mkdir_if_not_exists(img_dir) - return img_dir - class JmServerPlugin(JmOptionPlugin): plugin_key = 'jm_server' From a975fe88960fdd95111d35ea08a15aff2690a734 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 7 Jun 2025 10:39:25 +0800 Subject: [PATCH 2/6] docs --- README.md | 5 +++-- assets/docs/sources/option_file_syntax.md | 15 --------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 48abab89a..bba6ac41d 100644 --- a/README.md +++ b/README.md @@ -157,10 +157,11 @@ jmcomic 123 - `下载特定后缀图片插件` - `发送QQ邮件插件` - `自动使用浏览器cookies插件` - - `jpg图片合成为一个pdf插件` - `导出收藏夹为csv文件插件` - `合并所有图片为pdf文件插件` - - `合并所有图片为长图插件` + - `合并所有图片为长图png插件` + - `重复文件检测删除插件` + - `网页观看本地章节插件` ## 使用小说明 diff --git a/assets/docs/sources/option_file_syntax.md b/assets/docs/sources/option_file_syntax.md index ea4c9873d..073bd853c 100644 --- a/assets/docs/sources/option_file_syntax.md +++ b/assets/docs/sources/option_file_syntax.md @@ -264,19 +264,4 @@ plugins: img_dir: D:/pdf/ # 长图存放文件夹 filename_rule: Aname # 长图命名规则,同上 - # 请注意⚠ - # 下方的j2p插件的功能不如img2pdf插件,不建议使用。 - # 如有图片转pdf的需求,直接使用img2pdf即可,下面的内容请忽略。 - - - plugin: j2p # 图片合并插件,可以将下载下来的jpg图片合成为一个pdf插件 - # 请注意⚠ 该插件的使用前提是,下载下来的图片是jpg图片 - # 因此,使用该插件前,需要有如下配置:(下载图片格式转为jpg,上文有解释过此配置) - # download: - # image: - # suffix: .jpg - kwargs: - pdf_dir: D:/pdf/ # pdf存放文件夹 - filename_rule: Pid # pdf命名规则 - quality: 100 # pdf质量,0 - 100 - ``` From 8a8944b8b3e86bff17ad11072c6e903bbfa5ab1e Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:59:06 +0800 Subject: [PATCH 3/6] =?UTF-8?q?zip=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_plugin.py | 90 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 7efcea761..6abdce708 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -56,11 +56,12 @@ def require_param(self, case: Any, msg: str): raise PluginValidationException(self, msg) - def warning_lib_not_install(self, lib: str): + def warning_lib_not_install(self, lib: str, throw=False): msg = (f'插件`{self.plugin_key}`依赖库: {lib},请先安装{lib}再使用。' f'安装命令: [pip install {lib}]') import warnings warnings.warn(msg) + self.require_param(throw, msg) def execute_deletion(self, paths: List[str]): """ @@ -303,6 +304,11 @@ def do_filter(self, detail): class ZipPlugin(JmOptionPlugin): + """ + 感谢zip加密功能的贡献者: + - AXIS5 a.k.a AXIS5Hacker (https://github.com/hect0x7/JMComic-Crawler-Python/pull/375) + """ + plugin_key = 'zip' # noinspection PyAttributeOutsideInit @@ -316,6 +322,7 @@ def invoke(self, suffix='zip', zip_dir='./', dir_rule=None, + encrypt=None, ) -> None: from .jm_downloader import JmDownloader @@ -333,18 +340,19 @@ def invoke(self, if level == 'album': zip_path = self.decide_filepath(album, None, filename_rule, suffix, zip_dir, dir_rule) - self.zip_album(album, photo_dict, zip_path, path_to_delete) + self.zip_album(album, photo_dict, zip_path, path_to_delete, encrypt) elif level == 'photo': for photo, image_list in photo_dict.items(): zip_path = self.decide_filepath(photo.from_album, photo, filename_rule, suffix, zip_dir, dir_rule) - self.zip_photo(photo, image_list, zip_path, path_to_delete) + self.zip_photo(photo, image_list, zip_path, path_to_delete, encrypt) else: ExceptionTool.raises(f'Not Implemented Zip Level: {level}') self.after_zip(path_to_delete) + # noinspection PyMethodMayBeStatic def get_downloaded_photo(self, downloader, album, photo): return ( downloader.download_success_dict[album] @@ -352,7 +360,7 @@ def get_downloaded_photo(self, downloader, album, photo): else downloader.download_success_dict[photo.from_album] # after_photo ) - def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete): + def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete, encrypt_dict): """ 压缩photo文件夹 """ @@ -360,8 +368,11 @@ def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete): if len(image_list) == 0 \ else os.path.dirname(image_list[0][0]) - from common import backup_dir_to_zip - backup_dir_to_zip(photo_dir, zip_path) + with self.open_zip_file(zip_path, encrypt_dict) as f: + for file in files_of_dir(photo_dir): + abspath = os.path.join(photo_dir, file) + relpath = os.path.relpath(abspath, photo_dir) + f.write(abspath, relpath) self.log(f'压缩章节[{photo.photo_id}]成功 → {zip_path}', 'finish') path_to_delete.append(self.unified_path(photo_dir)) @@ -370,14 +381,13 @@ def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete): def unified_path(f): return fix_filepath(f, os.path.isdir(f)) - def zip_album(self, album, photo_dict: dict, zip_path, path_to_delete): + def zip_album(self, album, photo_dict: dict, zip_path, path_to_delete, encrypt_dict): """ 压缩album文件夹 """ album_dir = self.option.dir_rule.decide_album_root_dir(album) - import zipfile - with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as f: + with self.open_zip_file(zip_path, encrypt_dict) as f: for photo in photo_dict.keys(): # 定位到章节所在文件夹 photo_dir = self.unified_path(self.option.decide_image_save_dir(photo)) @@ -401,6 +411,67 @@ def after_zip(self, path_to_delete: List[str]): self.execute_deletion(image_paths) self.execute_deletion(dirs) + # noinspection PyMethodMayBeStatic + @classmethod + def generate_random_str(cls, random_length) -> str: + """ + 自动生成随机字符密码,长度由randomlength指定 + """ + import random + + random_str = '' + base_str = r'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + base_length = len(base_str) - 1 + for _ in range(random_length): + random_str += base_str[random.randint(0, base_length)] + return random_str + + def open_zip_file(self, zip_path: str, encrypt_dict: Optional[dict]): + if encrypt_dict is None: + import zipfile + return zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) + + password, is_random = self.decide_password(encrypt_dict, zip_path) + if encrypt_dict.get('impl', '') == '7z': + try: + # noinspection PyUnresolvedReferences + import py7zr + except ImportError: + self.warning_lib_not_install('py7zr') + + # noinspection PyUnboundLocalVariable + filters = [{'id': py7zr.FILTER_COPY}] + return py7zr.SevenZipFile(zip_path, mode='w', password=password, filters=filters, header_encryption=True) + else: + try: + # noinspection PyUnresolvedReferences + import pyzipper + except ImportError: + self.warning_lib_not_install('pyzipper') + + # noinspection PyUnboundLocalVariable + aes_zip_file = pyzipper.AESZipFile(zip_path, "w", pyzipper.ZIP_DEFLATED) + aes_zip_file.setencryption(pyzipper.WZ_AES, nbits=128) + password_bytes = str.encode(password) + aes_zip_file.setpassword(password_bytes) + if is_random: + aes_zip_file.comment = password_bytes + return aes_zip_file + + def decide_password(self, encrypt_dict: dict, zip_path: str): + encrypt_type = encrypt_dict.get('type', '') + is_random = False + + if encrypt_type == 'random': + is_random = True + password = self.generate_random_str(48) + self.log(f'生成随机密码: [{password}] → [{zip_path}]', 'encrypt') + else: + password = str(encrypt_dict['password']) + self.log(f'使用指定密码: [{password}] → [{zip_path}]', 'encrypt') + + return password, is_random + class ClientProxyPlugin(JmOptionPlugin): plugin_key = 'client_proxy' @@ -869,6 +940,7 @@ def invoke(self, # 服务器的代码位于一个独立库:plugin_jm_server,需要独立安装 # 源代码仓库:https://github.com/hect0x7/plugin-jm-server try: + # noinspection PyUnresolvedReferences import plugin_jm_server self.log(f'当前使用plugin_jm_server版本: {plugin_jm_server.__version__}') except ImportError: From 989aa66d0cfa1cd319018194354fe64443cb6e94 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:00:37 +0800 Subject: [PATCH 4/6] =?UTF-8?q?v2.5.40:=20=E5=8E=8B=E7=BC=A9=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=94=AF=E6=8C=81zip=E5=8A=A0=E5=AF=86=EF=BC=8C7z?= =?UTF-8?q?=E5=8A=A0=E5=AF=86;=20=E6=8F=92=E4=BB=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E4=BD=BF=E7=94=A8dir=5Frule=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0filename=5Frule=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 072710c35..ffca0eccc 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.5.39' +__version__ = '2.5.40' from .api import * from .jm_plugin import * From 674c02ad18ea8deee390f909c9820d75a8dd9a58 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:10:07 +0800 Subject: [PATCH 5/6] =?UTF-8?q?v2.6.0:=20=E5=AE=9E=E7=8E=B0API=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E5=8F=AF=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=96=B0;=20?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E6=8F=92=E4=BB=B6=E6=94=AF=E6=8C=81zip?= =?UTF-8?q?=E5=8A=A0=E5=AF=86=E3=80=817z=E5=8A=A0=E5=AF=86;=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E9=85=8D=E7=BD=AE=E6=94=AF=E6=8C=81dir=5Frule?= =?UTF-8?q?=EF=BC=88=E5=A2=9E=E5=BC=BA=E7=89=88filename=5Frule=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/__init__.py | 2 +- src/jmcomic/jm_client_impl.py | 41 +++++++++++++++++++++++++++++++++++ src/jmcomic/jm_config.py | 14 +++++++++--- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index ffca0eccc..edd91389d 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.5.40' +__version__ = '2.6.0' from .api import * from .jm_plugin import * diff --git a/src/jmcomic/jm_client_impl.py b/src/jmcomic/jm_client_impl.py index a9ac34632..2cb945206 100644 --- a/src/jmcomic/jm_client_impl.py +++ b/src/jmcomic/jm_client_impl.py @@ -1005,10 +1005,51 @@ def raise_if_resp_should_retry(self, resp): ExceptionTool.raises_resp(f'响应无数据!request_url=[{url}]', resp) def after_init(self): + # 自动更新禁漫API域名 + if JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN: + self.update_api_domain() + # 保证拥有cookies,因为移动端要求必须携带cookies,否则会直接跳转同一本子【禁漫娘】 if JmModuleConfig.FLAG_API_CLIENT_REQUIRE_COOKIES: self.ensure_have_cookies() + client_update_domain_lock = Lock() + + def update_api_domain(self): + if True is JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE: + return + + with self.client_update_domain_lock: + if True is JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE: + return + try: + # 获取域名列表 + resp = self.postman.get(JmModuleConfig.API_URL_DOMAIN_SERVER) + res_json = JmCryptoTool.decode_resp_data(resp.text, '', JmMagicConstants.API_DOMAIN_SERVER_SECRET) + res_data = json_loads(res_json) + + # 检查返回值 + if not res_data.get('Server', None): + jm_log('api.update_domain.empty', + f'获取禁漫最新API域名失败, 返回值: {res_json}') + return + new_server_list: list[str] = res_data['Server'] + old_server_list = JmModuleConfig.DOMAIN_API_LIST + jm_log('api.update_domain.success', + f'获取到最新的API域名,替换jmcomic内置域名:(new){new_server_list} ---→ (old){old_server_list}' + ) + # 更新域名 + if self.domain_list is old_server_list: + self.domain_list = new_server_list + JmModuleConfig.DOMAIN_API_LIST = new_server_list + except Exception as e: + jm_log('api.update_domain.error', + f'自动更新API域名失败,仍使用jmcomic内置域名。' + f'可通过代码[JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN=False]关闭自动更新API域名. 异常: {e}' + ) + finally: + JmModuleConfig.FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = True + client_init_cookies_lock = Lock() def ensure_have_cookies(self): diff --git a/src/jmcomic/jm_config.py b/src/jmcomic/jm_config.py index 3e473bac5..72880a238 100644 --- a/src/jmcomic/jm_config.py +++ b/src/jmcomic/jm_config.py @@ -76,7 +76,8 @@ class JmMagicConstants: APP_TOKEN_SECRET = '18comicAPP' APP_TOKEN_SECRET_2 = '18comicAPPContent' APP_DATA_SECRET = '185Hcomic3PAPP7R' - APP_VERSION = '1.7.9' + API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik' + APP_VERSION = '1.8.0' # 模块级别共用配置 @@ -128,11 +129,15 @@ class JmModuleConfig: # 移动端API域名 DOMAIN_API_LIST = shuffled(''' www.cdnmhwscc.vip - www.cdnblackmyth.club - www.cdnmhws.cc + www.cdnplaystation6.club + www.cdnplaystation6.org www.cdnuc.vip + www.cdn-mspjmapiproxy.xyz ''') + # 获取最新移动端API域名的地址 + API_URL_DOMAIN_SERVER = f'{PROT}jmappc01-1308024008.cos.ap-guangzhou.myqcloud.com/server-2024.txt' + APP_HEADERS_TEMPLATE = { 'Accept-Encoding': 'gzip, deflate', 'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, ' @@ -200,6 +205,9 @@ class JmModuleConfig: FLAG_USE_FIX_TIMESTAMP = True # 移动端Client初始化cookies FLAG_API_CLIENT_REQUIRE_COOKIES = True + # 自动更新禁漫API域名 + FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN = True + FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = None # log开关标记 FLAG_ENABLE_JM_LOG = True # log时解码url From d101a2bb0f08bbc766313171594abf213bdaeaca Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:15:41 +0800 Subject: [PATCH 6/6] polish [skip ci] --- src/jmcomic/jm_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 6abdce708..2f64dae0b 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -437,7 +437,7 @@ def open_zip_file(self, zip_path: str, encrypt_dict: Optional[dict]): # noinspection PyUnresolvedReferences import py7zr except ImportError: - self.warning_lib_not_install('py7zr') + self.warning_lib_not_install('py7zr', True) # noinspection PyUnboundLocalVariable filters = [{'id': py7zr.FILTER_COPY}] @@ -447,7 +447,7 @@ def open_zip_file(self, zip_path: str, encrypt_dict: Optional[dict]): # noinspection PyUnresolvedReferences import pyzipper except ImportError: - self.warning_lib_not_install('pyzipper') + self.warning_lib_not_install('pyzipper', True) # noinspection PyUnboundLocalVariable aes_zip_file = pyzipper.AESZipFile(zip_path, "w", pyzipper.ZIP_DEFLATED)