Compare commits
10 Commits
0efd122242
...
3db7e3c222
| Author | SHA1 | Date | |
|---|---|---|---|
| 3db7e3c222 | |||
| a1a106a955 | |||
| c563846017 | |||
| 58a8d6052d | |||
|
|
8b3e6a4b5c | ||
|
|
ddee295fa3 | ||
| c6e7658133 | |||
|
|
9ca80684b6 | ||
| 507f01d8d0 | |||
| 7694511601 |
184
README.md
184
README.md
@ -2,151 +2,81 @@
|
||||
|
||||
## 📝 插件简介
|
||||
|
||||
Nullbr资源搜索插件是为MoviePilot-v2设计的资源搜索增强插件,通过集成Nullbr API,为用户提供优先级资源搜索功能。
|
||||
Nullbr资源搜索插件是为MoviePilot设计的智能资源搜索插件,通过集成Nullbr API,支持115网盘、磁力、ed2k、m3u8等多种资源类型的搜索和自动转存。
|
||||
|
||||
**主要特点:**
|
||||
- 🚀 **优先搜索**: 在MoviePilot搜索其他资源站之前,优先使用Nullbr API查找资源
|
||||
- 🔍 **智能跳过**: 如果Nullbr找到资源,自动跳过后续搜索,提高效率
|
||||
- 🎯 **多种资源**: 支持115网盘、磁力链接、ed2k、m3u8等多种资源类型
|
||||
- 📊 **统计监控**: 提供详细的使用统计和状态监控
|
||||
- ⚙️ **灵活配置**: 支持自定义资源类型和搜索参数
|
||||
## 🛠️ 插件安装方法
|
||||
|
||||
## 🛠️ 安装方法
|
||||
复制本仓库地址到插件源配置即可在市场搜到本插件:
|
||||
|
||||
### 方法1: 直接复制文件
|
||||
1. 将`nullbrsearch`目录复制到MoviePilot的`app/plugins/`目录下
|
||||
2. 重启MoviePilot服务
|
||||
1. 在MoviePilot设置中找到「插件」→「插件源」
|
||||
2. 添加插件源:`https://github.com/Hqyel/MoviePilot-Plugins-Third`
|
||||
3. 保存后在插件市场搜索「Nullbr资源搜索」
|
||||
4. 点击安装并配置相关参数
|
||||
|
||||
### 方法2: Git克隆
|
||||
```bash
|
||||
cd /path/to/moviepilot/app/plugins/
|
||||
git clone <repository-url> nullbrsearch
|
||||
```
|
||||
## 📖 使用方法
|
||||
|
||||
**重要:与本插件交互必须以问号结尾!**
|
||||
|
||||
### 基本搜索
|
||||
在MoviePilot的任何消息界面发送搜索请求时,必须以问号结尾:
|
||||
|
||||
- ✅ `武林外传?`
|
||||
- ✅ `权力的游戏?`
|
||||
- ✅ `复仇者联盟?`
|
||||
- ❌ `武林外传` (不会触发搜索)
|
||||
- ❌ `权力的游戏` (不会触发搜索)
|
||||
|
||||
### 资源获取流程
|
||||
1. **搜索影片**:发送 `影片名?`
|
||||
2. **选择影片**:回复数字(如 `1?`)查看资源类型
|
||||
3. **获取资源**:
|
||||
- 自动获取(按优先级):直接发送数字 `2?`
|
||||
- 指定类型:发送 `2.115?` 或 `3.magnet?`
|
||||
|
||||
### CloudMediaSync转存
|
||||
如果配置了CMS系统,获取115网盘资源后:
|
||||
- 发送资源编号即可转存:`1?`、`2?`、`3?`...
|
||||
|
||||
## ⚠️ 重要注意事项
|
||||
|
||||
**本插件与ChatGPT插件冲突!**
|
||||
|
||||
如果你同时安装了ChatGPT插件,需要:
|
||||
1. 禁用ChatGPT插件,或
|
||||
2. 调整插件优先级,确保Nullbr插件优先级更高
|
||||
|
||||
这是因为两个插件都会监听用户消息,可能产生冲突。
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 必需配置
|
||||
- **APP_ID**: Nullbr API的应用ID,用于基本搜索功能(必填)
|
||||
- **APP_ID**: Nullbr API的应用ID(必填)
|
||||
- **API_KEY**: Nullbr API的密钥(可选,用于获取下载链接)
|
||||
|
||||
### 可选配置
|
||||
- **API_KEY**: Nullbr API的密钥,用于获取具体下载链接(可选)
|
||||
- **资源类型**: 可以选择启用/禁用不同的资源类型
|
||||
- 115网盘分享
|
||||
- 磁力链接
|
||||
- M3U8在线视频
|
||||
- ED2K链接
|
||||
|
||||
### 高级设置
|
||||
- **搜索超时**: 设置API请求的超时时间(10-120秒)
|
||||
|
||||
## 📖 使用方法
|
||||
|
||||
1. **获取API密钥**: 从Nullbr官方获取APP_ID和API_KEY
|
||||
2. **配置插件**: 在MoviePilot插件设置中填入相关信息
|
||||
3. **启用插件**: 打开插件开关,插件开始工作
|
||||
4. **正常使用**: 通过MoviePilot的任何搜索功能,插件会自动优先搜索Nullbr资源
|
||||
|
||||
## 🌐 支持的API接口
|
||||
|
||||
插件提供了以下REST API接口:
|
||||
|
||||
### `/nullbr/search`
|
||||
- **方法**: GET
|
||||
- **参数**: `keyword`, `page`
|
||||
- **功能**: 搜索影视资源
|
||||
|
||||
### `/nullbr/resources`
|
||||
- **方法**: POST
|
||||
- **参数**: `media_type`, `tmdbid`, `resource_type`
|
||||
- **功能**: 获取具体资源链接
|
||||
|
||||
### `/nullbr/test`
|
||||
- **方法**: GET
|
||||
- **功能**: 测试API连接状态
|
||||
|
||||
## 📊 工作原理
|
||||
|
||||
```
|
||||
用户搜索请求
|
||||
↓
|
||||
MoviePilot接收
|
||||
↓
|
||||
Nullbr插件拦截 ← 优先级最高
|
||||
↓
|
||||
调用Nullbr API
|
||||
↓
|
||||
找到资源? → 是 → 返回结果给用户 → 结束搜索
|
||||
↓
|
||||
否
|
||||
↓
|
||||
继续搜索其他资源站
|
||||
```
|
||||
|
||||
## 🔍 支持的媒体类型
|
||||
|
||||
- **电影** (movie): 支持获取电影资源
|
||||
- **剧集** (tv): 支持获取完整剧集资源
|
||||
- **合集** (collection): 支持搜索系列合集
|
||||
- **人物** (person): 支持人物相关搜索
|
||||
|
||||
## 📈 状态监控
|
||||
|
||||
插件提供详细的使用统计:
|
||||
- 总搜索次数
|
||||
- 成功搜索次数
|
||||
- 失败搜索次数
|
||||
- 最后搜索时间
|
||||
- API连接状态
|
||||
- 资源类型启用状态
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **API限制**: 请遵守Nullbr API的使用限制和频率限制
|
||||
2. **网络连接**: 确保MoviePilot服务器能够访问`api.nullbr.eu.org`
|
||||
3. **权限要求**: API_KEY的权限级别决定了能获取的资源类型
|
||||
4. **日志监控**: 如遇问题请检查MoviePilot日志中的相关错误信息
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: 插件显示已启用但不工作**
|
||||
A: 检查APP_ID是否正确配置,查看日志中的错误信息
|
||||
|
||||
**Q: 能搜索但无法获取下载链接**
|
||||
A: 需要配置有效的API_KEY才能获取具体资源链接
|
||||
|
||||
**Q: API请求超时**
|
||||
A: 可能是网络问题,尝试增加超时时间或检查网络连接
|
||||
|
||||
**Q: 搜索结果为空**
|
||||
A: 检查搜索关键词,或者查看Nullbr API是否有该资源
|
||||
|
||||
### 日志排查
|
||||
在MoviePilot日志中搜索以下关键词:
|
||||
- `Nullbr`
|
||||
- `nullbr`
|
||||
- `NullbrSearch`
|
||||
- **资源类型**: 启用/禁用不同资源类型(115网盘、磁力、M3U8、ED2K)
|
||||
- **资源优先级**: 设置资源获取的优先顺序
|
||||
- **CloudMediaSync**: 配置115网盘资源的自动转存功能
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0.0 (2024-08-06)
|
||||
- 🎉 首次发布
|
||||
- ✅ 支持基本搜索功能
|
||||
- ✅ 支持多种资源类型
|
||||
- ✅ 提供完整的配置界面
|
||||
- ✅ 集成API接口
|
||||
- ✅ 添加使用统计功能
|
||||
### v2.0.4 (2024-08-08)
|
||||
- 🎉 移除了数据统计页面,简化插件结构
|
||||
- ✅ 优化了用户交互流程
|
||||
- ✅ 修复了与其他插件的兼容性问题
|
||||
- ✅ 改进了微信消息格式兼容性
|
||||
- ✅ 完善了CloudMediaSync转存功能
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request来改进这个插件!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本插件基于GPL-3.0许可证开源。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- 感谢MoviePilot项目提供的优秀插件框架
|
||||
- 感谢Nullbr提供的资源API服务
|
||||
- 感谢Nullbr提供的资源API服务
|
||||
- 感谢iLay1678大佬,CMS接口部分参考了他项目:[nullbr_cms_bot](https://github.com/iLay1678/nullbr_cms_bot)
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本插件基于GPL-3.0许可证开源。
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
{
|
||||
"nullbrsearch": {
|
||||
"nullbr_search": {
|
||||
"name": "Nullbr资源搜索",
|
||||
"description": "优先使用Nullbr API搜索影视资源,支持115网盘、磁力、ed2k、m3u8等多种资源类型。在MoviePilot搜索其他资源站之前优先查找Nullbr资源,提高搜索效率。",
|
||||
"description": "支持nullbr api接口直接搜索影视资源。支持115网盘、磁力、ed2k、m3u8等多种资源类型。",
|
||||
"labels": "资源",
|
||||
"version": "1.0.5",
|
||||
"version": "2.0.0",
|
||||
"icon": "https://raw.githubusercontent.com/Hqyel/MoviePilot-Plugins/main/icons/nullbr.png",
|
||||
"author": "Hqyel",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.0.4": "初始上架版本",
|
||||
"v1.0.5": "增加资源优先度设置"
|
||||
}
|
||||
"v2.0.0": "重构整理代码。"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
110
plugins.v2/nullbr_search/cms_client.py
Normal file
110
plugins.v2/nullbr_search/cms_client.py
Normal file
@ -0,0 +1,110 @@
|
||||
import requests
|
||||
import time
|
||||
from app.log import logger
|
||||
|
||||
|
||||
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'
|
||||
})
|
||||
|
||||
# CMS一般为内网服务,禁用代理访问
|
||||
self.session.proxies = {
|
||||
'http': None,
|
||||
'https': None
|
||||
}
|
||||
|
||||
# 初始化时获取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=(10, 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=(10, 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=(10, 30)
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'CMS转存请求失败: {str(e)}')
|
||||
raise
|
||||
202
plugins.v2/nullbr_search/nullbr_client.py
Normal file
202
plugins.v2/nullbr_search/nullbr_client.py
Normal file
@ -0,0 +1,202 @@
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
from typing import Dict, Optional
|
||||
from app.log import logger
|
||||
|
||||
|
||||
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, 408],
|
||||
backoff_factor=1,
|
||||
allowed_methods=["HEAD", "GET", "OPTIONS"]
|
||||
)
|
||||
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
self._session.mount("http://", adapter)
|
||||
self._session.mount("https://", adapter)
|
||||
except Exception as e:
|
||||
logger.warning(f"重试策略配置失败: {str(e)}")
|
||||
|
||||
def _make_request(self, url: str, params: dict, headers: dict, use_proxy: bool = True) -> requests.Response:
|
||||
"""发起HTTP请求,支持代理重试机制"""
|
||||
session = self._session
|
||||
|
||||
# 如果不使用代理,创建临时session
|
||||
if not use_proxy:
|
||||
session = requests.Session()
|
||||
session.headers.update(self._session.headers)
|
||||
session.proxies = {'http': None, 'https': None}
|
||||
|
||||
timeout = 5 if use_proxy else (10, 30)
|
||||
|
||||
return session.get(url, params=params, headers=headers, timeout=timeout)
|
||||
|
||||
def search(self, query: str, page: int = 1) -> Optional[Dict]:
|
||||
"""搜索媒体资源"""
|
||||
try:
|
||||
headers = {'X-APP-ID': self._app_id}
|
||||
|
||||
if self._api_key:
|
||||
headers['X-API-KEY'] = self._api_key
|
||||
|
||||
params = {
|
||||
'query': query,
|
||||
'page': page
|
||||
}
|
||||
|
||||
logger.info(f"搜索请求: {query}")
|
||||
logger.debug(f"请求参数: {params}")
|
||||
logger.debug(f"请求头: X-APP-ID={self._app_id}, X-API-KEY={'已设置' if self._api_key else '未设置'}")
|
||||
|
||||
url = f"{self._base_url}/search"
|
||||
|
||||
# 首先尝试使用系统代理
|
||||
try:
|
||||
logger.debug("尝试使用系统代理访问Nullbr API")
|
||||
response = self._make_request(url, params, headers, use_proxy=True)
|
||||
logger.info(f"使用系统代理请求成功,状态码: {response.status_code}")
|
||||
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectTimeout,
|
||||
requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e:
|
||||
logger.warning(f"系统代理访问失败: {str(e)},尝试直连")
|
||||
try:
|
||||
response = self._make_request(url, params, headers, use_proxy=False)
|
||||
logger.info(f"直连请求成功,状态码: {response.status_code}")
|
||||
|
||||
except Exception as direct_error:
|
||||
logger.error(f"直连也失败: {str(direct_error)}")
|
||||
raise direct_error
|
||||
|
||||
# 检查响应状态
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析JSON响应
|
||||
result = response.json()
|
||||
logger.info(f"搜索完成,找到 {len(result.get('items', []))} 个结果")
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if response.status_code == 401:
|
||||
logger.error("API认证失败,请检查APP_ID和API_KEY")
|
||||
elif response.status_code == 403:
|
||||
logger.error("API访问被禁止,请检查权限")
|
||||
elif response.status_code == 429:
|
||||
logger.error("API请求频率超限,请稍后再试")
|
||||
else:
|
||||
logger.error(f"HTTP错误: {e}")
|
||||
return None
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"网络请求失败: {str(e)}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索异常: {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}
|
||||
url = f"{self._base_url}/movie/{tmdbid}/{resource_type}"
|
||||
|
||||
# 首先尝试使用系统代理
|
||||
try:
|
||||
logger.debug("尝试使用系统代理获取电影资源")
|
||||
response = self._make_request(url, {}, headers, use_proxy=True)
|
||||
logger.info(f"使用系统代理请求成功,状态码: {response.status_code}")
|
||||
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectTimeout,
|
||||
requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e:
|
||||
logger.warning(f"系统代理访问失败: {str(e)},尝试直连")
|
||||
try:
|
||||
response = self._make_request(url, {}, headers, use_proxy=False)
|
||||
logger.info(f"直连请求成功,状态码: {response.status_code}")
|
||||
|
||||
except Exception as direct_error:
|
||||
logger.error(f"直连也失败: {str(direct_error)}")
|
||||
raise direct_error
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
logger.info(f"获取电影资源成功: TMDB={tmdbid}, 类型={resource_type}")
|
||||
return result
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if response.status_code == 404:
|
||||
logger.warning(f"未找到电影资源: TMDB={tmdbid}, 类型={resource_type}")
|
||||
else:
|
||||
logger.error(f"获取电影资源失败: {e}")
|
||||
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}
|
||||
url = f"{self._base_url}/tv/{tmdbid}/{resource_type}"
|
||||
|
||||
# 首先尝试使用系统代理
|
||||
try:
|
||||
logger.debug("尝试使用系统代理获取剧集资源")
|
||||
response = self._make_request(url, {}, headers, use_proxy=True)
|
||||
logger.info(f"使用系统代理请求成功,状态码: {response.status_code}")
|
||||
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectTimeout,
|
||||
requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e:
|
||||
logger.warning(f"系统代理访问失败: {str(e)},尝试直连")
|
||||
try:
|
||||
response = self._make_request(url, {}, headers, use_proxy=False)
|
||||
logger.info(f"直连请求成功,状态码: {response.status_code}")
|
||||
|
||||
except Exception as direct_error:
|
||||
logger.error(f"直连也失败: {str(direct_error)}")
|
||||
raise direct_error
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
logger.info(f"获取剧集资源成功: TMDB={tmdbid}, 类型={resource_type}")
|
||||
return result
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if response.status_code == 404:
|
||||
logger.warning(f"未找到剧集资源: TMDB={tmdbid}, 类型={resource_type}")
|
||||
else:
|
||||
logger.error(f"获取剧集资源失败: {e}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取剧集资源异常: {str(e)}")
|
||||
return None
|
||||
Loading…
x
Reference in New Issue
Block a user