mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
fix(indexer): adapt Rousi Pro PeerGo personal API key
This commit is contained in:
+10
-3
@@ -343,8 +343,10 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
|||||||
|
|
||||||
def __rousi_test(self, site: Site) -> Tuple[bool, str]:
|
def __rousi_test(self, site: Site) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
判断站点是否已经登陆:rousi
|
使用 PeerGo 个人 API Key 验证 Rousi.pro 站点连接。
|
||||||
"""
|
"""
|
||||||
|
if not site.apikey:
|
||||||
|
return False, "未配置个人 API Key"
|
||||||
url = f"https://{site_rules.extract_domain(site.url)}/api/v1/profile"
|
url = f"https://{site_rules.extract_domain(site.url)}/api/v1/profile"
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -359,10 +361,15 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
|||||||
if res is None:
|
if res is None:
|
||||||
return False, "无法打开网站!"
|
return False, "无法打开网站!"
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
user_info = res.json()
|
try:
|
||||||
|
user_info = res.json()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False, "站点返回了无效的用户数据"
|
||||||
if user_info and user_info.get("code") == 0:
|
if user_info and user_info.get("code") == 0:
|
||||||
return True, "连接成功"
|
return True, "连接成功"
|
||||||
return False, "APIKEY已过期"
|
return False, "个人 API Key 已失效或权限不足"
|
||||||
|
elif res.status_code in (401, 403):
|
||||||
|
return False, "个人 API Key 已失效或权限不足"
|
||||||
else:
|
else:
|
||||||
return False, f"错误:{res.status_code} {res.reason}"
|
return False, f"错误:{res.status_code} {res.reason}"
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ from app.modules.indexer.parser import SiteParserBase, SiteSchema
|
|||||||
|
|
||||||
class RousiSiteUserInfo(SiteParserBase):
|
class RousiSiteUserInfo(SiteParserBase):
|
||||||
"""
|
"""
|
||||||
Rousi.pro 站点解析器
|
Rousi.pro PeerGo 用户数据解析器。
|
||||||
使用 API v1 接口,通过 Passkey (Bearer Token) 进行认证
|
|
||||||
|
使用具有 profile:read 权限的个人 API Key 访问兼容资料接口。
|
||||||
"""
|
"""
|
||||||
schema = SiteSchema.RousiPro
|
schema = SiteSchema.RousiPro
|
||||||
request_mode = "apikey"
|
request_mode = "apikey"
|
||||||
@@ -23,10 +24,10 @@ class RousiSiteUserInfo(SiteParserBase):
|
|||||||
def _parse_site_page(self, html_text: str):
|
def _parse_site_page(self, html_text: str):
|
||||||
"""
|
"""
|
||||||
配置 API 请求地址和请求头
|
配置 API 请求地址和请求头
|
||||||
使用 API v1 的 /profile 接口获取用户信息
|
使用 PeerGo MoviePilot 兼容的 /profile 接口获取用户信息。
|
||||||
"""
|
"""
|
||||||
self._base_url = f"https://{site_rules.extract_domain(self._site_url)}"
|
self._base_url = f"https://{site_rules.extract_domain(self._site_url)}"
|
||||||
self._user_basic_page = "api/v1/profile?include_fields[user]=seeding_leeching_data"
|
self._user_basic_page = "api/v1/profile"
|
||||||
self._user_basic_params = {}
|
self._user_basic_params = {}
|
||||||
self._user_basic_headers = {
|
self._user_basic_headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -84,13 +85,18 @@ class RousiSiteUserInfo(SiteParserBase):
|
|||||||
logger.error(f"{self._site_name} JSON 解析失败")
|
logger.error(f"{self._site_name} JSON 解析失败")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not data or data.get("code") != 0:
|
if not isinstance(data, dict):
|
||||||
|
self.err_msg = "用户数据响应结构无效"
|
||||||
|
logger.warning(f"{self._site_name} API 响应结构无效")
|
||||||
|
return
|
||||||
|
|
||||||
|
if data.get("code") != 0:
|
||||||
self.err_msg = data.get("message", "未知错误")
|
self.err_msg = data.get("message", "未知错误")
|
||||||
logger.warn(f"{self._site_name} API 错误: {self.err_msg}")
|
logger.warning(f"{self._site_name} API 错误: {self.err_msg}")
|
||||||
return
|
return
|
||||||
|
|
||||||
user_info = data.get("data")
|
user_info = data.get("data")
|
||||||
if not user_info:
|
if not isinstance(user_info, dict):
|
||||||
return
|
return
|
||||||
|
|
||||||
# 基本信息
|
# 基本信息
|
||||||
|
|||||||
@@ -2,30 +2,28 @@ import base64
|
|||||||
import json
|
import json
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from app.runtime.settings import get_runtime_setting
|
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
|
||||||
|
|
||||||
from app.application.configuration import get_configured_system_config
|
from app.application.configuration import get_configured_system_config
|
||||||
from app.runtime.log import logger
|
|
||||||
from app.schemas.types import MediaType
|
|
||||||
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
|
|
||||||
from app.domain import site as site_rules
|
from app.domain import site as site_rules
|
||||||
from app.foundation import temporal as time_tools
|
from app.foundation import temporal as time_tools
|
||||||
|
from app.runtime.log import logger
|
||||||
|
from app.runtime.settings import get_runtime_setting
|
||||||
|
from app.schemas.types import MediaType
|
||||||
|
|
||||||
|
|
||||||
class RousiSpider:
|
class RousiSpider:
|
||||||
"""
|
"""
|
||||||
Rousi.pro API v1 Spider
|
Rousi.pro PeerGo 兼容 API 索引器。
|
||||||
|
|
||||||
使用 API v1 接口进行种子搜索
|
使用个人 API Key 访问搜索兼容接口,并通过详情接口换取短时下载地址。
|
||||||
- 认证方式:Bearer Token (Passkey)
|
API Key 需要授予 torrent:read 和 torrent:download 权限。
|
||||||
- 搜索接口:/api/v1/torrents
|
|
||||||
- 详情接口:/api/v1/torrents/:id
|
|
||||||
"""
|
"""
|
||||||
_indexerid = None
|
_indexerid = None
|
||||||
_domain = None
|
_domain = None
|
||||||
_url = None
|
_url = None
|
||||||
_name = ""
|
_name = ""
|
||||||
_proxy = None
|
_proxy = None
|
||||||
|
_use_proxy = False
|
||||||
_cookie = None
|
_cookie = None
|
||||||
_ua = None
|
_ua = None
|
||||||
_size = 100
|
_size = 100
|
||||||
@@ -39,7 +37,7 @@ class RousiSpider:
|
|||||||
_tv_category = 'tv'
|
_tv_category = 'tv'
|
||||||
_music_category = 'music'
|
_music_category = 'music'
|
||||||
|
|
||||||
# API KEY
|
# PeerGo 个人 API Key
|
||||||
_apikey = None
|
_apikey = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -60,6 +58,7 @@ class RousiSpider:
|
|||||||
self._name = indexer.get('name')
|
self._name = indexer.get('name')
|
||||||
if indexer.get('proxy'):
|
if indexer.get('proxy'):
|
||||||
self._proxy = get_runtime_setting('PROXY')
|
self._proxy = get_runtime_setting('PROXY')
|
||||||
|
self._use_proxy = bool(indexer.get('proxy'))
|
||||||
self._cookie = indexer.get('cookie')
|
self._cookie = indexer.get('cookie')
|
||||||
self._ua = indexer.get('ua')
|
self._ua = indexer.get('ua')
|
||||||
self._apikey = indexer.get('apikey')
|
self._apikey = indexer.get('apikey')
|
||||||
@@ -134,23 +133,25 @@ class RousiSpider:
|
|||||||
:param res: 请求响应对象
|
:param res: 请求响应对象
|
||||||
:return: (是否发生错误, 种子列表)
|
:return: (是否发生错误, 种子列表)
|
||||||
"""
|
"""
|
||||||
if res and res.status_code == 200:
|
if res is not None and res.status_code == 200:
|
||||||
try:
|
try:
|
||||||
data = res.json()
|
data = res.json()
|
||||||
if data.get('code') == 0:
|
except (TypeError, ValueError) as err:
|
||||||
results = data.get('data', {}).get('torrents', [])
|
logger.warning(f"{self._name} 解析搜索响应失败:{str(err)}")
|
||||||
return False, self.__parse_result(results)
|
|
||||||
else:
|
|
||||||
logger.warn(f"{self._name} 搜索失败,错误信息:{data.get('message')}")
|
|
||||||
return True, []
|
|
||||||
except Exception as e:
|
|
||||||
logger.warn(f"{self._name} 解析响应失败:{e}")
|
|
||||||
return True, []
|
return True, []
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
logger.warning(f"{self._name} 搜索响应结构无效")
|
||||||
|
return True, []
|
||||||
|
if data.get('code') == 0:
|
||||||
|
results = data.get('data', {}).get('torrents', [])
|
||||||
|
return False, self.__parse_result(results)
|
||||||
|
logger.warning(f"{self._name} 搜索失败,错误信息:{data.get('message')}")
|
||||||
|
return True, []
|
||||||
elif res is not None:
|
elif res is not None:
|
||||||
logger.warn(f"{self._name} 搜索失败,HTTP 错误码:{res.status_code}")
|
logger.warning(f"{self._name} 搜索失败,HTTP 错误码:{res.status_code}")
|
||||||
return True, []
|
return True, []
|
||||||
else:
|
else:
|
||||||
logger.warn(f"{self._name} 搜索失败,无法连接 {self._domain}")
|
logger.warning(f"{self._name} 搜索失败,无法连接 {self._domain}")
|
||||||
return True, []
|
return True, []
|
||||||
|
|
||||||
def __parse_result(self, results: List[dict]) -> List[dict]:
|
def __parse_result(self, results: List[dict]) -> List[dict]:
|
||||||
@@ -203,10 +204,11 @@ class RousiSpider:
|
|||||||
if promotion.get('until'):
|
if promotion.get('until'):
|
||||||
freedate = time_tools.normalize_datetime(promotion.get('until'))
|
freedate = time_tools.normalize_datetime(promotion.get('until'))
|
||||||
|
|
||||||
|
torrent_id = result.get('id')
|
||||||
torrent = {
|
torrent = {
|
||||||
'title': result.get('title'),
|
'title': result.get('title'),
|
||||||
'description': result.get('subtitle'),
|
'description': result.get('subtitle'),
|
||||||
'enclosure': self.__get_download_url(result.get('id')),
|
'enclosure': self.__get_download_url(torrent_id),
|
||||||
'pubdate': time_tools.normalize_datetime(result.get('created_at')),
|
'pubdate': time_tools.normalize_datetime(result.get('created_at')),
|
||||||
'size': int(result.get('size') or 0),
|
'size': int(result.get('size') or 0),
|
||||||
'seeders': int(result.get('seeders') or 0),
|
'seeders': int(result.get('seeders') or 0),
|
||||||
@@ -215,7 +217,7 @@ class RousiSpider:
|
|||||||
'downloadvolumefactor': downloadvolumefactor,
|
'downloadvolumefactor': downloadvolumefactor,
|
||||||
'uploadvolumefactor': uploadvolumefactor,
|
'uploadvolumefactor': uploadvolumefactor,
|
||||||
'freedate': freedate,
|
'freedate': freedate,
|
||||||
'page_url': f"https://{self._domain}/torrent/{result.get('uuid')}",
|
'page_url': f"https://{self._domain}/torrents/{torrent_id}",
|
||||||
'labels': [],
|
'labels': [],
|
||||||
'category': category
|
'category': category
|
||||||
}
|
}
|
||||||
@@ -233,7 +235,7 @@ class RousiSpider:
|
|||||||
:return: (是否发生错误, 种子列表)
|
:return: (是否发生错误, 种子列表)
|
||||||
"""
|
"""
|
||||||
if not self._apikey:
|
if not self._apikey:
|
||||||
logger.warn(f"{self._name} 未配置 API Key (Passkey)")
|
logger.warning(f"{self._name} 未配置个人 API Key")
|
||||||
return True, []
|
return True, []
|
||||||
|
|
||||||
params = self.__get_params(keyword, mtype, cat, page)
|
params = self.__get_params(keyword, mtype, cat, page)
|
||||||
@@ -261,7 +263,7 @@ class RousiSpider:
|
|||||||
:return: (是否发生错误, 种子列表)
|
:return: (是否发生错误, 种子列表)
|
||||||
"""
|
"""
|
||||||
if not self._apikey:
|
if not self._apikey:
|
||||||
logger.warn(f"{self._name} 未配置 API Key (Passkey)")
|
logger.warning(f"{self._name} 未配置个人 API Key")
|
||||||
return True, []
|
return True, []
|
||||||
|
|
||||||
params = self.__get_params(keyword, mtype, cat, page)
|
params = self.__get_params(keyword, mtype, cat, page)
|
||||||
@@ -282,8 +284,7 @@ class RousiSpider:
|
|||||||
"""
|
"""
|
||||||
构建种子下载链接
|
构建种子下载链接
|
||||||
|
|
||||||
使用 base64 编码的方式告诉 MoviePilot 如何获取真实下载地址
|
MoviePilot 会携带个人 API Key 请求详情接口,再提取带 capability 的短时下载地址。
|
||||||
MoviePilot 会先请求详情接口,然后从响应中提取 data.download_url
|
|
||||||
|
|
||||||
:param torrent_id: 种子 ID
|
:param torrent_id: 种子 ID
|
||||||
:return: base64 编码的请求配置字符串 + 详情接口 URL
|
:return: base64 编码的请求配置字符串 + 详情接口 URL
|
||||||
@@ -294,10 +295,12 @@ class RousiSpider:
|
|||||||
# 2. 从 JSON 响应中提取 result 指定的字段值作为真实下载地址
|
# 2. 从 JSON 响应中提取 result 指定的字段值作为真实下载地址
|
||||||
params = {
|
params = {
|
||||||
'method': 'get',
|
'method': 'get',
|
||||||
|
'cookie': False,
|
||||||
'header': {
|
'header': {
|
||||||
'Authorization': f'Bearer {self._apikey}',
|
'Authorization': f'Bearer {self._apikey}',
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
},
|
},
|
||||||
|
'proxy': self._use_proxy,
|
||||||
'result': 'data.download_url'
|
'result': 'data.download_url'
|
||||||
}
|
}
|
||||||
base64_str = base64.b64encode(json.dumps(params).encode('utf-8')).decode('utf-8')
|
base64_str = base64.b64encode(json.dumps(params).encode('utf-8')).decode('utf-8')
|
||||||
|
|||||||
+213
-3
@@ -1,22 +1,42 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# pylint: disable=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from app.chain.site import SiteChain
|
||||||
|
from app.modules.indexer.parser.rousi import RousiSiteUserInfo
|
||||||
from app.modules.indexer.spider import rousi as rousi_module
|
from app.modules.indexer.spider import rousi as rousi_module
|
||||||
from app.modules.indexer.spider.rousi import RousiSpider
|
from app.modules.indexer.spider.rousi import RousiSpider
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
|
|
||||||
|
|
||||||
def _build_indexer() -> dict:
|
class _FakeResponse:
|
||||||
|
"""构造 PeerGo 兼容 API 测试使用的最小响应对象。"""
|
||||||
|
|
||||||
|
def __init__(self, payload: dict, status_code: int = 200):
|
||||||
|
"""保存响应数据和状态码。"""
|
||||||
|
self._payload = payload
|
||||||
|
self.status_code = status_code
|
||||||
|
self.reason = "OK"
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
"""返回预设 JSON 数据。"""
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
def _build_indexer(apikey: str = "rousi-secret", proxy: bool = False) -> dict:
|
||||||
"""构造 Rousi Pro API Spider 所需的最小站点配置。"""
|
"""构造 Rousi Pro API Spider 所需的最小站点配置。"""
|
||||||
return {
|
return {
|
||||||
"id": "rousipro",
|
"id": "rousipro",
|
||||||
"name": "Rousi Pro",
|
"name": "Rousi Pro",
|
||||||
"domain": "https://rousi.pro/",
|
"domain": "https://rousi.pro/",
|
||||||
"apikey": "rousi-secret",
|
"apikey": apikey,
|
||||||
"ua": "MoviePilot-Test",
|
"ua": "MoviePilot-Test",
|
||||||
"proxy": False,
|
"proxy": proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -54,3 +74,193 @@ def test_parse_result_marks_music_torrents(rousi_spider):
|
|||||||
MediaType.MOVIE.value,
|
MediaType.MOVIE.value,
|
||||||
MediaType.TV.value,
|
MediaType.TV.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_uses_peergo_personal_api_key_contract(monkeypatch):
|
||||||
|
"""搜索应使用个人 API Key 兼容响应并生成短时下载地址换取请求。"""
|
||||||
|
captured = {}
|
||||||
|
payload = {
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 100,
|
||||||
|
"total": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"torrents": [{
|
||||||
|
"category": "movie",
|
||||||
|
"category_name": "电影",
|
||||||
|
"created_at": "2026-06-09T12:04:15.184279Z",
|
||||||
|
"downloads": 21,
|
||||||
|
"id": 8461,
|
||||||
|
"leechers": 6,
|
||||||
|
"promotion": {
|
||||||
|
"down_multiplier": 0,
|
||||||
|
"is_active": True,
|
||||||
|
"until": "2026-09-21T16:36:31.008958Z",
|
||||||
|
"up_multiplier": 2,
|
||||||
|
},
|
||||||
|
"seeders": 31,
|
||||||
|
"size": 190438784128,
|
||||||
|
"subtitle": "电影中文副标题",
|
||||||
|
"title": "Movie.2026.2160p.UHD.BluRay",
|
||||||
|
"uuid": "8461",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_get_res(request, url: str, params: dict = None, **_kwargs):
|
||||||
|
"""记录搜索请求并回放 PeerGo MoviePilot 兼容响应。"""
|
||||||
|
captured.update({"url": url, "params": params, "headers": request._headers})
|
||||||
|
return _FakeResponse(payload)
|
||||||
|
|
||||||
|
monkeypatch.setattr(rousi_module, "get_configured_system_config", lambda: None)
|
||||||
|
monkeypatch.setattr(rousi_module, "get_runtime_setting", lambda _key: {"https": "proxy"})
|
||||||
|
monkeypatch.setattr(rousi_module.RequestUtils, "get_res", fake_get_res)
|
||||||
|
|
||||||
|
error, torrents = RousiSpider(_build_indexer(proxy=True)).search(
|
||||||
|
keyword="Movie",
|
||||||
|
mtype=MediaType.MOVIE,
|
||||||
|
page=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not error
|
||||||
|
assert captured == {
|
||||||
|
"url": "https://rousi.pro/api/v1/torrents",
|
||||||
|
"params": {
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 100,
|
||||||
|
"keyword": "Movie",
|
||||||
|
"category": "movie",
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer rousi-secret",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert len(torrents) == 1
|
||||||
|
assert torrents[0]["title"] == "Movie.2026.2160p.UHD.BluRay"
|
||||||
|
assert torrents[0]["page_url"] == "https://rousi.pro/torrents/8461"
|
||||||
|
assert torrents[0]["downloadvolumefactor"] == 0
|
||||||
|
assert torrents[0]["uploadvolumefactor"] == 2
|
||||||
|
assert torrents[0]["category"] == MediaType.MOVIE.value
|
||||||
|
|
||||||
|
encoded_config, detail_url = torrents[0]["enclosure"].split("]", 1)
|
||||||
|
request_config = json.loads(base64.b64decode(encoded_config[1:]).decode("utf-8"))
|
||||||
|
assert detail_url == "https://rousi.pro/api/v1/torrents/8461"
|
||||||
|
assert request_config == {
|
||||||
|
"method": "get",
|
||||||
|
"cookie": False,
|
||||||
|
"header": {
|
||||||
|
"Authorization": "Bearer rousi-secret",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
"proxy": True,
|
||||||
|
"result": "data.download_url",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_rejects_missing_personal_api_key(monkeypatch):
|
||||||
|
"""未配置个人 API Key 时不得向 PeerGo 发起搜索请求。"""
|
||||||
|
monkeypatch.setattr(rousi_module, "get_configured_system_config", lambda: None)
|
||||||
|
spider = RousiSpider(_build_indexer(apikey=""))
|
||||||
|
|
||||||
|
error, torrents = spider.search(keyword="Movie")
|
||||||
|
|
||||||
|
assert error
|
||||||
|
assert torrents == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_parser_reads_peergo_profile_with_personal_api_key(monkeypatch):
|
||||||
|
"""用户数据解析应通过个人 API Key 读取 PeerGo 兼容资料结构。"""
|
||||||
|
captured = {}
|
||||||
|
profile = {
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 619,
|
||||||
|
"username": "jxxghp",
|
||||||
|
"level_text": "Lv.1",
|
||||||
|
"registered_at": "2026-01-05T19:50:59.012Z",
|
||||||
|
"uploaded": 1099511627776,
|
||||||
|
"downloaded": 536870912000,
|
||||||
|
"ratio": 2.048,
|
||||||
|
"karma": 1079960,
|
||||||
|
"seeding_leeching_data": {
|
||||||
|
"seeding_count": 8,
|
||||||
|
"seeding_size": 2147483648000,
|
||||||
|
"leeching_count": 1,
|
||||||
|
"leeching_size": 10737418240,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_get_page_content(_self, url: str, headers: dict = None, **_kwargs):
|
||||||
|
"""记录用户资料请求并回放 PeerGo 兼容响应。"""
|
||||||
|
captured.update({"url": url, "headers": headers})
|
||||||
|
return json.dumps(profile)
|
||||||
|
|
||||||
|
monkeypatch.setattr(RousiSiteUserInfo, "_get_page_content", fake_get_page_content)
|
||||||
|
parser = RousiSiteUserInfo(
|
||||||
|
site_name="Rousi Pro",
|
||||||
|
url="https://rousi.pro/",
|
||||||
|
site_cookie="",
|
||||||
|
apikey="rousi-secret",
|
||||||
|
token=None,
|
||||||
|
ua="MoviePilot-Test",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.parse()
|
||||||
|
|
||||||
|
assert captured["url"] == "https://rousi.pro/api/v1/profile"
|
||||||
|
assert captured["headers"]["Authorization"] == "Bearer rousi-secret"
|
||||||
|
assert parser.userid == 619
|
||||||
|
assert parser.username == "jxxghp"
|
||||||
|
assert parser.user_level == "Lv.1"
|
||||||
|
assert parser.join_at == "2026-01-05 19:50:59"
|
||||||
|
assert parser.upload == 1099511627776
|
||||||
|
assert parser.download == 536870912000
|
||||||
|
assert parser.ratio == 2.05
|
||||||
|
assert parser.bonus == 1079960
|
||||||
|
assert parser.seeding == 8
|
||||||
|
assert parser.seeding_size == 2147483648000
|
||||||
|
assert parser.leeching == 1
|
||||||
|
assert parser.leeching_size == 10737418240
|
||||||
|
|
||||||
|
|
||||||
|
def test_site_connectivity_uses_peergo_personal_api_key(monkeypatch):
|
||||||
|
"""Rousi 连接测试应以 Bearer 个人 API Key 请求兼容资料接口。"""
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_get_res(request, url: str, **_kwargs):
|
||||||
|
"""记录连接测试请求并返回有效用户资料。"""
|
||||||
|
captured.update({"url": url, "headers": request._headers})
|
||||||
|
return _FakeResponse({"code": 0, "message": "success", "data": {"id": 619}})
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.chain.site.RequestUtils.get_res", fake_get_res)
|
||||||
|
site = SimpleNamespace(
|
||||||
|
url="https://rousi.pro/",
|
||||||
|
apikey="rousi-secret",
|
||||||
|
proxy=0,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
chain = object.__new__(SiteChain)
|
||||||
|
chain.runtime_config = SimpleNamespace(proxy=None)
|
||||||
|
|
||||||
|
state, message = chain._SiteChain__rousi_test(site)
|
||||||
|
|
||||||
|
assert state
|
||||||
|
assert message == "连接成功"
|
||||||
|
assert captured["url"] == "https://rousi.pro/api/v1/profile"
|
||||||
|
assert captured["headers"]["Authorization"] == "Bearer rousi-secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_site_connectivity_rejects_missing_personal_api_key():
|
||||||
|
"""Rousi 连接测试应在请求前报告个人 API Key 缺失。"""
|
||||||
|
site = SimpleNamespace(url="https://rousi.pro/", apikey="", proxy=0, timeout=15)
|
||||||
|
chain = object.__new__(SiteChain)
|
||||||
|
|
||||||
|
state, message = chain._SiteChain__rousi_test(site)
|
||||||
|
|
||||||
|
assert not state
|
||||||
|
assert message == "未配置个人 API Key"
|
||||||
|
|||||||
Reference in New Issue
Block a user