From 436bd4c14e6f147f8cb2f071b8f56bd077ab1a2a Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:00:58 +0800 Subject: [PATCH 1/6] domain_retry_strategy --- src/jmcomic/jm_client_impl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jmcomic/jm_client_impl.py b/src/jmcomic/jm_client_impl.py index 0a801f35..7aa840cb 100644 --- a/src/jmcomic/jm_client_impl.py +++ b/src/jmcomic/jm_client_impl.py @@ -15,6 +15,7 @@ def __init__(self, postman: Postman, domain_list: List[str], retry_times=0, + domain_retry_strategy=None, ): """ 创建JM客户端 @@ -26,7 +27,7 @@ def __init__(self, super().__init__(postman) self.retry_times = retry_times self.domain_list = domain_list - self.domain_retry_strategy = None + self.domain_retry_strategy = domain_retry_strategy self.CLIENT_CACHE = None self._username = None # help for favorite_folder method self.enable_cache() From e610e2bb678c77df98e871c032762ddfaf76016f Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 20 Sep 2025 20:45:31 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E5=B0=81=E9=9D=A2=E5=9B=BE=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=20(#480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/sources/tutorial/0_common_usage.md | 11 +++++++---- src/jmcomic/jm_client_interface.py | 8 ++++++++ src/jmcomic/jm_toolkit.py | 19 +++++++++++++++++++ tests/test_jmcomic/test_jm_client.py | 4 ++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/assets/docs/sources/tutorial/0_common_usage.md b/assets/docs/sources/tutorial/0_common_usage.md index 02b9dd58..2b5be939 100644 --- a/assets/docs/sources/tutorial/0_common_usage.md +++ b/assets/docs/sources/tutorial/0_common_usage.md @@ -38,7 +38,7 @@ download_album(123, option) option.download_album(123) ``` -## 获取本子/章节/图片的实体类,下载图片 +## 获取本子/章节/图片的实体类,下载图片/封面图 ```python from jmcomic import * @@ -49,23 +49,26 @@ client = JmOption.default().new_jm_client() # 本子实体类 album: JmAlbumDetail = client.get_album_detail('427413') +# 下载本子封面图,保存为 cover.png (图片后缀可指定为jpg、webp等) +client.download_album_cover('427413', './cover.png') + def fetch(photo: JmPhotoDetail): # 章节实体类 photo = client.get_photo_detail(photo.photo_id, False) print(f'章节id: {photo.photo_id}') - + # 图片实体类 image: JmImageDetail for image in photo: print(f'图片url: {image.img_url}') - + # 下载单个图片 client.download_by_image_detail(image, './a.jpg') # 如果是已知未混淆的图片,也可以直接使用url来下载 random_image_domain = JmModuleConfig.DOMAIN_IMAGE_LIST[0] client.download_image(f'https://{random_image_domain}/media/albums/416130.jpg', './a.jpg') - + # 多线程发起请求 multi_thread_launcher( diff --git a/src/jmcomic/jm_client_interface.py b/src/jmcomic/jm_client_interface.py index 59802827..54297d3c 100644 --- a/src/jmcomic/jm_client_interface.py +++ b/src/jmcomic/jm_client_interface.py @@ -293,6 +293,14 @@ def img_is_not_need_to_decode(cls, data_original: str, _resp) -> bool: # https://cdn-msp2.18comic.vip/media/photos/498976/00027.gif return data_original.endswith('.gif') + def download_album_cover(self, album_id, save_path: str): + self.download_image( + img_url=JmcomicText.get_album_cover_url(album_id), + img_save_path=save_path, + scramble_id=None, + decode_image=False, + ) + class JmSearchAlbumClient: """ diff --git a/src/jmcomic/jm_toolkit.py b/src/jmcomic/jm_toolkit.py index 7ec84d5c..3e4ee472 100644 --- a/src/jmcomic/jm_toolkit.py +++ b/src/jmcomic/jm_toolkit.py @@ -368,6 +368,25 @@ def limit_text(cls, text: str, limit: int) -> str: length = len(text) return text if length <= limit else (text[:limit] + f'...({length - limit}') + @classmethod + def get_album_cover_url(cls, + album_id: str, + image_domain: str = None, + size: str = '' + ) -> str: + """ + 根据本子id生成封面url + + :param album_id 本子id + :param image_domain: 图片cdn域名 + :param size: 尺寸后缀,例如搜索列表页会使用 size="_3x4" 的封面图 + """ + if image_domain is None: + import random + image_domain = random.choice(JmModuleConfig.DOMAIN_IMAGE_LIST) + + return f'{JmModuleConfig.PROT}{image_domain}/media/albums/{cls.parse_to_jm_id(album_id)}{size}.jpg' + # 支持dsl: #{???} -> os.getenv(???) JmcomicText.dsl_replacer.add_dsl_and_replacer(r'\$\{(.*?)\}', JmcomicText.match_os_env) diff --git a/tests/test_jmcomic/test_jm_client.py b/tests/test_jmcomic/test_jm_client.py index ed072b68..e5eaabdc 100644 --- a/tests/test_jmcomic/test_jm_client.py +++ b/tests/test_jmcomic/test_jm_client.py @@ -332,3 +332,7 @@ def print_page(page): # 打印page内容 for aid, atitle in page: print(aid, atitle) + + def test_download_cover(self): + album_id = 123 + self.client.download_album_cover(album_id, f'./{album_id}.jpg') From 2089104b0ec36f002e09b38e935f3e5271c85f45 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:02:53 +0800 Subject: [PATCH 3/6] Update src/jmcomic/jm_toolkit.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/jmcomic/jm_toolkit.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/jmcomic/jm_toolkit.py b/src/jmcomic/jm_toolkit.py index 3e4ee472..1f8baa28 100644 --- a/src/jmcomic/jm_toolkit.py +++ b/src/jmcomic/jm_toolkit.py @@ -370,23 +370,23 @@ def limit_text(cls, text: str, limit: int) -> str: @classmethod def get_album_cover_url(cls, - album_id: str, - image_domain: str = None, - size: str = '' + album_id: Union[str, int], + image_domain: Optional[str] = None, + size: str = '', ) -> str: """ 根据本子id生成封面url - :param album_id 本子id - :param image_domain: 图片cdn域名 + :param album_id: 本子id + :param image_domain: 图片cdn域名(可传入裸域或含协议的域名) :param size: 尺寸后缀,例如搜索列表页会使用 size="_3x4" 的封面图 """ if image_domain is None: import random - image_domain = random.choice(JmModuleConfig.DOMAIN_IMAGE_LIST) - - return f'{JmModuleConfig.PROT}{image_domain}/media/albums/{cls.parse_to_jm_id(album_id)}{size}.jpg' + image_domain = random.choice(JmModuleConfig.DOMAIN_IMAGE_LIST) # noqa: S311 + path = f'/media/albums/{cls.parse_to_jm_id(album_id)}{size}.jpg' + return cls.format_url(path, image_domain) # 支持dsl: #{???} -> os.getenv(???) JmcomicText.dsl_replacer.add_dsl_and_replacer(r'\$\{(.*?)\}', JmcomicText.match_os_env) From fc3531a402f4d6ba41beaecd521bc50c0e4035ef Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:05:10 +0800 Subject: [PATCH 4/6] v2.6.8 [skip ci] --- src/jmcomic/__init__.py | 2 +- src/jmcomic/jm_toolkit.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 5aff6914..915b5724 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.6.7' +__version__ = '2.6.8' from .api import * from .jm_plugin import * diff --git a/src/jmcomic/jm_toolkit.py b/src/jmcomic/jm_toolkit.py index 1f8baa28..083ef4c4 100644 --- a/src/jmcomic/jm_toolkit.py +++ b/src/jmcomic/jm_toolkit.py @@ -383,11 +383,12 @@ def get_album_cover_url(cls, """ if image_domain is None: import random - image_domain = random.choice(JmModuleConfig.DOMAIN_IMAGE_LIST) # noqa: S311 + image_domain = random.choice(JmModuleConfig.DOMAIN_IMAGE_LIST) path = f'/media/albums/{cls.parse_to_jm_id(album_id)}{size}.jpg' return cls.format_url(path, image_domain) + # 支持dsl: #{???} -> os.getenv(???) JmcomicText.dsl_replacer.add_dsl_and_replacer(r'\$\{(.*?)\}', JmcomicText.match_os_env) From f9fc27eb88d7b029b024d78a9c7ae2eb63961d3e Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sun, 21 Sep 2025 19:27:18 +0800 Subject: [PATCH 5/6] =?UTF-8?q?v2.6.8:=20=E5=A2=9E=E5=8A=A0=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E6=9C=AC=E5=AD=90=E5=B0=81=E9=9D=A2=E7=9A=84=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E4=BB=A5=E5=8F=8A=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/docs/sources/option_file_syntax.md | 13 +++++++++++-- src/jmcomic/jm_client_interface.py | 4 ++-- src/jmcomic/jm_option.py | 3 ++- src/jmcomic/jm_plugin.py | 21 ++++++++++++++++++++- tests/test_jmcomic/test_jm_client.py | 3 ++- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/assets/docs/sources/option_file_syntax.md b/assets/docs/sources/option_file_syntax.md index f5e54342..22b81062 100644 --- a/assets/docs/sources/option_file_syntax.md +++ b/assets/docs/sources/option_file_syntax.md @@ -109,12 +109,12 @@ dir_rule: # 规则: 根目录 / 本子id / 章节序号 / 图片文件 # rule: 'Bd / Aid / Pindex' # rule: 'Bd_Aid_Pindex' - # 默认规则是: 根目录 / 章节标题 / 图片文件 - rule: Bd_Ptitle + rule: Bd / Ptitle # jmcomic v2.5.36 以后,支持使用python的f-string的语法组合文件夹名,下为示例 # rule: Bd / Aauthor / (JM{Aid}-{Pindex})-{Pname} # {}大括号里的内容同样是写 Axxx 或 Pxxx,其他语法自行参考python f-string的语法 + # 另外,rule开头的Bd可忽略不写,因为程序会自动插入Bd ``` ## 3. option插件配置项 @@ -194,6 +194,15 @@ plugins: album_photo_dict: 324930: 424507 + before_album: + - plugin: download-cover # 额外下载本子封面的插件 + kwargs: + size: '_3x4' # 可选项,禁漫搜索页的封面图尺寸是 4x3,和详情页不一样,想下搜索页的封面就设置此项 + dir_rule: # 封面图存放路径规则,写法同上 + base_dir: D:/a/b/c/ + rule: '{Atitle}/{Aid}_cover.jpg' + + after_album: - plugin: zip # 压缩文件插件 kwargs: diff --git a/src/jmcomic/jm_client_interface.py b/src/jmcomic/jm_client_interface.py index 54297d3c..22f8856d 100644 --- a/src/jmcomic/jm_client_interface.py +++ b/src/jmcomic/jm_client_interface.py @@ -293,9 +293,9 @@ def img_is_not_need_to_decode(cls, data_original: str, _resp) -> bool: # https://cdn-msp2.18comic.vip/media/photos/498976/00027.gif return data_original.endswith('.gif') - def download_album_cover(self, album_id, save_path: str): + def download_album_cover(self, album_id, save_path: str, size: str = ''): self.download_image( - img_url=JmcomicText.get_album_cover_url(album_id), + img_url=JmcomicText.get_album_cover_url(album_id, size=size), img_save_path=save_path, scramble_id=None, decode_image=False, diff --git a/src/jmcomic/jm_option.py b/src/jmcomic/jm_option.py index f71b6c8f..77b5d661 100644 --- a/src/jmcomic/jm_option.py +++ b/src/jmcomic/jm_option.py @@ -158,7 +158,8 @@ def get_rule_parser(cls, rule: str): if rule.startswith(('A', 'P')): return cls.parse_detail_rule - ExceptionTool.raises(f'不支持的rule配置: "{rule}"') + return cls.parse_f_string_rule + # ExceptionTool.raises(f'不支持的rule配置: "{rule}"') @classmethod def apply_rule_to_filename(cls, album, photo, rule: str) -> str: diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 37a3ac42..c84e5b89 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -111,7 +111,7 @@ def wait_until_finish(self): def decide_filepath(self, album: Optional[JmAlbumDetail], photo: Optional[JmPhotoDetail], - filename_rule: str, suffix: str, base_dir: Optional[str], + filename_rule: Optional[str], suffix: Optional[str], base_dir: Optional[str], dir_rule_dict: Optional[dict] ): """ @@ -1306,3 +1306,22 @@ def update_failed_count(self, client: AbstractJmClient, domain: str): def failed_count(client: JmcomicClient, domain: str) -> int: # noinspection PyUnresolvedReferences return client.domain_req_failed_counter.get(domain, 0) + + +class DownloadCoverPlugin(JmOptionPlugin): + plugin_key = 'download-cover' + + def invoke(self, + dir_rule: dict, + size='', + photo: JmPhotoDetail = None, + album: JmAlbumDetail = None, + downloader=None, + **kwargs) -> None: + album_id = album.id if album else photo.album_id + save_path = self.decide_filepath( + album, photo, + None, None, None, + dir_rule + ) + downloader.client.download_album_cover(album_id, save_path, size) diff --git a/tests/test_jmcomic/test_jm_client.py b/tests/test_jmcomic/test_jm_client.py index e5eaabdc..d4cf4d8d 100644 --- a/tests/test_jmcomic/test_jm_client.py +++ b/tests/test_jmcomic/test_jm_client.py @@ -335,4 +335,5 @@ def print_page(page): def test_download_cover(self): album_id = 123 - self.client.download_album_cover(album_id, f'./{album_id}.jpg') + self.client.download_album_cover(album_id, f'{self.option.dir_rule.base_dir}/{album_id}.webp') + self.client.download_album_cover(album_id, f'{self.option.dir_rule.base_dir}/{album_id}_3x4.webp', '_3x4') From 205f3327860674aa9ca79ed371b67c50e6e718b9 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:22:31 +0800 Subject: [PATCH 6/6] polish code --- assets/option/option_test_api.yml | 2 +- src/jmcomic/jm_plugin.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/assets/option/option_test_api.yml b/assets/option/option_test_api.yml index f4c0b7b4..bc4a648a 100644 --- a/assets/option/option_test_api.yml +++ b/assets/option/option_test_api.yml @@ -27,7 +27,7 @@ plugins: proxy_client_key: photo_concurrent_fetcher_proxy whitelist: [ api, ] - - plugin: advanced-retry + - plugin: advanced_retry kwargs: retry_config: retry_rounds: 3 # 一共对域名列表重试3轮 diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index c84e5b89..cf6a16d9 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -1219,7 +1219,7 @@ def new_decide_dir(photo, ensure_exists=True) -> str: class AdvancedRetryPlugin(JmOptionPlugin): - plugin_key = 'advanced-retry' + plugin_key = 'advanced_retry' def __init__(self, option: JmOption): super().__init__(option) @@ -1309,7 +1309,7 @@ def failed_count(client: JmcomicClient, domain: str) -> int: class DownloadCoverPlugin(JmOptionPlugin): - plugin_key = 'download-cover' + plugin_key = 'download_cover' def invoke(self, dir_rule: dict, @@ -1324,4 +1324,7 @@ def invoke(self, None, None, None, dir_rule ) + if self.option.download.cache and os.path.exists(save_path): + self.log(f'album-{album_id}的封面已存在,跳过下载: [{save_path}]', 'skip') + return downloader.client.download_album_cover(album_id, save_path, size)