mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 04:14:38 +08:00
fix(indexer): honor HDDolby API resource URL (#7158)
This commit is contained in:
@@ -659,12 +659,16 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
||||
|
||||
def __hddolby_test(self, site: SiteSnapshot) -> Tuple[bool, str]:
|
||||
"""
|
||||
通过 HDDolby 用户 API 验证 API Key,与资料解析使用相同 API 主机。
|
||||
通过 HDDolby 用户 API 验证 API Key,与搜索和资料解析使用相同资源 API 基础地址。
|
||||
|
||||
未配置 api_url 时沿用旧站的 api 子域,兼容存量资源和自定义索引。
|
||||
|
||||
:param site: HDDolby 协议站点配置
|
||||
:return: 是否可用及认证或连接失败原因
|
||||
"""
|
||||
url = f"https://api.{site_rules.extract_domain(site.url)}/api/v1/user/data"
|
||||
domain = site.domain or site_rules.extract_domain(site.url)
|
||||
indexer = SitesHelper().get_indexer(domain) or {}
|
||||
api_url = str(indexer.get("api_url") or f"https://api.{domain}/api/v1").rstrip("/")
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
@@ -672,7 +676,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
||||
}
|
||||
with _open_site_response(
|
||||
method="GET",
|
||||
url=url,
|
||||
url=f"{api_url}/user/data",
|
||||
headers=headers,
|
||||
proxies=(self.runtime_config.proxy or {}) if site.proxy else {},
|
||||
timeout=site.timeout or 15,
|
||||
|
||||
@@ -48,13 +48,17 @@ class HDDolbySiteUserInfo(SiteParserBase):
|
||||
|
||||
def _parse_site_page(self, html_text: SitePage) -> None:
|
||||
"""
|
||||
获取站点页面地址。
|
||||
使用资源声明的 API 基础地址配置用户资料和做种接口。
|
||||
|
||||
未配置 api_url 时保留旧站的 api 子域,前端地址继续用于读取站点消息。
|
||||
|
||||
:param html_text: API 模式不使用首页内容
|
||||
"""
|
||||
# 更换api地址
|
||||
self._base_url = f"https://api.{site_rules.extract_domain(self._base_url)}"
|
||||
api_url = self._api_url or f"https://api.{site_rules.extract_domain(self._base_url)}/api/v1"
|
||||
self._base_url = f"{api_url.rstrip('/')}/"
|
||||
self._user_traffic_page = None
|
||||
self._user_detail_page = None
|
||||
self._user_basic_page = "api/v1/user/data"
|
||||
self._user_basic_page = "user/data"
|
||||
self._user_basic_params = {}
|
||||
self._user_basic_headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -63,7 +67,7 @@ class HDDolbySiteUserInfo(SiteParserBase):
|
||||
self._sys_mail_unread_page = None
|
||||
self._user_mail_unread_page = None
|
||||
self._mail_unread_params = {}
|
||||
self._torrent_seeding_page = "api/v1/user/peers"
|
||||
self._torrent_seeding_page = "user/peers"
|
||||
self._torrent_seeding_params = {}
|
||||
self._torrent_seeding_headers = {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -73,9 +73,12 @@ class HddolbySpider:
|
||||
"""
|
||||
return cls._size
|
||||
|
||||
def __init__(self, indexer: dict[str, Any]):
|
||||
def __init__(self, indexer: dict[str, Any]) -> None:
|
||||
"""
|
||||
使用站点配置初始化 HDDolby API 请求上下文。
|
||||
|
||||
:param indexer: 合并认证信息的索引配置;api_url 可覆盖包含版本路径的 API 基础地址,
|
||||
未配置时沿用旧站的 api 子域
|
||||
"""
|
||||
self.systemconfig = get_configured_system_config()
|
||||
if indexer:
|
||||
@@ -89,7 +92,10 @@ class HddolbySpider:
|
||||
self._ua = indexer.get('ua')
|
||||
self._apikey = indexer.get('apikey')
|
||||
self._timeout = indexer.get('timeout') or 15
|
||||
self._searchurl = f"https://api.{self._domain_host}/api/v1/torrent/search"
|
||||
api_url = str(
|
||||
indexer.get('api_url') or f"https://api.{self._domain_host}/api/v1"
|
||||
).rstrip('/')
|
||||
self._searchurl = f"{api_url}/torrent/search"
|
||||
self._pageurl = f"{self._domain}details.php?id=%s&hit=1"
|
||||
|
||||
def __get_params(self, keyword: str, mtype: Optional[MediaType] = None, page: Optional[int] = 0,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
验证 HDDolby 用户信息、未读消息与无有效用户标识时的请求边界。
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.indexer.parser.hddolby import HDDolbySiteUserInfo
|
||||
from app.modules.indexer.parser.nexus_php import NexusPhpSiteUserInfo
|
||||
|
||||
@@ -23,16 +27,56 @@ USER_DATA_JSON = """
|
||||
"""
|
||||
|
||||
|
||||
def _build_parser(site_cookie: str = "") -> HDDolbySiteUserInfo:
|
||||
def _build_parser(site_cookie: str = "", **kwargs) -> HDDolbySiteUserInfo:
|
||||
"""
|
||||
构造隔离 HTTP 会话、允许覆盖 API 地址的杜比用户数据解析器。
|
||||
"""
|
||||
return HDDolbySiteUserInfo(
|
||||
site_name="高清杜比",
|
||||
url="https://www.hddolby.com/",
|
||||
site_cookie=site_cookie,
|
||||
apikey="test-api-key",
|
||||
token=None,
|
||||
request_utils=Mock(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_url", "expected_base"),
|
||||
[
|
||||
(None, "https://api.hddolby.com/api/v1/"),
|
||||
("", "https://api.hddolby.com/api/v1/"),
|
||||
("https://www.uhddolby.com/api/v1", "https://www.uhddolby.com/api/v1/"),
|
||||
("https://www.uhddolby.com/api/v1/", "https://www.uhddolby.com/api/v1/"),
|
||||
("https://custom-api.test:8443/dolby/v1/", "https://custom-api.test:8443/dolby/v1/"),
|
||||
],
|
||||
)
|
||||
def test_userdata_uses_resource_api_url_for_profile_and_seeding(monkeypatch, api_url, expected_base):
|
||||
"""
|
||||
用户资料和做种统计使用同一资源 API 地址,保留 API Key 并关闭隔离会话。
|
||||
"""
|
||||
parser = _build_parser(api_url=api_url)
|
||||
get_page = Mock(side_effect=[
|
||||
USER_DATA_JSON,
|
||||
'{"status": 0, "data": [{"size": 2048, "seeders": 3}]}',
|
||||
])
|
||||
monkeypatch.setattr(parser, "_get_page_content", get_page)
|
||||
|
||||
parser.parse()
|
||||
|
||||
assert [call.kwargs["url"] for call in get_page.call_args_list] == [
|
||||
f"{expected_base}user/data", f"{expected_base}user/peers",
|
||||
]
|
||||
assert parser._addition_headers == {"x-api-key": "test-api-key"}
|
||||
assert parser.userid == "41088"
|
||||
assert parser.upload == 1000
|
||||
assert parser.download == 500
|
||||
assert parser.seeding == 1
|
||||
assert parser.seeding_size == 2048
|
||||
parser._request_utils.close.assert_called_once_with()
|
||||
|
||||
|
||||
def test_hddolby_parse_user_base_info_reads_unread_count():
|
||||
"""
|
||||
用户基础信息解析保留用户标识、名称和未读消息数量。
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
# MediaType 使用历史惰性导出入口,Pylint 无法静态解析。
|
||||
# pylint: disable=no-name-in-module
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.indexer.spider import hddolby as hddolby_module
|
||||
@@ -71,3 +74,53 @@ def test_parse_result_marks_music_torrents(hddolby_spider):
|
||||
MediaType.MOVIE.value,
|
||||
MediaType.TV.value,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asynchronous", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "api_url", "expected_url"),
|
||||
[
|
||||
("https://www.hddolby.com/", None, "https://api.hddolby.com/api/v1/torrent/search"),
|
||||
("https://www.hddolby.com/", "", "https://api.hddolby.com/api/v1/torrent/search"),
|
||||
("https://www.uhddolby.com/", "https://www.uhddolby.com/api/v1",
|
||||
"https://www.uhddolby.com/api/v1/torrent/search"),
|
||||
("https://www.uhddolby.com/", "https://www.uhddolby.com/api/v1/",
|
||||
"https://www.uhddolby.com/api/v1/torrent/search"),
|
||||
("https://www.uhddolby.com/", "https://custom-api.test:8443/dolby/v1/",
|
||||
"https://custom-api.test:8443/dolby/v1/torrent/search"),
|
||||
],
|
||||
)
|
||||
def test_search_honors_resource_api_url(
|
||||
monkeypatch, asynchronous, domain, api_url, expected_url,
|
||||
):
|
||||
"""
|
||||
同步和异步搜索使用资源 API 地址及 API Key,详情和下载链接仍属于站点前端。
|
||||
"""
|
||||
monkeypatch.setattr(hddolby_module, "get_configured_system_config", lambda: None)
|
||||
indexer = {**_build_indexer(), "domain": domain, "api_url": api_url}
|
||||
spider = HddolbySpider(indexer)
|
||||
response = Mock(status_code=200)
|
||||
response.json.return_value = {
|
||||
"status": 0,
|
||||
"data": [{"id": 42, "name": "Example.Movie", "category": 401, "downhash": "test-hash"}],
|
||||
}
|
||||
request = Mock()
|
||||
transport = Mock(return_value=response)
|
||||
if asynchronous:
|
||||
transport = AsyncMock(return_value=response)
|
||||
request.return_value.post_res = transport
|
||||
monkeypatch.setattr(hddolby_module, "AsyncRequestUtils" if asynchronous else "RequestUtils", request)
|
||||
|
||||
if asynchronous:
|
||||
error, torrents = asyncio.run(spider.async_search("Movie", MediaType.MOVIE, page=2))
|
||||
else:
|
||||
error, torrents = spider.search("Movie", MediaType.MOVIE, page=2)
|
||||
|
||||
assert error is False
|
||||
transport.assert_called_once_with(url=expected_url, json={
|
||||
"keyword": "Movie", "page_number": 2, "page_size": 100,
|
||||
"categories": HddolbySpider._movie_category, "visible": 1,
|
||||
})
|
||||
assert request.call_args.kwargs["headers"]["x-api-key"] == "dolby-secret"
|
||||
assert torrents[0]["page_url"] == f"{domain}details.php?id=42&hit=1"
|
||||
assert torrents[0]["enclosure"] == f"{domain}download.php?id=42&downhash=test-hash"
|
||||
|
||||
@@ -212,11 +212,12 @@ def test_api_protocols_use_their_own_authentication(
|
||||
("MTorrent", "api_domain", "https://custom-api.test:8443/api/member/profile"),
|
||||
("MTorrent", "api_host", "https://custom-api.test:8443/api/member/profile"),
|
||||
("SunnyPT", "api_url", "https://custom-api.test:8443/api/profile"),
|
||||
("HDDolby", "api_url", "https://custom-api.test:8443/api/user/data"),
|
||||
],
|
||||
)
|
||||
def test_api_probe_honors_resource_endpoint(monkeypatch, protocol, host_key, expected_url):
|
||||
"""
|
||||
MTorrent 和 SunnyPT 探测与搜索使用同一资源 API 地址配置。
|
||||
API 协议探测与搜索使用同一资源 API 地址配置。
|
||||
"""
|
||||
chain = SiteChain()
|
||||
site = SiteSnapshot(id=1, name="独立 API", domain="custom.test", url="https://www.custom.test/", apikey="key")
|
||||
@@ -225,7 +226,9 @@ def test_api_probe_honors_resource_endpoint(monkeypatch, protocol, host_key, exp
|
||||
indexer = Mock(return_value={"schema": protocol, host_key: "https://custom-api.test:8443/api"})
|
||||
monkeypatch.setattr("app.chain.site.SitesHelper", Mock(return_value=Mock(get_indexer=indexer)))
|
||||
context = MagicMock()
|
||||
context.__enter__.return_value = Mock(status_code=200, json=Mock(return_value={"code": "0", "data": {"id": "1"}}))
|
||||
context.__enter__.return_value = Mock(
|
||||
status_code=200, json=Mock(return_value={"code": "0", "status": 0, "data": {"id": "1"}}),
|
||||
)
|
||||
open_response = Mock(return_value=context)
|
||||
monkeypatch.setattr("app.chain.site._open_site_response", open_response)
|
||||
|
||||
@@ -234,6 +237,38 @@ def test_api_probe_honors_resource_endpoint(monkeypatch, protocol, host_key, exp
|
||||
assert open_response.call_args.kwargs["url"] == expected_url
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain", ["hddolby.com", "uhddolby.com"])
|
||||
@pytest.mark.parametrize("api_url", ["https://www.uhddolby.com/api/v1", "https://www.uhddolby.com/api/v1/"])
|
||||
def test_hddolby_probe_uses_migrated_resource_for_both_domains(monkeypatch, domain, api_url):
|
||||
"""
|
||||
旧域专用路由和新域协议路由均按资源配置探测主站 API,并保留认证与连接参数。
|
||||
"""
|
||||
chain = SiteChain()
|
||||
site = SiteSnapshot(
|
||||
id=1, name="高清杜比", domain=domain, url=f"https://www.{domain}/",
|
||||
apikey="test-key", proxy=1, timeout=25,
|
||||
)
|
||||
chain.site_repository = Mock()
|
||||
chain.site_repository.get_by_domain.return_value = site
|
||||
indexer = Mock(return_value={"schema": "HDDolby", "api_url": api_url})
|
||||
monkeypatch.setattr("app.chain.site.SitesHelper", Mock(return_value=Mock(get_indexer=indexer)))
|
||||
context = MagicMock()
|
||||
context.__enter__.return_value = Mock(status_code=200, json=Mock(return_value={"status": 0}))
|
||||
open_response = Mock(return_value=context)
|
||||
monkeypatch.setattr("app.chain.site._open_site_response", open_response)
|
||||
|
||||
assert chain.test(site.url) == (True, "连接成功")
|
||||
|
||||
indexer.assert_called_with(domain)
|
||||
request = open_response.call_args.kwargs
|
||||
assert request["method"] == "GET"
|
||||
assert request["url"] == "https://www.uhddolby.com/api/v1/user/data"
|
||||
assert request["headers"]["x-api-key"] == "test-key"
|
||||
assert request["proxies"] == (chain.runtime_config.proxy or {})
|
||||
assert request["timeout"] == 25
|
||||
context.__exit__.assert_called_once_with(None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("apikey", [None, "test-auth-key"])
|
||||
def test_yema_registered_domain_prefers_authkey_and_preserves_legacy_cookie(monkeypatch, apikey):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user