diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml index 0c42ef16..e1a20c09 100644 --- a/.github/workflows/download.yml +++ b/.github/workflows/download.yml @@ -27,17 +27,19 @@ jobs: with: python-version: "3.11" - - name: 安装依赖项(pip) - if: ${{ github.ref == 'refs/heads/workflow' }} + - name: Install Dependency run: | python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: 安装jmcomic(pip) + if: ${{ github.ref != 'refs/heads/dev' }} + run: | pip install jmcomic -i https://pypi.org/project --upgrade - - name: 安装依赖项(local) - if: ${{ github.ref != 'refs/heads/workflow' }} + - name: 安装jmcomic(local) + if: ${{ github.ref == 'refs/heads/dev' }} run: | - python -m pip install --upgrade pip - pip install commonX -i https://pypi.org/project --upgrade pip install -e ./ - name: 运行下载脚本 diff --git a/.github/workflows/download_dispatch.yml b/.github/workflows/download_dispatch.yml index 3dbe2a1f..9efad881 100644 --- a/.github/workflows/download_dispatch.yml +++ b/.github/workflows/download_dispatch.yml @@ -61,17 +61,19 @@ jobs: with: python-version: "3.11" - - name: 安装依赖项(pip) - if: ${{ github.ref != 'refs/heads/dev' }} + - name: Install Dependency run: | python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: 安装jmcomic(pip) + if: ${{ github.ref != 'refs/heads/dev' }} + run: | pip install jmcomic -i https://pypi.org/project --upgrade - - name: 安装依赖项(local) + - name: 安装jmcomic(local) if: ${{ github.ref == 'refs/heads/dev' }} run: | - python -m pip install --upgrade pip - pip install commonX -i https://pypi.org/project --upgrade pip install -e ./ - name: 运行下载脚本 diff --git a/README.md b/README.md index 144892c3..8aef4e53 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,22 @@ jmcomic.download_album('422866') # 传入要下载的album的id,即可下载 - 演示jmcomic模块的自定义功能点: `usage_custom.py` - 演示jmcomic模块的Plugin插件体系: `usage_plugin.py` - ## 项目特点 - **绕过Cloudflare的反爬虫** -- 支持使用**GitHub Actions**下载漫画,不会编程都能用([教程:使用GitHub Actions下载禁漫本子](./assets/docs/教程:使用GitHub%20Actions下载禁漫本子.md)) +- 支持使用**GitHub Actions**下载本子,网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/教程:使用GitHub%20Actions下载禁漫本子.md)) - **可配置性强** - 不配置也能使用,十分方便 - 配置可以从**配置文件**生成,支持多种文件格式,无需写Python代码 - 配置点有:`是否使用磁盘缓存` `并发下载图片数` `图片类型转换` `下载路径` `请求元信息(headers,cookies,proxies)`等 - **可扩展性强** - **支持Plugin插件,可以方便地扩展功能,以及使用别人的插件** + - 目前内置支持的插件:`登录插件`,`硬件占用监控插件` - 支持自定义本子/章节/图片下载前后的回调函数 - 支持自定义debug日志的开关/格式 - 支持自定义Downloader/Option/Client/实体类 - - ... -- 支持自动重试和域名切换机制 + - ...... +- 支持**自动重试和域名切换**机制 - **多线程下载**(可细化到一图一线程,效率极高) - 跟进了JM最新的图片分割算法(2023-02-08) diff --git a/assets/config/option_plugin.yml b/assets/config/option_plugin.yml index 2e3dbcb5..004cabe5 100644 --- a/assets/config/option_plugin.yml +++ b/assets/config/option_plugin.yml @@ -2,6 +2,12 @@ plugin: after_init: - login: - username: un - password: pw \ No newline at end of file + - plugin: usage-log # 实时打印硬件占用率的插件 + kwargs: + interval: 0.5 # 间隔时间 + enable_warning: true + + - plugin: login # 登录插件 + kwargs: + username: un + password: pw diff --git a/assets/config/option_test.yml b/assets/config/option_test.yml index b5d89d86..36e9cc8c 100644 --- a/assets/config/option_test.yml +++ b/assets/config/option_test.yml @@ -5,3 +5,11 @@ client: domain: - jmcomic1.me - jmcomic.me + +# 插件配置 +plugin: + after_init: + - plugin: usage-log # 实时打印硬件占用率的插件 + kwargs: + interval: 0.5 # 间隔时间 + enable_warning: false # 不告警 diff --git a/assets/config/option_workflow_download.yml b/assets/config/option_workflow_download.yml index 64226c0b..e95b4764 100644 --- a/assets/config/option_workflow_download.yml +++ b/assets/config/option_workflow_download.yml @@ -10,3 +10,11 @@ client: domain: - jmcomic1.me - jmcomic.me + +# 插件配置 +plugin: + after_init: + - plugin: usage-log # 实时打印硬件占用率的插件 + kwargs: + interval: 0.5 # 间隔时间 + enable_warning: false # 不告警 diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ab1dd42..bba8b7d8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ commonX curl_cffi PyYAML Pillow +psutil \ No newline at end of file diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index cb00bc5d..43a7b8ab 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.2.0' +__version__ = '2.2.1' from .api import * from .jm_plugin import * diff --git a/src/jmcomic/jm_option.py b/src/jmcomic/jm_option.py index 3ae2f11a..8bd27b9f 100644 --- a/src/jmcomic/jm_option.py +++ b/src/jmcomic/jm_option.py @@ -303,20 +303,21 @@ def download_album(self, photo_id): download_album(photo_id, self) # 下面的方法为调用插件提供支持 - def call_all_plugin(self, key: str): - plugin_dict: dict = self.plugin.get(key, {}) - if plugin_dict is None or len(plugin_dict) == 0: + def call_all_plugin(self, group: str): + plugin_list: List[dict] = self.plugin.get(group, []) + if plugin_list is None or len(plugin_list) == 0: return # 保证 jm_plugin.py 被加载 from .jm_plugin import JmOptionPlugin plugin_registry = JmModuleConfig.plugin_registry - for name, kwargs in plugin_dict.items(): - plugin_class: Optional[Type[JmOptionPlugin]] = plugin_registry.get(name, None) + for pinfo in plugin_list: + key, kwargs = pinfo['plugin'], pinfo['kwargs'] + plugin_class: Optional[Type[JmOptionPlugin]] = plugin_registry.get(key, None) if plugin_class is None: - raise JmModuleConfig.exception(f'[{key}] 未注册的plugin: {name}') + raise JmModuleConfig.exception(f'[{group}] 未注册的plugin: {key}') self.invoke_plugin(plugin_class, kwargs) diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 70896f85..18326252 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -1,8 +1,8 @@ """ -该文件存放的是option插件类 +该文件存放的是option扩展功能类 """ -from .jm_option import * +from .jm_option import JmOption, JmModuleConfig, jm_debug class JmOptionPlugin: @@ -28,11 +28,11 @@ def build(cls, option: JmOption) -> 'JmOptionPlugin': """ -插件功能:登录禁漫,并保存登录后的cookies,让所有client都带上此cookies +扩展功能:登录禁漫,并保存登录后的cookies,让所有client都带上此cookies """ -class LoginPlugin(JmOptionPlugin): +class JmLoginPlugin(JmOptionPlugin): plugin_key = 'login' def invoke(self, username, password) -> None: @@ -50,4 +50,92 @@ def invoke(self, username, password) -> None: jm_debug('plugin.login', '登录成功') -JmModuleConfig.register_plugin(LoginPlugin) +class UsageLogPlugin(JmOptionPlugin): + plugin_key = 'usage-log' + + def invoke(self, **kwargs) -> None: + import threading + threading.Thread( + target=self.monitor_resource_usage, + kwargs=kwargs, + daemon=True, + ).start() + + def monitor_resource_usage( + self, + interval=1, + enable_warning=True, + warning_cpu_percent=70, + warning_mem_percent=50, + warning_thread_count=100, + ): + try: + import psutil + except ImportError: + msg = (f'插件`{self.plugin_key}`依赖psutil库,请先安装psutil再使用。' + f'安装命令: [pip install psutil]') + import warnings + warnings.warn(msg) + # import sys + # print(msg, file=sys.stderr) + return + + from time import sleep + from threading import active_count + # 获取当前进程 + process = psutil.Process() + + cpu_percent = None + # noinspection PyUnusedLocal + thread_count = None + # noinspection PyUnusedLocal + mem_usage = None + + def warning(): + warning_msg_list = [] + if cpu_percent >= warning_cpu_percent: + warning_msg_list.append(f'进程占用cpu过高 ({cpu_percent}% >= {warning_cpu_percent}%)') + + mem_percent = psutil.virtual_memory().percent + if mem_percent >= warning_mem_percent: + warning_msg_list.append(f'系统内存占用过高 ({mem_percent}% >= {warning_mem_percent}%)') + + if thread_count >= warning_thread_count: + warning_msg_list.append(f'线程数过多 ({thread_count} >= {warning_thread_count})') + + if len(warning_msg_list) != 0: + warning_msg_list.insert(0, '硬件占用告警,占用过高可能导致系统卡死!') + warning_msg_list.append('') + jm_debug('plugin.psutil.warning', '\n'.join(warning_msg_list)) + + while True: + # 获取CPU占用率(0~100) + cpu_percent = process.cpu_percent() + # 获取内存占用(MB) + mem_usage = round(process.memory_info().rss / 1024 / 1024, 2) + thread_count = active_count() + # 获取网络占用情况 + # network_info = psutil.net_io_counters() + # network_bytes_sent = network_info.bytes_sent + # network_bytes_received = network_info.bytes_recv + + # 打印信息 + msg = ', '.join([ + f'线程数: {thread_count}', + f'CPU占用: {cpu_percent}%', + f'内存占用: {mem_usage}MB', + # f"发送的字节数: {network_bytes_sent}", + # f"接收的字节数: {network_bytes_received}", + ]) + jm_debug('plugin.psutil.log', msg) + + if enable_warning is True: + # 警告 + warning() + + # 等待一段时间 + sleep(interval) + + +JmModuleConfig.register_plugin(JmLoginPlugin) +JmModuleConfig.register_plugin(UsageLogPlugin) diff --git a/usage/usage_plugin.py b/usage/usage_plugin.py index 2f7502b8..bfcaa2e0 100644 --- a/usage/usage_plugin.py +++ b/usage/usage_plugin.py @@ -1,28 +1,26 @@ """ plugin(扩展/插件)是jmcomic=2.2.0新引入的机制, -plugin机制可以实现在`特定时间` 回调 `特定插件`,实现灵活无感知的功能增强。 - - -目前仅支持一个时机: after_init,表示在option对象的 __init__ 初始化方法的最后 -目前仅内置一个插件: login,实现的功能为:登录禁漫,并保存登录后的cookies,让所有client都带上此cookies。实现类是 - -你可以在option配置文件当中,配置如下内容,来实现在 after_init 时机,调用 login 插件 +plugin机制可以实现在`特定时间` 回调 `特定插件`,实现灵活无感知的功能增强, +目前仅支持一个时机: after_init,表示在option对象的 __init__ 初始化方法的最后。 +下面以内置插件[login]为例: +插件功能:登录JM,获取并保存cookies。 +你可以在option配置文件当中,配置如下内容,来实现在 after_init 时机,调用login插件。 plugin: after_init: # 时机 - login: # 插件的key - # 下面是给插件的参数 (kwargs),由插件类自定义 - username: un - password: pw - + - plugin: login # 插件的key + kwargs: + # 下面是给插件的参数 (kwargs),由插件类自定义 + username: un # 禁漫帐号 + password: pw # 密码 你也可以自定义插件和插件时机 自定义插件时机需要你重写Option类,示例请见 usage_custom 下面演示自定义插件,分为3步: 1. 自定义plugin类 -2. 让plugin类失效 +2. 让plugin类生效 3. 使用plugin的key 如果你有好的plugin想法,也欢迎向我提PR,将你的plugin内置到jmcomic模块中 @@ -42,15 +40,16 @@ def invoke(self, hello_plugin) -> None: print(hello_plugin) -# 2. 让plugin类失效 +# 2. 让plugin类生效 JmModuleConfig.register_plugin(MyPlugin) -# 3. 使用plugin的key +# 3. 在配置文件中使用plugin """ plugin: after_init: # 时机 - myplugin: # 插件的key - hello_plugin: this is my plugin invoke method's parameter # 你自定义的插件的参数 + - plugin: myplugin # 插件的key + kwargs: + hello_plugin: this is my plugin invoke method's parameter # 你自定义的插件的参数 """ # 当你使用上述配置文件创建option时,