Skip to content

Commit 3f8246d

Browse files
committed
feat: 完善 Feature 机制及插件健壮性优化
1. [核心] 在 JmDownloader 中实现 Feature 异常隔离与详细日志记录,确保单个 Feature 失败不影响下载流程。 2. [优化] 优化 PluginFeature:使用 type(self) 确保子类化安全,使用 dict(kwargs) 避免可变参数污染。 3. [测试] 补全 download_batch 对 extra 参数的传播测试,并新增 Album 模式下使用 Photo 规则的负面测试。 4. [文档] 修正教程文档中的 filename_rule 示例及相关错误,精简代码注释。 5. [其他] 开启 workflow 下载配置的美观打印日志。
1 parent 4cec892 commit 3f8246d

5 files changed

Lines changed: 35 additions & 7 deletions

File tree

assets/docs/sources/tutorial/13_export_and_feature.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,15 @@ download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
7575
download_album('123', option, extra=Feature.export_pdf(
7676
# 下面是自定义参数
7777
pdf_dir='D:/my_pdfs', # PDF 保存到 D:/my_pdfs 文件夹
78-
filename_rule='Ptitle', # 用章节标题作为文件名
78+
filename_rule='Atitle', # 用本子标题作为文件名
7979
delete_original_file=True, # 合并完 PDF 后删除原图
8080
))
8181

82+
> 💡 **小白必读:命名规则(filename_rule)的小知识**
83+
> - `A` 开头的占位符(如 `Atitle`, `Aid`)代表 **Album (本子)**,适用于 `download_album`
84+
> - `P` 开头的占位符(如 `Ptitle`, `Pid`)代表 **Photo (章节)**,适用于 `download_photo`
85+
> - 如果在下载整本(Album)时强行使用章节级(Photo)的规则,程序会因为不知道该用哪一章的标题而报错。
86+
8287
# 示例 2:全都要——ZIP 存盘 + 长图阅读
8388
combo = (
8489
Feature.export_zip(zip_dir='D:/zips')

assets/option/option_workflow_download.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# GitHub Actions 下载脚本配置
2+
log: pretty
3+
24
dir_rule:
35
base_dir: ${JM_DOWNLOAD_DIR}
46
rule: Bd_Aauthor_Atitle_Pindex

src/jmcomic/jm_downloader.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,10 @@ def _invoke_features_for(self, when: str, **kwargs):
308308
"""
309309
for feature, feature_from in self._feature_list:
310310
if feature.should_invoke(feature_from, when):
311-
feature.invoke(self.option, feature_from=feature_from, when=when, **kwargs)
311+
try:
312+
feature.invoke(self.option, feature_from=feature_from, when=when, **kwargs)
313+
except BaseException as e:
314+
jm_log('downloader.feature.exception', f'Feature执行失败: [{feature}], 来源: [{feature_from}], 异常: [{e}]')
312315

313316
def raise_if_has_exception(self):
314317
if not self.has_download_failures:

src/jmcomic/jm_feature.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class PluginFeature(Feature):
7878

7979
def __init__(self, plugin_key, **kwargs):
8080
self.plugin_key = plugin_key
81-
self.kwargs = kwargs
81+
self.kwargs = dict(kwargs)
8282

8383
def should_invoke(self, feature_from: str, when: str) -> bool:
8484
"""
@@ -95,7 +95,7 @@ def __call__(self, **kwargs):
9595
"""带自定义参数,返回新实例(继承默认参数)"""
9696
new_kwargs = self.kwargs.copy()
9797
new_kwargs.update(kwargs)
98-
new_instance = PluginFeature(self.plugin_key, **new_kwargs)
98+
new_instance = type(self)(self.plugin_key, **new_kwargs)
9999
return new_instance
100100

101101
def invoke(self, option: JmOption, feature_from: str, when: str, **extra):
@@ -158,9 +158,7 @@ def __repr__(self):
158158
return f'FeatureChain({self._features})'
159159

160160

161-
# 预定义特性(用插件类的 plugin_key 引用,附带默认参数)
162-
# filename_rule 会根据 feature_from 在 invoke 时动态适配 A/P 前缀
163-
# zip 的打包粒度由插件根据上下文(album/photo)自动推导,无需 level 参数
161+
# 内置的 PluginFeature
164162
Feature.export_pdf = PluginFeature(Img2pdfPlugin.plugin_key, pdf_dir='./')
165163
Feature.export_zip = PluginFeature(ZipPlugin.plugin_key, zip_dir='./')
166164
Feature.export_long_img = PluginFeature(LongImgPlugin.plugin_key, img_dir='./')

tests/test_jmcomic/test_jm_feature.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ def invoke(self, option, **kwargs):
110110
jmcomic.download_photo(photo_id, self.option, extra=counter_feature)
111111
self.assertEqual(custom_feature_call_count, 3)
112112

113+
# 测试 download_batch (Iterable 批量输入): 确保 extra 参数不被丢弃
114+
jmcomic.download_batch(jmcomic.download_album, [album_id], self.option, extra=counter_feature)
115+
# 上面增加了 1 个 album (包含 1 个 photo),因此 invoke 追加 2 次,总计 5 次
116+
self.assertEqual(custom_feature_call_count, 5)
117+
113118
def test_export_features(self):
114119
album_id = '438516'
115120

@@ -156,3 +161,18 @@ def test_export_features_photo(self):
156161
import os
157162
pdf_path = os.path.join(export_dir, pdf_name)
158163
self.assertTrue(os.path.exists(pdf_path), f"未生成精确匹配的 PDF 文件 (章节级): {pdf_path}")
164+
165+
def test_export_album_use_photo_rule(self):
166+
"""
167+
负面测试:在 Album 模式下强行使用 Photo 级规则(Ptitle),预期报错。
168+
本子=album,本子的章节=photo。下载本子时,photo对象为None。
169+
"""
170+
album_id = '438516'
171+
# 强行使用 Ptitle
172+
f = Feature.export_pdf(filename_rule='Ptitle')
173+
174+
# 验证底层 invoke 会抛出 AttributeError
175+
# 因为在 download_album 的 after_album 阶段,photo 为 None
176+
with self.assertRaises(AttributeError):
177+
album = self.client.get_album_detail(album_id)
178+
f.invoke(self.option, feature_from='download_album', when='after_album', album=album, photo=None)

0 commit comments

Comments
 (0)