diff --git a/package.json b/package.json deleted file mode 100644 index 742e84f..0000000 --- a/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "nullbrsearch": { - "name": "Nullbr资源搜索", - "description": "优先使用Nullbr API搜索影视资源,支持115网盘、磁力、ed2k、m3u8等多种资源类型。在MoviePilot搜索其他资源站之前优先查找Nullbr资源,提高搜索效率。", - "labels": "资源", - "version": "1.0.4", - "icon": "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png", - "author": "Hqyel", - "level": 1, - "history": { - "v1.0.4": "初始上架版本" - } - } -} \ No newline at end of file diff --git a/package.v2.json b/package.v2.json index 742e84f..9aaa23d 100644 --- a/package.v2.json +++ b/package.v2.json @@ -3,12 +3,13 @@ "name": "Nullbr资源搜索", "description": "优先使用Nullbr API搜索影视资源,支持115网盘、磁力、ed2k、m3u8等多种资源类型。在MoviePilot搜索其他资源站之前优先查找Nullbr资源,提高搜索效率。", "labels": "资源", - "version": "1.0.4", + "version": "1.0.5", "icon": "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png", "author": "Hqyel", "level": 1, "history": { - "v1.0.4": "初始上架版本" + "v1.0.4": "初始上架版本", + "v1.0.5": "增加资源优先度设置" } } } \ No newline at end of file diff --git a/plugins.v2/nullbrsearch/__init__.py b/plugins.v2/nullbrsearch/__init__.py index 8a94ac8..c1f2234 100644 --- a/plugins.v2/nullbrsearch/__init__.py +++ b/plugins.v2/nullbrsearch/__init__.py @@ -168,7 +168,7 @@ class NullbrSearch(_PluginBase): plugin_name = "Nullbr资源搜索" plugin_desc = "优先使用Nullbr API搜索影视资源,支持多种资源类型(115网盘、磁力、ed2k、m3u8)" plugin_icon = "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png" - plugin_version = "1.0.4" + plugin_version = "1.0.5" plugin_author = "Hqyel" author_url = "https://github.com/Hqyel" plugin_config_prefix = "nullbr_" @@ -180,7 +180,7 @@ class NullbrSearch(_PluginBase): self._enabled = False self._app_id = None self._api_key = None - self._resource_priority = ["115", "magnet", "video", "ed2k"] + self._resource_priority = ["115", "magnet", "ed2k", "video"] # 默认优先级 self._enable_115 = True self._enable_magnet = True self._enable_video = True @@ -196,12 +196,27 @@ class NullbrSearch(_PluginBase): self._enabled = config.get("enabled", False) self._app_id = config.get("app_id") self._api_key = config.get("api_key") - self._resource_priority = config.get("resource_priority", ["115", "magnet", "video", "ed2k"]) + + # 构建资源优先级列表 + priority_list = [] + for i in range(1, 5): + priority = config.get(f"priority_{i}") + if priority and priority not in priority_list: + priority_list.append(priority) + + # 如果配置不完整,使用默认优先级 + if len(priority_list) < 4: + self._resource_priority = ["115", "magnet", "ed2k", "video"] + else: + self._resource_priority = priority_list + self._enable_115 = config.get("enable_115", True) self._enable_magnet = config.get("enable_magnet", True) self._enable_video = config.get("enable_video", True) self._enable_ed2k = config.get("enable_ed2k", True) self._search_timeout = config.get("search_timeout", 30) + + logger.info(f"Nullbr资源优先级设置: {' > '.join(self._resource_priority)}") # 初始化API客户端 if self._enabled and self._app_id: @@ -395,6 +410,124 @@ class NullbrSearch(_PluginBase): } ] }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': {'cols': 12}, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal' + }, + 'content': [ + { + 'component': 'span', + 'text': '🎯 资源优先级设置 - 自动按优先级获取资源(可拖拽排序)' + } + ] + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'priority_1', + 'label': '第一优先级', + 'items': [ + {'title': '115网盘', 'value': '115'}, + {'title': '磁力链接', 'value': 'magnet'}, + {'title': 'ED2K链接', 'value': 'ed2k'}, + {'title': 'M3U8视频', 'value': 'video'} + ], + 'hint': '优先获取的资源类型', + 'persistent-hint': True + } + } + ] + }, + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'priority_2', + 'label': '第二优先级', + 'items': [ + {'title': '115网盘', 'value': '115'}, + {'title': '磁力链接', 'value': 'magnet'}, + {'title': 'ED2K链接', 'value': 'ed2k'}, + {'title': 'M3U8视频', 'value': 'video'} + ], + 'hint': '第二选择的资源类型', + 'persistent-hint': True + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'priority_3', + 'label': '第三优先级', + 'items': [ + {'title': '115网盘', 'value': '115'}, + {'title': '磁力链接', 'value': 'magnet'}, + {'title': 'ED2K链接', 'value': 'ed2k'}, + {'title': 'M3U8视频', 'value': 'video'} + ], + 'hint': '第三选择的资源类型', + 'persistent-hint': True + } + } + ] + }, + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'priority_4', + 'label': '第四优先级', + 'items': [ + {'title': '115网盘', 'value': '115'}, + {'title': '磁力链接', 'value': 'magnet'}, + {'title': 'ED2K链接', 'value': 'ed2k'}, + {'title': 'M3U8视频', 'value': 'video'} + ], + 'hint': '最后选择的资源类型', + 'persistent-hint': True + } + } + ] + } + ] + }, { 'component': 'VRow', 'content': [ @@ -439,6 +572,10 @@ class NullbrSearch(_PluginBase): "enable_magnet": True, "enable_video": True, "enable_ed2k": True, + "priority_1": "115", + "priority_2": "magnet", + "priority_3": "ed2k", + "priority_4": "video", "search_timeout": 30 } @@ -779,8 +916,8 @@ class NullbrSearch(_PluginBase): if self._api_key: reply_text += "📋 使用方法:\n" - reply_text += "• 发送数字选择项目: 如 \"1\"\n" - reply_text += "• 发送数字.资源类型获取链接: 如 \"1.115\" \"2.magnet\"" + reply_text += f"• 发送数字自动获取资源: 如 \"1\" (优先级: {' > '.join(self._resource_priority)})\n" + reply_text += "• 手动指定资源类型: 如 \"1.115\" \"2.magnet\" (可选)" else: reply_text += "💡 提示: 请配置API_KEY以获取下载链接" @@ -829,44 +966,54 @@ class NullbrSearch(_PluginBase): title = selected.get('title', '未知标题') media_type = selected.get('media_type', 'unknown') year = selected.get('release_date', selected.get('first_air_date', ''))[:4] if selected.get('release_date') or selected.get('first_air_date') else '' + tmdbid = selected.get('tmdbid') - # 显示详细信息 - reply_text = f"📺 选择的资源: {title}" - if year: - reply_text += f" ({year})" - reply_text += f"\n类型: {'电影' if media_type == 'movie' else '剧集' if media_type == 'tv' else media_type}" - reply_text += f"\nTMDB ID: {selected.get('tmdbid')}" - - if selected.get('overview'): - reply_text += f"\n简介: {selected.get('overview')[:100]}..." - - # 显示可用的资源类型 - reply_text += f"\n\n🔗 可用资源类型:" - resource_options = [] - - if selected.get('115-flg') and self._enable_115: - resource_options.append(f"• 115网盘: 发送 \"{number}.115\"") - if selected.get('magnet-flg') and self._enable_magnet: - resource_options.append(f"• 磁力链接: 发送 \"{number}.magnet\"") - if selected.get('video-flg') and self._enable_video: - resource_options.append(f"• 在线观看: 发送 \"{number}.video\"") - if selected.get('ed2k-flg') and self._enable_ed2k: - resource_options.append(f"• ED2K链接: 发送 \"{number}.ed2k\"") - - if resource_options: - reply_text += f"\n" + "\n".join(resource_options) + if not self._api_key: + # 如果没有API_KEY,显示详细信息 + reply_text = f"📺 选择的资源: {title}" + if year: + reply_text += f" ({year})" + reply_text += f"\n类型: {'电影' if media_type == 'movie' else '剧集' if media_type == 'tv' else media_type}" + reply_text += f"\nTMDB ID: {tmdbid}" - if not self._api_key: + if selected.get('overview'): + reply_text += f"\n简介: {selected.get('overview')[:100]}..." + + # 显示可用的资源类型 + reply_text += f"\n\n🔗 可用资源类型:" + resource_options = [] + + if selected.get('115-flg') and self._enable_115: + resource_options.append(f"• 115网盘") + if selected.get('magnet-flg') and self._enable_magnet: + resource_options.append(f"• 磁力链接") + if selected.get('video-flg') and self._enable_video: + resource_options.append(f"• 在线观看") + if selected.get('ed2k-flg') and self._enable_ed2k: + resource_options.append(f"• ED2K链接") + + if resource_options: + reply_text += f"\n" + "\n".join(resource_options) reply_text += "\n\n⚠️ 注意: 需要配置API_KEY才能获取具体下载链接" + else: + reply_text += f"\n暂无可用资源类型" + + self.post_message( + channel=channel, + title="资源详情", + text=reply_text, + userid=userid + ) else: - reply_text += f"\n暂无可用资源类型" - - self.post_message( - channel=channel, - title="资源详情", - text=reply_text, - userid=userid - ) + # 如果有API_KEY,直接按优先级获取资源 + self.post_message( + channel=channel, + title="获取中", + text=f"正在按优先级获取「{title}」的资源...", + userid=userid + ) + + self.get_resources_by_priority(selected, channel, userid) except Exception as e: logger.error(f"处理资源选择异常: {str(e)}") @@ -1018,6 +1165,92 @@ class NullbrSearch(_PluginBase): userid=userid ) + def get_resources_by_priority(self, selected: dict, channel: str, userid: str): + """按优先级获取资源""" + try: + title = selected.get('title', '未知标题') + media_type = selected.get('media_type', 'unknown') + tmdbid = selected.get('tmdbid') + + if not tmdbid: + self.post_message( + channel=channel, + title="错误", + text="该资源缺少TMDB ID,无法获取下载链接。", + userid=userid + ) + return + + logger.info(f"按优先级获取资源: {title} (TMDB: {tmdbid})") + logger.info(f"优先级顺序: {' > '.join(self._resource_priority)}") + + # 按优先级尝试获取资源 + for priority_type in self._resource_priority: + # 检查该资源类型是否可用 + flag_key = f"{priority_type}-flg" + if not selected.get(flag_key): + logger.info(f"跳过 {priority_type}: 资源不可用") + continue + + # 检查该资源类型是否启用 + enable_key = f"_enable_{priority_type}" + if not getattr(self, enable_key, True): + logger.info(f"跳过 {priority_type}: 已在配置中禁用") + continue + + logger.info(f"尝试获取 {priority_type} 资源...") + + # 调用相应的API获取资源 + resources = None + if media_type == 'movie': + resources = self._client.get_movie_resources(tmdbid, priority_type) + elif media_type == 'tv': + resources = self._client.get_tv_resources(tmdbid, priority_type) + + if resources and resources.get(priority_type): + # 找到资源,发送结果并结束 + resource_name = { + '115': '115网盘', + 'magnet': '磁力链接', + 'ed2k': 'ED2K链接', + 'video': 'M3U8视频' + }.get(priority_type, priority_type) + + logger.info(f"成功获取 {priority_type} 资源,共 {len(resources[priority_type])} 个") + + self.post_message( + channel=channel, + title="获取成功", + text=f"✅ 已获取「{title}」的{resource_name}资源", + userid=userid + ) + + # 格式化并发送资源链接 + self._format_and_send_resources(resources, priority_type, title, channel, userid) + return + else: + logger.info(f"{priority_type} 资源不可用,尝试下一优先级") + + # 所有优先级都没有找到资源,回退到MoviePilot搜索 + logger.info(f"所有优先级资源都不可用,回退到MoviePilot搜索") + self.post_message( + channel=channel, + title="切换搜索", + text=f"Nullbr没有找到「{title}」的任何资源,正在使用MoviePilot原始搜索...", + userid=userid + ) + + self.fallback_to_moviepilot_search(title, channel, 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): """回退到MoviePilot原始搜索功能""" logger.info(f"启动MoviePilot原始搜索: {title}") diff --git a/plugins/nullbrsearch/__init__.py b/plugins/nullbrsearch/__init__.py deleted file mode 100644 index 8a94ac8..0000000 --- a/plugins/nullbrsearch/__init__.py +++ /dev/null @@ -1,1095 +0,0 @@ -import re -import time -from typing import Any, List, Dict, Tuple, Optional -import requests -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry - -from app.core.config import settings -from app.core.event import eventmanager, Event -from app.log import logger -from app.plugins import _PluginBase -from app.schemas.types import EventType - - -class NullbrApiClient: - """Nullbr API客户端""" - - def __init__(self, app_id: str, api_key: str = None): - self._app_id = app_id - self._api_key = api_key - self._base_url = "https://api.nullbr.eu.org" - - # 配置请求会话 - self._session = requests.Session() - self._session.headers.update({ - 'User-Agent': 'MoviePilot-NullbrSearch/1.0.4', - 'Content-Type': 'application/json' - }) - - # 配置重试策略 - try: - retry_strategy = Retry( - total=3, - status_forcelist=[429, 500, 502, 503, 504], - allowed_methods=["HEAD", "GET", "OPTIONS"], - backoff_factor=1 - ) - except TypeError: - try: - retry_strategy = Retry( - total=3, - status_forcelist=[429, 500, 502, 503, 504], - method_whitelist=["HEAD", "GET", "OPTIONS"], - backoff_factor=1 - ) - except Exception: - retry_strategy = Retry(total=3, backoff_factor=1) - - adapter = HTTPAdapter(max_retries=retry_strategy) - self._session.mount("http://", adapter) - self._session.mount("https://", adapter) - - def search(self, query: str, page: int = 1) -> Optional[Dict]: - """搜索媒体资源""" - try: - # 根据API文档,APP_ID应该放在Header中 - headers = {'X-APP-ID': self._app_id} - - # API_KEY如果存在,也放在Header中 - if self._api_key: - headers['X-API-KEY'] = self._api_key - - params = { - 'query': query, - 'page': page - } - - logger.info(f"请求参数: {params}") - logger.info(f"请求头: X-APP-ID={self._app_id}, X-API-KEY={'已设置' if self._api_key else '未设置'}") - - response = self._session.get( - f"{self._base_url}/search", - params=params, - headers=headers, - timeout=30 - ) - - logger.info(f"响应状态码: {response.status_code}") - - if response.status_code == 200: - return response.json() - elif response.status_code == 403: - logger.error("Nullbr API认证失败,请检查APP_ID") - return None - else: - logger.warning(f"Nullbr API搜索失败: {response.status_code}, 响应内容: {response.text}") - return None - - except Exception as e: - logger.error(f"Nullbr API请求异常: {str(e)}") - return None - - def get_movie_resources(self, tmdbid: int, resource_type: str = "115") -> Optional[Dict]: - """获取电影资源链接""" - if not self._api_key: - logger.warning("获取资源链接需要API_KEY") - return None - - try: - headers = {'X-APP-ID': self._app_id, 'X-API-KEY': self._api_key} - - response = self._session.get( - f"{self._base_url}/movie/{tmdbid}/{resource_type}", - headers=headers, - timeout=30 - ) - - logger.info(f"获取电影资源响应状态码: {response.status_code}") - - if response.status_code == 200: - return response.json() - elif response.status_code == 401: - logger.error("API_KEY权限不足") - return None - elif response.status_code == 403: - logger.error("API认证失败") - return None - elif response.status_code == 429: - logger.warning("API请求过快,请稍后重试") - return None - else: - logger.warning(f"获取电影资源失败: {response.status_code}, 响应: {response.text}") - return None - - except Exception as e: - logger.error(f"获取电影资源异常: {str(e)}") - return None - - def get_tv_resources(self, tmdbid: int, resource_type: str = "115") -> Optional[Dict]: - """获取剧集资源链接""" - if not self._api_key: - logger.warning("获取资源链接需要API_KEY") - return None - - try: - headers = {'X-APP-ID': self._app_id, 'X-API-KEY': self._api_key} - - response = self._session.get( - f"{self._base_url}/tv/{tmdbid}/{resource_type}", - headers=headers, - timeout=30 - ) - - logger.info(f"获取剧集资源响应状态码: {response.status_code}") - - if response.status_code == 200: - return response.json() - elif response.status_code == 401: - logger.error("API_KEY权限不足") - return None - elif response.status_code == 403: - logger.error("API认证失败") - return None - elif response.status_code == 429: - logger.warning("API请求过快,请稍后重试") - return None - else: - logger.warning(f"获取剧集资源失败: {response.status_code}, 响应: {response.text}") - return None - - except Exception as e: - logger.error(f"获取剧集资源异常: {str(e)}") - return None - - -class NullbrSearch(_PluginBase): - # 插件基本信息 - plugin_name = "Nullbr资源搜索" - plugin_desc = "优先使用Nullbr API搜索影视资源,支持多种资源类型(115网盘、磁力、ed2k、m3u8)" - plugin_icon = "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png" - plugin_version = "1.0.4" - plugin_author = "Hqyel" - author_url = "https://github.com/Hqyel" - plugin_config_prefix = "nullbr_" - plugin_order = 1 - auth_level = 1 - - def __init__(self): - super().__init__() - self._enabled = False - self._app_id = None - self._api_key = None - self._resource_priority = ["115", "magnet", "video", "ed2k"] - self._enable_115 = True - self._enable_magnet = True - self._enable_video = True - self._enable_ed2k = True - self._search_timeout = 30 - self._client = None - - # 用户搜索结果缓存 - self._user_search_cache = {} # {userid: {'results': [...], 'timestamp': time.time()}} - - def init_plugin(self, config: dict = None): - if config: - self._enabled = config.get("enabled", False) - self._app_id = config.get("app_id") - self._api_key = config.get("api_key") - self._resource_priority = config.get("resource_priority", ["115", "magnet", "video", "ed2k"]) - self._enable_115 = config.get("enable_115", True) - self._enable_magnet = config.get("enable_magnet", True) - self._enable_video = config.get("enable_video", True) - self._enable_ed2k = config.get("enable_ed2k", True) - self._search_timeout = config.get("search_timeout", 30) - - # 初始化API客户端 - if self._enabled and self._app_id: - try: - self._client = NullbrApiClient(self._app_id, self._api_key) - logger.info("Nullbr资源搜索插件已启动") - except Exception as e: - logger.error(f"Nullbr插件初始化失败: {str(e)}") - self._enabled = False - else: - if not self._app_id: - logger.warning("Nullbr插件配置错误: 缺少APP_ID") - self._client = None - - 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}, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '🌟 Nullbr资源搜索插件将优先使用Nullbr API查找资源。支持115网盘、磁力、ed2k、m3u8等多种资源类型。' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enabled', - 'label': '启用插件', - 'hint': '开启后插件将开始工作,优先搜索Nullbr资源', - 'persistent-hint': True - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'app_id', - 'label': 'APP_ID *', - 'placeholder': '请输入Nullbr API的APP_ID', - 'hint': '必填:用于API认证的应用ID', - 'persistent-hint': True, - 'clearable': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'api_key', - 'label': 'API_KEY', - 'placeholder': '请输入Nullbr API的API_KEY', - 'hint': '可选:用于获取资源链接,没有则只能搜索不能获取下载链接', - 'persistent-hint': True, - 'clearable': True, - 'type': 'password' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12}, - 'content': [ - { - 'component': 'VExpansionPanels', - 'content': [ - { - 'component': 'VExpansionPanel', - 'props': {'title': '⚙️ 高级设置'}, - 'content': [ - { - 'component': 'VExpansionPanelText', - 'content': [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 3}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enable_115', - 'label': '115网盘', - 'hint': '搜索115网盘分享资源', - 'persistent-hint': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 3}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enable_magnet', - 'label': '磁力链接', - 'hint': '搜索磁力链接资源', - 'persistent-hint': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 3}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enable_video', - 'label': 'M3U8视频', - 'hint': '搜索在线观看资源', - 'persistent-hint': True - } - } - ] - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 3}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enable_ed2k', - 'label': 'ED2K链接', - 'hint': '搜索ED2K链接资源', - 'persistent-hint': True - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - '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 - } - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ], { - "enabled": False, - "app_id": "", - "api_key": "", - "enable_115": True, - "enable_magnet": True, - "enable_video": True, - "enable_ed2k": True, - "search_timeout": 30 - } - - def get_page(self) -> List[dict]: - stats = {"total_searches": 0, "success_searches": 0, "failed_searches": 0, "last_search": "从未"} - return [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12}, - 'content': [ - { - 'component': 'VCard', - 'props': {'class': 'mb-4'}, - 'content': [ - { - 'component': 'VCardTitle', - 'props': {'text': '🌟 Nullbr资源搜索状态'} - }, - { - 'component': 'VCardText', - 'content': [ - { - 'component': 'VList', - 'content': [ - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"插件状态: {'🟢 运行中' if self._enabled else '🔴 已停止'}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"API认证: {'✅ 已配置' if self._app_id else '❌ 未配置'}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"资源获取: {'✅ 可用' if self._api_key else '❌ 仅搜索'}"} - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VCard', - 'content': [ - { - 'component': 'VCardTitle', - 'props': {'text': '📊 支持的资源类型'} - }, - { - 'component': 'VCardText', - 'content': [ - { - 'component': 'VList', - 'content': [ - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"115网盘: {'✅ 启用' if self._enable_115 else '❌ 禁用'}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"磁力链接: {'✅ 启用' if self._enable_magnet else '❌ 禁用'}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"M3U8视频: {'✅ 启用' if self._enable_video else '❌ 禁用'}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"ED2K链接: {'✅ 启用' if self._enable_ed2k else '❌ 禁用'}"} - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VCard', - 'content': [ - { - 'component': 'VCardTitle', - 'props': {'text': '📈 使用统计'} - }, - { - 'component': 'VCardText', - 'content': [ - { - 'component': 'VList', - 'content': [ - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"总搜索次数: {stats.get('total_searches', 0)}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"成功次数: {stats.get('success_searches', 0)}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"失败次数: {stats.get('failed_searches', 0)}"} - } - ] - }, - { - 'component': 'VListItem', - 'content': [ - { - 'component': 'VListItemTitle', - 'props': {'text': f"最后搜索: {stats.get('last_search', '从未')}"} - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12}, - 'content': [ - { - 'component': 'VCard', - 'content': [ - { - 'component': 'VCardTitle', - 'props': {'text': '💡 使用说明'} - }, - { - 'component': 'VCardText', - 'props': { - 'text': '''🔑 配置步骤: - 1. 在插件设置中填入您的 Nullbr API APP_ID (必填) - 2. 如需获取下载链接,请填入 API_KEY (可选) - 3. 根据需要启用不同的资源类型 - 4. 保存配置并启用插件 - - ⚡ 工作原理: - • 插件通过API接口提供Nullbr资源搜索服务 - • 可在MoviePilot中手动调用搜索功能 - • 支持电影、剧集、合集等多种媒体类型 - • 支持115网盘、磁力、ed2k、m3u8等多种资源格式 - - 📞 技术支持: - 如遇问题请检查 MoviePilot 日志中的错误信息''' - } - } - ] - } - ] - } - ] - } - ] - - @eventmanager.register(EventType.UserMessage) - def talk(self, event: Event): - """ - 监听用户消息,识别搜索请求和编号选择 - """ - if not self._enabled or not self._client: - return - - text = event.event_data.get("text") - userid = event.event_data.get("userid") - channel = event.event_data.get("channel") - - if not text: - return - - logger.info(f"收到用户消息: {text}") - - # 检查是否为回退搜索触发的消息,避免无限循环 - if event.event_data.get('source') == 'nullbr_fallback': - logger.info("检测到回退搜索消息,跳过处理避免循环") - return - - # 先检查是否为获取资源的请求(包含问号的情况,如 "1.115?" "2.magnet?") - clean_text = text.rstrip('??').strip() - if re.match(r'^\d+\.(115|magnet|video|ed2k)$', clean_text): - parts = clean_text.split('.') - number = int(parts[0]) - resource_type = parts[1] - logger.info(f"检测到资源获取请求: {number}.{resource_type}") - self.handle_get_resources(number, resource_type, channel, userid) - - # 检查是否为编号选择(纯数字,包含问号的情况) - elif clean_text.isdigit(): - number = int(clean_text) - logger.info(f"检测到编号选择: {number}") - self.handle_resource_selection(number, channel, userid) - - # 检查是否为搜索请求(以?结尾,但不是数字或资源请求) - elif text.endswith('?') or text.endswith('?'): - # 提取搜索关键词(去掉问号) - keyword = clean_text - - if keyword: - logger.info(f"检测到搜索请求: {keyword}") - self.search_and_reply(keyword, channel, userid) - - def search_and_reply(self, keyword: str, channel: str, userid: str): - """执行搜索并回复结果""" - try: - # 调用Nullbr API搜索 - result = self._client.search(keyword) - - if not result or not result.get('items'): - # Nullbr没有搜索结果,回退到MoviePilot原始搜索 - logger.info(f"Nullbr未找到「{keyword}」的搜索结果,回退到MoviePilot搜索") - self.post_message( - channel=channel, - title="切换搜索", - text=f"Nullbr没有找到「{keyword}」的资源,正在使用MoviePilot原始搜索...", - userid=userid - ) - - # 调用MoviePilot的原始搜索功能 - self.fallback_to_moviepilot_search(keyword, channel, userid) - return - - items = result.get('items', [])[:10] # 最多显示10个结果 - - # 缓存搜索结果 - self._user_search_cache[userid] = { - 'results': items, - 'keyword': keyword, - 'timestamp': time.time() - } - - # 格式化搜索结果 - reply_text = f"🔍 找到「{keyword}」的资源:\n\n" - - for i, item in enumerate(items, 1): - title = item.get('title', '未知标题') - media_type = item.get('media_type', 'unknown') - year = item.get('release_date', item.get('first_air_date', ''))[:4] if item.get('release_date') or item.get('first_air_date') else '' - - # 检查可用的资源类型 - available_types = [] - if item.get('115-flg') and self._enable_115: - available_types.append('115') - if item.get('magnet-flg') and self._enable_magnet: - available_types.append('磁力') - if item.get('video-flg') and self._enable_video: - available_types.append('在线') - if item.get('ed2k-flg') and self._enable_ed2k: - available_types.append('ed2k') - - type_text = '、'.join(available_types) if available_types else '无' - media_text = '电影' if media_type == 'movie' else '剧集' if media_type == 'tv' else media_type - - reply_text += f"{i}. {title}" - if year: - reply_text += f" ({year})" - reply_text += f" - {media_text}\n" - reply_text += f" 资源: {type_text}\n\n" - - if len(result.get('items', [])) > 10: - reply_text += f"... 还有 {len(result.get('items', [])) - 10} 个结果\n\n" - - if self._api_key: - reply_text += "📋 使用方法:\n" - reply_text += "• 发送数字选择项目: 如 \"1\"\n" - reply_text += "• 发送数字.资源类型获取链接: 如 \"1.115\" \"2.magnet\"" - else: - reply_text += "💡 提示: 请配置API_KEY以获取下载链接" - - self.post_message( - channel=channel, - title="Nullbr搜索结果", - text=reply_text, - userid=userid - ) - - except Exception as e: - logger.error(f"搜索处理异常: {str(e)}") - self.post_message( - channel=channel, - title="搜索错误", - text=f"搜索「{keyword}」时出现错误: {str(e)}", - userid=userid - ) - - def handle_resource_selection(self, number: int, channel: str, userid: str): - """处理用户的编号选择""" - try: - # 检查缓存 - cache = self._user_search_cache.get(userid) - if not cache or time.time() - cache['timestamp'] > 3600: # 缓存1小时 - self.post_message( - channel=channel, - title="提示", - text="搜索结果已过期,请重新搜索。", - userid=userid - ) - return - - results = cache['results'] - if number < 1 or number > len(results): - self.post_message( - channel=channel, - title="提示", - text=f"请输入有效的编号 (1-{len(results)})。", - userid=userid - ) - return - - # 获取选中的项目 - selected = results[number - 1] - title = selected.get('title', '未知标题') - media_type = selected.get('media_type', 'unknown') - year = selected.get('release_date', selected.get('first_air_date', ''))[:4] if selected.get('release_date') or selected.get('first_air_date') else '' - - # 显示详细信息 - reply_text = f"📺 选择的资源: {title}" - if year: - reply_text += f" ({year})" - reply_text += f"\n类型: {'电影' if media_type == 'movie' else '剧集' if media_type == 'tv' else media_type}" - reply_text += f"\nTMDB ID: {selected.get('tmdbid')}" - - if selected.get('overview'): - reply_text += f"\n简介: {selected.get('overview')[:100]}..." - - # 显示可用的资源类型 - reply_text += f"\n\n🔗 可用资源类型:" - resource_options = [] - - if selected.get('115-flg') and self._enable_115: - resource_options.append(f"• 115网盘: 发送 \"{number}.115\"") - if selected.get('magnet-flg') and self._enable_magnet: - resource_options.append(f"• 磁力链接: 发送 \"{number}.magnet\"") - if selected.get('video-flg') and self._enable_video: - resource_options.append(f"• 在线观看: 发送 \"{number}.video\"") - if selected.get('ed2k-flg') and self._enable_ed2k: - resource_options.append(f"• ED2K链接: 发送 \"{number}.ed2k\"") - - if resource_options: - reply_text += f"\n" + "\n".join(resource_options) - - if not self._api_key: - reply_text += "\n\n⚠️ 注意: 需要配置API_KEY才能获取具体下载链接" - else: - reply_text += f"\n暂无可用资源类型" - - self.post_message( - channel=channel, - title="资源详情", - text=reply_text, - userid=userid - ) - - except Exception as e: - logger.error(f"处理资源选择异常: {str(e)}") - self.post_message( - channel=channel, - title="错误", - text=f"处理选择时出现错误: {str(e)}", - userid=userid - ) - - def handle_get_resources(self, number: int, resource_type: str, channel: str, userid: str): - """处理获取具体资源链接的请求""" - try: - # 检查API_KEY - if not self._api_key: - self.post_message( - channel=channel, - title="配置错误", - text="获取下载链接需要配置API_KEY,请在插件设置中添加。", - userid=userid - ) - return - - # 检查缓存 - cache = self._user_search_cache.get(userid) - if not cache or time.time() - cache['timestamp'] > 3600: - self.post_message( - channel=channel, - title="提示", - text="搜索结果已过期,请重新搜索。", - userid=userid - ) - return - - results = cache['results'] - if number < 1 or number > len(results): - self.post_message( - channel=channel, - title="提示", - text=f"请输入有效的编号 (1-{len(results)})。", - userid=userid - ) - return - - # 获取选中的项目 - selected = results[number - 1] - title = selected.get('title', '未知标题') - media_type = selected.get('media_type', 'unknown') - tmdbid = selected.get('tmdbid') - - if not tmdbid: - self.post_message( - channel=channel, - title="错误", - text="该资源缺少TMDB ID,无法获取下载链接。", - userid=userid - ) - return - - # 发送获取中的提示 - self.post_message( - channel=channel, - title="获取中", - text=f"正在获取「{title}」的{resource_type}资源...", - userid=userid - ) - - # 调用相应的API获取资源 - resources = None - if media_type == 'movie': - resources = self._client.get_movie_resources(tmdbid, resource_type) - elif media_type == 'tv': - resources = self._client.get_tv_resources(tmdbid, resource_type) - - if not resources: - # Nullbr没有找到资源,回退到MoviePilot原始搜索 - logger.info(f"Nullbr未找到「{title}」的{resource_type}资源,回退到MoviePilot搜索") - self.post_message( - channel=channel, - title="切换搜索", - text=f"Nullbr没有找到「{title}」的{resource_type}资源,正在使用MoviePilot原始搜索...", - userid=userid - ) - - # 调用MoviePilot的原始搜索功能 - self.fallback_to_moviepilot_search(title, channel, userid) - return - - # 格式化资源链接 - self._format_and_send_resources(resources, resource_type, title, channel, userid) - - except Exception as e: - logger.error(f"获取资源链接异常: {str(e)}") - self.post_message( - channel=channel, - title="错误", - text=f"获取资源链接时出现错误: {str(e)}", - userid=userid - ) - - def _format_and_send_resources(self, resources: dict, resource_type: str, title: str, channel: str, userid: str): - """格式化并发送资源链接""" - try: - reply_text = f"🎯 「{title}」的{resource_type}资源:\n\n" - - if resource_type == "115": - resource_list = resources.get('115', []) - for i, res in enumerate(resource_list[:10], 1): # 最多显示10个 - reply_text += f"{i}. {res.get('title', '未知')}\n" - reply_text += f" 大小: {res.get('size', '未知')}\n" - reply_text += f" 链接: {res.get('share_link', '无')}\n\n" - - elif resource_type == "magnet": - resource_list = resources.get('magnet', []) - for i, res in enumerate(resource_list[:10], 1): - reply_text += f"{i}. {res.get('name', '未知')}\n" - reply_text += f" 大小: {res.get('size', '未知')}\n" - reply_text += f" 分辨率: {res.get('resolution', '未知')}\n" - reply_text += f" 中文字幕: {'✅' if res.get('zh_sub') else '❌'}\n" - reply_text += f" 磁力: {res.get('magnet', '无')}\n\n" - - elif resource_type in ["video", "ed2k"]: - resource_list = resources.get(resource_type, []) - for i, res in enumerate(resource_list[:10], 1): - reply_text += f"{i}. {res.get('name', res.get('title', '未知'))}\n" - if res.get('size'): - reply_text += f" 大小: {res.get('size')}\n" - reply_text += f" 链接: {res.get('url', res.get('link', '无'))}\n\n" - - if len(reply_text) > 4000: # Telegram消息长度限制 - reply_text = reply_text[:3900] + "...\n\n(内容过长已截断)" - - if not reply_text.strip().endswith('无'): - reply_text += f"📊 共找到 {len(resources.get(resource_type, []))} 个资源" - - self.post_message( - channel=channel, - title=f"{resource_type.upper()}资源", - text=reply_text, - 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): - """回退到MoviePilot原始搜索功能""" - logger.info(f"启动MoviePilot原始搜索: {title}") - - # 直接尝试各种搜索方法,不再触发事件避免循环 - self.try_alternative_search(title, channel, userid) - - def try_alternative_search(self, title: str, channel: str, userid: str): - """尝试其他搜索方式""" - try: - logger.info(f"尝试MoviePilot原始搜索: {title}") - - # 简化策略:直接发送搜索建议和提示 - # 避免复杂的模块调用导致的错误 - - success = False - - # 方法1: 尝试调用站点助手的简单方法 - try: - from app.helper.sites import SitesHelper - sites_helper = SitesHelper() - - # 只是检查是否有配置的站点 - if hasattr(sites_helper, 'get_indexers'): - indexers = sites_helper.get_indexers() - if indexers: - logger.info(f"检测到 {len(indexers)} 个配置的站点") - - self.post_message( - channel=channel, - title="搜索提示", - text=f"🔍 Nullbr未找到「{title}」的资源\n\n" + - f"💡 系统检测到您已配置 {len(indexers)} 个搜索站点\n" + - f"建议通过以下方式继续搜索:\n\n" + - f"🌐 MoviePilot Web界面搜索\n" + - f"📱 其他搜索渠道\n" + - f"⚙️ 检查站点配置状态", - userid=userid - ) - success = True - - except Exception as e: - logger.warning(f"站点检测失败: {str(e)}") - - # 如果上面的方法也失败,发送通用建议 - if not success: - self._send_manual_search_suggestion(title, channel, userid) - - except Exception as e: - logger.error(f"备用搜索失败: {str(e)}") - self._send_manual_search_suggestion(title, channel, userid) - - - def _send_manual_search_suggestion(self, title: str, channel: str, userid: str): - """发送手动搜索建议""" - self.post_message( - channel=channel, - title="搜索建议", - text=f"📋 「{title}」未找到资源,建议:\n\n" + - f"🔍 在MoviePilot Web界面搜索\n" + - f"⚙️ 检查资源站点配置\n" + - f"🔄 尝试其他关键词\n" + - f"📱 使用其他搜索渠道", - userid=userid - ) - - def stop_service(self): - """ - 退出插件 - """ - if self._client and hasattr(self._client, '_session'): - self._client._session.close() - self._client = None - self._enabled = False - logger.info("Nullbr资源搜索插件已停止") \ No newline at end of file diff --git a/plugins/nullbrsearch/requirements.txt b/plugins/nullbrsearch/requirements.txt deleted file mode 100644 index 79fda6e..0000000 --- a/plugins/nullbrsearch/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests>=2.25.0 -urllib3>=1.26.0 \ No newline at end of file