mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-13 23:16:47 +00:00
Merge branch 'main' into qbcommand
This commit is contained in:
90
package.json
90
package.json
@@ -74,12 +74,14 @@
|
||||
"name": "豆瓣想看",
|
||||
"description": "同步豆瓣想看数据,自动添加订阅。",
|
||||
"labels": "订阅",
|
||||
"version": "1.8",
|
||||
"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安全性",
|
||||
"v1.6": "同步历史记录支持手动删除,需要主程序升级至v1.8.4+版本",
|
||||
@@ -296,11 +298,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,种子下载失败的问题",
|
||||
@@ -487,16 +490,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发送消息通知。",
|
||||
@@ -534,10 +527,18 @@
|
||||
"name": "TMDB剧集组刮削",
|
||||
"description": "从TMDB剧集组刮削季集的实际顺序。",
|
||||
"labels": "刮削",
|
||||
"version": "1.1",
|
||||
"version": "2.5",
|
||||
"icon": "Element_A.png",
|
||||
"author": "叮叮当",
|
||||
"level": 1
|
||||
"level": 1,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v2.5": "修复当媒体服务器中剧集的季不完整时会中断的问题",
|
||||
"v2.3": "修复v2版本无法读取媒体库的问题",
|
||||
"v2.2": "修复v2版本无法读取数据的问题",
|
||||
"v2.1": "增加发送通知提醒选择剧集组",
|
||||
"v2.0": "增加手动选择剧集组的功能"
|
||||
}
|
||||
},
|
||||
"CustomIndexer": {
|
||||
"name": "自定义索引站点",
|
||||
@@ -859,53 +860,66 @@
|
||||
"v2": true
|
||||
},
|
||||
"DynamicWeChat": {
|
||||
"name": "修改企业微信可信IP",
|
||||
"description": "优先使用cookie,可本地扫码刷新Cookie,当填写两个第三方token时可手机远程更新cookie。",
|
||||
"name": "动态企微可信IP",
|
||||
"description": "修改企微应用可信IP,支持Srever酱等第三方通知。验证码以?结尾发送到企业微信应用",
|
||||
"labels": "消息通知",
|
||||
"version": "1.3.1",
|
||||
"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进入正式版,显示了一个没用的参数",
|
||||
"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": {
|
||||
"name": "同步CookieCloud",
|
||||
"description": "同步MoviePilot站点Cookie到本地CookieCloud。",
|
||||
"labels": "站点",
|
||||
"version": "2.0",
|
||||
"version": "1.4",
|
||||
"icon": "Cookiecloud_A.png",
|
||||
"author": "thsrite",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v2.0": "调整逻辑,修复问题",
|
||||
"v1.4": "调整逻辑,修复问题",
|
||||
"v1.3": "感谢MidnightShake共享代码(同步时保留MoviePilot不匹配站点的cookie)",
|
||||
"v1.2": "同步到本地CookieCloud",
|
||||
"v1.1": "修复CookieCloud覆盖到浏览器",
|
||||
"v1.0": "同步MoviePilot站点Cookie到CookieCloud"
|
||||
}
|
||||
},
|
||||
"BangumiColl": {
|
||||
"name": "Bangumi收藏订阅",
|
||||
"description": "Bangumi用户收藏添加到订阅",
|
||||
"labels": "订阅",
|
||||
"version": "1.5.1",
|
||||
"icon": "bangumi_b.png",
|
||||
"author": "Attente",
|
||||
"level": 1,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.5.1": "修复季度信息未传递的问题. 新增站点列表同步删除",
|
||||
"v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项",
|
||||
"v1.4": "结构优化",
|
||||
"v1.3.1": "修复因修改季号导致未下载剧集而完成订阅的问题"
|
||||
"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变动的问题,增加开关选项"
|
||||
}
|
||||
},
|
||||
"IyuuMsg": {
|
||||
"name": "IYUU消息推送",
|
||||
"description": "支持使用IYUU发送消息通知。",
|
||||
"labels": "消息通知,IYUU",
|
||||
"version": "1.3",
|
||||
"icon": "Iyuu_A.png",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.3": "消息限流发送,以缓解IYUU服务器压力"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,32 +178,16 @@
|
||||
"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": "自动删除下载器中的下载任务。",
|
||||
"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 版本"
|
||||
}
|
||||
},
|
||||
@@ -211,11 +195,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 版本"
|
||||
}
|
||||
},
|
||||
@@ -230,5 +215,29 @@
|
||||
"history": {
|
||||
"v2.0": "适配MoviePilot V2 版本"
|
||||
}
|
||||
},
|
||||
"HistoryToV2": {
|
||||
"name": "历史记录迁移",
|
||||
"description": "将MoviePilot V1版本的整理历史记录迁移至V2版本。",
|
||||
"labels": "整理,历史记录",
|
||||
"version": "1.1",
|
||||
"icon": "Moviepilot_A.png",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
336
plugins.v2/historytov2/__init__.py
Normal file
336
plugins.v2/historytov2/__init__.py
Normal file
@@ -0,0 +1,336 @@
|
||||
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.1"
|
||||
# 插件作者
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
关闭开关
|
||||
"""
|
||||
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
|
||||
@@ -33,7 +33,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "IYUU.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.0.1"
|
||||
plugin_version = "2.1"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
|
||||
@@ -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
|
||||
|
||||
300
plugins.v2/synccookiecloud/__init__.py
Normal file
300
plugins.v2/synccookiecloud/__init__.py
Normal file
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
# 作者主页
|
||||
@@ -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())))
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class DoubanSync(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "douban.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.8"
|
||||
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,7 +514,12 @@ class DoubanSync(_PluginBase):
|
||||
continue
|
||||
logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...")
|
||||
url = self._interests_url % user_id
|
||||
results = self.rsshelper.parse(url)
|
||||
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
|
||||
|
||||
@@ -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.schemas.types import EventType, NotificationType
|
||||
from app.plugins.dynamicwechat.notify_helper import MySender
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
class DynamicWeChat(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "修改企业微信可信IP"
|
||||
plugin_name = "动态企微可信IP"
|
||||
# 插件描述
|
||||
plugin_desc = "优先使用cookie,可本地扫码刷新Cookie,当填写两个第三方token时手机微信可以更新cookie。"
|
||||
plugin_desc = "修改企微应用可信IP,详细说明查看'作者主页',支持第三方通知。验证码以?结尾发送到企业微信应用"
|
||||
# 插件图标
|
||||
plugin_icon = "Wecom_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.3.1"
|
||||
plugin_version = "1.5.1"
|
||||
# 插件作者
|
||||
plugin_author = "RamenRa"
|
||||
# 作者主页
|
||||
@@ -41,6 +42,8 @@ class DynamicWeChat(_PluginBase):
|
||||
plugin_order = 47
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
# 检测间隔时间,默认10分钟
|
||||
_refresh_cron = '*/20 * * * *'
|
||||
|
||||
# ------------------------------------------私有属性------------------------------------------
|
||||
_enabled = False # 开关
|
||||
@@ -54,6 +57,12 @@ class DynamicWeChat(_PluginBase):
|
||||
_cc_server = None
|
||||
# 本地扫码开关
|
||||
_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'
|
||||
@@ -63,28 +72,26 @@ 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 = ''
|
||||
# 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 = 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'
|
||||
@@ -93,42 +100,47 @@ 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._server = f'http://localhost:{settings.NGINX_PORT}/cookiecloud'
|
||||
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._cookie_from_CC = ""
|
||||
self._current_ip_address = self.get_ip_from_url(self._ip_urls[0])
|
||||
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._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()
|
||||
|
||||
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
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
if (self._enabled or self._onlyonce) and self._input_id_list:
|
||||
# 定时服务
|
||||
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 +148,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 +173,41 @@ 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
|
||||
# 先尝试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)
|
||||
def local_scanning(self, event: Event = None):
|
||||
"""
|
||||
@@ -180,12 +228,11 @@ 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)
|
||||
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 +244,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,22 +265,15 @@ 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():
|
||||
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"))
|
||||
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))
|
||||
@@ -324,22 +364,15 @@ 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):
|
||||
@@ -349,25 +382,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"企业微信登录二维码<br/><img src='{img_src}' />")
|
||||
# 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(title="企业微信登录二维码", image=img_src)
|
||||
if result:
|
||||
logger.info(f"二维码发送失败,原因:{result}")
|
||||
browser.close()
|
||||
return
|
||||
logger.info("二维码已经发送,等待用户 90 秒内扫码登录")
|
||||
# logger.info("如收到短信验证码请以?结束,发送到<企业微信应用> 如: 110301?")
|
||||
time.sleep(90) # 等待用户扫码
|
||||
@@ -378,17 +406,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:
|
||||
@@ -398,6 +424,7 @@ class DynamicWeChat(_PluginBase):
|
||||
|
||||
def _update_cookie(self, page, context):
|
||||
self._future_timestamp = 0 # 标记二维码失效
|
||||
PyCookieCloud.save_cookie_lifetime(self._settings_file_path, 0) # 重置cookie存活时间
|
||||
if self._use_cookiecloud:
|
||||
if not self._cc_server: # 连接失败返回 False
|
||||
self.try_connect_cc() # 再尝试一次连接
|
||||
@@ -411,7 +438,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
|
||||
@@ -421,7 +447,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:
|
||||
@@ -429,8 +455,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")
|
||||
|
||||
@@ -442,22 +467,23 @@ 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
|
||||
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}")
|
||||
# logger.info("尝试推送登录二维码")
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
@@ -474,21 +500,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'):
|
||||
logger.info("cookie已失效,下次IP变动推送二维码")
|
||||
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):
|
||||
@@ -536,8 +575,9 @@ 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):
|
||||
@@ -547,8 +587,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(",")
|
||||
@@ -581,81 +621,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):
|
||||
"""
|
||||
更新配置
|
||||
@@ -664,15 +638,12 @@ 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_from_CC": self._cookie_from_CC,
|
||||
"cookie_header": self._cookie_header,
|
||||
"use_cookiecloud": self._use_cookiecloud,
|
||||
})
|
||||
@@ -741,7 +712,6 @@ class DynamicWeChat(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
# 添加 "使用CookieCloud获取cookie" 开关按钮
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
@@ -772,7 +742,7 @@ class DynamicWeChat(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'local_scan',
|
||||
'label': '扫码刷新Cookie和改IP',
|
||||
'label': '本地扫码修改IP',
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -793,11 +763,29 @@ class DynamicWeChat(_PluginBase):
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '检测周期',
|
||||
'label': '[必填]检测周期',
|
||||
'placeholder': '0 * * * *'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'notification_token',
|
||||
'label': '[可选] 通知方式',
|
||||
'rows': 1,
|
||||
'placeholder': '支持微信、Server酱、PushPlus、AnPush等Token或API'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -814,7 +802,7 @@ class DynamicWeChat(_PluginBase):
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'input_id_list',
|
||||
'label': '应用ID',
|
||||
'label': '[必填]应用ID',
|
||||
'rows': 1,
|
||||
'placeholder': '输入应用ID,多个ID用英文逗号分隔。在企业微信应用页面URL末尾获取'
|
||||
}
|
||||
@@ -823,47 +811,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': [
|
||||
@@ -878,7 +825,7 @@ class DynamicWeChat(_PluginBase):
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '使用内建CookieCloud 或 自定义 或 填写两个token 至少三选一,否则无法正常使用'
|
||||
'text': '建议启用内建或自定义CookieCloud。支持微信、Server酱等第三方通知,具体请查看作者主页'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -898,7 +845,7 @@ class DynamicWeChat(_PluginBase):
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'text': '优先使用cookie,当IP变动 且 cookie失效 且 填写两个token才会调用API推送登录二维码。',
|
||||
'text': 'Cookie失效时通知用户,用户使用/push_qr让插件推送二维码。使用第三方通知时填写对应Token/API'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -913,14 +860,12 @@ 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]:
|
||||
# 获取当前时间戳
|
||||
current_time = datetime.now().timestamp()
|
||||
@@ -972,8 +917,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_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",
|
||||
@@ -985,24 +955,39 @@ 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": "40px"
|
||||
"display": "flex",
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
"flexDirection": "column", # 垂直排列
|
||||
"gap": "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 # 二维码图片或提示信息
|
||||
]
|
||||
},
|
||||
img_component # 二维码图片
|
||||
}
|
||||
]
|
||||
|
||||
return base_content
|
||||
@@ -1026,10 +1011,14 @@ 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"企业微信登录二维码<br/><img src='{img_src}' />")
|
||||
image_src, refuse_time = self.find_qrc(page)
|
||||
if image_src:
|
||||
if self._my_send:
|
||||
result = self._my_send.send("企业微信登录二维码", image=image_src)
|
||||
if result:
|
||||
logger.info(f"远程推送任务: 二维码发送失败,原因:{result}")
|
||||
browser.close()
|
||||
return
|
||||
logger.info("远程推送任务: 二维码已经发送,等待用户 90 秒内扫码登录")
|
||||
# logger.info("远程推送任务: 如收到短信验证码请以?结束,发送到<企业微信应用> 如: 110301?")
|
||||
time.sleep(90)
|
||||
@@ -1039,9 +1028,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}")
|
||||
|
||||
@@ -1051,7 +1041,7 @@ class DynamicWeChat(_PluginBase):
|
||||
{
|
||||
"cmd": "/push_qr",
|
||||
"event": EventType.PluginAction,
|
||||
"desc": "立即推送登录二维码到pushplus",
|
||||
"desc": "立即推送登录二维码",
|
||||
"category": "",
|
||||
"data": {
|
||||
"action": "push_qrcode"
|
||||
@@ -1109,5 +1099,3 @@ class DynamicWeChat(_PluginBase):
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
|
||||
|
||||
|
||||
148
plugins/dynamicwechat/notify_helper.py
Normal file
148
plugins/dynamicwechat/notify_helper.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import re
|
||||
import requests
|
||||
from app.modules.wechat import WeChat
|
||||
from app.schemas.types import NotificationType
|
||||
|
||||
|
||||
class MySender:
|
||||
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 "WeChat" in self.token:
|
||||
return "WeChat"
|
||||
|
||||
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=None, image=None, force_send=False, diy_channel=None):
|
||||
if not self.init_success:
|
||||
return # 如果初始化失败,直接返回
|
||||
|
||||
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
|
||||
else:
|
||||
channel = diy_channel
|
||||
|
||||
if channel == "WeChat":
|
||||
return MySender.send_wechat(title, content, image, self.token)
|
||||
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, 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, userid=actual_userid)
|
||||
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'' 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"<img src=\"{image}\" width=\"100%\">" 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"企业微信登录二维码<br/><img src='{image}' />" 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 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
|
||||
@@ -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)
|
||||
@@ -1,8 +1,9 @@
|
||||
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
|
||||
|
||||
@@ -34,6 +35,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
|
||||
@@ -52,20 +54,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})
|
||||
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
|
||||
|
||||
@@ -78,3 +93,29 @@ class PyCookieCloud:
|
||||
md5 = hashlib.md5()
|
||||
md5.update((self.uuid + '-' + self.password).encode('utf-8'))
|
||||
return md5.hexdigest()[:16]
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
return settings.get('_cookie_lifetime', 0)
|
||||
else:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
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(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(settings_file, new_lifetime)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +24,7 @@ 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
|
||||
|
||||
class ExistMediaInfo(BaseModel):
|
||||
# 类型 电影、电视剧
|
||||
@@ -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 = "1.1"
|
||||
plugin_version = "2.5"
|
||||
# 插件作者
|
||||
plugin_author = "叮叮当"
|
||||
# 作者主页
|
||||
@@ -63,25 +67,25 @@ class EpisodeGroupMeta(_PluginBase):
|
||||
_event = threading.Event()
|
||||
|
||||
# 私有属性
|
||||
mschain = None
|
||||
tv = None
|
||||
emby = None
|
||||
plex = None
|
||||
jellyfin = None
|
||||
mediaserver_helper = None
|
||||
|
||||
_enabled = False
|
||||
_notify = True
|
||||
_autorun = True
|
||||
_ignorelock = False
|
||||
_delay = 0
|
||||
_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")
|
||||
self._autorun = config.get("autorun")
|
||||
self._ignorelock = config.get("ignorelock")
|
||||
self._delay = config.get("delay") or 120
|
||||
self._allowlist = []
|
||||
@@ -90,6 +94,14 @@ 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 ("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} 配置修正 ...")
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
@@ -99,7 +111,80 @@ 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}")
|
||||
self.systemmessage.put("正在刮削中,请稍等!", title="剧集组刮削")
|
||||
if self.start_rt(mediainfo, episode_groups, group_id):
|
||||
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]]:
|
||||
"""
|
||||
@@ -116,7 +201,7 @@ class EpisodeGroupMeta(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@@ -132,18 +217,50 @@ class EpisodeGroupMeta(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'component': 'VCheckboxBtn',
|
||||
'props': {
|
||||
'model': 'ignorelock',
|
||||
'label': '媒体信息锁定时也进行刮削',
|
||||
'model': 'autorun',
|
||||
'label': '季集匹配时自动刮削',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCheckboxBtn',
|
||||
'props': {
|
||||
'model': 'ignorelock',
|
||||
'label': '锁定的剧集也刮削',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCheckboxBtn',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '开启通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -203,7 +320,7 @@ class EpisodeGroupMeta(_PluginBase):
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '注意:刮削白名单(留空), 则全部刮削. 否则仅刮削白名单.'
|
||||
'text': '注意:刮削白名单(留空)则全部刮削. 否则仅刮削白名单.'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -235,18 +352,220 @@ class EpisodeGroupMeta(_PluginBase):
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"autorun": True,
|
||||
"ignorelock": False,
|
||||
"allowlist": "",
|
||||
"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]:
|
||||
pass
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
# 查询待处理数据列表
|
||||
mediainfo_list: List[PluginData] = self.get_data()
|
||||
# 拼装页面
|
||||
contents = []
|
||||
for plugin_data in mediainfo_list:
|
||||
try:
|
||||
tmdb_id = plugin_data.key
|
||||
# fix v1版本数据读取问题
|
||||
if self.is_objstr(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,21 +598,111 @@ 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} 未勾选自动刮削, 无需处理")
|
||||
# 发送通知
|
||||
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))
|
||||
# 获取可用的媒体服务器
|
||||
_existsinfo = self.chain.media_exists(mediainfo=mediainfo)
|
||||
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} 在媒体库中不存在")
|
||||
# 开始处理
|
||||
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
|
||||
# 新增需要的属性
|
||||
existsinfo.server = _existsinfo.server
|
||||
existsinfo.type = _existsinfo.type
|
||||
self.log_info(f"{mediainfo.title_year} 存在于媒体服务器: {_existsinfo.server}")
|
||||
|
||||
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
|
||||
# 获取全部可用的媒体服务器, 兼容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',
|
||||
@@ -309,6 +718,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,25 +737,33 @@ 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} 季")
|
||||
# 遍历全部媒体项并更新
|
||||
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
|
||||
# 是否无视项目锁定
|
||||
# 锁定的剧集是否也刮削?
|
||||
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]
|
||||
@@ -361,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)
|
||||
@@ -376,25 +797,28 @@ class EpisodeGroupMeta(_PluginBase):
|
||||
continue
|
||||
|
||||
self.log_info(f"{mediainfo.title_year} 已经运行完毕了..")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __append_to_list(list, item):
|
||||
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"
|
||||
@@ -410,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()
|
||||
@@ -449,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")
|
||||
@@ -470,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()
|
||||
@@ -509,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:
|
||||
@@ -569,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:
|
||||
@@ -598,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:
|
||||
"""
|
||||
获得媒体项详情
|
||||
"""
|
||||
@@ -615,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:
|
||||
@@ -629,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:
|
||||
@@ -646,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
|
||||
@@ -675,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):
|
||||
"""
|
||||
更新媒体项详情
|
||||
"""
|
||||
@@ -705,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={
|
||||
@@ -726,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={
|
||||
@@ -747,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'],
|
||||
@@ -760,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):
|
||||
"""
|
||||
更新媒体项图片
|
||||
"""
|
||||
@@ -797,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={
|
||||
@@ -820,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:
|
||||
@@ -838,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)}")
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "IYUU.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.9.5"
|
||||
plugin_version = "1.9.6"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -23,7 +23,7 @@ class SyncCookieCloud(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Cookiecloud_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.0"
|
||||
plugin_version = "1.4"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
|
||||
Reference in New Issue
Block a user