fix(indexer): honor HDDolby API resource URL (#7158)

This commit is contained in:
jxxghp
2026-10-23 17:48:59 +08:00
parent 8c156d4f47
commit 4d32f25fe4
6 changed files with 159 additions and 13 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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,

View File

@@ -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():
"""
用户基础信息解析保留用户标识、名称和未读消息数量。

View File

@@ -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"

View File

@@ -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):
"""