Skip to content

Commit bf2ca6e

Browse files
authored
v2.3.17: 支持从浏览器获取cookies自动登录禁漫【插件】; 重构Client缓存机制、支持配置缓存级别 (#161)
1 parent 9e3fa7f commit bf2ca6e

File tree

12 files changed

+259
-39
lines changed

12 files changed

+259
-39
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ $ jmcomic 422866
5454
- **可扩展性强**
5555

5656
- **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件**
57-
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件` `日志主题过滤插件`
57+
- 目前内置支持的插件有:`登录插件` `硬件占用监控插件` `只下载新章插件` `压缩文件插件` `下载特定后缀图片插件` `发送QQ邮件插件` `日志主题过滤插件` `自动使用浏览器cookies插件`
5858
- 支持自定义本子/章节/图片下载前后的回调函数
5959
- 支持自定义debug/logging
6060
- 支持自定义类:`Downloader(负责调度)` `Option(负责配置)` `Client(负责请求)` `实体类`

assets/docs/sources/index.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ Python API for JMComic(禁漫天堂)
2525
- Highly extensible:
2626

2727
- Supports Plugin plugins for easy functionality extension and use of other plugins.
28-
- Currently built-in
29-
plugins: `login plugin`, `hardware usage monitoring plugin`, `only download new chapters plugin`, `zip compression plugin`, `image suffix filter plugin` `send qq email plugin` `debug logging topic filter plugin`.
28+
- Currently built-in plugins: `login plugin`, `hardware usage monitoring plugin`, `only download new chapters plugin`, `zip compression plugin`, `image suffix filter plugin` `send qq email plugin` `debug logging topic filter plugin` `auto set browser cookies plugin`.
3029
- Supports custom callback functions before and after downloading album/chapter/images.
3130
- Supports custom debug logging.
3231
- Supports custom core

assets/docs/sources/option_file_syntax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ plugins:
120120
proxy_client_key: cl_proxy_future # 代理类的client_key
121121
whitelist: [ api, ] # 白名单,当client.impl匹配白名单时才代理
122122

123+
- plugin: auto_set_browser_cookies # 自动获取浏览器cookies,详见插件类
124+
kwargs:
125+
browser: chrome
126+
domain: 18comic.vip
127+
123128
after_album:
124129
- plugin: zip # 压缩文件插件
125130
kwargs:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
package_dir={"": "src"},
2828
python_requires=">=3.7",
2929
install_requires=[
30-
'commonX>=0.5.7',
30+
'commonX>=0.6.2',
3131
'curl_cffi',
3232
'PyYAML',
3333
'Pillow',

src/jmcomic/__init__.py

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

5-
__version__ = '2.3.16'
5+
__version__ = '2.3.17'
66

77
from .api import *
88
from .jm_plugin import *

src/jmcomic/jm_client_impl.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def __init__(self,
2525
fallback_domain_list.insert(0, domain)
2626

2727
self.domain_list = fallback_domain_list
28+
self.CLIENT_CACHE = None
29+
self.enable_cache()
2830
self.after_init()
2931

3032
def after_init(self):
@@ -111,31 +113,59 @@ def debug_topic_request(self):
111113
def before_retry(self, e, kwargs, retry_count, url):
112114
jm_debug('req.error', str(e))
113115

114-
def enable_cache(self, debug=False):
115-
if self.is_cache_enabled():
116-
return
116+
def enable_cache(self):
117+
# noinspection PyDefaultArgument,PyShadowingBuiltins
118+
def make_key(args, kwds, typed,
119+
kwd_mark=(object(),),
120+
fasttypes={int, str},
121+
tuple=tuple, type=type, len=len):
122+
key = args
123+
if kwds:
124+
key += kwd_mark
125+
for item in kwds.items():
126+
key += item
127+
if typed:
128+
key += tuple(type(v) for v in args)
129+
if kwds:
130+
key += tuple(type(v) for v in kwds.values())
131+
elif len(key) == 1 and type(key[0]) in fasttypes:
132+
return key[0]
133+
return hash(key)
117134

118135
def wrap_func_with_cache(func_name, cache_field_name):
119136
if hasattr(self, cache_field_name):
120137
return
121138

122-
if sys.version_info > (3, 9):
123-
import functools
124-
cache = functools.cache
125-
else:
126-
from functools import lru_cache
127-
cache = lru_cache()
128-
129139
func = getattr(self, func_name)
130-
setattr(self, func_name, cache(func))
140+
141+
def cache_wrapper(*args, **kwargs):
142+
cache = self.CLIENT_CACHE
143+
144+
# Equivalent to not enable cache
145+
if cache is None:
146+
return func(*args, **kwargs)
147+
148+
key = make_key(args, kwargs, False)
149+
sentinel = object() # unique object used to signal cache misses
150+
151+
result = cache.get(key, sentinel)
152+
if result is not sentinel:
153+
return result
154+
155+
result = func(*args, **kwargs)
156+
cache[key] = result
157+
return result
158+
159+
setattr(self, func_name, cache_wrapper)
131160

132161
for func_name in self.func_to_cache:
133162
wrap_func_with_cache(func_name, f'__{func_name}.cache.dict__')
134163

135-
setattr(self, '__enable_cache__', True)
164+
def set_cache_dict(self, cache_dict: Optional[Dict]):
165+
self.CLIENT_CACHE = cache_dict
136166

137-
def is_cache_enabled(self) -> bool:
138-
return getattr(self, '__enable_cache__', False)
167+
def get_cache_dict(self):
168+
return self.CLIENT_CACHE
139169

140170
def get_domain_list(self):
141171
return self.domain_list
@@ -635,7 +665,7 @@ class FutureClientProxy(JmcomicClient):
635665
client_key = 'cl_proxy_future'
636666
proxy_methods = ['album_comment', 'enable_cache', 'get_domain_list',
637667
'get_html_domain', 'get_html_domain_all', 'get_jm_image',
638-
'is_cache_enabled', 'set_domain_list', ]
668+
'set_cache_dict', 'get_cache_dict', 'set_domain_list', ]
639669

640670
class FutureWrapper:
641671
def __init__(self, future):

src/jmcomic/jm_client_interface.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ def get_photo_detail(self,
160160
def of_api_url(self, api_path, domain):
161161
raise NotImplementedError
162162

163-
def enable_cache(self, debug=False):
163+
def set_cache_dict(self, cache_dict: Optional[Dict]):
164164
raise NotImplementedError
165165

166-
def is_cache_enabled(self) -> bool:
166+
def get_cache_dict(self) -> Optional[Dict]:
167167
raise NotImplementedError
168168

169169
def check_photo(self, photo: JmPhotoDetail):

src/jmcomic/jm_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ def new_postman(cls, session=False, **kwargs):
335335
},
336336
},
337337
'client': {
338-
'cache': None,
338+
'cache': None, # see CacheRegistry
339339
'domain': [],
340340
'postman': {
341341
'type': 'cffi',

src/jmcomic/jm_option.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,58 @@
11
from .jm_client_impl import *
22

33

4+
class CacheRegistry:
5+
REGISTRY = {}
6+
7+
@classmethod
8+
def level_option(cls, option, _client):
9+
registry = cls.REGISTRY
10+
registry.setdefault(option, {})
11+
return registry[option]
12+
13+
@classmethod
14+
def level_client(cls, _option, client):
15+
registry = cls.REGISTRY
16+
registry.setdefault(client, {})
17+
return registry[client]
18+
19+
@classmethod
20+
def enable_client_cache_on_condition(cls, option: 'JmOption', client: JmcomicClient, cache: Union[None, bool, str, Callable]):
21+
"""
22+
cache parameter
23+
24+
if None: no cache
25+
26+
if bool:
27+
true: level_option
28+
29+
false: no cache
30+
31+
if str:
32+
(invoke corresponding Cache class method)
33+
34+
:param option: JmOption
35+
:param client: JmcomicClient
36+
:param cache: config dsl
37+
"""
38+
if cache is None:
39+
return
40+
41+
elif isinstance(cache, bool):
42+
if cache is False:
43+
return
44+
else:
45+
cache = cls.level_option
46+
47+
elif isinstance(cache, str):
48+
func = getattr(cls, cache, None)
49+
assert func is not None, f'未实现的cache配置名: {cache}'
50+
cache = func
51+
52+
cache: Callable
53+
client.set_cache_dict(cache(option, client))
54+
55+
456
class DirRule:
557
rule_sample = [
658
# 根目录 / Album-id / Photo-序号 /
@@ -312,12 +364,17 @@ def build_jm_client(self, **kwargs):
312364
return self.new_jm_client(**kwargs)
313365

314366
def new_jm_client(self, domain=None, impl=None, cache=None, **kwargs) -> JmcomicClient:
367+
"""
368+
创建新的Client(客户端),不同Client之间的元数据不共享
369+
"""
370+
from copy import deepcopy
371+
315372
# 所有需要用到的 self.client 配置项如下
316-
postman_conf: dict = self.client.postman.src_dict # postman dsl 配置
317-
meta_data: dict = postman_conf['meta_data'] # 请求元信息
373+
postman_conf: dict = deepcopy(self.client.postman.src_dict) # postman dsl 配置
374+
meta_data: dict = postman_conf['meta_data'] # 元数据
318375
impl: str = impl or self.client.impl # client_key
319376
retry_times: int = self.client.retry_times # 重试次数
320-
cache: str = cache or self.client.cache # 启用缓存
377+
cache: str = cache if cache is not None else self.client.cache # 启用缓存
321378

322379
# domain
323380
def decide_domain():
@@ -357,11 +414,19 @@ def decide_domain():
357414
)
358415

359416
# enable cache
360-
if cache is True:
361-
client.enable_cache()
417+
CacheRegistry.enable_client_cache_on_condition(self, client, cache)
362418

363419
return client
364420

421+
def update_cookies(self, cookies: dict):
422+
metadata: dict = self.client.postman.meta_data.src_dict
423+
orig_cookies: Optional[Dict] = metadata.get('cookies', None)
424+
if orig_cookies is None:
425+
metadata['cookies'] = cookies
426+
else:
427+
orig_cookies.update(cookies)
428+
metadata['cookies'] = orig_cookies
429+
365430
# noinspection PyMethodMayBeStatic
366431
def decide_client_domain(self, client_key: str) -> List[str]:
367432
is_client_type = lambda ctype: self.client_key_is_given_type(client_key, ctype)

src/jmcomic/jm_plugin.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def require_true(self, case: Any, msg: str):
4949

5050
raise PluginValidationException(self, msg)
5151

52+
def warning_lib_not_install(self, lib='psutil'):
53+
msg = (f'插件`{self.plugin_key}`依赖库: {lib},请先安装{lib}再使用。'
54+
f'安装命令: [pip install {lib}]')
55+
import warnings
56+
warnings.warn(msg)
57+
5258

5359
class JmLoginPlugin(JmOptionPlugin):
5460
"""
@@ -111,12 +117,7 @@ def monitor_resource_usage(
111117
try:
112118
import psutil
113119
except ImportError:
114-
msg = (f'插件`{self.plugin_key}`依赖psutil库,请先安装psutil再使用。'
115-
f'安装命令: [pip install psutil]')
116-
import warnings
117-
warnings.warn(msg)
118-
# import sys
119-
# print(msg, file=sys.stderr)
120+
self.warning_lib_not_install('psutil')
120121
return
121122

122123
from time import sleep
@@ -464,3 +465,55 @@ def new_jm_debug(topic, msg):
464465
old_jm_debug(topic, msg)
465466

466467
JmModuleConfig.debug_executor = new_jm_debug
468+
469+
470+
class AutoSetBrowserCookiesPlugin(JmOptionPlugin):
471+
plugin_key = 'auto_set_browser_cookies'
472+
473+
accepted_cookies_keys = str_to_set('''
474+
yuo1
475+
remember_id
476+
remember
477+
''')
478+
479+
def invoke(self,
480+
browser: str,
481+
domain: str,
482+
) -> None:
483+
"""
484+
坑点预警:由于禁漫需要校验同一设备,使用该插件需要配置自己浏览器的headers,例如
485+
486+
```yml
487+
client:
488+
postman:
489+
meta_data:
490+
headers: {
491+
# 浏览器headers
492+
}
493+
494+
# 插件配置如下:
495+
plugins:
496+
after_init:
497+
- plugin: auto_set_browser_cookies
498+
kwargs:
499+
browser: chrome
500+
domain: 18comic.vip
501+
```
502+
503+
:param browser: chrome/edge/...
504+
:param domain: 18comic.vip/...
505+
:return: cookies
506+
"""
507+
cookies, e = get_browser_cookies(browser, domain, safe=True)
508+
509+
if cookies is None:
510+
if isinstance(e, ImportError):
511+
self.warning_lib_not_install('browser_cookie3')
512+
else:
513+
self.debug('获取浏览器cookies失败,请关闭浏览器重试')
514+
return
515+
516+
self.option.update_cookies(
517+
{k: v for k, v in cookies.items() if k in self.accepted_cookies_keys}
518+
)
519+
self.debug('获取浏览器cookies成功')

tests/test_jmcomic/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ def tearDown(self) -> None:
4646
@classmethod
4747
def setUpClass(cls):
4848
# 设置 JmOption,JmcomicClient
49-
try:
50-
option = create_option_by_env('JM_OPTION_PATH_TEST')
51-
except JmcomicException:
52-
option = create_option('./assets/option/option_test.yml')
53-
49+
option = cls.new_option()
5450
cls.option = option
5551
cls.client = option.build_jm_client()
5652

@@ -61,6 +57,13 @@ def setUpClass(cls):
6157
return
6258
cost_time_dict[cls.__name__] = ts()
6359

60+
@classmethod
61+
def new_option(cls):
62+
try:
63+
return create_option_by_env('JM_OPTION_PATH_TEST')
64+
except JmcomicException:
65+
return create_option('./assets/option/option_test.yml')
66+
6467
@classmethod
6568
def tearDownClass(cls) -> None:
6669
if skip_time_cost_debug:

0 commit comments

Comments
 (0)