202 lines
8.5 KiB
Python
202 lines
8.5 KiB
Python
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 |