Skip to content

Commit d019d97

Browse files
authored
v2.2.0: 引入plugin插件机制,增加相关文档,大幅提升可扩展性,希望构建一个插件生态 (#116)
1 parent b537a0e commit d019d97

10 files changed

+348
-5
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ jmcomic.download_album('422866') # 传入要下载的album的id,即可下载
4040
- 使用API的Filter过滤功能: `usage_feature_filter.py`
4141
- 测试你的ip可以访问哪些禁漫域名: `pick_domain.py`
4242
- 基于GitHub Actions下载本子: `workflow_download.py`
43+
- 演示jmcomic模块的自定义功能点: `usage_custom.py`
44+
- 演示jmcomic模块的Plugin插件体系: `usage_plugin.py`
45+
4346

4447
## 项目特点
4548

@@ -50,6 +53,7 @@ jmcomic.download_album('422866') # 传入要下载的album的id,即可下载
5053
- 配置可以从**配置文件**生成,支持多种文件格式,无需写Python代码
5154
- 配置点有:`是否使用磁盘缓存` `并发下载图片数` `图片类型转换` `下载路径` `请求元信息(headers,cookies,proxies)`
5255
- **可扩展性强**
56+
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件**
5357
- 支持自定义本子/章节/图片下载前后的回调函数
5458
- 支持自定义debug日志的开关/格式
5559
- 支持自定义Downloader/Option/Client/实体类

assets/config/option_plugin.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 插件的配置示例
2+
3+
plugin:
4+
after_init:
5+
login:
6+
username: un
7+
password: pw

src/jmcomic/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# 被依赖方 <--- 使用方
33
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
44

5-
__version__ = '2.1.21'
5+
__version__ = '2.2.0'
66

77
from .api import *
8+
from .jm_plugin import *

src/jmcomic/jm_client_impl.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def debug_topic_request(self):
8787

8888
# noinspection PyMethodMayBeStatic, PyUnusedLocal
8989
def before_retry(self, e, kwargs, retry_count, url):
90-
jm_debug('req.err', str(e))
90+
jm_debug('req.error', str(e))
9191

9292
def enable_cache(self, debug=False):
9393
def wrap_func_cache(func_name, cache_dict_name):
@@ -235,7 +235,6 @@ def get_jm_html(self, url, require_200=True, **kwargs):
235235
resp = self.get(url, **kwargs)
236236

237237
if require_200 is True and resp.status_code != 200:
238-
# write_text('./resp.html', resp.text)
239238
self.check_special_http_code(resp)
240239
self.raise_request_error(resp)
241240

@@ -342,7 +341,6 @@ def check_special_text(cls, resp):
342341
if content not in html:
343342
continue
344343

345-
write_text('./resp.html', html)
346344
cls.raise_request_error(
347345
resp,
348346
f'{reason}'

src/jmcomic/jm_config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class JmModuleConfig:
7878
# debug开关标记
7979
enable_jm_debug = True
8080

81+
# 插件注册表
82+
plugin_registry = {}
83+
8184
@classmethod
8285
def downloader_class(cls):
8386
if cls.CLASS_DOWNLOADER is not None:
@@ -256,7 +259,8 @@ def new_postman(cls, session=False, **kwargs):
256259
},
257260
'impl': 'html',
258261
'retry_times': 5
259-
}
262+
},
263+
'plugin': {},
260264
}
261265

262266
@classmethod
@@ -289,6 +293,10 @@ def option_default_dict(cls) -> dict:
289293

290294
return option_dict
291295

296+
@classmethod
297+
def register_plugin(cls, plugin_class):
298+
cls.plugin_registry[plugin_class.plugin_key] = plugin_class
299+
292300

293301
jm_debug = JmModuleConfig.jm_debug
294302
disable_jm_debug = JmModuleConfig.disable_jm_debug

src/jmcomic/jm_downloader.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ def before_album(self, album: JmAlbumDetail):
2121
f'本子获取成功: [{album.id}], '
2222
f'作者: [{album.author}], '
2323
f'章节数: [{len(album)}], '
24+
f'总页数: [{album.page_count}], '
2425
f'标题: [{album.title}], '
26+
f'关键词: [{album.keywords}], '
2527
)
2628

2729
def after_album(self, album: JmAlbumDetail):

src/jmcomic/jm_option.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def __init__(self,
109109
dir_rule: Dict,
110110
download: Dict,
111111
client: Dict,
112+
plugin: Dict,
112113
filepath=None,
113114
):
114115
# 版本号
@@ -119,9 +120,13 @@ def __init__(self,
119120
self.client = DictModel(client)
120121
# 下载配置
121122
self.download = DictModel(download)
123+
# 插件配置
124+
self.plugin = DictModel(plugin)
122125
# 其他配置
123126
self.filepath = filepath
124127

128+
self.call_all_plugin('after_init')
129+
125130
@property
126131
def download_cache(self):
127132
return self.download.cache
@@ -286,3 +291,48 @@ def merge_default_dict(cls, user_dict, default_dict=None):
286291
else:
287292
default_dict[key] = value
288293
return default_dict
294+
295+
# 下面的方法提供面向对象的调用风格
296+
297+
def download_album(self, album_id):
298+
from .api import download_album
299+
download_album(album_id, self)
300+
301+
def download_album(self, photo_id):
302+
from .api import download_album
303+
download_album(photo_id, self)
304+
305+
# 下面的方法为调用插件提供支持
306+
def call_all_plugin(self, key: str):
307+
plugin_dict: dict = self.plugin.get(key, {})
308+
if plugin_dict is None or len(plugin_dict) == 0:
309+
return
310+
311+
# 保证 jm_plugin.py 被加载
312+
from .jm_plugin import JmOptionPlugin
313+
314+
plugin_registry = JmModuleConfig.plugin_registry
315+
for name, kwargs in plugin_dict.items():
316+
plugin_class: Optional[Type[JmOptionPlugin]] = plugin_registry.get(name, None)
317+
318+
if plugin_class is None:
319+
raise JmModuleConfig.exception(f'[{key}] 未注册的plugin: {name}')
320+
321+
self.invoke_plugin(plugin_class, kwargs)
322+
323+
def invoke_plugin(self, plugin_class, kwargs: dict):
324+
# 保证 jm_plugin.py 被加载
325+
from .jm_plugin import JmOptionPlugin
326+
327+
plugin_class: Type[JmOptionPlugin]
328+
try:
329+
plugin = plugin_class.build(self)
330+
plugin.invoke(**kwargs)
331+
except JmcomicException as e:
332+
msg = str(e)
333+
jm_debug('plugin.exception', msg)
334+
raise JmModuleConfig.exception(msg)
335+
except BaseException as e:
336+
msg = str(e)
337+
jm_debug('plugin.error', msg)
338+
raise e

src/jmcomic/jm_plugin.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
该文件存放的是option插件类
3+
"""
4+
5+
from .jm_option import *
6+
7+
8+
class JmOptionPlugin:
9+
plugin_key: str
10+
11+
def __init__(self, option: JmOption):
12+
self.option = option
13+
14+
def invoke(self, **kwargs) -> None:
15+
"""
16+
执行插件的功能
17+
@param kwargs: 给插件的参数
18+
"""
19+
raise NotImplementedError
20+
21+
@classmethod
22+
def build(cls, option: JmOption) -> 'JmOptionPlugin':
23+
"""
24+
创建插件实例
25+
@param option: JmOption对象
26+
"""
27+
return cls(option)
28+
29+
30+
"""
31+
插件功能:登录禁漫,并保存登录后的cookies,让所有client都带上此cookies
32+
"""
33+
34+
35+
class LoginPlugin(JmOptionPlugin):
36+
plugin_key = 'login'
37+
38+
def invoke(self, username, password) -> None:
39+
assert isinstance(username, str), '用户名必须是str'
40+
assert isinstance(password, str), '密码必须是str'
41+
42+
client = self.option.new_jm_client()
43+
client.login(username, password)
44+
cookies = client['cookies']
45+
46+
postman: dict = self.option.client.postman.src_dict
47+
meta_data = postman.get('meta_data', {})
48+
meta_data['cookies'] = cookies
49+
postman['meta_data'] = meta_data
50+
jm_debug('plugin.login', '登录成功')
51+
52+
53+
JmModuleConfig.register_plugin(LoginPlugin)

usage/usage_custom.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""
2+
本文件演示对jmcomic模块进行自定义功能的方式,下面的每个函数都是一个独立的演示单元。
3+
本文件不演示【自定义配置】,有关配置的教程文档请见 ``
4+
"""
5+
from jmcomic import *
6+
7+
option = JmOption.default()
8+
client: JmcomicClient = option.build_jm_client()
9+
10+
11+
def custom_download_callback():
12+
"""
13+
该函数演示自定义下载时的回调函数
14+
"""
15+
16+
# jmcomic的下载功能由 JmModuleConfig.CLASS_DOWNLOADER 这个类来负责执行
17+
# 这个类默认是 JmDownloader,继承了DownloadCallback
18+
# 你可以写一个自定义类,继承JmDownloader,覆盖属于DownloadCallback的方法,来实现自定义回调
19+
class MyDownloader(JmDownloader):
20+
# 覆盖 album 下载完成后的回调
21+
def after_album(self, album: JmAlbumDetail):
22+
print(f'album下载完毕: {album}')
23+
pass
24+
25+
# 同样的,最后要让你的自定义类生效
26+
JmModuleConfig.CLASS_DOWNLOADER = MyDownloader
27+
28+
29+
def custom_option_class():
30+
"""
31+
该函数演示自定义option
32+
"""
33+
34+
# jmcomic模块支持自定义Option类,
35+
# 你可以写一个自己的类,继承JmOption,然后覆盖其中的一些方法。
36+
class MyOption(JmOption):
37+
38+
def __init__(self, *args, **kwargs):
39+
print('MyOption 初始化开始')
40+
super().__init__(*args, **kwargs)
41+
42+
@classmethod
43+
def default(cls):
44+
print('调用了MyOption.default()')
45+
return super().default()
46+
47+
# 最后,替换默认Option类即可
48+
JmModuleConfig.CLASS_OPTION = MyOption
49+
50+
51+
def custom_client_class():
52+
"""
53+
该文件演示自定义client类
54+
"""
55+
56+
# 默认情况下,JmOption使用client类是根据配置项 `client.impl` 决定的
57+
# JmOption会根据`client.impl`到 JmModuleConfig.CLASS_CLIENT_IMPL 中查找
58+
59+
# 你可以自定义一个`client.impl`,例如 'my-client',
60+
# 或者使用jmcomic内置 'html' 和 'api',
61+
# 然后把你的`client.impl`和类一起配置到JmModuleConfig中
62+
63+
# 1. 自定义Client类
64+
class MyClient(JmHtmlClient):
65+
pass
66+
67+
# 2. 让你的配置类生效
68+
JmModuleConfig.CLASS_CLIENT_IMPL['my-client'] = MyClient
69+
70+
# 3. 在配置文件中使用你定义的client.impl,后续使用这个option即可
71+
"""
72+
client:
73+
impl: 'my-client'
74+
"""
75+
76+
77+
def custom_album_photo_image_detail_class():
78+
"""
79+
该函数演示替换实体类(本子/章节/图片)
80+
"""
81+
82+
# 在使用路径规则 DirRule 时,可能会遇到需要自定义实体类属性的情况,例如:
83+
"""
84+
dir_rule:
85+
base_dir: ${workspace}
86+
rule: Bd_Acustom_Pcustom
87+
"""
88+
89+
# 上面的Acustom,Pcustom都是自定义字段
90+
# 如果你想要使用这种自定义字段,你就需要替换默认的实体类,例如
91+
92+
# 自定义本子实体类
93+
class MyAlbum(JmAlbumDetail):
94+
# 自定义 custom 属性
95+
@property
96+
def custom(self):
97+
return f'custom_{self.title}'
98+
99+
# 自定义章节实体类
100+
class MyPhoto(JmPhotoDetail):
101+
# 自定义 custom 属性
102+
@property
103+
def custom(self):
104+
return f'custom_{self.title}'
105+
106+
# 自定义图片实体类
107+
class MyImage(JmImageDetail):
108+
pass
109+
110+
# 最后,替换默认实体类来让你的自定义类生效
111+
JmModuleConfig.CLASS_ALBUM = MyAlbum
112+
JmModuleConfig.CLASS_PHOTO = MyPhoto
113+
JmModuleConfig.CLASS_IMAGE = MyImage
114+
115+
116+
def custom_jm_debug():
117+
"""
118+
该函数演示自定义debug
119+
"""
120+
121+
# jmcomic模块在运行过程中会使用 jm_debug() 这个函数进行打印信息
122+
# jm_debug() 这个函数 最后会调用 JmModuleConfig.debug_executor 函数
123+
# 你可以写一个自己的函数,替换 JmModuleConfig.debug_executor,实现自定义debug
124+
125+
# 1. 自定义debug函数
126+
def my_debug(topic: str, msg: str):
127+
"""
128+
这个debug函数的参数列表必须包含两个参数,topic和msg
129+
@param topic: debug主题,例如 'album.before', 'req.error', 'plugin.error'
130+
@param msg: 具体debug的信息
131+
"""
132+
pass
133+
134+
# 2. 让my_debug生效
135+
JmModuleConfig.debug_executor = my_debug
136+
137+
138+
def custom_exception_raise():
139+
"""
140+
该函数演示jmcomic的异常机制
141+
"""
142+
143+
# jmcomic 代码在运行过程中可能抛出异常,以获取album实体类为例:
144+
album = client.get_album_detail('999999')
145+
146+
# 上面这行代码用于获取本子id为 999999 的JmAlbumDetail
147+
# 如果本子不存在,则会抛出异常,异常类默认是 JmcomicException
148+
149+
# 你可以自定义抛出的异常类,做法如下:
150+
# 1. 自定义异常类
151+
class MyExceptionClass(Exception):
152+
pass
153+
154+
# 2. 替换默认异常类
155+
JmModuleConfig.CLASS_EXCEPTION = MyExceptionClass
156+
157+
# 这样一来,抛出的异常类就是 MyExceptionClass
158+
try:
159+
album = client.get_album_detail('999999')
160+
except MyExceptionClass as e:
161+
print('捕获MyExceptionClass异常')
162+
pass

0 commit comments

Comments
 (0)