Skip to content

Commit ab58ce2

Browse files
committed
v2.0.21: 更新禁漫API域名和版本号; 将合并PDF/ZIP/长图的功能简化为 Feature,方便小白使用; 增加美观日志; bugfix和优化文档
1 parent 530e74c commit ab58ce2

10 files changed

Lines changed: 114 additions & 62 deletions

File tree

assets/docs/sources/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
- [快速上手(GitHub README)](https://github.com/hect0x7/JMComic-Crawler-Python/tree/master?tab=readme-ov-file#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B)
1919
- [常用类和方法演示](tutorial/0_common_usage.md)
20-
- [下载同时转 PDF/ZIP/长图](tutorial/13_export_and_feature.md)
20+
- [下载后转为 PDF / ZIP / 长图](tutorial/13_export_and_feature.md)
2121
- [option配置以及插件写法](./option_file_syntax.md)
2222

2323
## 特殊用法教程

assets/docs/sources/option_file_syntax.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,15 @@ plugins:
236236
filename_rule: Atitle # 压缩文件的命名规则
237237
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
238238
# filename_rule和所在钩子有对应关系
239-
# 如果配置在 after_photo 下, filename_rule只能写 Pxxx
240-
# 如果配置在 after_album 下, filename_rule只能写 Axxx
239+
# 如果配置在 after_photo 下, filename_rule 可以写 Pxxx 和Axxx
240+
# 如果配置在 after_album 下, filename_rule 只能写 Axxx,不能写 Pxxx
241241

242242
# zip插件也支持dir_rule配置项,可以替代旧版本的zip_dir和filename_rule
243243
# 请注意⚠ 使用此配置项会使filename_rule,zip_dir,suffix三个配置项无效,与这三个配置项同时存在时仅会使用dir_rule
244244
# 示例如下:
245245
# dir_rule: # 新配置项,可取代旧的zip_dir和filename_rule
246-
# base_dir: D:/jmcomic-zip
247-
# rule: 'Bd / {Atitle} / [{Pid}]-{Ptitle}.zip' # 设置压缩文件夹规则,中间Atitle表示创建一层文件夹,名称是本子标题。[{Pid}]-{Ptitle}.zip 表示压缩文件的命名规则(需显式写出后缀名)
246+
# base_dir: D:/jmcomic-download/
247+
# rule: 'Bd / zip / JM{Aid}-{Atitle}.zip' # 设置压缩文件夹规则,Bd指代base_dir,中间zip表示在{base_dir}下创建一个名为zip的文件夹,JM{Aid}-{Atitle}.zip 表示压缩文件的命名规则(需显式写出后缀名)
248248

249249
delete_original_file: true # 压缩成功后,删除所有原文件和文件夹
250250

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

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Feature 机制——下载附加行为
1+
# 教程:下载后转为 PDF / ZIP / 长图
22

33
## 1. 需求场景
44

@@ -37,17 +37,13 @@ download_album('123', extra=Feature.export_pdf)
3737
download_album('123', option, extra=Feature.export_pdf)
3838
```
3939

40-
**效果**:在本子下载完以后,额外在**当前工作目录**下生成包含所有本子图片的 PDF 文件
40+
**效果**:在本子下载完以后,默认在**下载根目录**下生成包含所有本子图片的 PDF 文件。如果你没有自定义过option,下载根目录就是你的工作目录(即你运行python脚本或cli的目录)。如果你配置过option,会放在dir_rule.base_dir下面。
4141

4242
```text
4343
./
4444
├── [JM123]本子标题.pdf ← 整本合并为 1 个 PDF,注意pdf文件名的格式,默认包含本子禁漫车号+本子标题
4545
```
4646

47-
> 💡 **小白提示:当前工作目录在哪?**
48-
>
49-
> 默认情况下,文件会直接出现在你**运行 Python 脚本的那个文件夹里**。如果你不知道具体是哪,可以看后面的【自定义参数】章节,手动指定你想保存到的文件夹。
50-
5147
### 2.2 需要多种导出格式(PDF、ZIP等)——直接组合 Feature
5248

5349
`+` 号组合,同时导出多种格式:
@@ -61,7 +57,7 @@ download_album('123', option, extra=[Feature.export_pdf, Feature.export_zip])
6157
download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
6258
```
6359

64-
效果同pdf,会在本子下载完以后,额外在当前工作目录下,生成包含所有本子图片的 PDF 文件和 ZIP 文件:
60+
效果同pdf,会在本子下载完以后,额外在对应的下载目录下,生成包含所有本子图片的 PDF 文件和 ZIP 文件:
6561

6662
```text
6763
./
@@ -85,9 +81,10 @@ download_album('123', option, extra=Feature.export_pdf(
8581
```
8682

8783
> 💡 **小白必读:命名规则(filename_rule)的小知识**
88-
> - `A` 开头的占位符(如 `Atitle`, `Aid`)代表 **Album (本子)**,适用于 `download_album`
89-
> - `P` 开头的占位符(如 `Ptitle`, `Pid`)代表 **Photo (章节)**,适用于 `download_photo`
90-
> - 如果在下载整本(Album)时强行使用章节级(Photo)的规则,程序会因为不知道该用哪一章的标题而报错。
84+
> - `A` 开头的占位符(如 `Atitle`, `Aid`)代表 **Album (本子)**
85+
> - `P` 开头的占位符(如 `Ptitle`, `Pid`)代表 **Photo (章节)**
86+
> - `download_photo` (下载单章)时,由于程序既知道当前章节,也知道它属于哪个本子,所以 **`Pxxx``Axxx` 都可以用**
87+
> - `download_album` (下载整本)时,由于是按本子合并的,程序没有具体的“当前章节”,此时 **只能用 `Axxx`,不能用 `Pxxx`**,否则会报错。
9188
9289
```python
9390
# 示例 2:全都要——ZIP 存盘 + 长图阅读
@@ -107,7 +104,7 @@ from jmcomic import download_photo, Feature
107104
download_photo('456', option, extra=Feature.export_pdf)
108105
```
109106

110-
效果:在当前工作目录下生成以章节标题命名的 PDF:
107+
效果:在对应的下载目录下生成以章节标题命名的 PDF:
111108

112109
```text
113110
./
@@ -122,12 +119,12 @@ download_photo('456', option, extra=Feature.export_pdf)
122119

123120
| 调用方式 | Feature.export_pdf | Feature.export_zip | Feature.export_long_img |
124121
|-----------------|-------------------|-------------------|----------------------|
125-
| `download_album` | 整本合并为 1 个 PDF<br>`[本子标题].pdf` | 整本打包为 1 个 ZIP<br>`[本子标题].zip` | 所有章节合并为 1 张长图<br>`[本子ID].png` |
126-
| `download_photo` | 该章节导出为 PDF<br>`[章节标题].pdf` | 该章节打包为 ZIP<br>`[章节标题].zip` | 该章节拼接为长图<br>`[章节ID].png` |
122+
| `download_album` | 整本合并为 1 个 PDF<br>`[JM本子号]本子标题.pdf` | 整本打包为 1 个 ZIP<br>`[JM本子号]本子标题.zip` | 所有章节合并为 1 张长图<br>`[JM本子号]本子标题.png` |
123+
| `download_photo` | 该章节导出为 PDF<br>`[JM章节号]章节标题.pdf` | 该章节打包为 ZIP<br>`[JM章节号]章节标题.zip` | 该章节拼接为长图<br>`[JM章节号]章节标题.png` |
127124

128125
当你显式传入参数时(如 `filename_rule='Ptitle'`),**你的配置优先**,不会被自适应覆盖。
129126

130-
> 💡 **提示**:更多可选参数(如加密密码 `encrypt`、后缀名 `suffix` 等),参考 [Plugin 插件参数大全](./6_plugin.md#参数)
127+
> 💡 **提示**:更多可选参数(如加密密码 `encrypt`、后缀名 `suffix` 等),参考 [Plugin 插件参数大全](../option_file_syntax.md#3-option插件配置项)
131128
132129
## 3. 传统写法(YAML 插件配置)
133130

src/jmcomic/jm_config.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class JmMagicConstants:
102102
APP_TOKEN_SECRET_2 = '18comicAPPContent'
103103
APP_DATA_SECRET = '185Hcomic3PAPP7R'
104104
API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik'
105-
APP_VERSION = '2.0.19'
105+
APP_VERSION = '2.0.21'
106106

107107

108108
# 模块级别共用配置
@@ -153,10 +153,11 @@ class JmModuleConfig:
153153

154154
# 移动端API域名
155155
DOMAIN_API_LIST = shuffled('''
156-
www.cdnaspa.vip
157-
www.cdnaspa.club
158-
www.cdnplaystation6.vip
159-
www.cdnplaystation6.cc
156+
www.cdnhjk.net
157+
www.cdngwc.cc
158+
www.cdngwc.net
159+
www.cdngwc.club
160+
www.cdnhjk.cc
160161
''')
161162

162163
DOMAIN_API_UPDATED_LIST = None

src/jmcomic/jm_downloader.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ def wrapper(self, *args, **kwargs):
1313
detail: JmBaseEntity = args[0]
1414
if detail.is_image():
1515
detail: JmImageDetail
16-
jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: [{e}]')
16+
jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: [{e}]', e)
1717
self.download_failed_image.append((detail, e))
1818

1919
elif detail.is_photo():
2020
detail: JmPhotoDetail
21-
jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: [{e}]')
21+
jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: [{e}]', e)
2222
self.download_failed_photo.append((detail, e))
2323

2424
raise e
@@ -54,14 +54,14 @@ def after_photo(self, photo: JmPhotoDetail):
5454
f'章节下载完成: [{photo.id}] ({photo.album_id}[{photo.index}/{len(photo.from_album)}])')
5555

5656
def before_image(self, image: JmImageDetail, img_save_path):
57-
if image.exists:
57+
if image.exists and image.cache:
5858
jm_log('image.before',
5959
f'图片已存在: {image.tag} ← [{img_save_path}]'
6060
)
61-
else:
62-
jm_log('image.before',
63-
f'图片准备下载: {image.tag}, [{image.img_url}] → [{img_save_path}]'
64-
)
61+
return
62+
jm_log('image.before',
63+
f'图片准备下载: {image.tag}, [{image.img_url}] → [{img_save_path}]'
64+
)
6565

6666
def after_image(self, image: JmImageDetail, img_save_path):
6767
jm_log('image.after',
@@ -125,18 +125,18 @@ def download_by_image_detail(self, image: JmImageDetail):
125125

126126
image.save_path = img_save_path
127127
image.exists = file_exists(img_save_path)
128+
image.cache = self.option.decide_download_cache(image)
128129

129130
self.before_image(image, img_save_path)
130131

131132
if image.skip:
132133
return
133134

134135
# let option decide use_cache and decode_image
135-
use_cache = self.option.decide_download_cache(image)
136136
decode_image = self.option.decide_download_image_decode(image)
137137

138138
# skip download
139-
if use_cache is True and image.exists:
139+
if image.cache and image.exists:
140140
return
141141

142142
self.client.download_by_image_detail(
@@ -311,7 +311,8 @@ def _invoke_features_for(self, when: str, **kwargs):
311311
try:
312312
feature.invoke(self.option, feature_from=feature_from, when=when, **kwargs)
313313
except Exception as e:
314-
jm_log('downloader.feature.exception', f'Feature执行失败: [{feature}], 来源: [{feature_from}], 异常: [{e}]')
314+
jm_log('downloader.feature.exception', f'Feature执行失败: [{feature}], 来源: [{feature_from}], 异常: [{e}]',
315+
e)
315316

316317
def raise_if_has_exception(self):
317318
if not self.has_download_failures:

src/jmcomic/jm_entity.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def __init__(self):
1111
self.save_path: str = ''
1212
self.exists: bool = False
1313
self.skip = False
14+
self.cache = True
1415

1516

1617
class JmBaseEntity:
@@ -125,17 +126,23 @@ def idoname(self):
125126
return f'[{self.id}] {self.oname}'
126127

127128
def __str__(self):
128-
return f'''{self.__class__.__name__}({self.__alias__()}-{self.id}: "{self.title}")'''
129+
return f'''{self.__class__.__name__}({self.alias_en()}-{self.id}: "{self.title}")'''
129130

130131
__repr__ = __str__
131132

132133
@classmethod
133-
def __alias__(cls):
134+
def alias_en(cls):
134135
# "JmAlbumDetail" -> "album" (本子)
135136
# "JmPhotoDetail" -> "photo" (章节)
136137
cls_name = cls.__name__
137138
return cls_name[cls_name.index("m") + 1: cls_name.rfind("Detail")].lower()
138139

140+
@classmethod
141+
def alias_cn(cls) -> str:
142+
# "JmAlbumDetail" -> "album" (本子)
143+
# "JmPhotoDetail" -> "photo" (章节)
144+
return "本子" if issubclass(cls, JmAlbumDetail) else "章节"
145+
139146
@classmethod
140147
def get_dirname(cls, detail: 'DetailEntity', ref: str) -> str:
141148
"""

src/jmcomic/jm_feature.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def invoke(self, option: JmOption, feature_from: str, when: str, **extra):
107107
ExceptionTool.require_true(pclass is not None, f'PluginFeature 引用了未注册的插件: {self.plugin_key}, from {feature_from}, when {when}')
108108

109109
# 根据 feature_from 动态适配参数
110-
plugin_kwargs: dict = self._adapt_plugin_kwargs(feature_from, when)
110+
plugin_kwargs: dict = self._adapt_plugin_kwargs(option, feature_from, when)
111111

112112
option.invoke_plugin(
113113
pclass=pclass,
@@ -116,13 +116,22 @@ def invoke(self, option: JmOption, feature_from: str, when: str, **extra):
116116
pinfo={'plugin': self.plugin_key, 'kwargs': plugin_kwargs},
117117
)
118118

119-
def _adapt_plugin_kwargs(self, feature_from: str, when: str) -> dict:
119+
def _adapt_plugin_kwargs(self, option: JmOption, feature_from: str, when: str) -> dict:
120120
"""
121121
根据feature_from和when动态确定以下插件参数:
122122
filename_rule
123123
"""
124124
kwargs = self.kwargs.copy()
125125
kwargs.setdefault('filename_rule', '[JM{Aid}]{Atitle}' if when == 'after_album' else '[JM{Pid}]{Ptitle}')
126+
127+
# 动态适配导出目录:当且仅当用户未自定义目录时,根据插件类型自动将 dir 导向 option.dir_rule.base_dir
128+
if self.plugin_key == 'zip':
129+
kwargs.setdefault('zip_dir', option.dir_rule.base_dir)
130+
elif self.plugin_key == 'img2pdf':
131+
kwargs.setdefault('pdf_dir', option.dir_rule.base_dir)
132+
elif self.plugin_key == 'long_img':
133+
kwargs.setdefault('img_dir', option.dir_rule.base_dir)
134+
126135
return kwargs
127136

128137
def __repr__(self):
@@ -159,6 +168,6 @@ def __repr__(self):
159168

160169

161170
# 内置的 PluginFeature
162-
Feature.export_pdf = PluginFeature(Img2pdfPlugin.plugin_key, pdf_dir='./')
163-
Feature.export_zip = PluginFeature(ZipPlugin.plugin_key, zip_dir='./')
164-
Feature.export_long_img = PluginFeature(LongImgPlugin.plugin_key, img_dir='./')
171+
Feature.export_pdf = PluginFeature(Img2pdfPlugin.plugin_key)
172+
Feature.export_zip = PluginFeature(ZipPlugin.plugin_key)
173+
Feature.export_long_img = PluginFeature(LongImgPlugin.plugin_key)

src/jmcomic/jm_option.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def apply_rule_to_path(self, album, photo, only_album_rules=False) -> str:
9191
path = parser(album, photo, rule)
9292
except BaseException as e:
9393
# noinspection PyUnboundLocalVariable
94-
jm_log('dir_rule', f'路径规则"{rule}"的解析出错: {e}, album={album}, photo={photo}')
94+
jm_log('dir_rule', f'路径规则"{rule}"的解析出错: {e}, album={album}, photo={photo}', e)
9595
raise e
9696
if parser != self.parse_bd_rule:
9797
# 根据配置 normalize_zh 进行繁简体统一
@@ -580,19 +580,19 @@ def merge_default_dict(cls, user_dict, default_dict=None):
580580

581581
def download_album(self,
582582
album_id,
583-
downloader=None,
584-
callback=None,
583+
*args,
584+
**kwargs,
585585
):
586586
from .api import download_album
587-
download_album(album_id, self, downloader, callback)
587+
download_album(album_id, self, *args, **kwargs)
588588

589589
def download_photo(self,
590590
photo_id,
591-
downloader=None,
592-
callback=None
591+
*args,
592+
**kwargs,
593593
):
594594
from .api import download_photo
595-
download_photo(photo_id, self, downloader, callback)
595+
download_photo(photo_id, self, *args, **kwargs)
596596

597597
# 下面的方法为调用插件提供支持
598598

@@ -684,13 +684,13 @@ def handle_plugin_valid_exception(self, e, pinfo: dict, kwargs: dict, _plugin, _
684684
# noinspection PyMethodMayBeStatic,PyUnusedLocal
685685
def handle_plugin_unexpected_error(self, e, pinfo: dict, kwargs: dict, _plugin, pclass):
686686
msg = str(e)
687-
jm_log('plugin.error', f'插件 [{pclass.plugin_key}],运行遇到未捕获异常,异常信息: [{msg}]')
687+
jm_log('plugin.error', f'插件 [{pclass.plugin_key}],运行遇到未捕获异常,异常信息: [{msg}]', e)
688688
raise e
689689

690690
# noinspection PyMethodMayBeStatic,PyUnusedLocal
691691
def handle_plugin_jmcomic_exception(self, e, pinfo: dict, kwargs: dict, _plugin, pclass):
692692
msg = str(e)
693-
jm_log('plugin.exception', f'插件 [{pclass.plugin_key}] 调用失败,异常信息: [{msg}]')
693+
jm_log('plugin.exception', f'插件 [{pclass.plugin_key}] 调用失败,异常信息: [{msg}]', e)
694694
raise e
695695

696696
# noinspection PyMethodMayBeStatic

src/jmcomic/jm_plugin.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def build(cls, option: JmOption) -> 'JmOptionPlugin':
3636
return cls(option)
3737

3838
def log(self, msg, topic=None):
39-
if self.log_enable:
39+
if not self.log_enable:
4040
return
4141

4242
jm_log(
@@ -136,7 +136,7 @@ def decide_filepath(self,
136136
filepath = os.path.join(base_dir, DirRule.apply_rule_to_filename(album, photo, filename_rule) + fix_suffix(suffix))
137137

138138
mkdir_if_not_exists(base_dir)
139-
return filepath
139+
return fix_filepath(filepath)
140140

141141

142142
class JmLoginPlugin(JmOptionPlugin):
@@ -380,7 +380,9 @@ def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete, encr
380380
relpath = os.path.relpath(abspath, photo_dir)
381381
f.write(abspath, relpath)
382382

383-
self.log(f'压缩章节[{photo.photo_id}]成功 → {zip_path}', 'finish')
383+
# 打印结果
384+
self.log(f'{photo.alias_cn()}压缩成功!'
385+
f'[{photo}] → [{zip_path}]', 'finish')
384386
path_to_delete.append(self.unified_path(photo_dir))
385387

386388
@staticmethod
@@ -403,7 +405,9 @@ def zip_album(self, album, photo_dict: dict, zip_path, path_to_delete, encrypt_d
403405
abspath = os.path.join(photo_dir, file)
404406
relpath = os.path.relpath(abspath, album_dir)
405407
f.write(abspath, relpath)
406-
self.log(f'压缩本子[{album.album_id}]成功 → {zip_path}', 'finish')
408+
# 打印结果
409+
self.log(f'{album.alias_cn()}压缩成功!'
410+
f'[{album}] → [{zip_path}]', 'finish')
407411

408412
def after_zip(self, path_to_delete: List[str]):
409413
# 删除所有原文件
@@ -786,7 +790,13 @@ def invoke(self,
786790
if not result:
787791
return
788792
img_path_ls, img_dir_ls = result
789-
self.log(f'Convert Successfully: JM{album or photo}{pdf_filepath}')
793+
794+
# noinspection PyTypeChecker
795+
detail: DetailEntity = album or photo
796+
797+
# 打印结果
798+
self.log(f'{detail.alias_cn()}合并PDF成功!'
799+
f'[{detail}] → [{pdf_filepath}]', 'finish')
790800

791801
# 执行删除
792802
img_path_ls += img_dir_ls
@@ -863,7 +873,12 @@ def invoke(self,
863873
img_path_ls = self.write_img_2_long_img(long_img_path, album, photo)
864874
if not img_path_ls:
865875
return
866-
self.log(f'Convert Successfully: JM{album or photo}{long_img_path}')
876+
# noinspection PyTypeChecker
877+
detail: DetailEntity = album or photo
878+
879+
# 打印结果
880+
self.log(f'{detail.alias_cn()}合并长图成功!'
881+
f'[{detail}] → [{long_img_path}]', 'finish')
867882

868883
# 执行删除
869884
self.execute_deletion(img_path_ls)

0 commit comments

Comments
 (0)