From 8d257ab8e0d237197e4f70114200c239d0c3d938 Mon Sep 17 00:00:00 2001 From: FeranyDev <43532576+FeranyDev@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:01:40 +0800 Subject: [PATCH 01/27] Update iyuu_helper.py --- plugins/iyuuautoseed/iyuu_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/iyuuautoseed/iyuu_helper.py b/plugins/iyuuautoseed/iyuu_helper.py index 7fd1a70..d322385 100644 --- a/plugins/iyuuautoseed/iyuu_helper.py +++ b/plugins/iyuuautoseed/iyuu_helper.py @@ -11,7 +11,7 @@ class IyuuHelper(object): 适配新版本IYUU开发版 """ _version = "8.2.0" - _api_base = "https://dev.iyuu.cn" + _api_base = "https://2025.iyuu.cn" _sites = {} _token = None _sid_sha1 = None From 1d2d6343d8054dc43c9a1b0364c931fd3906b45e Mon Sep 17 00:00:00 2001 From: FeranyDev <43532576+FeranyDev@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:01:56 +0800 Subject: [PATCH 02/27] Update __init__.py --- plugins/iyuuautoseed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/iyuuautoseed/__init__.py b/plugins/iyuuautoseed/__init__.py index 64e4f2f..d1d528e 100644 --- a/plugins/iyuuautoseed/__init__.py +++ b/plugins/iyuuautoseed/__init__.py @@ -34,7 +34,7 @@ class IYUUAutoSeed(_PluginBase): # 插件图标 plugin_icon = "IYUU.png" # 插件版本 - plugin_version = "1.9.5" + plugin_version = "1.9.6" # 插件作者 plugin_author = "jxxghp" # 作者主页 From a3937f52ab384947a81022fa825e6547b3a4fb7e Mon Sep 17 00:00:00 2001 From: FeranyDev <43532576+FeranyDev@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:09:21 +0800 Subject: [PATCH 03/27] Update __init__.py --- plugins/iyuuautoseed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/iyuuautoseed/__init__.py b/plugins/iyuuautoseed/__init__.py index d1d528e..64e4f2f 100644 --- a/plugins/iyuuautoseed/__init__.py +++ b/plugins/iyuuautoseed/__init__.py @@ -34,7 +34,7 @@ class IYUUAutoSeed(_PluginBase): # 插件图标 plugin_icon = "IYUU.png" # 插件版本 - plugin_version = "1.9.6" + plugin_version = "1.9.5" # 插件作者 plugin_author = "jxxghp" # 作者主页 From 2014084521d85f586ec5eb8232f0c09bfaa222e6 Mon Sep 17 00:00:00 2001 From: FeranyDev <43532576+FeranyDev@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:11:35 +0800 Subject: [PATCH 04/27] Update __init__.py --- plugins/iyuuautoseed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/iyuuautoseed/__init__.py b/plugins/iyuuautoseed/__init__.py index 64e4f2f..d1d528e 100644 --- a/plugins/iyuuautoseed/__init__.py +++ b/plugins/iyuuautoseed/__init__.py @@ -34,7 +34,7 @@ class IYUUAutoSeed(_PluginBase): # 插件图标 plugin_icon = "IYUU.png" # 插件版本 - plugin_version = "1.9.5" + plugin_version = "1.9.6" # 插件作者 plugin_author = "jxxghp" # 作者主页 From 59634c035714b9bc2b90633f7195c543e19cfc2a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 4 Nov 2024 11:42:25 +0800 Subject: [PATCH 05/27] fix iyuu --- package.v2.json | 3 ++- plugins.v2/iyuuautoseed/__init__.py | 2 +- plugins.v2/iyuuautoseed/iyuu_helper.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.v2.json b/package.v2.json index 0f5e317..45ec900 100644 --- a/package.v2.json +++ b/package.v2.json @@ -211,11 +211,12 @@ "name": "IYUU自动辅种", "description": "基于IYUU官方Api实现自动辅种。", "labels": "做种,IYUU", - "version": "2.0.1", + "version": "2.1", "icon": "IYUU.png", "author": "jxxghp", "level": 2, "history": { + "v2.1": "调整IYUU最新域名", "v2.0": "兼容MoviePilot V2 版本" } } diff --git a/plugins.v2/iyuuautoseed/__init__.py b/plugins.v2/iyuuautoseed/__init__.py index dfba16c..5d1ffec 100644 --- a/plugins.v2/iyuuautoseed/__init__.py +++ b/plugins.v2/iyuuautoseed/__init__.py @@ -33,7 +33,7 @@ class IYUUAutoSeed(_PluginBase): # 插件图标 plugin_icon = "IYUU.png" # 插件版本 - plugin_version = "2.0.1" + plugin_version = "2.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 diff --git a/plugins.v2/iyuuautoseed/iyuu_helper.py b/plugins.v2/iyuuautoseed/iyuu_helper.py index 7fd1a70..d322385 100644 --- a/plugins.v2/iyuuautoseed/iyuu_helper.py +++ b/plugins.v2/iyuuautoseed/iyuu_helper.py @@ -11,7 +11,7 @@ class IyuuHelper(object): 适配新版本IYUU开发版 """ _version = "8.2.0" - _api_base = "https://dev.iyuu.cn" + _api_base = "https://2025.iyuu.cn" _sites = {} _token = None _sid_sha1 = None From e58169a4183980667da0ff4ac2a98e3c75f718b4 Mon Sep 17 00:00:00 2001 From: FeranyDev <43532576+FeranyDev@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:19:55 +0800 Subject: [PATCH 06/27] Update package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a40be9..b0e5f9a 100644 --- a/package.json +++ b/package.json @@ -296,11 +296,12 @@ "name": "IYUU自动辅种", "description": "基于IYUU官方Api实现自动辅种。", "labels": "做种,IYUU", - "version": "1.9.5", + "version": "1.9.6", "icon": "IYUU.png", "author": "jxxghp", "level": 2, "history": { + "v1.9.6": "调整IYUU最新域名", "v1.9.5": "Revert qBittorrent跳检之后自动开始", "v1.9.4": "修复qBittorrent辅种后不会自动开始做种", "v1.9.3": "修复Monika因缺少rsskey,种子下载失败的问题", From 07542afaa866acbc19d582895393987a9e36f9be Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 5 Nov 2024 16:48:54 +0800 Subject: [PATCH 07/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20package.v2.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/package.v2.json b/package.v2.json index 45ec900..c369fb7 100644 --- a/package.v2.json +++ b/package.v2.json @@ -178,22 +178,6 @@ "v2.0": "兼容MoviePilot V2 版本" } }, - "ConfigCenter": { - "name": "配置中心", - "description": "快速调整部分系统设定。", - "labels": "系统设置", - "version": "3.2", - "icon": "setting.png", - "author": "jxxghp", - "level": 1, - "history": { - "v3.2": "优化显示,按功能区分", - "v3.1": "重构配置更新逻辑,从而与主程序保持一致", - "v3.0": "兼容MoviePilot V2 版本", - "v2.6": "支持DOH相关配置项", - "v2.5": "增加Github加速服务器设置项" - } - }, "TorrentRemover": { "name": "自动删种", "description": "自动删除下载器中的下载任务。", From 1d8bec5db2ae6dc34a4b97aa8ddd5c2f69d3f2ba Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 5 Nov 2024 16:50:17 +0800 Subject: [PATCH 08/27] Delete plugins.v2/configcenter directory --- plugins.v2/configcenter/__init__.py | 966 ---------------------------- 1 file changed, 966 deletions(-) delete mode 100644 plugins.v2/configcenter/__init__.py diff --git a/plugins.v2/configcenter/__init__.py b/plugins.v2/configcenter/__init__.py deleted file mode 100644 index 62f266a..0000000 --- a/plugins.v2/configcenter/__init__.py +++ /dev/null @@ -1,966 +0,0 @@ -from typing import Any, List, Dict, Tuple - -from app.core.config import settings -from app.core.module import ModuleManager -from app.log import logger -from app.plugins import _PluginBase - - -class ConfigCenter(_PluginBase): - # 插件名称 - plugin_name = "配置中心" - # 插件描述 - plugin_desc = "快速调整部分系统设定。" - # 插件图标 - plugin_icon = "setting.png" - # 插件版本 - plugin_version = "3.2" - # 插件作者 - plugin_author = "jxxghp" - # 作者主页 - author_url = "https://github.com/jxxghp" - # 插件配置项ID前缀 - plugin_config_prefix = "configcenter_" - # 加载顺序 - plugin_order = 0 - # 可使用的用户级别 - auth_level = 1 - - # 私有属性 - _enabled = False - _params = "" - - def init_plugin(self, config: dict = None): - if not config: - return - - # 清理插件配置,从而实现默认使用.env中的数据源 - self._params = config.pop("params", "") - if "undefined" in config: - del config["undefined"] - if "_tabs" in config: - del config["_tabs"] - self.update_config(config={}) - - # 将自定义配置存储到 __ConfigCenter__ - self.update_config(plugin_id="__ConfigCenter__", config={"params": self._params}) - - logger.info(f"正在应用配置中心配置:{config}") - - # 追加自定义配置中的内容 - params = self.__parse_params(self._params) or {} - config.update(**params) - - # 批量更新配置,并获取更新结果 - update_results = settings.update_settings(config) - - # 遍历更新结果 - for key, (success, message) in update_results.items(): - if not success: - self.__log_and_notify_error(f"配置项 '{key}' 更新失败:{message}") - elif message: - self.__log_and_notify_error(f"配置项 '{key}' 更新时出现警告:{message}") - - # 重新加载模块 - ModuleManager().reload() - - def __log_and_notify_error(self, message): - """ - 记录错误日志并发送系统通知 - """ - logger.error(message) - self.systemmessage.put(message, title=self.plugin_name) - - @staticmethod - def __parse_params(param_str: str) -> dict: - """ - 解析自定义配置 - """ - if not param_str: - return {} - result = {} - params = param_str.split("\n") - for param in params: - if not param: - continue - if str(param).strip().startswith("#"): - continue - parts = param.split("=", 1) - if len(parts) != 2: - continue - key = parts[0].strip() - value = parts[1].strip() - if not key: - continue - if not value: - continue - result[key] = value - return result - - def get_state(self) -> bool: - return True - - @staticmethod - def get_command() -> List[Dict[str, Any]]: - pass - - def get_api(self) -> List[Dict[str, Any]]: - pass - - def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: - """ - 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 - """ - default_settings = {} - settings_model = self.get_settings_model() - keys = self.extract_keys(settings_model) - for key in keys: - if hasattr(settings, key): - default_settings[key] = getattr(settings, key) - - config = self.get_config(plugin_id="__ConfigCenter__") or {} - params_str = config.get("params") or "" - params = self.__parse_params(params_str) or {} - updated_params = {key: getattr(settings, key) for key in params if hasattr(settings, key)} - params_str = "\n".join(f"{key}={value}" for key, value in updated_params.items()) - default_settings["params"] = params_str - - return [ - { - "component": "VForm", - "content": settings_model - } - ], default_settings - - def extract_keys(self, components: List[dict]) -> List[str]: - """ - 递归提取所有组件中的model键 - """ - models = [] - for component in components: - # 检查当前组件的props中是否有model - props = component.get("props", {}) - model = props.get("model") - if model: - models.append(model) - - # 如果当前组件有嵌套的content,递归提取 - nested_content = component.get("content", []) - if isinstance(nested_content, list): - models.extend(self.extract_keys(nested_content)) - elif isinstance(nested_content, dict): - models.extend(self.extract_keys([nested_content])) - - return models - - @staticmethod - def get_settings_model() -> List[dict]: - """ - 获取配置项模型 - """ - return [ - { - "component": "VRow", - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - }, - "content": [ - { - "component": "VAlert", - "props": { - "type": "warning", - "variant": "tonal", - "text": "注意:部分配置项的更改可能需要重启服务才能生效,为确保配置一致性,已在环境变量中的相关配置项,请手动更新" - } - } - ] - } - ] - }, - { - "component": "VTabs", - "props": { - "model": "_tabs", - "height": 72, - "fixed-tabs": True, - "style": { - "margin-top": "8px", - "margin-bottom": "10px", - } - }, - "content": [ - { - "component": "VTab", - "props": { - "value": "basic_tab", - "style": { - "padding-top": "10px", - "padding-bottom": "10px", - "font-size": "16px" - }, - }, - "text": "基础设置" - }, - { - "component": "VTab", - "props": { - "value": "network_tab", - "style": { - "padding-top": "10px", - "padding-bottom": "10px", - "font-size": "16px" - }, - }, - "text": "网络设置" - }, - { - "component": "VTab", - "props": { - "value": "media_and_download_tab", - "style": { - "padding-top": "10px", - "padding-bottom": "10px", - "font-size": "16px" - }, - }, - "text": "媒体与下载" - }, - { - "component": "VTab", - "props": { - "value": "search_and_transfer_tab", - "style": { - "padding-top": "10px", - "padding-bottom": "10px", - "font-size": "16px" - }, - }, - "text": "搜索与整理" - }, - { - "component": "VTab", - "props": { - "value": "params_tab", - "style": { - "padding-top": "10px", - "padding-bottom": "10px", - "font-size": "16px" - }, - }, - "text": "自定义配置" - }, - ] - }, - { - "component": "VWindow", - "props": { - "model": "_tabs", - }, - "content": [ - # 备份分类块 - # { - # "component": "VWindowItem", - # "props": { - # "value": "client_setting", - # "style": { - # "padding-top": "20px", - # "padding-bottom": "20px" - # }, - # }, - # "content": [ - # { - # "component": "VRow", - # "props": { - # "align": "center" - # }, - # "content": [] - # } - # ] - # }, - # 基础 - { - "component": "VWindowItem", - "props": { - "value": "basic_tab", - "style": { - "padding-top": "20px", - "padding-bottom": "20px" - }, - }, - "content": [ - { - "component": "VRow", - "props": { - "align": "center" - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6, - }, - "content": [ - { - "component": "VSwitch", - "props": { - "model": "AUXILIARY_AUTH_ENABLE", - "label": "启用用户辅助认证", - "hint": "启用后允许通过外部服务进行认证、单点登录以及自动创建用户", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6, - }, - "content": [ - { - "component": "VSwitch", - "props": { - "model": "GLOBAL_IMAGE_CACHE", - "label": "全局图片缓存", - "hint": "是否启用全局图片缓存,将媒体图片缓存到本地", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6. - }, - "content": [ - { - "component": "VSelect", - "props": { - "model": "WALLPAPER", - "label": "登录首页电影海报", - "items": [ - { - "title": "TheMovieDb电影海报", - "value": "tmdb" - }, - { - "title": "Bing每日壁纸", - "value": "bing" - } - ], - "hint": "登录首页电影海报", - "persistent-hint": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6, - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "API_TOKEN", - "label": "API密钥", - "hint": "用于Jellyseerr/Overseerr、媒体服务器Webhook等配置以及部分支持API_TOKEN的API请求", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12 - }, - "content": [ - { - "component": "VTextarea", - "props": { - "model": "PLUGIN_MARKET", - "label": "插件市场", - "hint": "插件市场仓库地址,多个地址使用逗号分隔,确保每个地址以/结尾", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - - ] - } - ] - }, - # 网络 - { - "component": "VWindowItem", - "props": { - "value": "network_tab", - "style": { - "padding-top": "20px", - "padding-bottom": "20px" - }, - }, - "content": [ - # DOH - { - "component": "VRow", - "props": { - "align": "center", - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VSwitch", - "props": { - "model": "DOH_ENABLE", - "label": "启用DNS over HTTPS", - "hint": "启用后对特定域名使用DOH解析以避免DNS污染", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6, - }, - "content": [ - { - "component": "VAlert", - "props": { - "type": "info", - "variant": "tonal", - "style": "white-space: pre-line;", - "text": "如果已经配置好 'PROXY_HOST' ,建议关闭 'DOH' ", - }, - }, - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "DOH_DOMAINS", - "label": "DOH解析的域名", - "hint": "DOH解析的域名列表,多个域名使用逗号分隔", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "DOH_RESOLVERS", - "label": "DOH解析服务器", - "hint": "DOH解析服务器列表,多个服务器使用逗号分隔", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "GITHUB_TOKEN", - "label": "GitHub Token", - "placeholder": "格式: ghp_**** 或 github_pat_****", - "hint": "GitHub Token,提高请求API限流阈值", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "OCR_HOST", - "label": "验证码识别服务器", - "hint": "验证码识别服务器地址", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "GITHUB_PROXY", - "label": "GitHub加速服务器", - "placeholder": "格式: https://mirror.ghproxy.com/", - "hint": "留空则不使用GitHub加速服务器,(注意末尾需要带/)", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "PIP_PROXY", - "label": "PIP加速服务器", - "hint": "留空则不使用PIP加速服务器", - "placeholder": "格式: https://pypi.tuna.tsinghua.edu.cn/simple", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - ] - }, - # Tmdb相关 - { - "component": "VRow", - "props": { - "align": "center" - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "TMDB_API_DOMAIN", - "label": "TMDB API地址", - "hint": "访问正常时无需更改;无法访问时替换为其他中转服务地址,确保连通性", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "TMDB_IMAGE_DOMAIN", - "label": "TheMovieDb图片服务器", - "placeholder": "例如:static-mdb.v.geilijiasu.com", - "hint": "访问正常时无需更改;无法访问时可替换为其他可用地址,确保连通性", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - ] - }, - ] - }, - # 媒体与下载 - { - "component": "VWindowItem", - "props": { - "value": "media_and_download_tab", - "style": { - "padding-top": "20px", - "padding-bottom": "20px" - }, - }, - "content": [ - { - "component": "VRow", - "props": { - "align": "center", - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - "md": 9, - }, - "content": [ - { - "component": "VSwitch", - "props": { - "model": "DOWNLOAD_SUBTITLE", - "label": "自动下载站点字幕", - "hint": "自动下载站点字幕(如有)", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 3, - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "MEDIASERVER_SYNC_INTERVAL", - "label": "媒体服务器同步间隔", - "hint": "媒体服务器同步间隔", - "persistent-hint": True, - "prefix": "每", - "suffix": "小时", - "type": "number", - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 12, - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "AUTO_DOWNLOAD_USER", - "label": "交互搜索自动下载用户ID", - "hint": "使用,分割,设置为 all 代表所有用户自动择优下载,未设置需要用户手动选择资源或者回复`0`才自动择优下载", - "persistent-hint": True, - "clearable": True, - } - } - ] - }, - ] - }, - ] - }, - # 搜索与整理 - { - "component": "VWindowItem", - "props": { - "value": "search_and_transfer_tab", - "style": { - "padding-top": "20px", - "padding-bottom": "20px" - }, - }, - "content": [ - { - "component": "VRow", - "props": { - "align": "center" - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6, - }, - "content": [ - { - "component": "VSwitch", - "props": { - "model": "SEARCH_MULTIPLE_NAME", - "label": "资源搜索整合多名称结果", - "hint": "搜索多个名称时是整合多名称的结果", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 3, - }, - "content": [ - { - "component": "VSwitch", - "props": { - "model": "FANART_ENABLE", - "label": "使用Fanart图片数据源", - "hint": "启用Fanart图片数据源", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 3, - }, - "content": [ - { - "component": "VTextField", - "props": { - "model": "META_CACHE_EXPIRE", - "label": "元数据缓存时间", - "hint": "0或负值时,使用系统默认缓存时间", - "persistent-hint": True, - "prefix": "每", - "suffix": "小时", - "type": "number", - } - } - ] - }, - ] - }, - { - "component": "VRow", - "props": { - "align": "center" - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VSelect", - "props": { - "model": "RECOGNIZE_SOURCE", - "label": "媒体信息识别来源", - "items": [ - { - "title": "TheMovieDb", - "value": "themoviedb" - }, - { - "title": "豆瓣", - "value": "douban" - } - ], - "hint": "媒体信息识别来源", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 6 - }, - "content": [ - { - "component": "VSelect", - "props": { - "model": "SCRAP_SOURCE", - "label": "刮削元数据及图片使用的数据源", - "items": [ - { - "title": "TheMovieDb", - "value": "themoviedb" - }, - { - "title": "豆瓣", - "value": "douban" - } - ], - "hint": "刮削元数据及图片使用的数据源", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12 - }, - "content": [ - { - "component": "VTextarea", - "props": { - "model": "MOVIE_RENAME_FORMAT", - "label": "电影重命名格式", - "hint": "电影重命名格式,使用Jinja2语法,每行一个配置项,参考:https://jinja.palletsprojects.com/en/3.0.x/templates/", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12 - }, - "content": [ - { - "component": "VTextarea", - "props": { - "model": "TV_RENAME_FORMAT", - "label": "电视剧重命名格式", - "hint": "电视剧重命名格式,使用Jinja2语法", - "persistent-hint": True - } - } - ] - }, - { - "component": "VCol", - "props": { - "cols": 12, - "md": 12, - }, - "content": [ - { - "component": "VAlert", - "props": { - "type": "warning", - "variant": "tonal", - "style": "white-space: pre-line;", - "text": "Jinja2语法参考:" - }, - "content": [ - { - "component": "a", - "props": { - "href": "https://jinja.palletsprojects.com/en/3.0.x/templates/", - "target": "_blank" - }, - "content": [ - { - "component": "u", - "text": "https://jinja.palletsprojects.com/en/3.0.x/templates/" - } - ] - } - ] - }, - ] - } - ] - } - ] - }, - # 自定义 - { - "component": "VWindowItem", - "props": { - "value": "params_tab", - "style": { - "padding-top": "20px", - "padding-bottom": "20px" - }, - }, - "content": [ - { - "component": "VRow", - "props": { - "align": "center", - }, - "content": [ - { - "component": "VCol", - "props": { - "cols": 12, - }, - "content": [ - { - "component": "VTextarea", - "props": { - "model": "params", - "label": "自定义配置", - "hint": "自定义配置,每行一个配置项,格式:配置项=值", - "persistent-hint": True - } - } - ] - } - ] - }, - ] - } - ] - }, - ] - - def get_page(self) -> List[dict]: - pass - - def stop_service(self): - """ - 退出插件 - """ - pass From a8de2183462c7551495a273cc9bc35c41a871a7b Mon Sep 17 00:00:00 2001 From: ramen <1205925392@qq.com> Date: Wed, 6 Nov 2024 17:06:27 +0800 Subject: [PATCH 09/27] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E6=9B=B4=E6=94=B9IP=E6=97=B6=E9=85=8D=E7=BD=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=BB=B6=E6=97=B6=E8=BF=87=E9=95=BF=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- plugins/dynamicwechat/__init__.py | 112 +++++++++++++++------ plugins/dynamicwechat/src/debug.py | 144 --------------------------- plugins/dynamicwechat/update_help.py | 31 +++++- 4 files changed, 113 insertions(+), 177 deletions(-) delete mode 100644 plugins/dynamicwechat/src/debug.py diff --git a/package.json b/package.json index b0e5f9a..a573177 100644 --- a/package.json +++ b/package.json @@ -863,12 +863,13 @@ "name": "修改企业微信可信IP", "description": "优先使用cookie,可本地扫码刷新Cookie,当填写两个第三方token时可手机远程更新cookie。", "labels": "消息通知", - "version": "1.3.1", + "version": "1.4.0", "icon": "Wecom_A.png", "author": "RamenRa", "level": 2, "v2": true, "history": { + "v1.4.0": "修复强制更改IP时配置面板延时过长的问题。庆祝v2进入正式版,显示了一个没用的参数", "v1.3.1": "修正一些逻辑判断,修改ip成功会通知一次", "v1.3.0": "兼容v2,操作cookie前检查一次CookieCloud", "v1.2.0": "远程命令/push_qr,立即推送一次二维码到pushplus。添加<本地扫码刷新cookie>", diff --git a/plugins/dynamicwechat/__init__.py b/plugins/dynamicwechat/__init__.py index 6cc8bbf..5e654ca 100644 --- a/plugins/dynamicwechat/__init__.py +++ b/plugins/dynamicwechat/__init__.py @@ -30,7 +30,7 @@ class DynamicWeChat(_PluginBase): # 插件图标 plugin_icon = "Wecom_A.png" # 插件版本 - plugin_version = "1.3.1" + plugin_version = "1.4.0" # 插件作者 plugin_author = "RamenRa" # 作者主页 @@ -80,11 +80,11 @@ class DynamicWeChat(_PluginBase): _future_timestamp = 0 # cookie有效检测 - # _cookie_valid = False + _cookie_valid = True + # cookie存活时间 + _cookie_lifetime = 0 # 使用CookieCloud开关 _use_cookiecloud = True - # 从CookieCloud获取的cookie - _cookie_from_CC = "" # 登录cookie _cookie_header = "" _server = f'http://localhost:{settings.NGINX_PORT}/cookiecloud' @@ -95,7 +95,6 @@ class DynamicWeChat(_PluginBase): def init_plugin(self, config: dict = None): # 清空配置 - self._server = f'http://localhost:{settings.NGINX_PORT}/cookiecloud' self._helloimg_s_token = '' self._pushplus_token = '' self._ip_changed = True @@ -104,8 +103,8 @@ class DynamicWeChat(_PluginBase): self._local_scan = False self._input_id_list = '' self._cookie_header = "" - self._cookie_from_CC = "" self._current_ip_address = self.get_ip_from_url(self._ip_urls[0]) + self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime() if config: self._enabled = config.get("enabled") self._cron = config.get("cron") @@ -114,13 +113,11 @@ class DynamicWeChat(_PluginBase): self._current_ip_address = config.get("current_ip_address") self._pushplus_token = config.get("pushplus_token") self._helloimg_s_token = config.get("helloimg_s_token") - self._cookie_from_CC = config.get("cookie_from_CC") self._forced_update = config.get("forced_update") self._local_scan = config.get("local_scan") self._use_cookiecloud = config.get("use_cookiecloud") self._cookie_header = config.get("cookie_header") self._ip_changed = config.get("ip_changed") - self.try_connect_cc() # 停止现有任务 self.stop_service() @@ -128,7 +125,7 @@ class DynamicWeChat(_PluginBase): # 定时服务 self._scheduler = BackgroundScheduler(timezone=settings.TZ) # 运行一次定时服务 - if self._onlyonce or self._forced_update: + if self._onlyonce: logger.info("立即检测公网IP") self._scheduler.add_job(func=self.check, trigger='date', run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), @@ -136,6 +133,12 @@ class DynamicWeChat(_PluginBase): # 关闭一次性开关 self._onlyonce = False + if self._forced_update: + self._scheduler.add_job(func=self.forced_change, trigger='date', + run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="强制更新公网IP") # 添加任务 + self._forced_update = False + if self._local_scan: self._scheduler.add_job(func=self.local_scanning, trigger='date', run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), @@ -155,11 +158,24 @@ class DynamicWeChat(_PluginBase): if self._scheduler.get_jobs(): self._scheduler.print_jobs() self._scheduler.start() - if self._forced_update: - time.sleep(4) - self._forced_update = False self.__update_config() + @eventmanager.register(EventType.PluginAction) + def forced_change(self, event: Event = None): + """ + 强制修改IP + """ + if not self._enabled: + logger.error("插件未开启") + return + if event: + event_data = event.event_data + if not event_data or event_data.get("action") != "dynamicwechat": + return + self.ChangeIP() + self.__update_config() + logger.info("----------------------本次任务结束----------------------") + @eventmanager.register(EventType.PluginAction) def local_scanning(self, event: Event = None): """ @@ -185,7 +201,7 @@ class DynamicWeChat(_PluginBase): current_time = datetime.now() future_time = current_time + timedelta(seconds=110) self._future_timestamp = int(future_time.timestamp()) - logger.info("请重新进入插件面板扫码!,每20秒检查登录状态,最大尝试5次") + logger.info("请重新进入插件面板扫码! 每20秒检查登录状态,最大尝试5次") max_attempts = 5 attempt = 0 while attempt < max_attempts: @@ -197,9 +213,9 @@ class DynamicWeChat(_PluginBase): self.click_app_management_buttons(page) break else: - logger.info("未检测到登录,任务结束") + logger.info("用户可能没有扫码或登录失败") else: - logger.info("未找到二维码,任务结束") + logger.error("未找到二维码,任务结束") logger.info("----------------------本次任务结束----------------------") browser.close() except Exception as e: @@ -218,10 +234,6 @@ class DynamicWeChat(_PluginBase): event_data = event.event_data if not event_data or event_data.get("action") != "dynamicwechat": return - # logger.info("收到命令,开始检测公网IP ...") - # self.post_message(channel=event.event_data.get("channel"), - # title="开始检测公网IP ...", - # userid=event.event_data.get("user")) logger.info("开始检测公网IP") if self.CheckIP(): @@ -398,6 +410,7 @@ class DynamicWeChat(_PluginBase): def _update_cookie(self, page, context): self._future_timestamp = 0 # 标记二维码失效 + PyCookieCloud.save_cookie_lifetime(0) # 重置cookie存活时间 if self._use_cookiecloud: if not self._cc_server: # 连接失败返回 False self.try_connect_cc() # 再尝试一次连接 @@ -453,7 +466,6 @@ class DynamicWeChat(_PluginBase): else: # 不使用CookieCloud return cookie = self.parse_cookie_header(cookie_header) - self._cookie_from_CC = cookie return cookie except Exception as e: logger.error(f"从CookieCloud获取cookie错误,错误原因:{e}") @@ -485,7 +497,11 @@ class DynamicWeChat(_PluginBase): page.goto(self._wechatUrl) time.sleep(3) if not self.check_login_status(page, task='refresh_cookie'): + self._cookie_valid = False logger.info("cookie已失效,下次IP变动推送二维码") + else: + PyCookieCloud.increase_cookie_lifetime(1200) + self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime() browser.close() except Exception as e: logger.error(f"cookie校验失败:{e}") @@ -536,8 +552,8 @@ class DynamicWeChat(_PluginBase): except Exception as e: # logger.debug(str(e)) # 基于bug运行,请不要将错误输出到日志 # try: # 没有登录成功,也没有短信验证码 - if self.find_qrc(page) and not task == 'refresh_cookie': # 延长任务找到的二维码不会被发送,所以不算用户没有扫码 - logger.error(f"用户没有扫描二维码") + if self.find_qrc(page) and not task == 'refresh_cookie' and not task == 'local_scanning': # 延长任务找到的二维码不会被发送,所以不算用户没有扫码 + logger.warning(f"用户没有扫描二维码") return False def click_app_management_buttons(self, page): @@ -672,7 +688,6 @@ class DynamicWeChat(_PluginBase): "helloimg_s_token": self._helloimg_s_token, "pushplus_token": self._pushplus_token, "input_id_list": self._input_id_list, - "cookie_from_CC": self._cookie_from_CC, "cookie_header": self._cookie_header, "use_cookiecloud": self._use_cookiecloud, }) @@ -772,7 +787,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VSwitch', 'props': { 'model': 'local_scan', - 'label': '扫码刷新Cookie和改IP', + 'label': '本地扫码修改IP', } } ] @@ -878,7 +893,7 @@ class DynamicWeChat(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '使用内建CookieCloud 或 自定义 或 填写两个token 至少三选一,否则无法正常使用' + 'text': '使用内建CookieCloud 或 自定义 或 填写两个token 至少三选一。任何扫码操作都会更新Cookie!' } } ] @@ -920,7 +935,6 @@ class DynamicWeChat(_PluginBase): "input_id_list": "", } - def get_page(self) -> List[dict]: # 获取当前时间戳 current_time = datetime.now().timestamp() @@ -973,6 +987,34 @@ class DynamicWeChat(_PluginBase): } } + # 计算 cookie_lifetime 的天数、小时数和分钟数 + cookie_lifetime_days = self._cookie_lifetime // 86400 # 一天的秒数为 86400 + cookie_lifetime_hours = (self._cookie_lifetime % 86400) // 3600 # 计算小时数 + cookie_lifetime_minutes = (self._cookie_lifetime % 3600) // 60 # 计算分钟数 + if self._cookie_valid: + bg_color = "#40bb45" + else: + bg_color = "#ff0000" + cookie_lifetime_text = ( + f"Cookie 已使用: {cookie_lifetime_days}天{cookie_lifetime_hours}小时{cookie_lifetime_minutes}分钟" + ) + cookie_lifetime_component = { + "component": "div", + "text": cookie_lifetime_text, + "props": { + "style": { + "fontSize": "18px", + "color": "#ffffff", # 白色字体 + "backgroundColor": bg_color, + "padding": "10px", + "borderRadius": "5px", + "textAlign": "center", + "marginTop": "10px", + "display": "inline-block" + } + } + } + # 页面内容,显示二维码状态信息和二维码图片或提示信息 base_content = [ { @@ -996,13 +1038,23 @@ class DynamicWeChat(_PluginBase): "borderRadius": "5px", "display": "inline-block", "textAlign": "center", - "marginBottom": "40px" + "marginBottom": "10px" } } - } + }, + { + "component": "div", + "content": [cookie_lifetime_component], + "props": { + "style": { + "textAlign": "center", + "marginBottom": "10px" + } + } + }, + img_component # 二维码图片或提示信息 ] - }, - img_component # 二维码图片 + } ] return base_content diff --git a/plugins/dynamicwechat/src/debug.py b/plugins/dynamicwechat/src/debug.py deleted file mode 100644 index c6499d7..0000000 --- a/plugins/dynamicwechat/src/debug.py +++ /dev/null @@ -1,144 +0,0 @@ - -from Cryptodome import Random -from Cryptodome.Cipher import AES -import base64 -import json -import hashlib -import requests -from playwright.sync_api import sync_playwright -from typing import Dict, Any - - -class PyCookieCloud: - def __init__(self, url: str, uuid: str, password: str): - self.url: str = url - self.uuid: str = uuid - self.password: str = password - self.BLOCK_SIZE = 16 - - def check_connection(self) -> bool: - """ - Test the connection to the CookieCloud server. - - :return: True if the connection is successful, False otherwise. - """ - try: - resp = requests.get(self.url) - # print(self.url) - if resp.status_code == 200: - return True - else: - return False - except Exception as e: - print(str(e)) - return False - - def update_cookie(self, cookie: Dict[str, Any]) -> bool: - """ - Update cookie data to CookieCloud. - - :param cookie: cookie value to update, if this cookie does not contain 'cookie_data' key, it will be added into 'cookie_data'. - :return: if update success, return True, else return False. - """ - # 确保 cookie 是完整的结构,并直接放入 cookie_data 中 - # cookie_data = { - # "cookie_data": cookie # 直接将 cookie 数据放入 cookie_data - # } - raw_data = json.dumps(cookie) - encrypted_data = self.encrypt(raw_data.encode('utf-8'), self.get_the_key().encode('utf-8')).decode('utf-8') - - request_data = {'uuid': self.uuid, 'encrypted': encrypted_data} - print("请求数据:", request_data) # 打印请求数据 - # headers = {'Content-Type': 'application/json'} # 设置请求头为 JSON - cookie_cloud_request = requests.post(self.url + '/update', json=request_data) - print(cookie_cloud_request) # 打印响应对象 - - if cookie_cloud_request.status_code != 200: - print("错误信息:", cookie_cloud_request.text) # 打印错误信息 - - if cookie_cloud_request.status_code == 200: - if cookie_cloud_request.json().get('action') == 'done': - return True - return False - - def get_the_key(self) -> str: - """ - Get the key used to encrypt and decrypt data. - - :return: the key. - """ - md5 = hashlib.md5() - md5.update((self.uuid + '-' + self.password).encode('utf-8')) - return md5.hexdigest()[:16] - - @staticmethod - def bytes_to_key(data, salt, output=48): - # extended from https://gist.github.com/gsakkis/4546068 - assert len(salt) == 8, len(salt) - data += salt - key = hashlib.md5(data).digest() - final_key = key - while len(final_key) < output: - key = hashlib.md5(key + data).digest() - final_key += key - return final_key[:output] - - def pad(self, data): - length = self.BLOCK_SIZE - (len(data) % self.BLOCK_SIZE) - return data + (chr(length) * length).encode() - - def encrypt(self, message: bytes, passphrase: bytes) -> bytes: - # 请替换为实际的加密实现,以下是示例 - # 使用 AES 或其他算法进行加密 - # 这里只是一个占位符,实际实现请根据需要修改 - # def encrypt(message, passphrase): - salt = Random.new().read(8) - key_iv = self.bytes_to_key(passphrase, salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message))) - # return message # 示例:返回原始消息 - - -def main(server: str, url: str, uuid: str, password: str): - with sync_playwright() as p: - browser = p.chromium.launch(headless=False) - page = browser.new_page() - - # 打开指定的 URL - page.goto(url) - - # 等待 60 秒用户登录 - print("请在30秒内完成登录...") - page.wait_for_timeout(30000) # 等待60秒 - - # 获取 cookies - cookies = page.context.cookies() - - # 关闭浏览器 - browser.close() - - # 创建 PyCookieCloud 实例并上传 cookies - py_cookie_cloud = PyCookieCloud(url=server, uuid=uuid, password=password) - cookie_data = {cookie['name']: cookie['value'] for cookie in cookies} # 转换为字典形式 - if py_cookie_cloud.check_connection(): - print("连接成功,请稍等片刻...") - result = py_cookie_cloud.update_cookie(cookie_data) - else: - print("连接失败,请检查网络连接") - result = False - - if result: - print("Cookies 上传成功!") - else: - print("Cookies 上传失败!") - - -if __name__ == "__main__": - # 设置参数 - server = "http://172.16.8.110:43000/cookiecloud" - target_url = "https://work.weixin.qq.com/wework_admin/loginpage_wx?from=myhome" # 请替换为实际的目标 URL - uuid = "hFQrymvqMBX11d14TTmKb6" # 替换为实际的 UUID - password = "2Bfr3LmzVy3t3bsQ5FLAbZ" # 替换为实际的密码 - main(server, target_url, uuid, password) diff --git a/plugins/dynamicwechat/update_help.py b/plugins/dynamicwechat/update_help.py index 504833e..dc0fdeb 100644 --- a/plugins/dynamicwechat/update_help.py +++ b/plugins/dynamicwechat/update_help.py @@ -1,11 +1,15 @@ -from typing import Dict, Any +import os import json import requests import base64 import hashlib +from typing import Dict, Any from Crypto import Random from Crypto.Cipher import AES +script_dir = os.path.dirname(os.path.abspath(__file__)) +settings_file = os.path.join(script_dir, 'settings.json') + def bytes_to_key(data: bytes, salt: bytes, output=48) -> bytes: # 兼容v2 将bytes_to_key和encrypt导入 @@ -34,6 +38,7 @@ def encrypt(message: bytes, passphrase: bytes) -> bytes: data = message + (chr(length) * length).encode() return base64.b64encode(b"Salted__" + salt + aes.encrypt(data)) + class PyCookieCloud: def __init__(self, url: str, uuid: str, password: str): self.url: str = url @@ -63,7 +68,8 @@ class PyCookieCloud: cookie = {'cookie_data': cookie} raw_data = json.dumps(cookie) encrypted_data = encrypt(raw_data.encode('utf-8'), self.get_the_key().encode('utf-8')).decode('utf-8') - cookie_cloud_request = requests.post(self.url + '/update', json={'uuid': self.uuid, 'encrypted': encrypted_data}) + cookie_cloud_request = requests.post(self.url + '/update', + json={'uuid': self.uuid, 'encrypted': encrypted_data}) if cookie_cloud_request.status_code == 200: if cookie_cloud_request.json()['action'] == 'done': return True @@ -78,3 +84,24 @@ class PyCookieCloud: md5 = hashlib.md5() md5.update((self.uuid + '-' + self.password).encode('utf-8')) return md5.hexdigest()[:16] + + @staticmethod + def load_cookie_lifetime(): # 返回时间戳 单位秒 + if os.path.exists(settings_file): + with open(settings_file, 'r') as file: + settings = json.load(file) + return settings.get('_cookie_lifetime', 0) + else: + return 0 + + @staticmethod + def save_cookie_lifetime(cookie_lifetime): # 传入时间戳 单位秒 + with open(settings_file, 'w') as file: + json.dump({'_cookie_lifetime': cookie_lifetime}, file) + + @staticmethod + def increase_cookie_lifetime(seconds: int): + current_lifetime = PyCookieCloud.load_cookie_lifetime() + new_lifetime = current_lifetime + seconds + # 保存新的 _cookie_lifetime + PyCookieCloud.save_cookie_lifetime(new_lifetime) From c2220a7288d8792614b81cf589c2fd374fee7b61 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 7 Nov 2024 13:21:07 +0800 Subject: [PATCH 10/27] =?UTF-8?q?=E4=B8=8B=E6=9E=B6IYUU=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/package.json b/package.json index a573177..183169a 100644 --- a/package.json +++ b/package.json @@ -488,16 +488,6 @@ "level": 1, "v2": true }, - "IyuuMsg": { - "name": "IYUU消息推送", - "description": "支持使用IYUU发送消息通知。", - "labels": "消息通知,IYUU", - "version": "1.2", - "icon": "Iyuu_A.png", - "author": "jxxghp", - "level": 1, - "v2": true - }, "PushDeerMsg": { "name": "PushDeer消息推送", "description": "支持使用PushDeer发送消息通知。", From 188c541261ef8da8d76b7e27ee45a20f8583b773 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 7 Nov 2024 19:02:59 +0800 Subject: [PATCH 11/27] fix https://github.com/jxxghp/MoviePilot-Plugins/issues/540 --- package.json | 3 ++- plugins/doubansync/__init__.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 183169a..e71ce20 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,13 @@ "name": "豆瓣想看", "description": "同步豆瓣想看数据,自动添加订阅。", "labels": "订阅", - "version": "1.8", + "version": "1.9", "icon": "douban.png", "author": "jxxghp", "level": 2, "v2": true, "history": { + "v1.9": "请求豆瓣RSS时增加请求头", "v1.8": "不同步在看条目", "v1.7": "增强API安全性", "v1.6": "同步历史记录支持手动删除,需要主程序升级至v1.8.4+版本", diff --git a/plugins/doubansync/__init__.py b/plugins/doubansync/__init__.py index 173367d..ffb6570 100644 --- a/plugins/doubansync/__init__.py +++ b/plugins/doubansync/__init__.py @@ -34,7 +34,7 @@ class DoubanSync(_PluginBase): # 插件图标 plugin_icon = "douban.png" # 插件版本 - plugin_version = "1.8" + plugin_version = "1.9" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -509,7 +509,9 @@ class DoubanSync(_PluginBase): continue logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...") url = self._interests_url % user_id - results = self.rsshelper.parse(url) + results = self.rsshelper.parse(url, headers={ + "User-Agent": settings.USER_AGENT + }) if not results: logger.warn(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}") continue From b0e1d73e0fafeb7b7d42874a7096617f8e39cfa7 Mon Sep 17 00:00:00 2001 From: ramen <1205925392@qq.com> Date: Thu, 7 Nov 2024 19:45:31 +0800 Subject: [PATCH 12/27] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=EF=BC=8C=E4=B8=BA=E4=BA=86=E5=92=8C=E5=8E=9F?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=81=9A=E5=87=BA=E6=98=8E=E6=98=BE=E5=8C=BA?= =?UTF-8?q?=E5=88=AB=E5=9C=A8=E6=8F=92=E4=BB=B6=E5=90=8D=E7=A7=B0=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E7=89=B9=E6=AE=8A=E5=AD=97=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 +- plugins/dynamicwechat/__init__.py | 139 ++++++++++++++------------- plugins/dynamicwechat/update_help.py | 22 ++++- 3 files changed, 92 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index e71ce20..68f421f 100644 --- a/package.json +++ b/package.json @@ -851,15 +851,16 @@ "v2": true }, "DynamicWeChat": { - "name": "修改企业微信可信IP", - "description": "优先使用cookie,可本地扫码刷新Cookie,当填写两个第三方token时可手机远程更新cookie。", + "name": "假的企业微信可信IP", + "description": "优先使用cookie,可本地扫码刷新Cookie,验证码以?结尾发送到企业微信应用", "labels": "消息通知", - "version": "1.4.0", + "version": "1.4.1", "icon": "Wecom_A.png", "author": "RamenRa", "level": 2, "v2": true, "history": { + "v1.4.1": "完善面板说明,为了和原仓库做出明显区别在插件名称添加了特殊字符", "v1.4.0": "修复强制更改IP时配置面板延时过长的问题。庆祝v2进入正式版,显示了一个没用的参数", "v1.3.1": "修正一些逻辑判断,修改ip成功会通知一次", "v1.3.0": "兼容v2,操作cookie前检查一次CookieCloud", diff --git a/plugins/dynamicwechat/__init__.py b/plugins/dynamicwechat/__init__.py index 5e654ca..d4d0f19 100644 --- a/plugins/dynamicwechat/__init__.py +++ b/plugins/dynamicwechat/__init__.py @@ -24,13 +24,13 @@ from app.schemas.types import EventType, NotificationType class DynamicWeChat(_PluginBase): # 插件名称 - plugin_name = "修改企业微信可信IP" + plugin_name = "假的企业微信可信IP" # 插件描述 - plugin_desc = "优先使用cookie,可本地扫码刷新Cookie,当填写两个第三方token时手机微信可以更新cookie。" + plugin_desc = "优先使用cookie,可本地扫码刷新Cookie,验证码以?结尾发送到企业微信应用" # 插件图标 plugin_icon = "Wecom_A.png" # 插件版本 - plugin_version = "1.4.0" + plugin_version = "1.4.1" # 插件作者 plugin_author = "RamenRa" # 作者主页 @@ -54,6 +54,8 @@ class DynamicWeChat(_PluginBase): _cc_server = None # 本地扫码开关 _local_scan = False + # 类初始化时添加标记变量 + _is_special_upload = False # 匹配ip地址的正则 _ip_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b' @@ -353,7 +355,6 @@ class DynamicWeChat(_PluginBase): } response = requests.post(pushplus_url, json=pushplus_data) - def ChangeIP(self): logger.info("开始请求企业微信管理更改可信IP") try: @@ -424,7 +425,6 @@ class DynamicWeChat(_PluginBase): if current_cookies is None: logger.error("无法获取当前 cookies") return - formatted_cookies = {} for cookie in current_cookies: domain = cookie.get('domain') # 使用 get() 方法避免 KeyError @@ -434,7 +434,7 @@ class DynamicWeChat(_PluginBase): if domain not in formatted_cookies: formatted_cookies[domain] = [] formatted_cookies[domain].append(cookie) - flag = self._cc_server.update_cookie({'cookie_data': formatted_cookies}) + flag = self._cc_server.update_cookie(formatted_cookies) if flag: logger.info("更新 CookieCloud 成功") else: @@ -442,8 +442,7 @@ class DynamicWeChat(_PluginBase): else: logger.error("连接 CookieCloud 失败", self._server) except Exception as e: - logger.error( - f"更新 cookie 发生错误: {e}") + logger.error(f"更新 cookie 发生错误: {e}") else: logger.error("CookieCloud没有启用或配置错误, 不刷新cookie") @@ -455,11 +454,14 @@ class DynamicWeChat(_PluginBase): if not cookies: # CookieCloud获取cookie失败 logger.error(f"CookieCloud获取cookie失败,失败原因:{msg}") return - # cookie_header = self._cookie_header else: for domain, cookie in cookies.items(): if domain == ".work.weixin.qq.com": cookie_header = cookie + if '_upload_type=A' in cookie: + self._is_special_upload = True + else: + self._is_special_upload = False break if cookie_header == '': cookie_header = self._cookie_header @@ -469,7 +471,6 @@ class DynamicWeChat(_PluginBase): return cookie except Exception as e: logger.error(f"从CookieCloud获取cookie错误,错误原因:{e}") - # logger.info("尝试推送登录二维码") return @staticmethod @@ -498,8 +499,9 @@ class DynamicWeChat(_PluginBase): time.sleep(3) if not self.check_login_status(page, task='refresh_cookie'): self._cookie_valid = False - logger.info("cookie已失效,下次IP变动推送二维码") + logger.warning("cookie已失效,下次IP变动推送二维码") else: + self._cookie_valid = True PyCookieCloud.increase_cookie_lifetime(1200) self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime() browser.close() @@ -552,7 +554,8 @@ class DynamicWeChat(_PluginBase): except Exception as e: # logger.debug(str(e)) # 基于bug运行,请不要将错误输出到日志 # try: # 没有登录成功,也没有短信验证码 - if self.find_qrc(page) and not task == 'refresh_cookie' and not task == 'local_scanning': # 延长任务找到的二维码不会被发送,所以不算用户没有扫码 + if self.find_qrc( + page) and not task == 'refresh_cookie' and not task == 'local_scanning': # 延长任务找到的二维码不会被发送,所以不算用户没有扫码 logger.warning(f"用户没有扫描二维码") return False @@ -563,8 +566,8 @@ class DynamicWeChat(_PluginBase): # ("//span[@class='frame_nav_item_title' and text()='应用管理']", "应用管理"), # ("//div[@class='app_index_item_title ' and contains(text(), 'MoviePilot')]", "MoviePilot"), ( - "//div[contains(@class, 'js_show_ipConfig_dialog')]//a[contains(@class, '_mod_card_operationLink') and text()='配置']", - "配置") + "//div[contains(@class, 'js_show_ipConfig_dialog')]//a[contains(@class, '_mod_card_operationLink') and text()='配置']", + "配置") ] if self._input_id_list: id_list = self._input_id_list.split(",") @@ -808,7 +811,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VTextField', 'props': { 'model': 'cron', - 'label': '检测周期', + 'label': '[必填]检测周期', 'placeholder': '0 * * * *' } } @@ -829,7 +832,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VTextarea', 'props': { 'model': 'input_id_list', - 'label': '应用ID', + 'label': '[必填]应用ID', 'rows': 1, 'placeholder': '输入应用ID,多个ID用英文逗号分隔。在企业微信应用页面URL末尾获取' } @@ -852,7 +855,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VTextarea', 'props': { 'model': 'pushplus_token', - 'label': 'pushplus_token', + 'label': '[可选]pushplus_token', 'rows': 1, 'placeholder': '[可选] 请输入 pushplus_token' } @@ -870,7 +873,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VTextarea', 'props': { 'model': 'helloimg_s_token', - 'label': 'helloimg_s_token', + 'label': '[可选]helloimg_s_token', 'rows': 1, 'placeholder': '[可选] 请输入 helloimg_token' } @@ -893,7 +896,7 @@ class DynamicWeChat(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '使用内建CookieCloud 或 自定义 或 填写两个token 至少三选一。任何扫码操作都会更新Cookie!' + 'text': '使用内建CookieCloud 或 自定义 或 填写两个token 至少三选一。具体请查看作者主页' } } ] @@ -986,36 +989,33 @@ class DynamicWeChat(_PluginBase): } } } + if self._is_special_upload: + # 计算 cookie_lifetime 的天数、小时数和分钟数 + cookie_lifetime_days = self._cookie_lifetime // 86400 # 一天的秒数为 86400 + cookie_lifetime_hours = (self._cookie_lifetime % 86400) // 3600 # 计算小时数 + cookie_lifetime_minutes = (self._cookie_lifetime % 3600) // 60 # 计算分钟数 + bg_color = "#40bb45" if self._cookie_valid else "#ff0000" + cookie_lifetime_text = f"Cookie 已使用: {cookie_lifetime_days}天{cookie_lifetime_hours}小时{cookie_lifetime_minutes}分钟" - # 计算 cookie_lifetime 的天数、小时数和分钟数 - cookie_lifetime_days = self._cookie_lifetime // 86400 # 一天的秒数为 86400 - cookie_lifetime_hours = (self._cookie_lifetime % 86400) // 3600 # 计算小时数 - cookie_lifetime_minutes = (self._cookie_lifetime % 3600) // 60 # 计算分钟数 - if self._cookie_valid: - bg_color = "#40bb45" - else: - bg_color = "#ff0000" - cookie_lifetime_text = ( - f"Cookie 已使用: {cookie_lifetime_days}天{cookie_lifetime_hours}小时{cookie_lifetime_minutes}分钟" - ) - cookie_lifetime_component = { - "component": "div", - "text": cookie_lifetime_text, - "props": { - "style": { - "fontSize": "18px", - "color": "#ffffff", # 白色字体 - "backgroundColor": bg_color, - "padding": "10px", - "borderRadius": "5px", - "textAlign": "center", - "marginTop": "10px", - "display": "inline-block" + cookie_lifetime_component = { + "component": "div", + "text": cookie_lifetime_text, + "props": { + "style": { + "fontSize": "18px", + "color": "#ffffff", + "backgroundColor": bg_color, + "padding": "10px", + "borderRadius": "5px", + "textAlign": "center", + "marginTop": "10px", + "display": "block" + } } } - } + else: + cookie_lifetime_component = None # 不生成该组件 - # 页面内容,显示二维码状态信息和二维码图片或提示信息 base_content = [ { "component": "div", @@ -1027,30 +1027,35 @@ class DynamicWeChat(_PluginBase): "content": [ { "component": "div", - "text": vaild_text, "props": { "style": { - "fontSize": "22px", - "fontWeight": "bold", - "color": "#ffffff", - "backgroundColor": color, - "padding": "8px", - "borderRadius": "5px", - "display": "inline-block", - "textAlign": "center", - "marginBottom": "10px" + "display": "flex", + "justifyContent": "center", + "alignItems": "center", + "flexDirection": "column", # 垂直排列 + "gap": "10px" # 控制间距 } - } - }, - { - "component": "div", - "content": [cookie_lifetime_component], - "props": { - "style": { - "textAlign": "center", - "marginBottom": "10px" - } - } + }, + "content": [ + { + "component": "div", + "text": vaild_text, + "props": { + "style": { + "fontSize": "22px", + "fontWeight": "bold", + "color": "#ffffff", + "backgroundColor": color, + "padding": "8px", + "borderRadius": "5px", + "textAlign": "center", + "marginBottom": "10px", + "display": "inline-block" + } + } + }, + cookie_lifetime_component if cookie_lifetime_component else {}, + ] }, img_component # 二维码图片或提示信息 ] @@ -1161,5 +1166,3 @@ class DynamicWeChat(_PluginBase): self._scheduler = None except Exception as e: logger.error(str(e)) - - diff --git a/plugins/dynamicwechat/update_help.py b/plugins/dynamicwechat/update_help.py index dc0fdeb..9da818a 100644 --- a/plugins/dynamicwechat/update_help.py +++ b/plugins/dynamicwechat/update_help.py @@ -57,21 +57,33 @@ class PyCookieCloud: except Exception as e: return False - def update_cookie(self, cookie: Dict[str, Any]) -> bool: + def update_cookie(self, formatted_cookies: Dict[str, Any]) -> bool: """ Update cookie data to CookieCloud. - :param cookie: cookie value to update, if this cookie does not contain 'cookie_data' key, it will be added into 'cookie_data'. + :param formatted_cookies: cookie value to update. :return: if update success, return True, else return False. """ - if 'cookie_data' not in cookie: - cookie = {'cookie_data': cookie} + if '.work.weixin.qq.com' not in formatted_cookies: + formatted_cookies['.work.weixin.qq.com'] = [] + formatted_cookies['.work.weixin.qq.com'].append({ + 'name': '_upload_type', + 'value': 'A', + 'domain': '.work.weixin.qq.com', + 'path': '/', + 'expires': -1, + 'httpOnly': False, + 'secure': False, + 'sameSite': 'Lax' + }) + + cookie = {'cookie_data': formatted_cookies} raw_data = json.dumps(cookie) encrypted_data = encrypt(raw_data.encode('utf-8'), self.get_the_key().encode('utf-8')).decode('utf-8') cookie_cloud_request = requests.post(self.url + '/update', json={'uuid': self.uuid, 'encrypted': encrypted_data}) if cookie_cloud_request.status_code == 200: - if cookie_cloud_request.json()['action'] == 'done': + if cookie_cloud_request.json().get('action') == 'done': return True return False From 32d732d5f33c0596cc5b9a1d597ded37f4e18d16 Mon Sep 17 00:00:00 2001 From: Attente <19653207+wikrin@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:53:41 +0800 Subject: [PATCH 13/27] =?UTF-8?q?=E5=A4=9A=E8=A1=8C=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E5=8D=83=E4=B8=87=E4=B8=8D=E8=83=BD=E5=8F=AA=E5=86=99=E4=B8=80?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/bangumicoll/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/bangumicoll/__init__.py b/plugins/bangumicoll/__init__.py index 2abb002..83565e2 100644 --- a/plugins/bangumicoll/__init__.py +++ b/plugins/bangumicoll/__init__.py @@ -49,7 +49,7 @@ class BangumiColl(_PluginBase): auth_level = 1 # 私有属性 - _scheduler: Optional[BackgroundScheduler] = None + _scheduler = None siteoper: SiteOper = None subscribehelper: SubscribeHelper = None subscribeoper: SubscribeOper = None @@ -148,8 +148,10 @@ class BangumiColl(_PluginBase): return form(sites_options) def get_service(self) -> List[Dict[str, Any]]: - """注册插件公共服务""" - if self._enabled: + """ + 注册插件公共服务 + """ + if self._enabled or self._cron: trigger = CronTrigger.from_crontab(self._cron) if self._cron else "interval" kwargs = {"hours": 6} if not self._cron else {} return [ @@ -207,8 +209,6 @@ class BangumiColl(_PluginBase): # 新增和移除条目 self.manage_subscriptions(items) - - logger.info("Bangumi收藏订阅执行完成") except Exception as e: logger.error(f"执行失败: {str(e)}") @@ -249,10 +249,12 @@ class BangumiColl(_PluginBase): del_items = {db_sub[i]: i for i in del_sub} logger.info("开始移除订阅...") self.delete_subscribe(del_items) + logger.info("移除完成") if new_sub: logger.info("开始添加订阅...") msg = self.add_subscribe({i: items[i] for i in new_sub}) + logger.info("添加完成") if msg: logger.info("\n".ljust(49, ' ').join(list(msg.values()))) From 882d928e29b8e307d349b7894beba5de5675d3dd Mon Sep 17 00:00:00 2001 From: Attente <19653207+wikrin@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:01:12 +0800 Subject: [PATCH 14/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 +++---- plugins/bangumicoll/__init__.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 68f421f..3c15381 100644 --- a/package.json +++ b/package.json @@ -890,16 +890,15 @@ "name": "Bangumi收藏订阅", "description": "Bangumi用户收藏添加到订阅", "labels": "订阅", - "version": "1.5.1", + "version": "1.5.2", "icon": "bangumi_b.png", "author": "Attente", "level": 1, "v2": true, "history": { + "v1.5.2": "修复定时任务未正确注册的问题", "v1.5.1": "修复季度信息未传递的问题. 新增站点列表同步删除", - "v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项", - "v1.4": "结构优化", - "v1.3.1": "修复因修改季号导致未下载剧集而完成订阅的问题" + "v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项" } } } diff --git a/plugins/bangumicoll/__init__.py b/plugins/bangumicoll/__init__.py index 83565e2..5728e41 100644 --- a/plugins/bangumicoll/__init__.py +++ b/plugins/bangumicoll/__init__.py @@ -36,7 +36,7 @@ class BangumiColl(_PluginBase): # 插件图标 plugin_icon = "bangumi_b.png" # 插件版本 - plugin_version = "1.5.1" + plugin_version = "1.5.2" # 插件作者 plugin_author = "Attente" # 作者主页 From ac04bb809f54714de809f04dbff619ffcd0c3538 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 9 Nov 2024 17:51:11 +0800 Subject: [PATCH 15/27] fix #543 --- package.json | 29 +++++++++++++++-------------- plugins/doubansync/__init__.py | 16 ++++++++++++---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 3c15381..4d332fa 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,13 @@ "name": "豆瓣想看", "description": "同步豆瓣想看数据,自动添加订阅。", "labels": "订阅", - "version": "1.9", + "version": "1.9.1", "icon": "douban.png", "author": "jxxghp", "level": 2, "v2": true, "history": { + "v1.9.1": "修复版本兼容问题", "v1.9": "请求豆瓣RSS时增加请求头", "v1.8": "不同步在看条目", "v1.7": "增强API安全性", @@ -887,18 +888,18 @@ } }, "BangumiColl": { - "name": "Bangumi收藏订阅", - "description": "Bangumi用户收藏添加到订阅", - "labels": "订阅", - "version": "1.5.2", - "icon": "bangumi_b.png", - "author": "Attente", - "level": 1, - "v2": true, - "history": { - "v1.5.2": "修复定时任务未正确注册的问题", - "v1.5.1": "修复季度信息未传递的问题. 新增站点列表同步删除", - "v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项" - } + "name": "Bangumi收藏订阅", + "description": "Bangumi用户收藏添加到订阅", + "labels": "订阅", + "version": "1.5.2", + "icon": "bangumi_b.png", + "author": "Attente", + "level": 1, + "v2": true, + "history": { + "v1.5.2": "修复定时任务未正确注册的问题", + "v1.5.1": "修复季度信息未传递的问题. 新增站点列表同步删除", + "v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项" + } } } diff --git a/plugins/doubansync/__init__.py b/plugins/doubansync/__init__.py index ffb6570..1c3508d 100644 --- a/plugins/doubansync/__init__.py +++ b/plugins/doubansync/__init__.py @@ -34,7 +34,7 @@ class DoubanSync(_PluginBase): # 插件图标 plugin_icon = "douban.png" # 插件版本 - plugin_version = "1.9" + plugin_version = "1.9.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -498,6 +498,11 @@ class DoubanSync(_PluginBase): """ if not self._users: return + # 版本 + if hasattr(settings, 'VERSION_FLAG'): + version = settings.VERSION_FLAG # V2 + else: + version = "v1" # 读取历史记录 if self._clearflag: history = [] @@ -509,9 +514,12 @@ class DoubanSync(_PluginBase): continue logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...") url = self._interests_url % user_id - results = self.rsshelper.parse(url, headers={ - "User-Agent": settings.USER_AGENT - }) + if version == "v2": + results = self.rsshelper.parse(url, headers={ + "User-Agent": settings.USER_AGENT + }) + else: + results = self.rsshelper.parse(url) if not results: logger.warn(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}") continue From 8def52ab6b53faca0c91766ab2897dd9ab1e0d73 Mon Sep 17 00:00:00 2001 From: cikezhu <604054726@qq.com> Date: Tue, 12 Nov 2024 10:49:29 +0800 Subject: [PATCH 16/27] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=89=A7=E9=9B=86=E7=BB=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- plugins/episodegroupmeta/__init__.py | 363 +++++++++++++++++++++++++-- 2 files changed, 353 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 4d332fa..3ee61b2 100644 --- a/package.json +++ b/package.json @@ -527,10 +527,14 @@ "name": "TMDB剧集组刮削", "description": "从TMDB剧集组刮削季集的实际顺序。", "labels": "刮削", - "version": "1.1", + "version": "2.0", "icon": "Element_A.png", "author": "叮叮当", - "level": 1 + "level": 1, + "v2": true, + "history": { + "v2.0": "新增 手动选择剧集组功能" + } }, "CustomIndexer": { "name": "自定义索引站点", diff --git a/plugins/episodegroupmeta/__init__.py b/plugins/episodegroupmeta/__init__.py index 7a3be26..a76fb7f 100644 --- a/plugins/episodegroupmeta/__init__.py +++ b/plugins/episodegroupmeta/__init__.py @@ -22,7 +22,8 @@ from app.plugins import _PluginBase from app.schemas.types import EventType from app.utils.common import retry from app.utils.http import RequestUtils - +from app.db.models import PluginData +from app.utils.object import ObjectUtils class ExistMediaInfo(BaseModel): # 类型 电影、电视剧 @@ -47,7 +48,7 @@ class EpisodeGroupMeta(_PluginBase): # 主题色 plugin_color = "#098663" # 插件版本 - plugin_version = "1.1" + plugin_version = "2.0" # 插件作者 plugin_author = "叮叮当" # 作者主页 @@ -70,6 +71,7 @@ class EpisodeGroupMeta(_PluginBase): jellyfin = None _enabled = False + _autorun = True _ignorelock = False _delay = 0 _allowlist = [] @@ -82,6 +84,7 @@ class EpisodeGroupMeta(_PluginBase): self.jellyfin = Jellyfin() if config: self._enabled = config.get("enabled") + self._autorun = config.get("autorun") self._ignorelock = config.get("ignorelock") self._delay = config.get("delay") or 120 self._allowlist = [] @@ -90,6 +93,12 @@ class EpisodeGroupMeta(_PluginBase): if s and s not in self._allowlist: self._allowlist.append(s) self.log_info(f"白名单数量: {len(self._allowlist)} > {self._allowlist}") + if not ("autorun" in config): + # 新版本v1.2更新插件配置默认配置 + self._autorun = True + config["autorun"] = True + self.update_config(config) + self.log_warn(f"新版本v{self.plugin_version} 配置修正 ...") def get_state(self) -> bool: return self._enabled @@ -99,7 +108,65 @@ class EpisodeGroupMeta(_PluginBase): pass def get_api(self) -> List[Dict[str, Any]]: - pass + # plugin/EpisodeGroupMeta/delete_media_database + # plugin/EpisodeGroupMeta/start_rt + self.log_warn("api已添加: /start_rt") + self.log_warn("api已添加: /delete_media_database") + return [ + { + "path": "/delete_media_database", + "endpoint": self.delete_media_database, + "methods": ["GET"], + "summary": "剧集组刮削", + "description": "移除待处理媒体信息", + }, + { + "path": "/start_rt", + "endpoint": self.go_start_rt, + "methods": ["GET"], + "summary": "剧集组刮削", + "description": "刮削指定剧集组", + } + ] + + def delete_media_database(self, tmdb_id: str, apikey: str) -> schemas.Response: + """ + 删除待处理剧集组的媒体信息 + """ + if apikey != settings.API_TOKEN: + return schemas.Response(success=False, message="API密钥错误") + if not tmdb_id: + return schemas.Response(success=False, message="缺少重要参数") + self.del_data(tmdb_id) + return schemas.Response(success=True, message="删除成功") + + def go_start_rt(self, tmdb_id: str, group_id: str, apikey: str) -> schemas.Response: + if apikey != settings.API_TOKEN: + return schemas.Response(success=False, message="API密钥错误") + if not tmdb_id or not group_id: + return schemas.Response(success=False, message="缺少重要参数") + # 解析待处理数据 + try: + # 查询待处理数据 + data = self.get_data(tmdb_id) + if not data: + return schemas.Response(success=False, message="未找到待处理数据") + mediainfo_dict = data.get("mediainfo_dict") + mediainfo: schemas.MediaInfo = schemas.MediaInfo.parse_obj(mediainfo_dict) + episode_groups = data.get("episode_groups") + except Exception as e: + self.log_error(f"解析媒体信息失败: {str(e)}") + return schemas.Response(success=False, message="解析媒体信息失败") + # 开始刮削 + self.log_info(f"开始刮削: {mediainfo.title} | {mediainfo.year} | {episode_groups}") + if self.start_rt(mediainfo, episode_groups, group_id): + self.log_info("刮削剧集组, 执行成功! 后台正在执行,请稍等!") + self.systemmessage.put("后台正在执行,请稍等!", title="剧集组刮削") + return schemas.Response(success=True, message="刮削剧集组, 执行成功!") + else: + self.log_error("执行失败, 请查看插件日志!") + self.systemmessage.put("执行失败, 请查看插件日志!", title="剧集组刮削") + return schemas.Response(success=False, message="执行失败, 请查看插件日志") def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: """ @@ -116,7 +183,7 @@ class EpisodeGroupMeta(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -132,14 +199,30 @@ class EpisodeGroupMeta(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { - 'component': 'VSwitch', + 'component': 'VCheckboxBtn', + 'props': { + 'model': 'autorun', + 'label': '季集匹配时自动刮削', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VCheckboxBtn', 'props': { 'model': 'ignorelock', - 'label': '媒体信息锁定时也进行刮削', + 'label': '强制刮削已锁定的媒体信息', } } ] @@ -203,7 +286,7 @@ class EpisodeGroupMeta(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '注意:刮削白名单(留空), 则全部刮削. 否则仅刮削白名单.' + 'text': '注意:刮削白名单(留空)则全部刮削. 否则仅刮削白名单.' } } ] @@ -235,18 +318,212 @@ class EpisodeGroupMeta(_PluginBase): } ], { "enabled": False, + "autorun": True, "ignorelock": False, "allowlist": "", "delay": 120 } def get_page(self) -> List[dict]: - pass + """ + 拼装插件详情页面,需要返回页面配置,同时附带数据 + """ + # 查询待处理数据列表 + mediainfo_list: List[PluginData] = self.get_data() + # 拼装页面 + contents = [] + for plugin_data in mediainfo_list: + try: + tmdb_id = plugin_data.key + # fix v1版本数据读取问题 + if ObjectUtils.is_obj(plugin_data.value): + data = json.loads(plugin_data.value) + else: + data = plugin_data.value + mediainfo: schemas.MediaInfo = schemas.MediaInfo.parse_obj(data.get("mediainfo_dict")) + episode_groups = data.get("episode_groups") + except Exception as e: + self.log_error(f"解析媒体信息失败: {plugin_data.key} -> {plugin_data.value} \n ------ \n {str(e)}") + continue + # 剧集组菜单明细 + groups_menu = [] + index = 0 + for group in episode_groups: + index += 1 + title = group.get('name') + groups_menu.append({ + 'component': 'VListItem', + 'props': { + ':key': str(index), + ':value': str(index) + }, + 'events': { + 'click': { + 'api': 'plugin/EpisodeGroupMeta/start_rt', + 'method': 'get', + 'params': { + 'apikey': settings.API_TOKEN, + 'tmdb_id': tmdb_id, + 'group_id': group.get('id') + } + } + }, + 'content': [ + { + 'component': 'VListItemTitle', + 'text': title + }, + { + 'component': 'VListItemSubtitle', + 'text': f"{group.get('group_count')}组, {group.get('episode_count')}集" + }, + ] + }) + # 拼装待处理媒体卡片 + contents.append( + { + 'component': 'VCard', + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': mediainfo.backdrop_path or mediainfo.poster_path, + 'height': '120px', + 'cover': True + }, + }, + { + 'component': 'VCardTitle', + 'content': [ + { + 'component': 'a', + 'props': { + 'href': f"{mediainfo.detail_link}/episode_groups", + 'target': '_blank' + }, + 'text': mediainfo.title + } + ] + }, + { + 'component': 'VCardSubtitle', + 'content': [ + { + 'component': 'a', + 'props': { + 'href': f"{mediainfo.detail_link}/episode_groups", + 'target': '_blank' + }, + 'text': f"{mediainfo.year} | 共{len(episode_groups)}个剧集组" + } + ] + }, + { + 'component': 'VCardActions', + 'props': { + 'style': 'min-height:64px;' + }, + 'content': [ + { + 'component': 'VBtn', + 'props': { + 'class': 'ms-2', + 'size': 'small', + 'rounded': 'xl', + 'elevation': '20', + 'append-icon': 'mdi-chevron-right' + }, + 'text': '选择剧集组', + 'content': [ + { + 'component': 'VMenu', + 'props': { + 'activator': 'parent' + }, + 'content': [ + { + 'component': 'VList', + 'content': groups_menu + } + ] + } + ] + }, + { + 'component': 'VBtn', + 'props': { + 'class': 'ms-2', + 'size': 'small', + 'elevation': '20', + 'rounded': 'xl', + }, + 'text': '忽略', + 'events': { + 'click': { + 'api': 'plugin/EpisodeGroupMeta/delete_media_database', + 'method': 'get', + 'params': { + 'apikey': settings.API_TOKEN, + 'tmdb_id': tmdb_id + } + } + }, + } + ] + } + ] + } + ) + + if not contents: + return [ + { + 'component': 'div', + 'text': '暂无待处理数据', + 'props': { + 'class': 'text-center', + } + } + ] + + return [ + { + 'component': 'VRow', + 'props': { + 'class': 'mb-3' + }, + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '注意:1. 点击名字可跳转tmdb剧集组页面。2. 选择剧集组时后台已经开始执行,请通过日志查看进度,不要重复执行。' + } + } + ] + } + ] + }, + { + 'component': 'div', + 'props': { + 'class': 'grid gap-6 grid-info-card', + }, + 'content': contents + } + ] @eventmanager.register(EventType.TransferComplete) def scrap_rt(self, event: Event): """ - 根据事件实时刮削剧集组信息 + 根据事件判断是否需要刮削 """ if not self.get_state(): return @@ -279,17 +556,62 @@ class EpisodeGroupMeta(_PluginBase): except Exception as e: self.log_error(f"{mediainfo.title} {str(e)}") return + # 写入至插件数据 + mediainfo_dict = None + try: + # 实际传递的不是基于BaseModel的实例 + mediainfo_dict = mediainfo.dict() + except Exception as e: + # app.core.context.MediaInfo + try: + mediainfo_dict = mediainfo.to_dict() + except Exception as e: + self.log_error(f"{mediainfo.title} 无法处理MediaInfo数据 {str(e)}") + if mediainfo_dict: + data = { + "episode_groups": episode_groups, + "mediainfo_dict": mediainfo_dict + } + self.save_data(str(mediainfo.tmdb_id), data) + self.log_info("写入待处理数据 - ok") + # 禁止自动刮削时直接返回 + if not self._autorun: + self.log_warn(f"{mediainfo.title} 未勾选自动刮削, 无需处理") + return # 延迟 if self._delay: self.log_warn(f"{mediainfo.title} 将在 {self._delay} 秒后开始处理..") time.sleep(int(self._delay)) + # 开始处理 + self.start_rt(mediainfo=mediainfo, episode_groups=episode_groups) + + def start_rt(self, mediainfo: schemas.MediaInfo, episode_groups: Any | None, group_id: str = None) -> bool: + """ + 通过媒体信息读取剧集组并刮削季集信息 + """ + # 当不是从事件触发时,应再次判断是否存在剧集组 + if not episode_groups: + try: + episode_groups = self.tv.episode_groups(mediainfo.tmdb_id) + if not episode_groups: + self.log_warn(f"{mediainfo.title} 没有剧集组, 无需处理") + return False + self.log_info(f"{mediainfo.title_year} 剧集组数量: {len(episode_groups)} - {episode_groups}") + # episodegroup = self.tv.group_episodes(episode_groups[0].get('id')) + except Exception as e: + self.log_error(f"{mediainfo.title} {str(e)}") + return False # 获取可用的媒体服务器 _existsinfo = self.chain.media_exists(mediainfo=mediainfo) + if not _existsinfo: + self.log_warn(f"{mediainfo.title_year} 无可用的媒体服务器") + return False + # 存在媒体服务器 existsinfo: ExistMediaInfo = self.__media_exists(server=_existsinfo.server, mediainfo=mediainfo, existsinfo=_existsinfo) if not existsinfo or not existsinfo.itemid: self.log_warn(f"{mediainfo.title_year} 在媒体库中不存在") - return + return False # 新增需要的属性 existsinfo.server = _existsinfo.server existsinfo.type = _existsinfo.type @@ -309,6 +631,9 @@ class EpisodeGroupMeta(_PluginBase): name = episode_group.get('name') if not id: continue + # 指定剧集组id时, 跳过其他剧集组 + if group_id and str(id) != str(group_id): + continue # 处理 self.log_info(f"正在匹配剧集组: {id}") groups_meta = self.tv.group_episodes(id) @@ -325,9 +650,14 @@ class EpisodeGroupMeta(_PluginBase): continue # 进行集数匹配, 确定剧集组信息 ep = existsinfo.groupep.get(order) - if not ep or len(ep) != len(episodes): - continue - self.log_info(f"已匹配剧集组: {name}, {id}, 第 {order} 季") + # 指定剧集组id时, 不再通过季集数量匹配 + if group_id: + self.log_info(f"已指定剧集组: {name}, {id}, 第 {order} 季") + else: + # 进行集数匹配, 确定剧集组信息 + if not ep or len(ep) != len(episodes): + continue + self.log_info(f"已匹配剧集组: {name}, {id}, 第 {order} 季") # 遍历全部媒体项并更新 for _index, _ids in enumerate(existsinfo.groupid.get(order)): # 提取出媒体库中集id对应的集数index @@ -338,8 +668,8 @@ class EpisodeGroupMeta(_PluginBase): if not iteminfo: self.log_info(f"未找到媒体项 - itemid: {_id}, 第 {order} 季, 第 {ep_num} 集") continue - # 是否无视项目锁定 - if not self._ignorelock: + # 是否无视项目锁定, 指定剧集组id时也属于无视项目锁定 + if not self._ignorelock and not group_id: if iteminfo.get("LockData") or ( "Name" in iteminfo.get("LockedFields", []) and "Overview" in iteminfo.get("LockedFields", [])): @@ -376,6 +706,7 @@ class EpisodeGroupMeta(_PluginBase): continue self.log_info(f"{mediainfo.title_year} 已经运行完毕了..") + return True @staticmethod def __append_to_list(list, item): From acb441e746006cc5b20080beaea6a5458acc10ae Mon Sep 17 00:00:00 2001 From: cikezhu <604054726@qq.com> Date: Tue, 12 Nov 2024 13:23:06 +0800 Subject: [PATCH 17/27] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=91=E9=80=81?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E6=8F=90=E9=86=92=E9=80=89=E6=8B=A9=E5=89=A7?= =?UTF-8?q?=E9=9B=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- plugins/episodegroupmeta/__init__.py | 73 +++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 3ee61b2..2639509 100644 --- a/package.json +++ b/package.json @@ -527,13 +527,14 @@ "name": "TMDB剧集组刮削", "description": "从TMDB剧集组刮削季集的实际顺序。", "labels": "刮削", - "version": "2.0", + "version": "2.1", "icon": "Element_A.png", "author": "叮叮当", "level": 1, "v2": true, "history": { - "v2.0": "新增 手动选择剧集组功能" + "v2.1": "增加发送通知提醒选择剧集组", + "v2.0": "增加手动选择剧集组的功能" } }, "CustomIndexer": { diff --git a/plugins/episodegroupmeta/__init__.py b/plugins/episodegroupmeta/__init__.py index a76fb7f..371ad1c 100644 --- a/plugins/episodegroupmeta/__init__.py +++ b/plugins/episodegroupmeta/__init__.py @@ -48,7 +48,7 @@ class EpisodeGroupMeta(_PluginBase): # 主题色 plugin_color = "#098663" # 插件版本 - plugin_version = "2.0" + plugin_version = "2.1" # 插件作者 plugin_author = "叮叮当" # 作者主页 @@ -71,6 +71,7 @@ class EpisodeGroupMeta(_PluginBase): jellyfin = None _enabled = False + _notify = True _autorun = True _ignorelock = False _delay = 0 @@ -84,6 +85,7 @@ class EpisodeGroupMeta(_PluginBase): self.jellyfin = Jellyfin() if config: self._enabled = config.get("enabled") + self._notify = config.get("notify") self._autorun = config.get("autorun") self._ignorelock = config.get("ignorelock") self._delay = config.get("delay") or 120 @@ -93,9 +95,11 @@ class EpisodeGroupMeta(_PluginBase): if s and s not in self._allowlist: self._allowlist.append(s) self.log_info(f"白名单数量: {len(self._allowlist)} > {self._allowlist}") - if not ("autorun" in config): - # 新版本v1.2更新插件配置默认配置 + if not ("notify" in config): + # 新版本v2.0更新插件配置默认配置 + self._notify = True self._autorun = True + config["notify"] = True config["autorun"] = True self.update_config(config) self.log_warn(f"新版本v{self.plugin_version} 配置修正 ...") @@ -159,13 +163,28 @@ class EpisodeGroupMeta(_PluginBase): return schemas.Response(success=False, message="解析媒体信息失败") # 开始刮削 self.log_info(f"开始刮削: {mediainfo.title} | {mediainfo.year} | {episode_groups}") + self.systemmessage.put("正在刮削中,请稍等!", title="剧集组刮削") if self.start_rt(mediainfo, episode_groups, group_id): - self.log_info("刮削剧集组, 执行成功! 后台正在执行,请稍等!") - self.systemmessage.put("后台正在执行,请稍等!", title="剧集组刮削") + self.log_info("刮削剧集组, 执行成功!") + self.systemmessage.put("刮削剧集组, 执行成功!", title="剧集组刮削") + # 处理成功时, 发送通知 + if self._notify: + self.post_message( + mtype=schemas.NotificationType.Manual, + title="【剧集组处理结果: 成功】", + text=f"媒体名称:{mediainfo.title}\n发行年份: {mediainfo.year}\n剧集组数: {len(episode_groups)}" + ) return schemas.Response(success=True, message="刮削剧集组, 执行成功!") else: self.log_error("执行失败, 请查看插件日志!") self.systemmessage.put("执行失败, 请查看插件日志!", title="剧集组刮削") + # 处理成功时, 发送通知 + if self._notify: + self.post_message( + mtype=schemas.NotificationType.Manual, + title="【剧集组处理结果: 失败】", + text=f"媒体名称:{mediainfo.title}\n发行年份: {mediainfo.year}\n剧集组数: {len(episode_groups)}\n注意: 失败原因请查看日志.." + ) return schemas.Response(success=False, message="执行失败, 请查看插件日志") def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: @@ -183,7 +202,7 @@ class EpisodeGroupMeta(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -199,7 +218,7 @@ class EpisodeGroupMeta(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -215,18 +234,34 @@ class EpisodeGroupMeta(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { 'component': 'VCheckboxBtn', 'props': { 'model': 'ignorelock', - 'label': '强制刮削已锁定的媒体信息', + 'label': '无视锁定的媒体', } } ] - } + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCheckboxBtn', + 'props': { + 'model': 'notify', + 'label': '开启通知', + } + } + ] + }, ] }, { @@ -318,6 +353,7 @@ class EpisodeGroupMeta(_PluginBase): } ], { "enabled": False, + "notify": True, "autorun": True, "ignorelock": False, "allowlist": "", @@ -577,13 +613,28 @@ class EpisodeGroupMeta(_PluginBase): # 禁止自动刮削时直接返回 if not self._autorun: self.log_warn(f"{mediainfo.title} 未勾选自动刮削, 无需处理") + # 发送通知 + if self._notify and mediainfo_dict: + self.post_message( + mtype=schemas.NotificationType.Manual, + title="【待手动处理的剧集组】", + text=f"媒体名称:{mediainfo.title}\n发行年份: {mediainfo.year}\n剧集组数: {len(episode_groups)}" + ) return # 延迟 if self._delay: self.log_warn(f"{mediainfo.title} 将在 {self._delay} 秒后开始处理..") time.sleep(int(self._delay)) # 开始处理 - self.start_rt(mediainfo=mediainfo, episode_groups=episode_groups) + if self.start_rt(mediainfo=mediainfo, episode_groups=episode_groups): + # 处理完成时, 属于自动匹配的, 发送通知 + if self._notify and mediainfo_dict: + self.post_message( + mtype=schemas.NotificationType.Manual, + title="【已自动匹配的剧集组】", + text=f"媒体名称:{mediainfo.title}\n发行年份: {mediainfo.year}\n剧集组数: {len(episode_groups)}" + ) + return def start_rt(self, mediainfo: schemas.MediaInfo, episode_groups: Any | None, group_id: str = None) -> bool: """ From 6e39f5a854b25ce935a97bf5cba626af2084f97a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 12 Nov 2024 20:25:24 +0800 Subject: [PATCH 18/27] =?UTF-8?q?add=20=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=BF=81=E7=A7=BB=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 9 + plugins.v2/historytov2/__init__.py | 335 +++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 plugins.v2/historytov2/__init__.py diff --git a/package.v2.json b/package.v2.json index c369fb7..3a90b35 100644 --- a/package.v2.json +++ b/package.v2.json @@ -203,5 +203,14 @@ "v2.1": "调整IYUU最新域名", "v2.0": "兼容MoviePilot V2 版本" } + }, + "HistoryToV2": { + "name": "历史记录迁移", + "description": "将MoviePilot V1版本的整理历史记录迁移至V2版本。", + "labels": "整理,历史记录", + "version": "1.0", + "icon": "Moviepilot_A.png", + "author": "jxxghp", + "level": 1 } } \ No newline at end of file diff --git a/plugins.v2/historytov2/__init__.py b/plugins.v2/historytov2/__init__.py new file mode 100644 index 0000000..6bc2144 --- /dev/null +++ b/plugins.v2/historytov2/__init__.py @@ -0,0 +1,335 @@ +import json +from pathlib import Path +from typing import Any, List, Dict, Tuple, Optional + +from app.db import SessionFactory +from app.db.models import TransferHistory +from app.log import logger +from app.plugins import _PluginBase +from app.utils.http import RequestUtils + + +class HistoryToV2(_PluginBase): + # 插件名称 + plugin_name = "历史记录迁移" + # 插件描述 + plugin_desc = "将MoviePilot V1版本的整理历史记录迁移至V2版本。" + # 插件图标 + plugin_icon = "Moviepilot_A.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "historytov2_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + historyoper = None + _enabled = False + _host = None + _username = None + _password = None + + def init_plugin(self, config: dict = None): + if config: + self._enabled = config.get("enabled") + self._host = config.get("host") + self._username = config.get("username") + self._password = config.get("password") + + if self._enabled and self._host and self._username and self._password: + # 关闭开关 + self.__close_config() + # 登录MP获取token + token = self.__login_mp() + if token: + # 当前页码 + page = 1 + # 总记录数 + total = 0 + # 获取历史记录 + history = self.__get_history(token) + while history: + # 处理历史记录 + logger.info(f"开始处理第 {page} 页历史记录 ...") + self.__insert_history(history) + # 处理成功一批 + total += len(history) + logger.info(f"第 {page} 页处理完成,共处理 {total} 条记录") + # 获取下一页历史记录 + page += 1 + history = self.__get_history(token, page=page) + # 处理完成 + logger.info(f"历史记录迁移完成,共迁移 {total} 条记录!") + self.systemmessage.put(f"历史记录迁移完成,共迁移 {total} 条记录!", title="MoviePilot历史记录迁移") + else: + self.systemmessage.put(f"配置不完整,服务启动失败!", title="MoviePilot历史记录迁移") + # 关闭开关 + self.__close_config() + + def __close_config(self): + """ + 关闭开关 + """ + self._enabled = False + self.update_config({ + "enabled": self._enabled, + "host": self._host, + "username": self._username, + "password": self._password + }) + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'host', + 'label': 'MoviePilot V1地址', + 'placeholder': 'http://localhost:3000', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'username', + 'label': '登录用户名', + 'placeholder': 'admin' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'password', + 'label': '登录密码', + 'type': 'password', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': 'MoviePilot V1 需要是启动状态且能正常访问,V1版本和V2版本目录映射需要保持一致,迁移时间可能较长,完成后会收到系统通知。' + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "host": None, + "username": None, + "password": None + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + pass + + def __login_mp(self) -> Optional[str]: + """ + 登录MP获取token + """ + if not self._host or not self._username or not self._password: + return None + url = f"{self._host}/api/v1/login/access-token" + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + data = { + "username": self._username, + "password": self._password + } + logger.info(f"登录MoviePilot: {url}") + # 发送POST请求 + response = RequestUtils(headers=headers).post_res(url, data=data) + # 检查响应状态 + if response.status_code == 200: + # 成功获取token + token_data = response.json() + logger.info(f"登录MoviePilot成功,获取token:{token_data['access_token']}", ) + return token_data["access_token"] + else: + # 处理失败响应 + logger.warn(f"登录MoviePilot失败: {response.json()}") + self.systemmessage.put(f"登录MoviePilot失败,无法同步历史记录!", title="MoviePilot历史记录迁移") + return None + + def __get_history(self, token: str, page: int = 1, count: int = 30) -> Optional[List[dict]]: + """ + 获取历史记录 + """ + if not token: + return [] + url = f"{self._host}/api/v1/history/transfer" + headers = { + "Authorization": f"Bearer {token}" + } + params = { + "page": page, + "count": count + } + logger.info(f"查询转移历史记录: {url},params: {params}") + # 发送GET请求 + response = RequestUtils(headers=headers).get_res(url, params=params) + # 检查响应状态 + if response.status_code == 200: + # 返回数据 + response_data = response.json() + data = response_data.get("data") + logger.info(f"查询转移历史记录成功,共 {len(data.get('list'))} 条记录") + return data.get("list") + else: + # 处理失败响应 + logger.warn("查询转移历史记录失败:", response.json()) + self.systemmessage.put(f"查询转移历史记录失败,无法同步历史记录!", title="MoviePilot历史记录迁移") + return [] + + @staticmethod + def __insert_history(history: List[dict]): + """ + 插入历史记录 + """ + if not history: + return + with SessionFactory() as db: + for item in history: + if item.get("src"): + transferhistory = TransferHistory.get_by_src(db, item.get("src")) + if transferhistory: + transferhistory.delete(db, transferhistory.id) + try: + TransferHistory( + src=item.get("src"), + src_storage="local", + src_fileitem={ + "storage": "local", + "type": "file", + "path": item.get("src"), + "name": Path(item.get("src")).name, + "basename": Path(item.get("src")).stem, + "extension": Path(item.get("src")).suffix[1:], + }, + dest=item.get("dest"), + dest_storage="local", + dest_fileitem={ + "storage": "local", + "type": "file", + "path": item.get("dest"), + "name": Path(item.get("dest")).name, + "basename": Path(item.get("dest")).stem, + "extension": Path(item.get("dest")).suffix[1:], + }, + mode=item.get("mode"), + type=item.get("type"), + category=item.get("category"), + title=item.get("title"), + year=item.get("year"), + tmdbid=item.get("tmdbid"), + imdbid=item.get("imdbid"), + tvdbid=item.get("tvdbid"), + doubanid=item.get("doubanid"), + seasons=item.get("seasons"), + episodes=item.get("episodes"), + image=item.get("image"), + download_hash=item.get("download_hash"), + status=item.get("status"), + files=json.loads(item.get("files")) if item.get("files") else [], + date=item.get("date"), + errmsg=item.get("errmsg") + ).create(db) + except Exception as e: + logger.error(f"插入历史记录失败:{e}") + continue From 61dd0ef9180ca8f0d3739dbe1e0268413910dc26 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 12 Nov 2024 20:55:54 +0800 Subject: [PATCH 19/27] =?UTF-8?q?=E5=90=8C=E6=AD=A5CookieCloud=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20V2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2639509..71b0244 100644 --- a/package.json +++ b/package.json @@ -884,6 +884,7 @@ "icon": "Cookiecloud_A.png", "author": "thsrite", "level": 1, + "v2": true, "history": { "v2.0": "调整逻辑,修复问题", "v1.3": "感谢MidnightShake共享代码(同步时保留MoviePilot不匹配站点的cookie)", From c9cc458dca540e6bb15614e0a92c6b0bf53219be Mon Sep 17 00:00:00 2001 From: ramen <1205925392@qq.com> Date: Wed, 13 Nov 2024 03:35:07 +0800 Subject: [PATCH 20/27] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BC=81=E5=BE=AE?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E9=80=9A=E7=9F=A5=E5=92=8C=E7=AC=ACServer?= =?UTF-8?q?=E3=80=81Anpush=E3=80=81PushPlus=E7=AD=89=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E6=8E=A8=E9=80=81=E3=80=82=E6=8C=89=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8F=92=E4=BB=B6=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 14 +- plugins/dynamicwechat/__init__.py | 320 ++++++++++--------------- plugins/dynamicwechat/notify_helper.py | 133 ++++++++++ plugins/dynamicwechat/update_help.py | 18 +- 4 files changed, 271 insertions(+), 214 deletions(-) create mode 100644 plugins/dynamicwechat/notify_helper.py diff --git a/package.json b/package.json index 71b0244..048729b 100644 --- a/package.json +++ b/package.json @@ -857,23 +857,21 @@ "v2": true }, "DynamicWeChat": { - "name": "假的企业微信可信IP", - "description": "优先使用cookie,可本地扫码刷新Cookie,验证码以?结尾发送到企业微信应用", + "name": "动态企微可信IP", + "description": "修改企微应用可信IP,详细说明查看'作者主页',支持第三方通知。验证码以?结尾发送到企业微信应用", "labels": "消息通知", - "version": "1.4.1", + "version": "1.5.0", "icon": "Wecom_A.png", "author": "RamenRa", "level": 2, "v2": true, "history": { - "v1.4.1": "完善面板说明,为了和原仓库做出明显区别在插件名称添加了特殊字符", + "v1.5.0": "支持企微应用通知和第Serve酱等第三方推送。按要求修改插件名称", + "v1.4.1": "完善面板说明", "v1.4.0": "修复强制更改IP时配置面板延时过长的问题。庆祝v2进入正式版,显示了一个没用的参数", "v1.3.1": "修正一些逻辑判断,修改ip成功会通知一次", "v1.3.0": "兼容v2,操作cookie前检查一次CookieCloud", - "v1.2.0": "远程命令/push_qr,立即推送一次二维码到pushplus。添加<本地扫码刷新cookie>", - "v1.1.5": "将chromium运行设置为headless模式", - "v1.1.4": "放弃self.post_message()的消息推送,还原成send_pushplus_message()", - "v1.1.3": "关闭cookie输入框,延长cookie任务成功时不输出日志,使用设定中的CookieCloud设置" + "v1.2.0": "远程命令/push_qr,立即推送一次二维码到pushplus。添加<本地扫码刷新cookie>" } }, "SyncCookieCloud": { diff --git a/plugins/dynamicwechat/__init__.py b/plugins/dynamicwechat/__init__.py index d4d0f19..0b1942a 100644 --- a/plugins/dynamicwechat/__init__.py +++ b/plugins/dynamicwechat/__init__.py @@ -19,18 +19,19 @@ from app.helper.cookiecloud import CookieCloudHelper from app.log import logger from app.plugins import _PluginBase from app.plugins.dynamicwechat.update_help import PyCookieCloud +from app.plugins.dynamicwechat.notify_helper import MySender from app.schemas.types import EventType, NotificationType class DynamicWeChat(_PluginBase): # 插件名称 - plugin_name = "假的企业微信可信IP" + plugin_name = "动态企微可信IP" # 插件描述 - plugin_desc = "优先使用cookie,可本地扫码刷新Cookie,验证码以?结尾发送到企业微信应用" + plugin_desc = "修改企微应用可信IP,详细说明查看'作者主页',支持第三方通知。验证码以?结尾发送到企业微信应用" # 插件图标 plugin_icon = "Wecom_A.png" # 插件版本 - plugin_version = "1.4.1" + plugin_version = "1.5.0" # 插件作者 plugin_author = "RamenRa" # 作者主页 @@ -56,6 +57,10 @@ class DynamicWeChat(_PluginBase): _local_scan = False # 类初始化时添加标记变量 _is_special_upload = False + # 聚合通知 + _my_send = None + # 通知方式token/api + _notification_token = '' # 匹配ip地址的正则 _ip_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b' @@ -69,17 +74,16 @@ class DynamicWeChat(_PluginBase): _refresh_cron = '*/20 * * * *' # 输入的企业应用id _input_id_list = '' - # helloimg的token - _helloimg_s_token = "" - # pushplus的token - _pushplus_token = "" # 二维码 _qr_code_image = None + # 用户消息 text = "" # 手机验证码 _verification_code = '' # 过期时间 _future_timestamp = 0 + # 配置文件路径 + _settings_file_path = None # cookie有效检测 _cookie_valid = True @@ -97,30 +101,31 @@ class DynamicWeChat(_PluginBase): def init_plugin(self, config: dict = None): # 清空配置 - self._helloimg_s_token = '' - self._pushplus_token = '' + self._notification_token = '' self._ip_changed = True self._forced_update = False self._use_cookiecloud = True self._local_scan = False self._input_id_list = '' self._cookie_header = "" - self._current_ip_address = self.get_ip_from_url(self._ip_urls[0]) - self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime() + self._current_ip_address = self.get_ip_from_url(random.choice(self._ip_urls)) + self._settings_file_path = self.get_data_path() / "settings.json" + # self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime() if config: self._enabled = config.get("enabled") + self._notification_token = config.get("notification_token") self._cron = config.get("cron") self._onlyonce = config.get("onlyonce") self._input_id_list = config.get("input_id_list") self._current_ip_address = config.get("current_ip_address") - self._pushplus_token = config.get("pushplus_token") - self._helloimg_s_token = config.get("helloimg_s_token") self._forced_update = config.get("forced_update") self._local_scan = config.get("local_scan") self._use_cookiecloud = config.get("use_cookiecloud") self._cookie_header = config.get("cookie_header") self._ip_changed = config.get("ip_changed") - + self._my_send = MySender(self._notification_token) + if not self._my_send.init_success: # 没有输入通知方式,不通知 + self._my_send = None # 停止现有任务 self.stop_service() if (self._enabled or self._onlyonce) and self._input_id_list: @@ -174,8 +179,25 @@ class DynamicWeChat(_PluginBase): event_data = event.event_data if not event_data or event_data.get("action") != "dynamicwechat": return - self.ChangeIP() - self.__update_config() + # 先尝试cookie登陆 + try: + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=['--lang=zh-CN']) + context = browser.new_context() + cookie = self.get_cookie() + if cookie: + context.add_cookies(cookie) + page = context.new_page() + page.goto(self._wechatUrl) + time.sleep(3) + if self.check_login_status(page, task='forced_change'): + self.click_app_management_buttons(page) + else: + logger.error("cookie失效,强制修改IP失败:请使用'本地扫码修改IP'") + browser.close() + except Exception as err: + logger.error(f"强制修改IP失败:{err}") + logger.info("----------------------本次任务结束----------------------") @eventmanager.register(EventType.PluginAction) @@ -198,7 +220,6 @@ class DynamicWeChat(_PluginBase): page = context.new_page() page.goto(self._wechatUrl) time.sleep(3) # 页面加载等待时间 - if self.find_qrc(page): current_time = datetime.now() future_time = current_time + timedelta(seconds=110) @@ -242,12 +263,7 @@ class DynamicWeChat(_PluginBase): self.ChangeIP() self.__update_config() - # logger.info("检测公网IP完毕") logger.info("----------------------本次任务结束----------------------") - # if event: - # self.post_message(channel=event.event_data.get("channel"), - # title="检测公网IP完毕", - # userid=event.event_data.get("user")) def CheckIP(self): retry_urls = random.sample(self._ip_urls, len(self._ip_urls)) @@ -338,22 +354,16 @@ class DynamicWeChat(_PluginBase): qr_code_data = requests.get(qr_code_url).content self._qr_code_image = io.BytesIO(qr_code_data) - return True + refuse_time = (datetime.now() + timedelta(seconds=115)).strftime("%Y-%m-%d %H:%M:%S") + return qr_code_url, refuse_time else: logger.warning("未找到二维码") - return False + return None, None except Exception as e: logger.debug(str(e)) - return False + return None, None + - def send_pushplus_message(self, title, content): - pushplus_url = f"http://www.pushplus.plus/send/{self._pushplus_token}" - pushplus_data = { - "title": title, - "content": content, - "template": "html" - } - response = requests.post(pushplus_url, json=pushplus_data) def ChangeIP(self): logger.info("开始请求企业微信管理更改可信IP") @@ -362,25 +372,20 @@ class DynamicWeChat(_PluginBase): # 启动 Chromium 浏览器并设置语言为中文 browser = p.chromium.launch(headless=True, args=['--lang=zh-CN']) context = browser.new_context() - # ----------cookie addd----------------- cookie = self.get_cookie() if cookie: context.add_cookies(cookie) - # ----------cookie END----------------- page = context.new_page() page.goto(self._wechatUrl) time.sleep(3) - if self.find_qrc(page): - if self._pushplus_token and self._helloimg_s_token: - img_src, refuse_time = self.upload_image(self._qr_code_image) - self.send_pushplus_message(refuse_time, f"企业微信登录二维码
") - # if img_src: - # self.post_message( - # mtype=NotificationType.Plugin, - # title="企业微信登录二维码", - # text=refuse_time, - # image=img_src - # ) + img_src, refuse_time = self.find_qrc(page) + if img_src: + if self._my_send: + result = self._my_send.send("企业微信登录二维码", content=None, image=img_src, force_send=False) + if result: + logger.info(f"二维码发送失败,原因:{result}") + browser.close() + return logger.info("二维码已经发送,等待用户 90 秒内扫码登录") # logger.info("如收到短信验证码请以?结束,发送到<企业微信应用> 如: 110301?") time.sleep(90) # 等待用户扫码 @@ -391,17 +396,15 @@ class DynamicWeChat(_PluginBase): else: self._ip_changed = False else: - logger.info("cookie失效,请重新上传或者配置pushplus_token和helloimg_s_token。") + self._ip_changed = False + logger.info("cookie已失效") else: # 如果直接进入企业微信 logger.info("尝试cookie登录") - # ----------cookie addd----------------- login_status = self.check_login_status(page, "") if login_status: self.click_app_management_buttons(page) else: - # ----------cookie END----------------- self._ip_changed = False - return browser.close() except Exception as e: @@ -411,7 +414,7 @@ class DynamicWeChat(_PluginBase): def _update_cookie(self, page, context): self._future_timestamp = 0 # 标记二维码失效 - PyCookieCloud.save_cookie_lifetime(0) # 重置cookie存活时间 + PyCookieCloud.save_cookie_lifetime(self._settings_file_path, 0) # 重置cookie存活时间 if self._use_cookiecloud: if not self._cc_server: # 连接失败返回 False self.try_connect_cc() # 再尝试一次连接 @@ -487,26 +490,34 @@ class DynamicWeChat(_PluginBase): return cookies def refresh_cookie(self): # 保活 - try: - with sync_playwright() as p: - browser = p.chromium.launch(headless=True, args=['--lang=zh-CN']) - context = browser.new_context() - cookie = self.get_cookie() - if cookie: - context.add_cookies(cookie) - page = context.new_page() - page.goto(self._wechatUrl) - time.sleep(3) - if not self.check_login_status(page, task='refresh_cookie'): - self._cookie_valid = False - logger.warning("cookie已失效,下次IP变动推送二维码") - else: - self._cookie_valid = True - PyCookieCloud.increase_cookie_lifetime(1200) - self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime() - browser.close() - except Exception as e: - logger.error(f"cookie校验失败:{e}") + if self._use_cookiecloud: + try: + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=['--lang=zh-CN']) + context = browser.new_context() + cookie = self.get_cookie() + if cookie: + context.add_cookies(cookie) + page = context.new_page() + page.goto(self._wechatUrl) + time.sleep(3) + if not self.check_login_status(page, task='refresh_cookie'): + self._cookie_valid = False + if self._my_send: + result = self._my_send.send(title="cookie已失效,请及时更新", + content="请在企业微信应用发送/push_qr,让插件推送二维码。如果是使用微信通知请确保公网IP还没有变动", + image=None, force_send=False) # 标题,内容,图片,是否强制发送 + if result: + logger.info(f"cookie失效通知发送失败,原因:{result}") + else: + self._cookie_valid = True + if self._my_send: + self._my_send.reset_limit() + PyCookieCloud.increase_cookie_lifetime(self._settings_file_path, 1200) + self._cookie_lifetime = PyCookieCloud.load_cookie_lifetime(self._settings_file_path) + browser.close() + except Exception as e: + logger.error(f"cookie校验失败:{e}") # def check_login_status(self, page, task): @@ -600,81 +611,15 @@ class DynamicWeChat(_PluginBase): logger.info(f"应用: {app_id} 输入IP:" + self._current_ip_address) ip_parts = self._current_ip_address.split('.') masked_ip = f"{ip_parts[0]}.{len(ip_parts[1]) * '*'}.{len(ip_parts[2]) * '*'}.{ip_parts[3]}" - self.post_message( - mtype=NotificationType.Plugin, - title="更新可信IP成功", - text='应用: ' + app_id + ' 输入IP:' + masked_ip, - # image=img_src - ) + if self._my_send: + result = self._my_send.send(title="更新可信IP成功", + content='应用: ' + app_id + ' 输入IP:' + masked_ip, + force_send=True, diy_channel="WeChat") return else: logger.error("未找到应用id,修改IP失败") return - def upload_image(self, file_obj, permission=1, strategy_id=1, album_id=1): - """ - 上传图片到 helloimg 图床,支持传入文件路径或 BytesIO 对象。 - - :param file_obj: 文件对象,可以是路径 (str) 或 BytesIO 对象 - :param permission: 上传图片的权限设置,默认 1 - :param strategy_id: 上传策略 ID,默认 1 - :param album_id: 相册 ID,默认 1 - :return: 上传成功返回图片链接,失败返回 None - """ - helloimg_token = "Bearer " + self._helloimg_s_token - helloimg_url = "https://www.helloimg.com/api/v1/upload" - headers = { - "Authorization": helloimg_token, - "Accept": "application/json", - } - - # 构造上传的文件,支持传入 BytesIO 或文件路径 - if isinstance(file_obj, io.BytesIO): - # 如果是 BytesIO 对象,直接使用 - files = { - "file": ('qr_code.png', file_obj, 'image/png') - } - else: - # 如果是文件路径,打开文件进行读取 - files = { - "file": open(file_obj, "rb") - } - - expired_at = (datetime.now() + timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S") - helloimg_data = { - "token": "你的临时上传 Token", # 确保这里的 token 是有效的 - "permission": permission, - "strategy_id": strategy_id, - "album_id": album_id, - "expired_at": expired_at - } - refuse_time = (datetime.now() + timedelta(seconds=110)).strftime("%Y-%m-%d %H:%M:%S") - - # 发送上传请求 - response = requests.post(helloimg_url, headers=headers, files=files, data=helloimg_data) - - # 检查响应内容是否符合预期 - response_data = None - try: - response_data = response.json() - if not response_data['status']: - if response_data['message'] == "Unauthenticated.": - logger.error("Token失效,无法上传图片。请检查你的上传Token。") - logger.info(f"使用的Token: {helloimg_token}") - # self._ip_changed = False - return - else: - logger.error(f"上传到图床失败: {response_data['message']}") - self._ip_changed = False - return - - img_src = response_data['data']['links']['html'] - return img_src.split('"')[1], refuse_time # 提取 img src - except KeyError as e: - logger.error(f"上传图片时解析响应失败: {e}, 响应内容: {response_data}") - self._ip_changed = False - return - def __update_config(self): """ 更新配置 @@ -683,13 +628,11 @@ class DynamicWeChat(_PluginBase): "enabled": self._enabled, "onlyonce": self._onlyonce, "cron": self._cron, - # "wechatUrl": self._wechatUrl, + "notification_token": self._notification_token, "current_ip_address": self._current_ip_address, "ip_changed": self._ip_changed, "forced_update": self._forced_update, "local_scan": self._local_scan, - "helloimg_s_token": self._helloimg_s_token, - "pushplus_token": self._pushplus_token, "input_id_list": self._input_id_list, "cookie_header": self._cookie_header, "use_cookiecloud": self._use_cookiecloud, @@ -759,7 +702,6 @@ class DynamicWeChat(_PluginBase): } ] }, - # 添加 "使用CookieCloud获取cookie" 开关按钮 { 'component': 'VRow', 'content': [ @@ -816,6 +758,24 @@ class DynamicWeChat(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'notification_token', + 'label': '[可选] 通知方式', + 'rows': 1, + 'placeholder': '支持微信、Server酱、PushPlus、AnPush等Token或API' + } + } + ] } ] }, @@ -841,47 +801,6 @@ class DynamicWeChat(_PluginBase): } ] }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VTextarea', - 'props': { - 'model': 'pushplus_token', - 'label': '[可选]pushplus_token', - 'rows': 1, - 'placeholder': '[可选] 请输入 pushplus_token' - } - } - ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VTextarea', - 'props': { - 'model': 'helloimg_s_token', - 'label': '[可选]helloimg_s_token', - 'rows': 1, - 'placeholder': '[可选] 请输入 helloimg_token' - } - } - ] - } - ] - }, { 'component': 'VRow', 'content': [ @@ -896,7 +815,7 @@ class DynamicWeChat(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '使用内建CookieCloud 或 自定义 或 填写两个token 至少三选一。具体请查看作者主页' + 'text': '建议启用内建或自定义CookieCloud。支持微信、Server酱、PushPlus、AnPush,具体请查看作者主页' } } ] @@ -916,7 +835,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VAlert', 'props': { 'type': 'info', - 'text': '优先使用cookie,当IP变动 且 cookie失效 且 填写两个token才会调用API推送登录二维码。', + 'text': 'cookie失效时通知用户,用户使用/push_qr让插件推送二维码。使用第三方通知时填写对应Token/API' } } ] @@ -931,11 +850,10 @@ class DynamicWeChat(_PluginBase): "onlyonce": False, "forceUpdate": False, "use_cookiecloud": True, - "use_local_qr": False, # 默认关闭本地扫码 + "use_local_qr": False, "cookie_header": "", - "pushplus_token": "", - "helloimg_token": "", - "input_id_list": "", + "notification_token": "", + "input_id_list": "" } def get_page(self) -> List[dict]: @@ -1083,10 +1001,15 @@ class DynamicWeChat(_PluginBase): page = context.new_page() page.goto(self._wechatUrl) time.sleep(3) - if self.find_qrc(page): - if self._pushplus_token and self._helloimg_s_token: - img_src, refuse_time = self.upload_image(self._qr_code_image) - self.send_pushplus_message(refuse_time, f"企业微信登录二维码
") + image_src, refuse_time = self.find_qrc(page) + if image_src: + if self._my_send: + # logger.info(f"远程推送任务: {image_src}") + result = self._my_send.send("企业微信登录二维码", content=None, image=image_src, force_send=False) + if result: + logger.info(f"远程推送任务: 二维码发送失败,原因:{result}") + browser.close() + return logger.info("远程推送任务: 二维码已经发送,等待用户 90 秒内扫码登录") # logger.info("远程推送任务: 如收到短信验证码请以?结束,发送到<企业微信应用> 如: 110301?") time.sleep(90) @@ -1096,9 +1019,10 @@ class DynamicWeChat(_PluginBase): # logger.info("远程推送任务: 没有可用的CookieCloud服务器,只修改可信IP") self.click_app_management_buttons(page) else: - logger.warning("远程推送任务: 未配置pushplus_token和helloimg_s_token") + logger.warning("远程推送任务: 任何通知方式") else: logger.warning("远程推送任务: 未找到二维码") + browser.close() except Exception as e: logger.error(f"远程推送任务: 推送二维码失败: {e}") @@ -1108,7 +1032,7 @@ class DynamicWeChat(_PluginBase): { "cmd": "/push_qr", "event": EventType.PluginAction, - "desc": "立即推送登录二维码到pushplus", + "desc": "立即推送登录二维码到", "category": "", "data": { "action": "push_qrcode" diff --git a/plugins/dynamicwechat/notify_helper.py b/plugins/dynamicwechat/notify_helper.py new file mode 100644 index 0000000..92c2e35 --- /dev/null +++ b/plugins/dynamicwechat/notify_helper.py @@ -0,0 +1,133 @@ +import re +import requests +from app.modules.wechat.wechat import WeChat + + +class MySender: + def __init__(self, token=None): + if not token: # 如果 token 为空 + self.token = None + self.channel = None + self.init_success = False # 标识初始化失败 + else: + self.token = token + self.channel = self.send_channel() # 初始化时确定发送渠道 + self.first_text_sent = False # 记录是否已经发送过纯文本消息 + self.init_success = True # 标识初始化成功 + + def send_channel(self): + if self.token: + if self.token == "WeChat": + return "WeChat" + + letters_only = ''.join(re.findall(r'[A-Za-z]', self.token)) + # 判断其他推送渠道 + if self.token.startswith("SCT"): + return "ServerChan" + elif letters_only.isupper(): + return "AnPush" + else: + return "PushPlus" + return None + + # 标题,内容,图片,是否强制发送 + def send(self, title, content, image=None, force_send=False, diy_channel=None): + if not self.init_success: + return # 如果初始化失败,直接返回 + # 判断发送的内容类型 + contains_image = bool(image) # 是否包含图片 + + if not contains_image and not force_send: + if self.first_text_sent: + return + else: + self.first_text_sent = True + + try: + if not diy_channel: + channel = self.channel + else: + channel = diy_channel + + if channel == "WeChat": + return MySender.send_wechat(title, content, image) + elif channel == "ServerChan": + return self.send_serverchan(title, content, image) + elif channel == "AnPush": + return self.send_anpush(title, content, image) + elif channel == "PushPlus": + return self.send_pushplus(title, content, image) + else: + return "Unknown channel" + except Exception as e: + return f"Error occurred: {str(e)}" + + @staticmethod + def send_wechat(title, content, image): + wechat = WeChat() + if image: + send_status = wechat.send_msg(title='企业微信登录二维码', image=image, link=image) + else: + send_status = wechat.send_msg(title=title, text=content) + + if send_status is None: + return "微信通知发送错误" + return None + + def send_serverchan(self, title, content, image): + if self.token.startswith('sctp'): + match = re.match(r'sctp(\d+)t', self.token) + if match: + num = match.group(1) + url = f'https://{num}.push.ft07.com/send/{self.token}.send' + else: + raise ValueError('Invalid sendkey format for sctp') + else: + url = f'https://sctapi.ftqq.com/{self.token}.send' + + params = {'title': title, 'desp': f'![img]({image})' if image else content} + headers = {'Content-Type': 'application/json;charset=utf-8'} + response = requests.post(url, json=params, headers=headers) + result = response.json() + if result.get('code') != 0: + return f"Server酱通知错误: {result.get('message')}" + return None + + def send_anpush(self, title, content, image): + if ',' in self.token: + channel, token = self.token.split(',', 1) + else: + return + url = f"https://api.anpush.com/push/{token}" + payload = { + "title": title, + "content": f"" if image else content, + "channel": channel + } + headers = {"Content-Type": "application/x-www-form-urlencoded"} + response = requests.post(url, headers=headers, data=payload) + result = response.json() + # 判断返回的code和msgIds + if result.get('code') != 200: + return f"AnPush: {result.get('msg')}" + elif not result.get('data') or not result['data'].get('msgIds'): + return "AnPush 消息通道未找到" + return None + + def send_pushplus(self, title, content, image): + pushplus_url = f"http://www.pushplus.plus/send/{self.token}" + # PushPlus发送逻辑 + data = { + "title": title, + "content": f"企业微信登录二维码
" if image else content, + "template": "html" + } + response = requests.post(pushplus_url, json=data) + result = response.json() + if result.get('code') != 200: + return f"PushPlus send failed: {result.get('msg')}" + return None + + def reset_limit(self): + """解除限制,允许再次发送纯文本消息""" + self.first_text_sent = False diff --git a/plugins/dynamicwechat/update_help.py b/plugins/dynamicwechat/update_help.py index 9da818a..e3cd6f7 100644 --- a/plugins/dynamicwechat/update_help.py +++ b/plugins/dynamicwechat/update_help.py @@ -7,9 +7,6 @@ from typing import Dict, Any from Crypto import Random from Crypto.Cipher import AES -script_dir = os.path.dirname(os.path.abspath(__file__)) -settings_file = os.path.join(script_dir, 'settings.json') - def bytes_to_key(data: bytes, salt: bytes, output=48) -> bytes: # 兼容v2 将bytes_to_key和encrypt导入 @@ -98,7 +95,7 @@ class PyCookieCloud: return md5.hexdigest()[:16] @staticmethod - def load_cookie_lifetime(): # 返回时间戳 单位秒 + def load_cookie_lifetime(settings_file: str = None): # 返回时间戳 单位秒 if os.path.exists(settings_file): with open(settings_file, 'r') as file: settings = json.load(file) @@ -107,13 +104,18 @@ class PyCookieCloud: return 0 @staticmethod - def save_cookie_lifetime(cookie_lifetime): # 传入时间戳 单位秒 + def save_cookie_lifetime(settings_file, cookie_lifetime): # 传入时间戳 单位秒 with open(settings_file, 'w') as file: json.dump({'_cookie_lifetime': cookie_lifetime}, file) @staticmethod - def increase_cookie_lifetime(seconds: int): - current_lifetime = PyCookieCloud.load_cookie_lifetime() + def increase_cookie_lifetime(settings_file, seconds: int): + if os.path.exists(settings_file): + with open(settings_file, 'r') as file: + settings = json.load(file) + current_lifetime = settings.get('_cookie_lifetime', 0) + else: + current_lifetime = 0 new_lifetime = current_lifetime + seconds # 保存新的 _cookie_lifetime - PyCookieCloud.save_cookie_lifetime(new_lifetime) + PyCookieCloud.save_cookie_lifetime(settings_file, new_lifetime) From 90738219f156205c4f25c5a1d2305b963120e837 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 13 Nov 2024 08:41:06 +0800 Subject: [PATCH 21/27] =?UTF-8?q?IYUU=E6=B6=88=E6=81=AF=E6=8E=A8=E9=80=81?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=20=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 17 ++++- plugins/iyuumsg/__init__.py | 127 ++++++++++++++++++++++++------------ 2 files changed, 100 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 048729b..1fd28e0 100644 --- a/package.json +++ b/package.json @@ -891,7 +891,7 @@ "v1.0": "同步MoviePilot站点Cookie到CookieCloud" } }, - "BangumiColl": { + "BangumiColl": { "name": "Bangumi收藏订阅", "description": "Bangumi用户收藏添加到订阅", "labels": "订阅", @@ -905,5 +905,18 @@ "v1.5.1": "修复季度信息未传递的问题. 新增站点列表同步删除", "v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项" } - } + }, + "IyuuMsg": { + "name": "IYUU消息推送", + "description": "支持使用IYUU发送消息通知。", + "labels": "消息通知,IYUU", + "version": "1.3", + "icon": "Iyuu_A.png", + "author": "jxxghp", + "level": 1, + "v2": true, + "history": { + "v1.3": "消息限流发送,以缓解IYUU服务器压力" + } + } } diff --git a/plugins/iyuumsg/__init__.py b/plugins/iyuumsg/__init__.py index f492ccb..542bf4c 100644 --- a/plugins/iyuumsg/__init__.py +++ b/plugins/iyuumsg/__init__.py @@ -1,11 +1,14 @@ +import threading +from queue import Queue +from time import time, sleep +from typing import Any, List, Dict, Tuple from urllib.parse import urlencode -from app.plugins import _PluginBase from app.core.event import eventmanager, Event +from app.log import logger +from app.plugins import _PluginBase from app.schemas.types import EventType, NotificationType from app.utils.http import RequestUtils -from typing import Any, List, Dict, Tuple -from app.log import logger class IyuuMsg(_PluginBase): @@ -16,7 +19,7 @@ class IyuuMsg(_PluginBase): # 插件图标 plugin_icon = "Iyuu_A.png" # 插件版本 - plugin_version = "1.2" + plugin_version = "1.3" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -33,12 +36,30 @@ class IyuuMsg(_PluginBase): _token = None _msgtypes = [] + # 消息处理线程 + processing_thread = None + # 上次发送时间 + last_send_time = 0 + # 消息队列 + message_queue = Queue() + # 消息发送间隔(秒) + send_interval = 5 + # 退出事件 + __event = threading.Event() + def init_plugin(self, config: dict = None): + self.__event.clear() if config: self._enabled = config.get("enabled") self._token = config.get("token") self._msgtypes = config.get("msgtypes") or [] + if self._enabled and self._token: + # 启动处理队列的后台线程 + self.processing_thread = threading.Thread(target=self.process_queue) + self.processing_thread.daemon = True + self.processing_thread.start() + def get_state(self) -> bool: return self._enabled and (True if self._token else False) @@ -143,55 +164,77 @@ class IyuuMsg(_PluginBase): @eventmanager.register(EventType.NoticeMessage) def send(self, event: Event): """ - 消息发送事件 + 消息发送事件,将消息加入队列 """ - if not self.get_state(): - return - - if not event.event_data: + if not self.get_state() or not event.event_data: return msg_body = event.event_data - # 渠道 - channel = msg_body.get("channel") - if channel: - return - # 类型 - msg_type: NotificationType = msg_body.get("type") - # 标题 - title = msg_body.get("title") - # 文本 - text = msg_body.get("text") - - if not title and not text: + # 验证消息的有效性 + if not msg_body.get("title") and not msg_body.get("text"): logger.warn("标题和内容不能同时为空") return - if (msg_type and self._msgtypes - and msg_type.name not in self._msgtypes): - logger.info(f"消息类型 {msg_type.value} 未开启消息发送") - return + # 将消息加入队列 + self.message_queue.put(msg_body) + logger.info("消息已加入队列等待发送") - try: - sc_url = "https://iyuu.cn/%s.send?%s" % (self._token, urlencode({"text": title, "desp": text})) - res = RequestUtils().get_res(sc_url) - if res and res.status_code == 200: - ret_json = res.json() - errno = ret_json.get('errcode') - error = ret_json.get('errmsg') - if errno == 0: - logger.info("IYUU消息发送成功") + def process_queue(self): + """ + 处理队列中的消息,按间隔时间发送 + """ + while True: + if self.__event.is_set(): + logger.info("消息发送线程正在退出...") + break + # 获取队列中的下一条消息 + msg_body = self.message_queue.get() + + # 检查是否满足发送间隔时间 + current_time = time() + time_since_last_send = current_time - self.last_send_time + if time_since_last_send < self.send_interval: + sleep(self.send_interval - time_since_last_send) + + # 处理消息内容 + channel = msg_body.get("channel") + if channel: + continue + msg_type: NotificationType = msg_body.get("type") + title = msg_body.get("title") + text = msg_body.get("text") + + # 检查消息类型是否已启用 + if msg_type and self._msgtypes and msg_type.name not in self._msgtypes: + logger.info(f"消息类型 {msg_type.value} 未开启消息发送") + continue + + # 尝试发送消息 + try: + sc_url = "https://iyuu.cn/%s.send?%s" % (self._token, urlencode({"text": title, "desp": text})) + res = RequestUtils().get_res(sc_url) + if res and res.status_code == 200: + ret_json = res.json() + errno = ret_json.get('errcode') + error = ret_json.get('errmsg') + if errno == 0: + logger.info("IYUU消息发送成功") + # 更新上次发送时间 + self.last_send_time = time() + else: + logger.warn(f"IYUU消息发送失败,错误码:{errno},错误原因:{error}") + elif res is not None: + logger.warn(f"IYUU消息发送失败,错误码:{res.status_code},错误原因:{res.reason}") else: - logger.warn(f"IYUU消息发送失败,错误码:{errno},错误原因:{error}") - elif res is not None: - logger.warn(f"IYUU消息发送失败,错误码:{res.status_code},错误原因:{res.reason}") - else: - logger.warn("IYUU消息发送失败,未获取到返回信息") - except Exception as msg_e: - logger.error(f"IYUU消息发送失败,{str(msg_e)}") + logger.warn("IYUU消息发送失败,未获取到返回信息") + except Exception as msg_e: + logger.error(f"IYUU消息发送失败,{str(msg_e)}") + + # 标记任务完成 + self.message_queue.task_done() def stop_service(self): """ 退出插件 """ - pass + self.__event.set() From d53b3965f5f31433c7dc7325e01600a6f40b62e9 Mon Sep 17 00:00:00 2001 From: cikezhu <604054726@qq.com> Date: Wed, 13 Nov 2024 15:34:35 +0800 Subject: [PATCH 22/27] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dv2=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=AF=BB=E5=8F=96=E6=95=B0=E6=8D=AE=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- plugins/episodegroupmeta/__init__.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1fd28e0..3e35740 100644 --- a/package.json +++ b/package.json @@ -527,12 +527,13 @@ "name": "TMDB剧集组刮削", "description": "从TMDB剧集组刮削季集的实际顺序。", "labels": "刮削", - "version": "2.1", + "version": "2.2", "icon": "Element_A.png", "author": "叮叮当", "level": 1, "v2": true, "history": { + "v2.2": "修复v2版本无法读取数据的问题", "v2.1": "增加发送通知提醒选择剧集组", "v2.0": "增加手动选择剧集组的功能" } diff --git a/plugins/episodegroupmeta/__init__.py b/plugins/episodegroupmeta/__init__.py index 371ad1c..293db25 100644 --- a/plugins/episodegroupmeta/__init__.py +++ b/plugins/episodegroupmeta/__init__.py @@ -23,7 +23,6 @@ from app.schemas.types import EventType from app.utils.common import retry from app.utils.http import RequestUtils from app.db.models import PluginData -from app.utils.object import ObjectUtils class ExistMediaInfo(BaseModel): # 类型 电影、电视剧 @@ -48,7 +47,7 @@ class EpisodeGroupMeta(_PluginBase): # 主题色 plugin_color = "#098663" # 插件版本 - plugin_version = "2.1" + plugin_version = "2.2" # 插件作者 plugin_author = "叮叮当" # 作者主页 @@ -360,6 +359,13 @@ class EpisodeGroupMeta(_PluginBase): "delay": 120 } + def is_objstr(self, obj: Any): + if not isinstance(obj, str): + return False + return str(obj).startswith("{") \ + or str(obj).startswith("[") \ + or str(obj).startswith("(") + def get_page(self) -> List[dict]: """ 拼装插件详情页面,需要返回页面配置,同时附带数据 @@ -372,7 +378,7 @@ class EpisodeGroupMeta(_PluginBase): try: tmdb_id = plugin_data.key # fix v1版本数据读取问题 - if ObjectUtils.is_obj(plugin_data.value): + if self.is_objstr(plugin_data.value): data = json.loads(plugin_data.value) else: data = plugin_data.value From ee030e1e46830eee1b84681db49c6409e4ed487e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AE=E5=8F=AE=E5=BD=93?= <604054726@qq.com> Date: Thu, 14 Nov 2024 20:00:51 +0800 Subject: [PATCH 23/27] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dv2=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=AF=BB=E5=8F=96=E5=AA=92=E4=BD=93=E5=BA=93?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- plugins/episodegroupmeta/__init__.py | 235 +++++++++++++++++++-------- 2 files changed, 169 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 3e35740..f73f643 100644 --- a/package.json +++ b/package.json @@ -527,12 +527,14 @@ "name": "TMDB剧集组刮削", "description": "从TMDB剧集组刮削季集的实际顺序。", "labels": "刮削", - "version": "2.2", + "version": "2.5", "icon": "Element_A.png", "author": "叮叮当", "level": 1, "v2": true, "history": { + "v2.5": "修复当媒体服务器中剧集的季不完整时会中断的问题", + "v2.3": "修复v2版本无法读取媒体库的问题", "v2.2": "修复v2版本无法读取数据的问题", "v2.1": "增加发送通知提醒选择剧集组", "v2.0": "增加手动选择剧集组的功能" diff --git a/plugins/episodegroupmeta/__init__.py b/plugins/episodegroupmeta/__init__.py index 293db25..e2f919b 100644 --- a/plugins/episodegroupmeta/__init__.py +++ b/plugins/episodegroupmeta/__init__.py @@ -2,6 +2,8 @@ import base64 import json import threading import time +import importlib.util +import sys from pathlib import Path from typing import Any, List, Dict, Tuple, Optional, Union @@ -31,7 +33,9 @@ class ExistMediaInfo(BaseModel): groupep: Optional[Dict[int, list]] = {} # 集在媒体服务器的ID groupid: Optional[Dict[int, List[list]]] = {} - # 媒体服务器 + # 媒体服务器类型 + server_type: Optional[str] = None + # 媒体服务器名字 server: Optional[str] = None # 媒体ID itemid: Optional[Union[str, int]] = None @@ -47,7 +51,7 @@ class EpisodeGroupMeta(_PluginBase): # 主题色 plugin_color = "#098663" # 插件版本 - plugin_version = "2.2" + plugin_version = "2.5" # 插件作者 plugin_author = "叮叮当" # 作者主页 @@ -63,11 +67,11 @@ class EpisodeGroupMeta(_PluginBase): _event = threading.Event() # 私有属性 - mschain = None tv = None emby = None plex = None jellyfin = None + mediaserver_helper = None _enabled = False _notify = True @@ -77,11 +81,7 @@ class EpisodeGroupMeta(_PluginBase): _allowlist = [] def init_plugin(self, config: dict = None): - self.mschain = MediaServerChain() self.tv = TV() - self.emby = Emby() - self.plex = Plex() - self.jellyfin = Jellyfin() if config: self._enabled = config.get("enabled") self._notify = config.get("notify") @@ -240,7 +240,7 @@ class EpisodeGroupMeta(_PluginBase): 'component': 'VCheckboxBtn', 'props': { 'model': 'ignorelock', - 'label': '无视锁定的媒体', + 'label': '锁定的剧集也刮削', } } ] @@ -658,21 +658,51 @@ class EpisodeGroupMeta(_PluginBase): except Exception as e: self.log_error(f"{mediainfo.title} {str(e)}") return False - # 获取可用的媒体服务器 - _existsinfo = self.chain.media_exists(mediainfo=mediainfo) - if not _existsinfo: - self.log_warn(f"{mediainfo.title_year} 无可用的媒体服务器") - return False - # 存在媒体服务器 - existsinfo: ExistMediaInfo = self.__media_exists(server=_existsinfo.server, mediainfo=mediainfo, - existsinfo=_existsinfo) - if not existsinfo or not existsinfo.itemid: - self.log_warn(f"{mediainfo.title_year} 在媒体库中不存在") - return False - # 新增需要的属性 - existsinfo.server = _existsinfo.server - existsinfo.type = _existsinfo.type - self.log_info(f"{mediainfo.title_year} 存在于媒体服务器: {_existsinfo.server}") + # 获取全部可用的媒体服务器, 兼容v2 + service_infos = self.service_infos() + if self.mediaserver_helper == None: + # v1版本 单一媒体服务器的方式 + _existsinfo = self.chain.media_exists(mediainfo=mediainfo) + if not _existsinfo: + self.log_warn(f"{mediainfo.title_year} 无可用的媒体服务器") + return False + # 存在媒体服务器 + existsinfo: ExistMediaInfo = self.__media_exists(mediainfo=mediainfo, existsinfo=_existsinfo) + if not existsinfo or not existsinfo.itemid: + self.log_warn(f"{mediainfo.title_year} 在媒体库中不存在") + return False + return self.__start_rt_mediaserver(mediainfo=mediainfo, existsinfo=existsinfo, episode_groups=episode_groups, group_id=group_id) + else: + # v2版本 遍历所有媒体服务器的方式 + if not service_infos: + self.log_warn(f"{mediainfo.title_year} 无可用的媒体服务器") + return False + # 遍历媒体服务器 + relust_bool = False + for name, info in service_infos.items(): + self.log_info(f"正在查询媒体服务器: ({info.type}){name}") + _existsinfo = self.chain.media_exists(mediainfo=mediainfo, server=name) + if not _existsinfo: + self.log_warn(f"{mediainfo.title_year} 在 ({info.type}){name} 媒体服务器中不存在") + continue + existsinfo: ExistMediaInfo = self.__media_exists(mediainfo=mediainfo, existsinfo=_existsinfo, mediaserver_instance=info.instance) + if not existsinfo or not existsinfo.itemid: + self.log_warn(f"{mediainfo.title_year} 在 ({info.type}){name} 媒体服务器中不存在") + continue + _bool = self.__start_rt_mediaserver(mediainfo=mediainfo, existsinfo=existsinfo, episode_groups=episode_groups, group_id=group_id, mediaserver_instance=info.instance) + relust_bool = relust_bool or _bool + return relust_bool + + def __start_rt_mediaserver(self, + mediainfo: schemas.MediaInfo, + existsinfo: ExistMediaInfo, + episode_groups: Any | None, + group_id: str = None, + mediaserver_instance: Any = None) -> bool: + """ + 遍历媒体服务器剧集信息,并匹配合适的剧集组刷新季集信息 + """ + self.log_info(f"{mediainfo.title_year} 存在于 {existsinfo.server_type} 媒体服务器: {existsinfo.server}") # 获取全部剧集组信息 copy_keys = ['Id', 'Name', 'ChannelNumber', 'OriginalTitle', 'ForcedSortName', 'SortName', 'CommunityRating', 'CriticRating', 'IndexNumber', 'ParentIndexNumber', 'SortParentIndexNumber', 'SortIndexNumber', @@ -716,21 +746,24 @@ class EpisodeGroupMeta(_PluginBase): continue self.log_info(f"已匹配剧集组: {name}, {id}, 第 {order} 季") # 遍历全部媒体项并更新 + if existsinfo.groupid.get(order) is None: + self.log_info(f"媒体库中不存在: {mediainfo.title_year}, 第 {order} 季") + continue for _index, _ids in enumerate(existsinfo.groupid.get(order)): # 提取出媒体库中集id对应的集数index ep_num = ep[_index] for _id in _ids: # 获取媒体服务器媒体项 - iteminfo = self.get_iteminfo(server=existsinfo.server, itemid=_id) + iteminfo = self.get_iteminfo(server_type=existsinfo.server_type, itemid=_id, mediaserver_instance=mediaserver_instance) if not iteminfo: self.log_info(f"未找到媒体项 - itemid: {_id}, 第 {order} 季, 第 {ep_num} 集") continue - # 是否无视项目锁定, 指定剧集组id时也属于无视项目锁定 - if not self._ignorelock and not group_id: + # 锁定的剧集是否也刮削? + if not self._ignorelock: if iteminfo.get("LockData") or ( "Name" in iteminfo.get("LockedFields", []) and "Overview" in iteminfo.get("LockedFields", [])): - self.log_warn(f"已锁定媒体项 - itemid: {_id}, 第 {order} 季, 第 {ep_num} 集") + self.log_warn(f"已锁定媒体项 - itemid: {_id}, 第 {order} 季, 第 {ep_num} 集, 如果需要刮削请打开设置中的“锁定的剧集也刮削”选项") continue # 替换项目数据 episode = episodes[ep_num - 1] @@ -748,11 +781,12 @@ class EpisodeGroupMeta(_PluginBase): self.__append_to_list(new_dict["LockedFields"], "Name") self.__append_to_list(new_dict["LockedFields"], "Overview") # 更新数据 - self.set_iteminfo(server=existsinfo.server, itemid=_id, iteminfo=new_dict) + self.set_iteminfo(server_type=existsinfo.server_type, itemid=_id, iteminfo=new_dict, mediaserver_instance=mediaserver_instance) # still_path 图片 if episode.get("still_path"): - self.set_item_image(server=existsinfo.server, itemid=_id, - imageurl=f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episode['still_path']}") + self.set_item_image(server_type=existsinfo.server_type, itemid=_id, + imageurl=f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episode['still_path']}", + mediaserver_instance=mediaserver_instance) self.log_info(f"已修改剧集 - itemid: {_id}, 第 {order} 季, 第 {ep_num} 集") # 移除已经处理成功的季 existsinfo.groupep.pop(order, 0) @@ -770,19 +804,21 @@ class EpisodeGroupMeta(_PluginBase): if item not in list: list.append(item) - def __media_exists(self, server: str, mediainfo: schemas.MediaInfo, - existsinfo: schemas.ExistMediaInfo) -> ExistMediaInfo: + def __media_exists(self, mediainfo: schemas.MediaInfo, existsinfo: schemas.ExistMediaInfo, mediaserver_instance: Any = None) -> ExistMediaInfo: """ 根据媒体信息,返回剧集列表与剧集ID列表 :param mediainfo: 媒体信息 :return: 剧集列表与剧集ID列表 """ + # fix v1版本对比v2版本, 属性含义发生变化, 代码做兼容处理 + server_type = existsinfo.server_type if hasattr(existsinfo, 'server_type') else existsinfo.server def __emby_media_exists(): # 获取系列id item_id = None try: - res = self.emby.get_data(("[HOST]emby/Items?" + instance = mediaserver_instance or self.emby + res = instance.get_data(("[HOST]emby/Items?" "IncludeItemTypes=Series" "&Fields=ProductionYear" "&StartIndex=0" @@ -798,18 +834,18 @@ class EpisodeGroupMeta(_PluginBase): not mediainfo.year or str(res_item.get('ProductionYear')) == str(mediainfo.year)): item_id = res_item.get('Id') except Exception as e: - self.log_error(f"连接Items出错:" + str(e)) + self.log_error(f"媒体服务器 ({server_type}){existsinfo.server} 发生了错误, 连接Items出错:" + str(e)) if not item_id: return None # 验证tmdbid是否相同 - item_info = self.emby.get_iteminfo(item_id) + item_info = instance.get_iteminfo(item_id) if item_info: if mediainfo.tmdb_id and item_info.tmdbid: if str(mediainfo.tmdb_id) != str(item_info.tmdbid): self.log_error(f"tmdbid不匹配或不存在") return None try: - res_json = self.emby.get_data( + res_json = instance.get_data( "[HOST]emby/Shows/%s/Episodes?Season=&IsMissing=false&api_key=[APIKEY]" % item_id) if res_json: tv_item = res_json.json() @@ -837,17 +873,21 @@ class EpisodeGroupMeta(_PluginBase): return ExistMediaInfo( itemid=item_id, groupep=group_ep, - groupid=group_id + groupid=group_id, + type=existsinfo.type, + server_type=server_type, + server=existsinfo.server, ) except Exception as e: - self.log_error(f"连接Shows/Id/Episodes出错:{str(e)}") + self.log_error(f"媒体服务器 ({server_type}){existsinfo.server} 发生了错误, 连接Shows/Id/Episodes出错:{str(e)}") return None def __jellyfin_media_exists(): # 获取系列id item_id = None try: - res = self.jellyfin.get_data(url=f"[HOST]Users/[USER]/Items?api_key=[APIKEY]" + instance = mediaserver_instance or self.jellyfin + res = instance.get_data(url=f"[HOST]Users/[USER]/Items?api_key=[APIKEY]" f"&searchTerm={mediainfo.title}" f"&IncludeItemTypes=Series" f"&Limit=10&Recursive=true") @@ -858,18 +898,18 @@ class EpisodeGroupMeta(_PluginBase): not mediainfo.year or str(res_item.get('ProductionYear')) == str(mediainfo.year)): item_id = res_item.get('Id') except Exception as e: - self.log_error(f"连接Items出错:" + str(e)) + self.log_error(f"媒体服务器 ({server_type}){existsinfo.server} 发生了错误, 连接Items出错:" + str(e)) if not item_id: return None # 验证tmdbid是否相同 - item_info = self.jellyfin.get_iteminfo(item_id) + item_info = instance.get_iteminfo(item_id) if item_info: if mediainfo.tmdb_id and item_info.tmdbid: if str(mediainfo.tmdb_id) != str(item_info.tmdbid): self.log_error(f"tmdbid不匹配或不存在") return None try: - res_json = self.jellyfin.get_data( + res_json = instance.get_data( "[HOST]emby/Shows/%s/Episodes?Season=&IsMissing=false&api_key=[APIKEY]" % item_id) if res_json: tv_item = res_json.json() @@ -897,15 +937,19 @@ class EpisodeGroupMeta(_PluginBase): return ExistMediaInfo( itemid=item_id, groupep=group_ep, - groupid=group_id + groupid=group_id, + type=existsinfo.type, + server_type=server_type, + server=existsinfo.server, ) except Exception as e: - self.log_error(f"连接Shows/Id/Episodes出错:{str(e)}") + self.log_error(f"媒体服务器 ({server_type}){existsinfo.server} 发生了错误, 连接Shows/Id/Episodes出错:{str(e)}") return None def __plex_media_exists(): try: - _plex = self.plex.get_plex() + instance = mediaserver_instance or self.plex.get_plex() + _plex = instance.get_plex() if not _plex: return None if existsinfo.itemid: @@ -957,10 +1001,13 @@ class EpisodeGroupMeta(_PluginBase): return ExistMediaInfo( itemid=videos.key, groupep=group_ep, - groupid=group_id + groupid=group_id, + type=existsinfo.type, + server_type=server_type, + server=existsinfo.server, ) except Exception as e: - self.log_error(f"连接Shows/Id/Episodes出错:{str(e)}") + self.log_error(f"媒体服务器 ({server_type}){existsinfo.server} 发生了错误, 连接Shows/Id/Episodes出错:{str(e)}") return None def __get_ids(guids: List[Any]) -> dict: @@ -986,14 +1033,14 @@ class EpisodeGroupMeta(_PluginBase): break return ids - if server == "emby": + if server_type == "emby": return __emby_media_exists() - elif server == "jellyfin": + elif server_type == "jellyfin": return __jellyfin_media_exists() else: return __plex_media_exists() - def get_iteminfo(self, server: str, itemid: str) -> dict: + def get_iteminfo(self, server_type: str, itemid: str, mediaserver_instance: Any = None) -> dict: """ 获得媒体项详情 """ @@ -1003,9 +1050,10 @@ class EpisodeGroupMeta(_PluginBase): 获得Emby媒体项详情 """ try: + instance = mediaserver_instance or self.emby url = f'[HOST]emby/Users/[USER]/Items/{itemid}?' \ f'Fields=ChannelMappingInfo&api_key=[APIKEY]' - res = self.emby.get_data(url=url) + res = instance.get_data(url=url) if res: return res.json() except Exception as err: @@ -1017,8 +1065,9 @@ class EpisodeGroupMeta(_PluginBase): 获得Jellyfin媒体项详情 """ try: + instance = mediaserver_instance or self.jellyfin url = f'[HOST]Users/[USER]/Items/{itemid}?Fields=ChannelMappingInfo&api_key=[APIKEY]' - res = self.jellyfin.get_data(url=url) + res = instance.jellyfin.get_data(url=url) if res: result = res.json() if result: @@ -1034,7 +1083,8 @@ class EpisodeGroupMeta(_PluginBase): """ iteminfo = {} try: - plexitem = self.plex.get_plex().library.fetchItem(ekey=itemid) + instance = mediaserver_instance or self.plex + plexitem = instance.get_plex().library.fetchItem(ekey=itemid) if 'movie' in plexitem.METADATA_TYPE: iteminfo['Type'] = 'Movie' iteminfo['IsFolder'] = False @@ -1063,27 +1113,27 @@ class EpisodeGroupMeta(_PluginBase): if plexitem.title.locked: iteminfo['LockedFields'].append('Name') except Exception as err: - logger.warn(f"获取Plex媒体项详情失败:{str(err)}") + self.log_warn(f"获取Plex媒体项详情失败:{str(err)}") pass try: if plexitem.summary.locked: iteminfo['LockedFields'].append('Overview') except Exception as err: - logger.warn(f"获取Plex媒体项详情失败:{str(err)}") + self.log_warn(f"获取Plex媒体项详情失败:{str(err)}") pass return iteminfo except Exception as err: self.log_error(f"获取Plex媒体项详情失败:{str(err)}") return {} - if server == "emby": + if server_type == "emby": return __get_emby_iteminfo() - elif server == "jellyfin": + elif server_type == "jellyfin": return __get_jellyfin_iteminfo() else: return __get_plex_iteminfo() - def set_iteminfo(self, server: str, itemid: str, iteminfo: dict): + def set_iteminfo(self, server_type: str, itemid: str, iteminfo: dict, mediaserver_instance: Any = None): """ 更新媒体项详情 """ @@ -1093,7 +1143,8 @@ class EpisodeGroupMeta(_PluginBase): 更新Emby媒体项详情 """ try: - res = self.emby.post_data( + instance = mediaserver_instance or self.emby + res = instance.post_data( url=f'[HOST]emby/Items/{itemid}?api_key=[APIKEY]&reqformat=json', data=json.dumps(iteminfo), headers={ @@ -1114,7 +1165,8 @@ class EpisodeGroupMeta(_PluginBase): 更新Jellyfin媒体项详情 """ try: - res = self.jellyfin.post_data( + instance = mediaserver_instance or self.jellyfin + res = instance.post_data( url=f'[HOST]Items/{itemid}?api_key=[APIKEY]', data=json.dumps(iteminfo), headers={ @@ -1135,7 +1187,8 @@ class EpisodeGroupMeta(_PluginBase): 更新Plex媒体项详情 """ try: - plexitem = self.plex.get_plex().library.fetchItem(ekey=itemid) + instance = mediaserver_instance or self.plex + plexitem = instance.get_plex().library.fetchItem(ekey=itemid) if 'CommunityRating' in iteminfo and iteminfo['CommunityRating']: edits = { 'audienceRating.value': iteminfo['CommunityRating'], @@ -1148,15 +1201,15 @@ class EpisodeGroupMeta(_PluginBase): self.log_error(f"更新Plex媒体项详情失败:{str(err)}") return False - if server == "emby": + if server_type == "emby": return __set_emby_iteminfo() - elif server == "jellyfin": + elif server_type == "jellyfin": return __set_jellyfin_iteminfo() else: return __set_plex_iteminfo() @retry(RequestException, logger=logger) - def set_item_image(self, server: str, itemid: str, imageurl: str): + def set_item_image(self, server_type: str, itemid: str, imageurl: str, mediaserver_instance: Any = None): """ 更新媒体项图片 """ @@ -1185,8 +1238,9 @@ class EpisodeGroupMeta(_PluginBase): 更新Emby媒体项图片 """ try: + instance = mediaserver_instance or self.emby url = f'[HOST]emby/Items/{itemid}/Images/Primary?api_key=[APIKEY]' - res = self.emby.post_data( + res = instance.post_data( url=url, data=_base64, headers={ @@ -1208,9 +1262,10 @@ class EpisodeGroupMeta(_PluginBase): # FIXME 改为预下载图片 """ try: + instance = mediaserver_instance or self.jellyfin url = f'[HOST]Items/{itemid}/RemoteImages/Download?' \ f'Type=Primary&ImageUrl={imageurl}&ProviderName=TheMovieDb&api_key=[APIKEY]' - res = self.jellyfin.post_data(url=url) + res = instance.post_data(url=url) if res and res.status_code in [200, 204]: return True else: @@ -1226,24 +1281,66 @@ class EpisodeGroupMeta(_PluginBase): # FIXME 改为预下载图片 """ try: - plexitem = self.plex.get_plex().library.fetchItem(ekey=itemid) + instance = mediaserver_instance or self.plex + plexitem = instance.get_plex().library.fetchItem(ekey=itemid) plexitem.uploadPoster(url=imageurl) return True except Exception as err: self.log_error(f"更新Plex媒体项图片失败:{err}") return False - if server == "emby": + if server_type == "emby": # 下载图片获取base64 image_base64 = __download_image() if image_base64: return __set_emby_item_image(image_base64) - elif server == "jellyfin": + elif server_type == "jellyfin": return __set_jellyfin_item_image() else: return __set_plex_item_image() return None + def service_infos(self, type_filter: Optional[str] = None): + """ + 服务信息 + """ + if self.mediaserver_helper == None: + # 动态载入媒体服务器帮助类 + module_name = "app.helper.mediaserver" + spec = importlib.util.find_spec(module_name) + if spec is not None: + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + if hasattr(module, 'MediaServerHelper'): + self.log_info(f"v2版本初始化媒体库类") + self.mediaserver_helper = module.MediaServerHelper() + if self.mediaserver_helper == None: + if self.emby == None: + self.log_info(f"v1版本初始化媒体库类") + self.emby = Emby() + self.plex = Plex() + self.jellyfin = Jellyfin() + return None + + services = self.mediaserver_helper.get_services(type_filter=type_filter)#, name_filters=self._mediaservers) + if not services: + self.log_warn("获取媒体服务器实例失败,请检查配置") + return None + + active_services = {} + for service_name, service_info in services.items(): + if service_info.instance.is_inactive(): + self.log_warn(f"媒体服务器 {service_name} 未连接,请检查配置") + else: + active_services[service_name] = service_info + + if not active_services: + self.log_warn("没有已连接的媒体服务器,请检查配置") + return None + + return active_services + def log_error(self, ss: str): logger.error(f"<{self.plugin_name}> {str(ss)}") From 765008fc692338928791ade8a8ab900076f83204 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 14 Nov 2024 22:43:38 +0800 Subject: [PATCH 24/27] fix #552 --- package.v2.json | 4 ++-- plugins.v2/torrentremover/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.v2.json b/package.v2.json index 3a90b35..353b7a4 100644 --- a/package.v2.json +++ b/package.v2.json @@ -182,12 +182,12 @@ "name": "自动删种", "description": "自动删除下载器中的下载任务。", "labels": "做种", - "version": "2.1", + "version": "2.1.1", "icon": "delete.jpg", "author": "jxxghp", "level": 2, "history": { - "v2.1": "修复兼容MoviePilot V2 版本", + "v2.1.1": "修复兼容MoviePilot V2 版本", "v2.0": "兼容MoviePilot V2 版本" } }, diff --git a/plugins.v2/torrentremover/__init__.py b/plugins.v2/torrentremover/__init__.py index 6aa67ee..f3ab683 100644 --- a/plugins.v2/torrentremover/__init__.py +++ b/plugins.v2/torrentremover/__init__.py @@ -26,7 +26,7 @@ class TorrentRemover(_PluginBase): # 插件图标 plugin_icon = "delete.jpg" # 插件版本 - plugin_version = "2.1" + plugin_version = "2.1.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -814,7 +814,7 @@ class TorrentRemover(_PluginBase): name = remove_torrent.get("name") size = remove_torrent.get("size") for torrent in torrents: - if downloader == "qbittorrent": + if downloader_config.type == "qbittorrent": plus_id = torrent.hash plus_name = torrent.name plus_size = torrent.size From 1127d88f3b7cc4cb18cc6f2a7a983d15800c7e21 Mon Sep 17 00:00:00 2001 From: ramen <1205925392@qq.com> Date: Fri, 15 Nov 2024 00:33:16 +0800 Subject: [PATCH 25/27] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dv2=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E9=80=9A=E7=9F=A5=EF=BC=8C=E5=8F=AF=E4=BB=A5=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=E5=BE=AE=E4=BF=A1=E9=80=9A=E7=9F=A5ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- plugins/dynamicwechat/__init__.py | 45 +++++++++------- plugins/dynamicwechat/notify_helper.py | 75 +++++++++++++++----------- 3 files changed, 75 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index f73f643..764f2bb 100644 --- a/package.json +++ b/package.json @@ -861,14 +861,15 @@ }, "DynamicWeChat": { "name": "动态企微可信IP", - "description": "修改企微应用可信IP,详细说明查看'作者主页',支持第三方通知。验证码以?结尾发送到企业微信应用", + "description": "修改企微应用可信IP,支持Srever酱等第三方通知。验证码以?结尾发送到企业微信应用", "labels": "消息通知", - "version": "1.5.0", + "version": "1.5.1", "icon": "Wecom_A.png", "author": "RamenRa", "level": 2, "v2": true, "history": { + "v1.5.1": "修复v2微信通知,可以指定微信通知ID", "v1.5.0": "支持企微应用通知和第Serve酱等第三方推送。按要求修改插件名称", "v1.4.1": "完善面板说明", "v1.4.0": "修复强制更改IP时配置面板延时过长的问题。庆祝v2进入正式版,显示了一个没用的参数", diff --git a/plugins/dynamicwechat/__init__.py b/plugins/dynamicwechat/__init__.py index 0b1942a..aa16cfc 100644 --- a/plugins/dynamicwechat/__init__.py +++ b/plugins/dynamicwechat/__init__.py @@ -20,7 +20,7 @@ from app.log import logger from app.plugins import _PluginBase from app.plugins.dynamicwechat.update_help import PyCookieCloud from app.plugins.dynamicwechat.notify_helper import MySender -from app.schemas.types import EventType, NotificationType +from app.schemas.types import EventType class DynamicWeChat(_PluginBase): @@ -31,7 +31,7 @@ class DynamicWeChat(_PluginBase): # 插件图标 plugin_icon = "Wecom_A.png" # 插件版本 - plugin_version = "1.5.0" + plugin_version = "1.5.1" # 插件作者 plugin_author = "RamenRa" # 作者主页 @@ -42,6 +42,8 @@ class DynamicWeChat(_PluginBase): plugin_order = 47 # 可使用的用户级别 auth_level = 2 + # 检测间隔时间,默认10分钟 + _refresh_cron = '*/20 * * * *' # ------------------------------------------私有属性------------------------------------------ _enabled = False # 开关 @@ -70,8 +72,7 @@ class DynamicWeChat(_PluginBase): _current_ip_address = '0.0.0.0' # 企业微信登录 _wechatUrl = 'https://work.weixin.qq.com/wework_admin/loginpage_wx?from=myhome' - # 检测间隔时间,默认10分钟 - _refresh_cron = '*/20 * * * *' + # 输入的企业应用id _input_id_list = '' # 二维码 @@ -99,6 +100,10 @@ class DynamicWeChat(_PluginBase): # 定时器 _scheduler: Optional[BackgroundScheduler] = None + if hasattr(settings, 'VERSION_FLAG'): + version = settings.VERSION_FLAG # V2 + else: + version = "v1" def init_plugin(self, config: dict = None): # 清空配置 self._notification_token = '' @@ -123,7 +128,10 @@ class DynamicWeChat(_PluginBase): self._use_cookiecloud = config.get("use_cookiecloud") self._cookie_header = config.get("cookie_header") self._ip_changed = config.get("ip_changed") - self._my_send = MySender(self._notification_token) + if self.version != "v1": + self._my_send = MySender(self._notification_token, func=self.post_message) + else: + self._my_send = MySender(self._notification_token) if not self._my_send.init_success: # 没有输入通知方式,不通知 self._my_send = None # 停止现有任务 @@ -258,12 +266,14 @@ class DynamicWeChat(_PluginBase): if not event_data or event_data.get("action") != "dynamicwechat": return - logger.info("开始检测公网IP") - if self.CheckIP(): - self.ChangeIP() - self.__update_config() - - logger.info("----------------------本次任务结束----------------------") + if self._cookie_valid: + logger.info("开始检测公网IP") + if self.CheckIP(): + self.ChangeIP() + self.__update_config() + logger.info("----------------------本次任务结束----------------------") + else: + logger.warning("cookie已失效请及时更新,不检测公网IP") def CheckIP(self): retry_urls = random.sample(self._ip_urls, len(self._ip_urls)) @@ -381,7 +391,7 @@ class DynamicWeChat(_PluginBase): img_src, refuse_time = self.find_qrc(page) if img_src: if self._my_send: - result = self._my_send.send("企业微信登录二维码", content=None, image=img_src, force_send=False) + result = self._my_send.send(title="企业微信登录二维码", image=img_src) if result: logger.info(f"二维码发送失败,原因:{result}") browser.close() @@ -815,7 +825,7 @@ class DynamicWeChat(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '建议启用内建或自定义CookieCloud。支持微信、Server酱、PushPlus、AnPush,具体请查看作者主页' + 'text': '建议启用内建或自定义CookieCloud。支持微信、Server酱等第三方通知,具体请查看作者主页' } } ] @@ -835,7 +845,7 @@ class DynamicWeChat(_PluginBase): 'component': 'VAlert', 'props': { 'type': 'info', - 'text': 'cookie失效时通知用户,用户使用/push_qr让插件推送二维码。使用第三方通知时填写对应Token/API' + 'text': 'Cookie失效时通知用户,用户使用/push_qr让插件推送二维码。使用第三方通知时填写对应Token/API' } } ] @@ -1004,8 +1014,7 @@ class DynamicWeChat(_PluginBase): image_src, refuse_time = self.find_qrc(page) if image_src: if self._my_send: - # logger.info(f"远程推送任务: {image_src}") - result = self._my_send.send("企业微信登录二维码", content=None, image=image_src, force_send=False) + result = self._my_send.send("企业微信登录二维码", image=image_src) if result: logger.info(f"远程推送任务: 二维码发送失败,原因:{result}") browser.close() @@ -1019,7 +1028,7 @@ class DynamicWeChat(_PluginBase): # logger.info("远程推送任务: 没有可用的CookieCloud服务器,只修改可信IP") self.click_app_management_buttons(page) else: - logger.warning("远程推送任务: 任何通知方式") + logger.warning("远程推送任务: 没有找到可用的通知方式") else: logger.warning("远程推送任务: 未找到二维码") browser.close() @@ -1032,7 +1041,7 @@ class DynamicWeChat(_PluginBase): { "cmd": "/push_qr", "event": EventType.PluginAction, - "desc": "立即推送登录二维码到", + "desc": "立即推送登录二维码", "category": "", "data": { "action": "push_qrcode" diff --git a/plugins/dynamicwechat/notify_helper.py b/plugins/dynamicwechat/notify_helper.py index 92c2e35..6c70a5d 100644 --- a/plugins/dynamicwechat/notify_helper.py +++ b/plugins/dynamicwechat/notify_helper.py @@ -1,48 +1,44 @@ import re import requests -from app.modules.wechat.wechat import WeChat +from app.modules.wechat import WeChat +from app.schemas.types import NotificationType class MySender: - def __init__(self, token=None): - if not token: # 如果 token 为空 - self.token = None - self.channel = None - self.init_success = False # 标识初始化失败 - else: - self.token = token - self.channel = self.send_channel() # 初始化时确定发送渠道 - self.first_text_sent = False # 记录是否已经发送过纯文本消息 - self.init_success = True # 标识初始化成功 + def __init__(self, token=None, func=None): + self.token = token + self.channel = self.send_channel() if token else None # 初始化时确定发送渠道 + self.first_text_sent = False # 记录是否已发送过纯文本消息 + self.init_success = bool(token) # 标识初始化成功 + self.post_message_func = func # V2微信模式的 post_message 方法 def send_channel(self): - if self.token: - if self.token == "WeChat": - return "WeChat" + if "WeChat" in self.token: + return "WeChat" - letters_only = ''.join(re.findall(r'[A-Za-z]', self.token)) - # 判断其他推送渠道 - if self.token.startswith("SCT"): - return "ServerChan" - elif letters_only.isupper(): - return "AnPush" - else: - return "PushPlus" - return None + letters_only = ''.join(re.findall(r'[A-Za-z]', self.token)) + if self.token.lower().startswith("sct".lower()): + return "ServerChan" + elif letters_only.isupper(): + return "AnPush" + else: + return "PushPlus" # 标题,内容,图片,是否强制发送 - def send(self, title, content, image=None, force_send=False, diy_channel=None): + def send(self, title, content=None, image=None, force_send=False, diy_channel=None): if not self.init_success: return # 如果初始化失败,直接返回 - # 判断发送的内容类型 - contains_image = bool(image) # 是否包含图片 - if not contains_image and not force_send: + if not image and not force_send: if self.first_text_sent: return else: self.first_text_sent = True + # # 如果是 V2 微信通知直接处理 + if self.channel == "WeChat" and self.post_message_func: + return self.send_v2_wechat(title, content, image) + try: if not diy_channel: channel = self.channel @@ -50,7 +46,7 @@ class MySender: channel = diy_channel if channel == "WeChat": - return MySender.send_wechat(title, content, image) + return MySender.send_wechat(title, content, image, self.token) elif channel == "ServerChan": return self.send_serverchan(title, content, image) elif channel == "AnPush": @@ -63,10 +59,14 @@ class MySender: return f"Error occurred: {str(e)}" @staticmethod - def send_wechat(title, content, image): + def send_wechat(title, content, image, token): wechat = WeChat() + if ',' in token: + channel, actual_userid = token.split(',', 1) + else: + actual_userid = None if image: - send_status = wechat.send_msg(title='企业微信登录二维码', image=image, link=image) + send_status = wechat.send_msg(title='企业微信登录二维码', image=image, link=image, userid=actual_userid) else: send_status = wechat.send_msg(title=title, text=content) @@ -128,6 +128,21 @@ class MySender: return f"PushPlus send failed: {result.get('msg')}" return None + def send_v2_wechat(self, title, content, image): + """V2 微信通知发送""" + if not self.token or ',' not in self.token: + return '没有指定V2微信用户ID' + channel, actual_userid = self.token.split(',', 1) + self.post_message_func( + mtype=NotificationType.Plugin, + title=title, + text=content, + image=image, + link=image, + userid=actual_userid + ) + return None + def reset_limit(self): """解除限制,允许再次发送纯文本消息""" self.first_text_sent = False From 80d2d31088f1dc4efc93ff75c2c19b8f1430ed46 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 15 Nov 2024 08:11:57 +0800 Subject: [PATCH 26/27] =?UTF-8?q?IYUU=E6=B6=88=E6=81=AF=E6=8E=A8=E9=80=81?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=20=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- package.v2.json | 12 + plugins.v2/synccookiecloud/__init__.py | 300 +++++++++++++++++++++++++ plugins/synccookiecloud/__init__.py | 2 +- 4 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 plugins.v2/synccookiecloud/__init__.py diff --git a/package.json b/package.json index 1fd28e0..34881dd 100644 --- a/package.json +++ b/package.json @@ -878,13 +878,12 @@ "name": "同步CookieCloud", "description": "同步MoviePilot站点Cookie到本地CookieCloud。", "labels": "站点", - "version": "2.0", + "version": "1.4", "icon": "Cookiecloud_A.png", "author": "thsrite", "level": 1, - "v2": true, "history": { - "v2.0": "调整逻辑,修复问题", + "v1.4": "调整逻辑,修复问题", "v1.3": "感谢MidnightShake共享代码(同步时保留MoviePilot不匹配站点的cookie)", "v1.2": "同步到本地CookieCloud", "v1.1": "修复CookieCloud覆盖到浏览器", diff --git a/package.v2.json b/package.v2.json index 3a90b35..5c2da96 100644 --- a/package.v2.json +++ b/package.v2.json @@ -212,5 +212,17 @@ "icon": "Moviepilot_A.png", "author": "jxxghp", "level": 1 + }, + "SyncCookieCloud": { + "name": "同步CookieCloud", + "description": "同步MoviePilot站点Cookie到本地CookieCloud。", + "labels": "站点", + "version": "2.1", + "icon": "Cookiecloud_A.png", + "author": "thsrite", + "level": 1, + "history": { + "v2.1": "兼容MoviePilot V2" + } } } \ No newline at end of file diff --git a/plugins.v2/synccookiecloud/__init__.py b/plugins.v2/synccookiecloud/__init__.py new file mode 100644 index 0000000..6fcd4c4 --- /dev/null +++ b/plugins.v2/synccookiecloud/__init__.py @@ -0,0 +1,300 @@ +import json +from datetime import datetime, timedelta +from hashlib import md5 +from urllib.parse import urlparse + +import pytz + +from app.core.config import settings +from app.db.site_oper import SiteOper +from app.plugins import _PluginBase +from typing import Any, List, Dict, Tuple, Optional +from app.log import logger +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +from app.utils.crypto import CryptoJsUtils + + +class SyncCookieCloud(_PluginBase): + # 插件名称 + plugin_name = "同步CookieCloud" + # 插件描述 + plugin_desc = "同步MoviePilot站点Cookie到本地CookieCloud。" + # 插件图标 + plugin_icon = "Cookiecloud_A.png" + # 插件版本 + plugin_version = "2.1" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "synccookiecloud_" + # 加载顺序 + plugin_order = 28 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _enabled: bool = False + _onlyonce: bool = False + _cron: str = "" + siteoper = None + _scheduler: Optional[BackgroundScheduler] = None + + def init_plugin(self, config: dict = None): + self.siteoper = SiteOper() + + # 停止现有任务 + self.stop_service() + + if config: + self._enabled = config.get("enabled") + self._onlyonce = config.get("onlyonce") + self._cron = config.get("cron") + + if self._enabled or self._onlyonce: + # 定时服务 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + # 立即运行一次 + if self._onlyonce: + logger.info(f"同步CookieCloud服务启动,立即运行一次") + self._scheduler.add_job(self.__sync_to_cookiecloud, 'date', + run_date=datetime.now( + tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="同步CookieCloud") + # 关闭一次性开关 + self._onlyonce = False + + # 保存配置 + self.__update_config() + + # 周期运行 + if self._cron: + try: + self._scheduler.add_job(func=self.__sync_to_cookiecloud, + trigger=CronTrigger.from_crontab(self._cron), + name="同步CookieCloud") + except Exception as err: + logger.error(f"定时任务配置错误:{err}") + # 推送实时消息 + self.systemmessage.put(f"执行周期配置错误:{err}") + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def __sync_to_cookiecloud(self): + """ + 同步站点cookie到cookiecloud + """ + # 获取所有站点 + sites = self.siteoper.list_order_by_pri() + if not sites: + return + + if not settings.COOKIECLOUD_ENABLE_LOCAL: + logger.error('本地CookieCloud服务器未启用') + return + + cookies = {} + for site in sites: + domain = urlparse(site.url).netloc + cookie = site.cookie + + if not cookie: + logger.error(f"站点 {domain} 无cookie,跳过处理...") + continue + + # 解析cookie + site_cookies = [] + for ck in cookie.split(";"): + kv = ck.split("=") + if len(kv) < 2: + continue + site_cookies.append({ + "domain": domain, + "name": ck.split("=")[0], + "value": ck.split("=")[1] + }) + # 存储cookies + cookies[domain] = site_cookies + if cookies: + crypt_key = self._get_crypt_key() + try: + cookies = {'cookie_data': cookies} + encrypted_data = CryptoJsUtils.encrypt(json.dumps(cookies).encode('utf-8'), crypt_key).decode('utf-8') + except Exception as e: + logger.error(f"CookieCloud加密失败,{e}") + return + ck = {'encrypted': encrypted_data} + cookie_path = settings.COOKIE_PATH / f"{settings.COOKIECLOUD_KEY}.json" + cookie_path.write_bytes(json.dumps(ck).encode('utf-8')) + logger.info(f"同步站点cookie到本地CookieCloud成功") + else: + logger.error(f"同步站点cookie到本地CookieCloud失败,未获取到站点cookie") + + def __decrypted(self, encrypt_data: dict): + """ + 获取并解密本地CookieCloud数据 + """ + encrypted = encrypt_data.get("encrypted") + if not encrypted: + return {}, "未获取到cookie密文" + else: + crypt_key = self._get_crypt_key() + try: + decrypted_data = CryptoJsUtils.decrypt(encrypted, crypt_key).decode('utf-8') + result = json.loads(decrypted_data) + except Exception as e: + return {}, "cookie解密失败:" + str(e) + + if not result: + return {}, "cookie解密为空" + + if result.get("cookie_data"): + contents = result.get("cookie_data") + else: + contents = result + return contents + + @staticmethod + def _get_crypt_key() -> bytes: + """ + 使用UUID和密码生成CookieCloud的加解密密钥 + """ + md5_generator = md5() + md5_generator.update( + (str(settings.COOKIECLOUD_KEY).strip() + '-' + str(settings.COOKIECLOUD_PASSWORD).strip()).encode('utf-8')) + return (md5_generator.hexdigest()[:16]).encode('utf-8') + + def __update_config(self): + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "cron": self._cron + }) + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '执行周期', + 'placeholder': '5位cron表达式,留空自动' + } + } + ] + }, + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + 'text': '需要MoviePilot设定-站点启用本地CookieCloud服务器。' + } + } + ] + } + ] + }, + ] + } + ], { + "enabled": False, + "onlyonce": False, + "cron": "5 1 * * *", + } + + def get_page(self) -> List[dict]: + pass + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + logger.error("退出插件失败:%s" % str(e)) diff --git a/plugins/synccookiecloud/__init__.py b/plugins/synccookiecloud/__init__.py index 4f29226..97696d5 100644 --- a/plugins/synccookiecloud/__init__.py +++ b/plugins/synccookiecloud/__init__.py @@ -23,7 +23,7 @@ class SyncCookieCloud(_PluginBase): # 插件图标 plugin_icon = "Cookiecloud_A.png" # 插件版本 - plugin_version = "2.0" + plugin_version = "1.4" # 插件作者 plugin_author = "thsrite" # 作者主页 From 1baefc6054472f3d006ee1b3d35a14288185dbcc Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 16 Nov 2024 19:48:10 +0800 Subject: [PATCH 27/27] fix HistoryToV2 --- package.v2.json | 7 +++- plugins.v2/historytov2/__init__.py | 61 +++++++++++++++--------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/package.v2.json b/package.v2.json index b6c3b5e..297417e 100644 --- a/package.v2.json +++ b/package.v2.json @@ -208,10 +208,13 @@ "name": "历史记录迁移", "description": "将MoviePilot V1版本的整理历史记录迁移至V2版本。", "labels": "整理,历史记录", - "version": "1.0", + "version": "1.1", "icon": "Moviepilot_A.png", "author": "jxxghp", - "level": 1 + "level": 1, + "history": { + "v1.1": "修复启动提示信息" + } }, "SyncCookieCloud": { "name": "同步CookieCloud", diff --git a/plugins.v2/historytov2/__init__.py b/plugins.v2/historytov2/__init__.py index 6bc2144..e163abe 100644 --- a/plugins.v2/historytov2/__init__.py +++ b/plugins.v2/historytov2/__init__.py @@ -17,7 +17,7 @@ class HistoryToV2(_PluginBase): # 插件图标 plugin_icon = "Moviepilot_A.png" # 插件版本 - plugin_version = "1.0" + plugin_version = "1.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -43,35 +43,36 @@ class HistoryToV2(_PluginBase): self._username = config.get("username") self._password = config.get("password") - if self._enabled and self._host and self._username and self._password: - # 关闭开关 - self.__close_config() - # 登录MP获取token - token = self.__login_mp() - if token: - # 当前页码 - page = 1 - # 总记录数 - total = 0 - # 获取历史记录 - history = self.__get_history(token) - while history: - # 处理历史记录 - logger.info(f"开始处理第 {page} 页历史记录 ...") - self.__insert_history(history) - # 处理成功一批 - total += len(history) - logger.info(f"第 {page} 页处理完成,共处理 {total} 条记录") - # 获取下一页历史记录 - page += 1 - history = self.__get_history(token, page=page) - # 处理完成 - logger.info(f"历史记录迁移完成,共迁移 {total} 条记录!") - self.systemmessage.put(f"历史记录迁移完成,共迁移 {total} 条记录!", title="MoviePilot历史记录迁移") - else: - self.systemmessage.put(f"配置不完整,服务启动失败!", title="MoviePilot历史记录迁移") - # 关闭开关 - self.__close_config() + if self._enabled: + if self._host and self._username and self._password: + # 关闭开关 + self.__close_config() + # 登录MP获取token + token = self.__login_mp() + if token: + # 当前页码 + page = 1 + # 总记录数 + total = 0 + # 获取历史记录 + history = self.__get_history(token) + while history: + # 处理历史记录 + logger.info(f"开始处理第 {page} 页历史记录 ...") + self.__insert_history(history) + # 处理成功一批 + total += len(history) + logger.info(f"第 {page} 页处理完成,共处理 {total} 条记录") + # 获取下一页历史记录 + page += 1 + history = self.__get_history(token, page=page) + # 处理完成 + logger.info(f"历史记录迁移完成,共迁移 {total} 条记录!") + self.systemmessage.put(f"历史记录迁移完成,共迁移 {total} 条记录!", title="MoviePilot历史记录迁移") + else: + self.systemmessage.put(f"配置不完整,服务启动失败!", title="MoviePilot历史记录迁移") + # 关闭开关 + self.__close_config() def __close_config(self): """