add:增加CloudSyncMedia转存任务配置

This commit is contained in:
hitmant 2025-08-07 10:26:03 +08:00
parent 0efd122242
commit 7694511601
2 changed files with 424 additions and 33 deletions

View File

@ -3,13 +3,14 @@
"name": "Nullbr资源搜索", "name": "Nullbr资源搜索",
"description": "优先使用Nullbr API搜索影视资源支持115网盘、磁力、ed2k、m3u8等多种资源类型。在MoviePilot搜索其他资源站之前优先查找Nullbr资源提高搜索效率。", "description": "优先使用Nullbr API搜索影视资源支持115网盘、磁力、ed2k、m3u8等多种资源类型。在MoviePilot搜索其他资源站之前优先查找Nullbr资源提高搜索效率。",
"labels": "资源", "labels": "资源",
"version": "1.0.5", "version": "1.0.6",
"icon": "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png", "icon": "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png",
"author": "Hqyel", "author": "Hqyel",
"level": 1, "level": 1,
"history": { "history": {
"v1.0.4": "初始上架版本", "v1.0.4": "初始上架版本",
"v1.0.5": "增加资源优先度设置" "v1.0.5": "增加资源优先度设置",
"v1.0.6": "增加CloudSyncMedia转存配置"
} }
} }
} }

View File

@ -12,6 +12,107 @@ from app.plugins import _PluginBase
from app.schemas.types import EventType from app.schemas.types import EventType
class CloudSyncMediaClient:
"""CloudSyncMedia客户端"""
def __init__(self, base_url: str, username: str, password: str):
self.base_url = base_url.rstrip('/')
self.username = username
self.password = password
self.token = None
self.token_expiry = 0
# 配置请求会话
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
# 初始化时获取token
self._ensure_valid_token()
def _login(self) -> dict:
"""登录CMS系统获取token"""
try:
response = self.session.post(
f'{self.base_url}/api/auth/login',
json={
'username': self.username,
'password': self.password
},
timeout=30
)
response.raise_for_status()
data = response.json()
if data.get('code') != 200 or 'data' not in data:
raise ValueError(f'CMS登录失败: {data}')
return data['data']
except requests.exceptions.RequestException as e:
logger.error(f'CMS登录失败: {str(e)}')
raise
def _ensure_valid_token(self):
"""确保有效的token"""
current_time = time.time()
# 如果token不存在或距离过期时间不到1小时重新获取token
if not self.token or current_time >= (self.token_expiry - 3600):
login_data = self._login()
self.token = login_data['token']
# 设置token过期时间为24小时后
self.token_expiry = current_time + 86400
# 更新session的Authorization header
self.session.headers.update({
'Authorization': f'Bearer {self.token}'
})
logger.info("CMS token已更新")
def add_share_down(self, url: str) -> dict:
"""添加分享链接到CMS系统进行转存"""
if not url:
raise ValueError('转存链接不能为空')
try:
self._ensure_valid_token()
response = self.session.post(
f'{self.base_url}/api/cloud/add_share_down',
json={'url': url},
timeout=30
)
response.raise_for_status()
result = response.json()
logger.info(f"CMS转存请求已发送: {url}")
return result
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
# token可能过期强制重新获取
self.token = None
self._ensure_valid_token()
# 重试请求
response = self.session.post(
f'{self.base_url}/api/cloud/add_share_down',
json={'url': url},
timeout=30
)
response.raise_for_status()
return response.json()
raise
except Exception as e:
logger.error(f'CMS转存请求失败: {str(e)}')
raise
class NullbrApiClient: class NullbrApiClient:
"""Nullbr API客户端""" """Nullbr API客户端"""
@ -188,8 +289,16 @@ class NullbrSearch(_PluginBase):
self._search_timeout = 30 self._search_timeout = 30
self._client = None self._client = None
# 用户搜索结果缓存 # CloudSyncMedia配置
self._cms_enabled = False
self._cms_url = ""
self._cms_username = ""
self._cms_password = ""
self._cms_client = None
# 用户搜索结果缓存和资源缓存
self._user_search_cache = {} # {userid: {'results': [...], 'timestamp': time.time()}} self._user_search_cache = {} # {userid: {'results': [...], 'timestamp': time.time()}}
self._user_resource_cache = {} # {userid: {'resources': [...], 'title': str, 'timestamp': time.time()}}
def init_plugin(self, config: dict = None): def init_plugin(self, config: dict = None):
if config: if config:
@ -216,7 +325,15 @@ class NullbrSearch(_PluginBase):
self._enable_ed2k = config.get("enable_ed2k", True) self._enable_ed2k = config.get("enable_ed2k", True)
self._search_timeout = config.get("search_timeout", 30) self._search_timeout = config.get("search_timeout", 30)
# CloudSyncMedia配置
self._cms_enabled = config.get("cms_enabled", False)
self._cms_url = config.get("cms_url", "")
self._cms_username = config.get("cms_username", "")
self._cms_password = config.get("cms_password", "")
logger.info(f"Nullbr资源优先级设置: {' > '.join(self._resource_priority)}") logger.info(f"Nullbr资源优先级设置: {' > '.join(self._resource_priority)}")
if self._cms_enabled:
logger.info(f"CloudSyncMedia已启用: {self._cms_url}")
# 初始化API客户端 # 初始化API客户端
if self._enabled and self._app_id: if self._enabled and self._app_id:
@ -230,6 +347,22 @@ class NullbrSearch(_PluginBase):
if not self._app_id: if not self._app_id:
logger.warning("Nullbr插件配置错误: 缺少APP_ID") logger.warning("Nullbr插件配置错误: 缺少APP_ID")
self._client = None self._client = None
# 初始化CloudSyncMedia客户端
if self._cms_enabled and self._cms_url and self._cms_username and self._cms_password:
try:
self._cms_client = CloudSyncMediaClient(
self._cms_url,
self._cms_username,
self._cms_password
)
logger.info("CloudSyncMedia客户端已初始化")
except Exception as e:
logger.error(f"CloudSyncMedia初始化失败: {str(e)}")
self._cms_enabled = False
self._cms_client = None
else:
self._cms_client = None
def get_state(self) -> bool: def get_state(self) -> bool:
return self._enabled return self._enabled
@ -531,25 +664,123 @@ class NullbrSearch(_PluginBase):
{ {
'component': 'VRow', 'component': 'VRow',
'content': [ 'content': [
{
'component': 'VCol',
'props': {'cols': 12, 'md': 6},
'content': [
{ {
'component': 'VTextField', 'component': 'VCol',
'props': { 'props': {'cols': 12},
'model': 'search_timeout', 'content': [
'label': '搜索超时时间(秒)', {
'placeholder': '30', 'component': 'VAlert',
'hint': '单次API请求的超时时间', 'props': {
'persistent-hint': True, 'type': 'info',
'type': 'number', 'variant': 'tonal'
'min': 10, },
'max': 120 'content': [
} {
'component': 'span',
'text': '🚀 CloudSyncMedia转存配置 - 自动转存资源到CMS系统'
}
]
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {'cols': 12, 'md': 6},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'cms_enabled',
'label': '启用CloudSyncMedia',
'hint': '开启后支持自动转存资源到CMS系统',
'persistent-hint': True
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {'cols': 12, 'md': 6},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'cms_url',
'label': 'CMS服务器地址',
'placeholder': 'http://your-cms-domain.com',
'hint': 'CloudSyncMedia服务器的完整URL地址',
'persistent-hint': True
}
}
]
},
{
'component': 'VCol',
'props': {'cols': 12, 'md': 6},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'cms_username',
'label': 'CMS用户名',
'placeholder': '请输入CMS登录用户名',
'hint': '用于登录CMS系统的用户名',
'persistent-hint': True
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {'cols': 12, 'md': 6},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'cms_password',
'label': 'CMS密码',
'placeholder': '请输入CMS登录密码',
'hint': '用于登录CMS系统的密码',
'persistent-hint': True,
'type': 'password'
}
}
]
},
{
'component': 'VCol',
'props': {'cols': 12, 'md': 6},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'search_timeout',
'label': '搜索超时时间(秒)',
'placeholder': '30',
'hint': '单次API请求的超时时间',
'persistent-hint': True,
'type': 'number',
'min': 10,
'max': 120
}
}
]
} }
]
}
] ]
} }
] ]
@ -576,6 +807,10 @@ class NullbrSearch(_PluginBase):
"priority_2": "magnet", "priority_2": "magnet",
"priority_3": "ed2k", "priority_3": "ed2k",
"priority_4": "video", "priority_4": "video",
"cms_enabled": False,
"cms_url": "",
"cms_username": "",
"cms_password": "",
"search_timeout": 30 "search_timeout": 30
} }
@ -842,6 +1077,16 @@ class NullbrSearch(_PluginBase):
# 检查是否为编号选择(纯数字,包含问号的情况) # 检查是否为编号选择(纯数字,包含问号的情况)
elif clean_text.isdigit(): elif clean_text.isdigit():
number = int(clean_text) number = int(clean_text)
# 先检查是否有资源缓存用于CMS转存
if self._cms_enabled and self._cms_client and userid in self._user_resource_cache:
cache = self._user_resource_cache[userid]
if time.time() - cache['timestamp'] < 3600: # 1小时内有效
if 1 <= number <= len(cache['resources']):
logger.info(f"检测到资源转存请求: {number}")
self.handle_resource_transfer(number, channel, userid)
return
logger.info(f"检测到编号选择: {number}") logger.info(f"检测到编号选择: {number}")
self.handle_resource_selection(number, channel, userid) self.handle_resource_selection(number, channel, userid)
@ -1117,17 +1362,54 @@ class NullbrSearch(_PluginBase):
def _format_and_send_resources(self, resources: dict, resource_type: str, title: str, channel: str, userid: str): def _format_and_send_resources(self, resources: dict, resource_type: str, title: str, channel: str, userid: str):
"""格式化并发送资源链接""" """格式化并发送资源链接"""
try: try:
resource_list = resources.get(resource_type, [])
if not resource_list:
self.post_message(
channel=channel,
title="无资源",
text=f"没有找到「{title}」的{resource_type}资源。",
userid=userid
)
return
# 缓存资源到用户缓存中用于CMS转存
resource_cache = []
for res in resource_list[:10]: # 最多缓存10个
if resource_type == "115":
url = res.get('share_link', '')
elif resource_type == "magnet":
url = res.get('magnet', '')
elif resource_type in ["video", "ed2k"]:
url = res.get('url', res.get('link', ''))
else:
url = ''
if url:
resource_cache.append({
'url': url,
'title': res.get('title', res.get('name', '未知')),
'size': res.get('size', '未知'),
'type': resource_type
})
# 保存到用户资源缓存
self._user_resource_cache[userid] = {
'resources': resource_cache,
'title': title,
'resource_type': resource_type,
'timestamp': time.time()
}
# 格式化显示文本
reply_text = f"🎯 「{title}」的{resource_type}资源:\n\n" reply_text = f"🎯 「{title}」的{resource_type}资源:\n\n"
if resource_type == "115": if resource_type == "115":
resource_list = resources.get('115', []) for i, res in enumerate(resource_list[:10], 1):
for i, res in enumerate(resource_list[:10], 1): # 最多显示10个
reply_text += f"{i}. {res.get('title', '未知')}\n" reply_text += f"{i}. {res.get('title', '未知')}\n"
reply_text += f" 大小: {res.get('size', '未知')}\n" reply_text += f" 大小: {res.get('size', '未知')}\n"
reply_text += f" 链接: {res.get('share_link', '')}\n\n" reply_text += f" 链接: {res.get('share_link', '')}\n\n"
elif resource_type == "magnet": elif resource_type == "magnet":
resource_list = resources.get('magnet', [])
for i, res in enumerate(resource_list[:10], 1): for i, res in enumerate(resource_list[:10], 1):
reply_text += f"{i}. {res.get('name', '未知')}\n" reply_text += f"{i}. {res.get('name', '未知')}\n"
reply_text += f" 大小: {res.get('size', '未知')}\n" reply_text += f" 大小: {res.get('size', '未知')}\n"
@ -1136,18 +1418,21 @@ class NullbrSearch(_PluginBase):
reply_text += f" 磁力: {res.get('magnet', '')}\n\n" reply_text += f" 磁力: {res.get('magnet', '')}\n\n"
elif resource_type in ["video", "ed2k"]: elif resource_type in ["video", "ed2k"]:
resource_list = resources.get(resource_type, [])
for i, res in enumerate(resource_list[:10], 1): for i, res in enumerate(resource_list[:10], 1):
reply_text += f"{i}. {res.get('name', res.get('title', '未知'))}\n" reply_text += f"{i}. {res.get('name', res.get('title', '未知'))}\n"
if res.get('size'): if res.get('size'):
reply_text += f" 大小: {res.get('size')}\n" reply_text += f" 大小: {res.get('size')}\n"
reply_text += f" 链接: {res.get('url', res.get('link', ''))}\n\n" reply_text += f" 链接: {res.get('url', res.get('link', ''))}\n\n"
if len(reply_text) > 4000: # Telegram消息长度限制 if len(reply_text) > 3500: # 留出空间给CMS提示
reply_text = reply_text[:3900] + "...\n\n(内容过长已截断)" reply_text = reply_text[:3400] + "...\n\n(内容过长已截断)\n\n"
if not reply_text.strip().endswith(''): reply_text += f"📊 共找到 {len(resource_list)} 个资源\n\n"
reply_text += f"📊 共找到 {len(resources.get(resource_type, []))} 个资源"
# 如果启用了CloudSyncMedia添加转存提示
if self._cms_enabled and self._cms_client:
reply_text += "🚀 CloudSyncMedia转存:\n"
reply_text += "发送资源编号进行转存,如: 1、2、3..."
self.post_message( self.post_message(
channel=channel, channel=channel,
@ -1251,6 +1536,97 @@ class NullbrSearch(_PluginBase):
userid=userid userid=userid
) )
def handle_resource_transfer(self, resource_id: int, channel: str, userid: str):
"""处理资源转存请求"""
try:
# 检查CMS是否启用
if not self._cms_enabled or not self._cms_client:
self.post_message(
channel=channel,
title="功能未启用",
text="CloudSyncMedia转存功能未启用请在设置中配置。",
userid=userid
)
return
# 检查资源缓存
cache = self._user_resource_cache.get(userid)
if not cache or time.time() - cache['timestamp'] > 3600:
self.post_message(
channel=channel,
title="缓存过期",
text="资源缓存已过期,请重新获取资源。",
userid=userid
)
return
resources = cache['resources']
if resource_id < 1 or resource_id > len(resources):
self.post_message(
channel=channel,
title="无效编号",
text=f"请输入有效的资源编号 (1-{len(resources)})。",
userid=userid
)
return
# 获取指定的资源
selected_resource = resources[resource_id - 1]
title = selected_resource['title']
url = selected_resource['url']
size = selected_resource['size']
resource_type = selected_resource['type']
logger.info(f"开始转存资源: {title} ({resource_type}) -> {url}")
# 发送转存中的提示
self.post_message(
channel=channel,
title="转存中",
text=f"🚀 正在转存资源到CloudSyncMedia:\n\n"
f"📁 {title}\n"
f"💾 大小: {size}\n"
f"🔗 类型: {resource_type}\n\n"
f"请稍等...",
userid=userid
)
# 调用CMS转存API
result = self._cms_client.add_share_down(url)
# 处理转存结果
if result.get('code') == 200:
self.post_message(
channel=channel,
title="转存成功",
text=f"✅ 资源转存成功!\n\n"
f"📁 {title}\n"
f"💾 大小: {size}\n"
f"🚀 {result.get('msg', '已添加到转存队列')}\n\n"
f"请到CloudSyncMedia查看转存进度。",
userid=userid
)
else:
error_msg = result.get('msg', '转存失败')
self.post_message(
channel=channel,
title="转存失败",
text=f"❌ 资源转存失败:\n\n"
f"📁 {title}\n"
f"🚫 错误: {error_msg}\n\n"
f"请检查CMS配置或稍后重试。",
userid=userid
)
except Exception as e:
logger.error(f"资源转存异常: {str(e)}")
self.post_message(
channel=channel,
title="转存错误",
text=f"转存过程中发生错误: {str(e)}",
userid=userid
)
def fallback_to_moviepilot_search(self, title: str, channel: str, userid: str): def fallback_to_moviepilot_search(self, title: str, channel: str, userid: str):
"""回退到MoviePilot原始搜索功能""" """回退到MoviePilot原始搜索功能"""
logger.info(f"启动MoviePilot原始搜索: {title}") logger.info(f"启动MoviePilot原始搜索: {title}")
@ -1321,8 +1697,22 @@ class NullbrSearch(_PluginBase):
""" """
退出插件 退出插件
""" """
if self._client and hasattr(self._client, '_session'): try:
self._client._session.close() # 清理Nullbr客户端
self._client = None if self._client and hasattr(self._client, '_session'):
self._enabled = False self._client._session.close()
logger.info("Nullbr资源搜索插件已停止") self._client = None
# 清理CMS客户端
if self._cms_client and hasattr(self._cms_client, 'session'):
self._cms_client.session.close()
self._cms_client = None
# 清理缓存
self._user_search_cache.clear()
self._user_resource_cache.clear()
self._enabled = False
logger.info("Nullbr资源搜索插件已停止")
except Exception as e:
logger.error(f"插件停止异常: {str(e)}")