From fcd99c05e18456f09e0401000564188f57f198e6 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 28 Mar 2026 23:18:16 +0800 Subject: [PATCH 1/5] =?UTF-8?q?v2.6.17:=20=E4=BF=AE=E5=A4=8D=20download=5F?= =?UTF-8?q?cover=20=E6=8F=92=E4=BB=B6=E4=B8=8D=E5=85=BC=E5=AE=B9=20downloa?= =?UTF-8?q?d=5Fphoto=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/__init__.py | 2 +- src/jmcomic/jm_config.py | 8 +++--- src/jmcomic/jm_option.py | 6 ++--- src/jmcomic/jm_plugin.py | 30 ++++++++++++++++------ tests/test_jmcomic/test_jm_plugin.py | 37 ++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 tests/test_jmcomic/test_jm_plugin.py diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 4c2fce5e4..a052b0943 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.6.16' +__version__ = '2.6.17' from .api import * from .jm_plugin import * diff --git a/src/jmcomic/jm_config.py b/src/jmcomic/jm_config.py index 59ab93ca5..9a1c8c257 100644 --- a/src/jmcomic/jm_config.py +++ b/src/jmcomic/jm_config.py @@ -102,7 +102,7 @@ class JmMagicConstants: APP_TOKEN_SECRET_2 = '18comicAPPContent' APP_DATA_SECRET = '185Hcomic3PAPP7R' API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik' - APP_VERSION = '2.0.18' + APP_VERSION = '2.0.19' # 模块级别共用配置 @@ -405,8 +405,8 @@ def get_fix_ts_token_tokenparam(cls): return ts, token, tokenparam @classmethod - def jm_log(cls, topic: str, msg: str, e: Exception = None): - if cls.FLAG_ENABLE_JM_LOG is True: + def jm_log(cls, topic: str, msg, e: Exception = None): + if cls.FLAG_ENABLE_JM_LOG: executor = cls.EXECUTOR_LOG if e is None: executor(topic, msg) @@ -440,7 +440,7 @@ def new_postman(cls, session=False, **kwargs): from common import Postmans - if session is True: + if session: return Postmans.new_session(**kwargs) return Postmans.new_postman(**kwargs) diff --git a/src/jmcomic/jm_option.py b/src/jmcomic/jm_option.py index d1841494a..2d5502a03 100644 --- a/src/jmcomic/jm_option.py +++ b/src/jmcomic/jm_option.py @@ -298,7 +298,7 @@ def construct(cls, origdic: Dict, cover_default=True) -> 'JmOption': # log log = dic.pop('log', True) - if log is False: + if not log: disable_jm_log() # version @@ -522,7 +522,7 @@ def download_photo(self, # 下面的方法为调用插件提供支持 - def call_all_plugin(self, group: str, safe=True, **extra): + def call_all_plugin(self, group: str, safe=None, **extra): plugin_list: List[dict] = self.plugins.get(group, []) if plugin_list is None or len(plugin_list) == 0: return @@ -540,7 +540,7 @@ def call_all_plugin(self, group: str, safe=True, **extra): try: self.invoke_plugin(pclass, kwargs, extra, pinfo) except BaseException as e: - if safe is True: + if safe is True or pinfo.get('safe', True): jm_log('plugin.exception', e) else: raise e diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 71cc8f83d..38b1b0637 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -36,7 +36,7 @@ def build(cls, option: JmOption) -> 'JmOptionPlugin': return cls(option) def log(self, msg, topic=None): - if self.log_enable is not True: + if self.log_enable: return jm_log( @@ -68,7 +68,7 @@ def execute_deletion(self, paths: List[str]): 删除文件和文件夹 :param paths: 路径列表 """ - if self.delete_original_file is not True: + if self.delete_original_file: return for p in paths: @@ -120,7 +120,11 @@ def decide_filepath(self, 参数 dir_rule_dict 优先级最高, 如果 dir_rule_dict 不为空,优先用 dir_rule_dict 否则使用 base_dir + filename_rule + suffix + + 当album为空时,自动复制为photo.from_album,防止底层dir_rule的dsl包含Axx报错 """ + if album is None: + album = photo.from_album filepath: str base_dir: str if dir_rule_dict is not None: @@ -248,7 +252,7 @@ def warning(): ]) self.log(msg, topic='log') - if enable_warning is True: + if enable_warning: # 警告 warning() @@ -776,7 +780,10 @@ def invoke(self, 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, encrypt) + result = self.write_img_2_pdf(pdf_filepath, album, photo, encrypt) + if not result: + return + img_path_ls, img_dir_ls = result self.log(f'Convert Successfully: JM{album or photo} → {pdf_filepath}') # 执行删除 @@ -801,6 +808,7 @@ def write_img_2_pdf(self, pdf_filepath, album: JmAlbumDetail, photo: JmPhotoDeta if len(img_path_ls) == 0: self.log(f'所有文件夹都不存在图片,无法生成pdf:{img_dir_ls}', 'error') + return with open(pdf_filepath, 'wb') as f: f.write(img2pdf.convert(img_path_ls)) @@ -851,12 +859,14 @@ def invoke(self, # 调用 PIL 把 photo_dir 下的所有图片合并为长图 img_path_ls = self.write_img_2_long_img(long_img_path, album, photo) + if not img_path_ls: + return self.log(f'Convert Successfully: JM{album or photo} → {long_img_path}') # 执行删除 self.execute_deletion(img_path_ls) - def write_img_2_long_img(self, long_img_path, album: JmAlbumDetail, photo: JmPhotoDetail) -> List[str]: + def write_img_2_long_img(self, long_img_path, album: JmAlbumDetail, photo: JmPhotoDetail) -> Optional[List[str]]: import itertools from PIL import Image @@ -868,6 +878,10 @@ def write_img_2_long_img(self, long_img_path, album: JmAlbumDetail, photo: JmPho img_paths = itertools.chain(*map(files_of_dir, img_dir_items)) img_paths = list(filter(lambda x: not x.startswith('.'), img_paths)) # 过滤系统文件 + if not img_paths: + self.log(f'所有文件夹都不存在图片,无法生成long_img:{img_paths}', 'error') + return + images = self.open_images(img_paths) try: @@ -954,11 +968,11 @@ def invoke(self, base_run_kwargs.update(run) run = base_run_kwargs - if self.running is True: + if self.running: return with self.run_server_lock: - if self.running is True: + if self.running: return # 服务器的代码位于一个独立库:plugin_jm_server,需要独立安装 @@ -1074,7 +1088,7 @@ def invoke(self, self.log('Exception happened: ' + str(e), 'check_update.error') continue - if has_update is False: + if not has_update: continue self.log(f'album={album_id},发现新章节: {photo_new_list},准备开始下载') diff --git a/tests/test_jmcomic/test_jm_plugin.py b/tests/test_jmcomic/test_jm_plugin.py new file mode 100644 index 000000000..8efa286db --- /dev/null +++ b/tests/test_jmcomic/test_jm_plugin.py @@ -0,0 +1,37 @@ +from test_jmcomic import * + + +class Test_Plugin(JmTestConfigurable): + + def test_plugin_missing_album_context(self): + """ + source: https://github.com/hect0x7/JMComic-Crawler-Python/issues/523 + + 测试当仅下载单章(photo)时,如果上下文中缺少 album 对象, + 各个包含路径生成的插件(如 download_cover, img2pdf, long_img, zip) + 是否能正确从 photo.from_album 中提取专辑属性, + 避免解析需要 {Atitle} 等本子级占位符时报错 KeyError。 + """ + photo_id = '350234' + option = self.new_option() + + flawed_rule = { + 'base_dir': option.dir_rule.base_dir, + 'rule': '{Atitle}/{Aid}_photo.jpg' + } + + from jmcomic.jm_downloader import DoNotDownloadImage + + # 将四个需要校验的插件全部进行孤立测试,避免前一个插件后续报错导致循环终端 + test_plugins = ['download_cover', 'img2pdf', 'long_img', 'zip'] + option.plugins['before_photo'] = [ + { + 'plugin': plugin_key, + 'kwargs': {'dir_rule': flawed_rule}, + 'safe': False # 防止内部catch异常 + } + for plugin_key in test_plugins + ] + + download_photo(photo_id, option, downloader=DoNotDownloadImage) + print('✅ All folder rule plugins assert completed safely without KeyError.') From 89a7e385c7c501517bba05bb750644bf142577c0 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:29:51 +0800 Subject: [PATCH 2/5] tests fix --- tests/test_jmcomic/test_jm_custom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_jmcomic/test_jm_custom.py b/tests/test_jmcomic/test_jm_custom.py index 1bbb222c7..174b119f3 100644 --- a/tests/test_jmcomic/test_jm_custom.py +++ b/tests/test_jmcomic/test_jm_custom.py @@ -36,6 +36,8 @@ def pname(self): os.path.abspath(save_dir), os.path.abspath(base_dir + dic[1] + '/' + dic[2]), ) + JmModuleConfig.CLASS_ALBUM = JmAlbumDetail + JmModuleConfig.CLASS_PHOTO = JmPhotoDetail def test_extends_api_client(self): class MyClient(JmApiClient): From 4d36f70ab9bb940bbf345a710102b9dac5b6295b Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:38:35 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=20CodeRabbit=20Co?= =?UTF-8?q?de=20Review=20=E6=8F=90=E5=87=BA=E7=9A=84=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=8F=8D=E8=BD=AC=E4=B8=8E=E7=BC=BA=E7=9C=81=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20(PR=20#524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.txt | 3 ++- src/jmcomic/jm_option.py | 2 +- src/jmcomic/jm_plugin.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f6ed9b825..07ed2aa11 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,5 @@ psutil pycryptodome requests plugin_jm_server -zhconv \ No newline at end of file +zhconv +img2pdf \ No newline at end of file diff --git a/src/jmcomic/jm_option.py b/src/jmcomic/jm_option.py index 2d5502a03..edff61d55 100644 --- a/src/jmcomic/jm_option.py +++ b/src/jmcomic/jm_option.py @@ -298,7 +298,7 @@ def construct(cls, origdic: Dict, cover_default=True) -> 'JmOption': # log log = dic.pop('log', True) - if not log: + if log is False: disable_jm_log() # version diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 38b1b0637..8da3e1cbb 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -68,7 +68,7 @@ def execute_deletion(self, paths: List[str]): 删除文件和文件夹 :param paths: 路径列表 """ - if self.delete_original_file: + if not self.delete_original_file: return for p in paths: From 2b96c269cbfb40210ba6e60c93e9f1d360a9806a Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:48:28 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=20CodeRabbit=20?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E8=BD=AE=E5=AE=A1=E6=9F=A5(=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=97=A0=E4=BC=A4=E6=B5=8B=E8=AF=95=E4=BD=93=E7=B3=BB?= =?UTF-8?q?=E4=B8=8E=E7=A9=BA=E7=9B=B8=E5=86=8C=E5=85=9C=E5=BA=95=E6=97=A5?= =?UTF-8?q?=E5=BF=97)=20(PR=20#524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_plugin.py | 2 ++ tests/test_jmcomic/test_jm_plugin.py | 36 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 8da3e1cbb..6ce584b54 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -125,6 +125,8 @@ def decide_filepath(self, """ if album is None: album = photo.from_album + if album is None: + jm_log('plugin', f'Warning: album context is None for photo={photo.photo_id}, dir_rule placeholders may fail') filepath: str base_dir: str if dir_rule_dict is not None: diff --git a/tests/test_jmcomic/test_jm_plugin.py b/tests/test_jmcomic/test_jm_plugin.py index 8efa286db..120bdae48 100644 --- a/tests/test_jmcomic/test_jm_plugin.py +++ b/tests/test_jmcomic/test_jm_plugin.py @@ -12,7 +12,6 @@ def test_plugin_missing_album_context(self): 是否能正确从 photo.from_album 中提取专辑属性, 避免解析需要 {Atitle} 等本子级占位符时报错 KeyError。 """ - photo_id = '350234' option = self.new_option() flawed_rule = { @@ -20,18 +19,27 @@ def test_plugin_missing_album_context(self): 'rule': '{Atitle}/{Aid}_photo.jpg' } - from jmcomic.jm_downloader import DoNotDownloadImage + # 构建本地 fixture,打断真实网络与 API 下载依赖 + album = JmAlbumDetail('111', '', None, {'id': '111', 'title': 'TestAlbum'}) + photo_with_album = JmPhotoDetail('222', '', None, {'id': '222', 'title': 'TestPhoto'}) + photo_with_album.from_album = album - # 将四个需要校验的插件全部进行孤立测试,避免前一个插件后续报错导致循环终端 test_plugins = ['download_cover', 'img2pdf', 'long_img', 'zip'] - option.plugins['before_photo'] = [ - { - 'plugin': plugin_key, - 'kwargs': {'dir_rule': flawed_rule}, - 'safe': False # 防止内部catch异常 - } - for plugin_key in test_plugins - ] - - download_photo(photo_id, option, downloader=DoNotDownloadImage) - print('✅ All folder rule plugins assert completed safely without KeyError.') + + for plugin_key in test_plugins: + plugin_class = option.plugin_dict.get(plugin_key) + if not plugin_class: + continue + + # 实例化 + plugin: JmOptionPlugin = plugin_class(option) + + # 孤立测试路径解析,绕过外部 optional dependencies (如 img2pdf, Pillow) 的阻扰 + try: + # 传入 album=None,预期插件内部提取 photo_with_album 的 from_album 并在路径中成功解析 TestAlbum + filepath = plugin.decide_filepath(None, photo_with_album, '{Atitle}', '.jpg', None, flawed_rule) + self.assertIn('TestAlbum', filepath, f"Plugin {plugin_key} Failed to resolve Atitle.") + except KeyError as e: + self.fail(f"Plugin {plugin_key} Failed to populate album from photo object: {e}") + + print('✅ All folder rule plugins passed self-contained path generation tests without dependency requirements.') From 93c3b67111640e4fbe7792f74f288fd4e5a7250a Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:51:25 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E7=9A=84=E9=98=B2=E5=BE=A1=E6=80=A7=E6=97=A5=E5=BF=97?= =?UTF-8?q?=EF=BC=8C=E6=81=A2=E5=A4=8D=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=20(PR=20#524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/jmcomic/jm_plugin.py | 2 -- tests/test_jmcomic/test_jm_plugin.py | 36 +++++++++++----------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 6ce584b54..8da3e1cbb 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -125,8 +125,6 @@ def decide_filepath(self, """ if album is None: album = photo.from_album - if album is None: - jm_log('plugin', f'Warning: album context is None for photo={photo.photo_id}, dir_rule placeholders may fail') filepath: str base_dir: str if dir_rule_dict is not None: diff --git a/tests/test_jmcomic/test_jm_plugin.py b/tests/test_jmcomic/test_jm_plugin.py index 120bdae48..8efa286db 100644 --- a/tests/test_jmcomic/test_jm_plugin.py +++ b/tests/test_jmcomic/test_jm_plugin.py @@ -12,6 +12,7 @@ def test_plugin_missing_album_context(self): 是否能正确从 photo.from_album 中提取专辑属性, 避免解析需要 {Atitle} 等本子级占位符时报错 KeyError。 """ + photo_id = '350234' option = self.new_option() flawed_rule = { @@ -19,27 +20,18 @@ def test_plugin_missing_album_context(self): 'rule': '{Atitle}/{Aid}_photo.jpg' } - # 构建本地 fixture,打断真实网络与 API 下载依赖 - album = JmAlbumDetail('111', '', None, {'id': '111', 'title': 'TestAlbum'}) - photo_with_album = JmPhotoDetail('222', '', None, {'id': '222', 'title': 'TestPhoto'}) - photo_with_album.from_album = album + from jmcomic.jm_downloader import DoNotDownloadImage + # 将四个需要校验的插件全部进行孤立测试,避免前一个插件后续报错导致循环终端 test_plugins = ['download_cover', 'img2pdf', 'long_img', 'zip'] - - for plugin_key in test_plugins: - plugin_class = option.plugin_dict.get(plugin_key) - if not plugin_class: - continue - - # 实例化 - plugin: JmOptionPlugin = plugin_class(option) - - # 孤立测试路径解析,绕过外部 optional dependencies (如 img2pdf, Pillow) 的阻扰 - try: - # 传入 album=None,预期插件内部提取 photo_with_album 的 from_album 并在路径中成功解析 TestAlbum - filepath = plugin.decide_filepath(None, photo_with_album, '{Atitle}', '.jpg', None, flawed_rule) - self.assertIn('TestAlbum', filepath, f"Plugin {plugin_key} Failed to resolve Atitle.") - except KeyError as e: - self.fail(f"Plugin {plugin_key} Failed to populate album from photo object: {e}") - - print('✅ All folder rule plugins passed self-contained path generation tests without dependency requirements.') + option.plugins['before_photo'] = [ + { + 'plugin': plugin_key, + 'kwargs': {'dir_rule': flawed_rule}, + 'safe': False # 防止内部catch异常 + } + for plugin_key in test_plugins + ] + + download_photo(photo_id, option, downloader=DoNotDownloadImage) + print('✅ All folder rule plugins assert completed safely without KeyError.')