v2.5.0: 正则表达式适配国内直连网站,引入新插件[jm-server],实现基于浏览器观看本地本子 (#192) (#194)

This commit is contained in:
hect0x7 2024-01-14 15:13:59 +08:00 committed by GitHub
parent fc2f2a908d
commit 14050d4657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 203 additions and 12 deletions

View File

@ -1,7 +1,8 @@
# Plan For Update Content
| 版本范围 | 更新内容 |
| 版本范围 | 更新计划 |
|:--------:|:--------------------------------------:|
| v2.5.* | 引入新插件`jm-server`,实现基于浏览器观看本地本子。 |
| v2.4.* | 项目实现基本稳定,进入维护期,按需增加功能。 |
| v2.3.* | 实现移动端API的基础功能统一HTML和API的实现。 |
| v2.2.* | 新的插件体系,新的命令行调用,完善搜索功能。 |

View File

@ -5,7 +5,6 @@
* option有`默认值`当你使用配置文件来创建option时你配置文件中的值会覆盖`默认值`。
因此在配置option时不需要配置全部的值只需要配置特定部分即可。
* 你可以使用下面的代码来得到option的默认值你可以删除其中的大部分配置项只保留你要覆盖的配置项
```python
@ -13,12 +12,15 @@ from jmcomic import JmOption
JmOption.default().to_file('./option.yml') # 创建默认option导出为option.yml文件
```
## 2. option常配置项
## 2. option常配置项
```yml
# 配置客户端相关
client:
# impl: 客户端实现类不配默认是html表示网页端
# impl: 客户端实现类不配置默认会使用JmModuleConfig.DEFAULT_CLIENT_IMPL
# 可配置:
# html - 表示网页端
# api - 表示使用APP端
impl: html
# domain: 域名配置,默认是 [],表示运行时自动获取域名。
@ -89,8 +91,8 @@ dir_rule:
rule: Bd_Ptitle
```
## 3. option插件配置项
```yml
# 插件的配置示例
# 当kwargs的值为字符串类型时支持使用环境变量语法为 ${环境变量名}
@ -109,11 +111,11 @@ plugins:
- plugin: find_update # 只下载新章插件
kwargs:
145504: 290266 # 下载本子145504的章节290266以后的新章
- plugin: image_suffix_filter # 图片后缀过滤器插件,可以控制只下载哪些后缀的图片
kwargs:
allowed_orig_suffix: # 后缀列表,表示只想下载以.gif结尾的图片
- .gif
- .gif
- plugin: client_proxy # 客户端实现类代理插件,不建议非开发人员使用
kwargs:
@ -124,6 +126,28 @@ plugins:
kwargs:
browser: chrome
domain: 18comic.vip
# v2.5.0 引入的插件
# 可以启动一个服务器,可以在浏览器上查看本子
# 基于flask框架需要安装额外库: pip install plugin_jm_server
# 源码https://github.com/hect0x7/plugin-jm-server
- plugin: jm_server
kwargs:
password: '3333' # 服务器访问密码
base_dir: D:/a/b/c/ # 根目录默认使用dir_rule.base_dir
# 下面是高级配置,不配置也可以
# run下的参数是flask框架的app对象的run方法参数详见flask文档
run:
host: 0.0.0.0 # 默认接收所有ip的请求
port: 80 # 服务器端口默认为80
debug: false # 是否开启debug模式默认为false
# 支持重写背景图片,可以使用你喜欢的背景图片作为背景
img_overwrite:
bg.jpg: D:/浏览器的背景图
m_bg.jpeg: D:/移动设备浏览器的背景图
after_album:
- plugin: zip # 压缩文件插件
@ -156,4 +180,4 @@ plugins:
filename_rule: Pid # pdf命名规则
quality: 100 # pdf质量0 - 100
```
```

View File

@ -4,4 +4,5 @@ PyYAML
Pillow
psutil
pycryptodome
requests
requests
plugin_jm_server

View File

@ -42,6 +42,7 @@ setup(
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",

View File

@ -2,7 +2,7 @@
# 被依赖方 <--- 使用方
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
__version__ = '2.4.14'
__version__ = '2.5.0'
from .api import *
from .jm_plugin import *

View File

@ -187,6 +187,9 @@ class JmOption:
# 其他配置
self.filepath = filepath
# 需要主线程等待完成的插件
self.need_wait_plugins = []
self.call_all_plugin('after_init', safe=True)
"""
@ -630,3 +633,9 @@ class JmOption:
)
return new_kwargs
def wait_all_plugins_finish(self):
from .jm_plugin import JmOptionPlugin
for plugin in self.need_wait_plugins:
plugin: JmOptionPlugin
plugin.wait_until_finish()

View File

@ -94,6 +94,15 @@ class JmOptionPlugin:
import subprocess
subprocess.run(cmd, shell=True, check=True)
def enter_wait_list(self):
self.option.need_wait_plugins.append(self)
def leave_wait_list(self):
self.option.need_wait_plugins.remove(self)
def wait_until_finish(self):
pass
class JmLoginPlugin(JmOptionPlugin):
"""
@ -407,7 +416,7 @@ class ClientProxyPlugin(JmOptionPlugin):
proxy_clazz = JmModuleConfig.client_impl_class(proxy_client_key)
clazz_init_kwargs = kwargs
new_jm_client = self.option.new_jm_client
new_jm_client: Callable = self.option.new_jm_client
def hook_new_jm_client(*args, **kwargs):
client = new_jm_client(*args, **kwargs)
@ -749,3 +758,149 @@ class ConvertJpgToPdfPlugin(JmOptionPlugin):
paths.append(self.option.decide_image_save_dir(photo, ensure_exists=False))
self.execute_deletion(paths)
class JmServerPlugin(JmOptionPlugin):
plugin_key = 'jm_server'
default_run_kwargs = {
'host': '0.0.0.0',
'port': '80',
'debug': False,
}
from threading import Lock
single_instance_lock = Lock()
def __init__(self, option: JmOption):
super().__init__(option)
self.run_server_lock = Lock()
self.running = False
self.server_thread: Optional[Thread] = None
def invoke(self,
password='',
base_dir=None,
album=None,
photo=None,
downloader=None,
run=None,
**kwargs
):
"""
:param password: 密码
:param base_dir: 初始访问服务器的根路径
:param album: 为了支持 after_album 这种调用时机
:param photo: 为了支持 after_album 这种调用时机
:param downloader: 为了支持 after_album 这种调用时机
:param run: 用于启动服务器: app.run(**run_kwargs)
:param kwargs: 用于JmServer构造函数: JmServer(base_dir, password, **kwargs)
"""
if base_dir is None:
base_dir = self.option.dir_rule.base_dir
if run is None:
run = self.default_run_kwargs
else:
base_run_kwargs = self.default_run_kwargs.copy()
base_run_kwargs.update(run)
run = base_run_kwargs
if self.running is True:
return
with self.run_server_lock:
if self.running is True:
return
self.running = True
# 服务器的代码位于一个独立库plugin_jm_server需要独立安装
# 源代码仓库https://github.com/hect0x7/plugin-jm-server
try:
import plugin_jm_server
self.log(f'当前使用plugin_jm_server版本: {plugin_jm_server.__version__}')
except ImportError:
self.warning_lib_not_install('plugin_jm_server')
return
# 核心函数,启动服务器,会阻塞当前线程
def blocking_run_server():
self.server_thread = current_thread()
self.enter_wait_list()
server = plugin_jm_server.JmServer(base_dir, password, **kwargs)
# run方法会阻塞当前线程直到flask退出
server.run(**run)
# 对于debug模式特殊处理
if run['debug'] is True:
run.setdefault('use_reloader', False)
# debug模式只能在主线程启动判断当前线程是不是主线程
if current_thread() is not threading.main_thread():
# 不是主线程return
return self.warning_wrong_usage_of_debug()
else:
# 是主线程,启动服务器
blocking_run_server()
else:
# 非debug模式开新线程启动
threading.Thread(target=blocking_run_server, daemon=True).start()
atexit_register(self.wait_server_stop)
def warning_wrong_usage_of_debug(self):
self.log('注意当配置debug=True时请确保当前插件是在主线程中被调用。\n'
'因为如果本插件配置在 [after_album/after_photo] 这种时机调用,\n'
'会使得flask框架不在主线程debug运行\n'
'导致报错ValueError: signal only works in main thread of the main interpreter\n',
'【基于上述原因,当前线程非主线程,不启动服务器】'
'warning'
)
def wait_server_stop(self, proactive=False):
st = self.server_thread
if (
st is None
or st == current_thread()
or not st.is_alive()
):
return
if proactive:
msg = f'[{self.plugin_key}]的服务器线程仍运行中可按下ctrl+c结束程序'
else:
msg = f'主线程执行完毕,但插件[{self.plugin_key}]的服务器线程仍运行中可按下ctrl+c结束程序'
self.log(msg, 'wait')
while st.is_alive():
try:
st.join(timeout=0.5)
except KeyboardInterrupt:
self.log('收到ctrl+c结束程序', 'wait')
return
def wait_until_finish(self):
self.wait_server_stop(proactive=True)
@classmethod
def build(cls, option: JmOption) -> 'JmOptionPlugin':
"""
单例模式
"""
field_name = 'single_instance'
instance = getattr(cls, field_name, None)
if instance is not None:
return instance
with cls.single_instance_lock:
instance = getattr(cls, field_name, None)
if instance is not None:
return instance
instance = JmServerPlugin(option)
setattr(cls, field_name, instance)
return instance

View File

@ -51,7 +51,7 @@ class JmcomicText:
# 點擊喜歡
pattern_html_album_likes = compile(r'<span id="albim_likes_\d+">(.*?)</span>')
# 觀看
pattern_html_album_views = compile(r'<span>(.*?)</span>\n<span>(次觀看|观看次数)</span>')
pattern_html_album_views = compile(r'<span>(.*?)</span>\n *<span>(次觀看|观看次数)</span>')
# 評論(div)
pattern_html_album_comment_count = compile(r'<div class="badge"[^>]*?id="total_video_comments">(\d+)</div>'), 0