mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
fix(indexer): identify HDDolby API connection failures (#7164)
This commit is contained in:
@@ -662,6 +662,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
||||
通过 HDDolby 用户 API 验证 API Key,与搜索和资料解析使用相同资源 API 基础地址。
|
||||
|
||||
未配置 api_url 时沿用旧站的 api 子域,兼容存量资源和自定义索引。
|
||||
API 失败提示仅描述接口主机,不据此断言网页不可访问,也不以网页成功掩盖 API 故障。
|
||||
|
||||
:param site: HDDolby 协议站点配置
|
||||
:return: 是否可用及认证或连接失败原因
|
||||
@@ -669,6 +670,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
||||
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("/")
|
||||
api_host = urlparse(api_url).hostname
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
@@ -682,13 +684,13 @@ class SiteChain(InteractionChainMixin, ChainBase):
|
||||
timeout=site.timeout or 15,
|
||||
) as res:
|
||||
if res is None:
|
||||
return False, "无法打开网站!"
|
||||
return False, f"HDDolby API 主机 {api_host} 无法连接,请检查 API 地址及网络代理"
|
||||
if res.status_code == 200:
|
||||
user_info = res.json()
|
||||
if user_info and user_info.get("status") == 0:
|
||||
return True, "连接成功"
|
||||
return False, "APIKEY已过期"
|
||||
return False, f"错误:{res.status_code} {res.reason}"
|
||||
return False, f"HDDolby API 主机 {api_host} 返回 HTTP {res.status_code} {res.reason}"
|
||||
|
||||
def __rousi_test(self, site: SiteSnapshot) -> Tuple[bool, str]:
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"""
|
||||
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
|
||||
from app.application.configuration import get_configured_system_config
|
||||
@@ -202,6 +203,8 @@ class HddolbySpider:
|
||||
def __process_response(self, res: Any) -> Tuple[bool, List[dict[str, Any]]]:
|
||||
"""
|
||||
统一判定搜索响应状态、业务错误并投影 HDDolby 结果。
|
||||
|
||||
连接错误只记录实际 API 主机,避免把接口故障误报为网页不可达或输出 URL 中的凭据。
|
||||
"""
|
||||
if res and res.status_code == 200:
|
||||
result = res.json()
|
||||
@@ -212,7 +215,8 @@ class HddolbySpider:
|
||||
if res is not None:
|
||||
logger.warn(f"{self._name} 搜索失败,错误码:{res.status_code}")
|
||||
return True, []
|
||||
logger.warn(f"{self._name} 搜索失败,无法连接 {self._domain}")
|
||||
api_host = urlparse(self._searchurl or "").hostname
|
||||
logger.warn(f"{self._name} 搜索失败,HDDolby API 主机 {api_host} 无法连接,请检查 API 地址及网络代理")
|
||||
return True, []
|
||||
|
||||
def search(self, keyword: str, mtype: Optional[MediaType] = None, page: Optional[int] = 0,
|
||||
|
||||
@@ -82,6 +82,12 @@ focused 测试验证,不要求启动产品全套测试。
|
||||
|
||||
## 外部依赖:一律 mock,零真实网络
|
||||
|
||||
高清杜比换域回归由 `tests/test_hddolby_spider.py`、`tests/test_hddolby_parser.py` 和
|
||||
`tests/test_site_connectivity.py` 验证:网页使用 `www.uhddolby.com` 时,搜索、用户资料和
|
||||
做种统计仍可按资源的 `api_url` 访问 `api.hddolby.com`,并保留站点代理和认证配置。
|
||||
API 连接失败或返回 HTTP 错误时,连通性提示指出实际接口主机;网页解析成功不能代替
|
||||
API 验证。错误提示和搜索告警不得输出地址中的账号、密码或查询令牌。测试不使用真实站点凭据。
|
||||
|
||||
社区勋章使用 `tests/test_community_medal_background.py` 验证启动主动预热、无人访问时的六小时
|
||||
更新、失败后的自动重试及 TaskRegistry 关闭。外部下载或远端开关被屏障阻塞时,查询必须在
|
||||
屏障释放前返回当前快照;冷启动通过 `refreshing` 表示后台尚未完成,不能把刷新锁或网络等待
|
||||
|
||||
@@ -27,13 +27,13 @@ USER_DATA_JSON = """
|
||||
"""
|
||||
|
||||
|
||||
def _build_parser(site_cookie: str = "", **kwargs) -> HDDolbySiteUserInfo:
|
||||
def _build_parser(site_cookie: str = "", url: str = "https://www.hddolby.com/", **kwargs) -> HDDolbySiteUserInfo:
|
||||
"""
|
||||
构造隔离 HTTP 会话、允许覆盖 API 地址的杜比用户数据解析器。
|
||||
构造隔离 HTTP 会话、允许分别指定网页域名和 API 地址的杜比用户数据解析器。
|
||||
"""
|
||||
return HDDolbySiteUserInfo(
|
||||
site_name="高清杜比",
|
||||
url="https://www.hddolby.com/",
|
||||
url=url,
|
||||
site_cookie=site_cookie,
|
||||
apikey="test-api-key",
|
||||
token=None,
|
||||
@@ -49,6 +49,8 @@ def _build_parser(site_cookie: str = "", **kwargs) -> HDDolbySiteUserInfo:
|
||||
("", "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://api.hddolby.com/api/v1", "https://api.hddolby.com/api/v1/"),
|
||||
("https://api.hddolby.com/api/v1/", "https://api.hddolby.com/api/v1/"),
|
||||
("https://custom-api.test:8443/dolby/v1/", "https://custom-api.test:8443/dolby/v1/"),
|
||||
],
|
||||
)
|
||||
@@ -77,6 +79,25 @@ def test_userdata_uses_resource_api_url_for_profile_and_seeding(monkeypatch, api
|
||||
parser._request_utils.close.assert_called_once_with()
|
||||
|
||||
|
||||
def test_migrated_page_domain_keeps_old_api_for_userdata(monkeypatch):
|
||||
"""
|
||||
网页迁移后仍从旧域 API 读取资料和做种统计,不修改网页地址或代理选择。
|
||||
"""
|
||||
proxy = {"https": "socks5h://proxy.test:1080"}
|
||||
parser = _build_parser(url="https://www.uhddolby.com/", api_url="https://api.hddolby.com/api/v1", proxy=proxy)
|
||||
get_page = Mock(side_effect=[USER_DATA_JSON, '{"status": 0, "data": []}'])
|
||||
monkeypatch.setattr(parser, "_get_page_content", get_page)
|
||||
|
||||
parser.parse()
|
||||
|
||||
assert [call.kwargs["url"] for call in get_page.call_args_list] == [
|
||||
"https://api.hddolby.com/api/v1/user/data", "https://api.hddolby.com/api/v1/user/peers",
|
||||
]
|
||||
assert parser.userid == "41088"
|
||||
assert parser._site_url == "https://www.uhddolby.com/"
|
||||
assert parser._proxy == proxy
|
||||
|
||||
|
||||
def test_hddolby_parse_user_base_info_reads_unread_count():
|
||||
"""
|
||||
用户基础信息解析保留用户标识、名称和未读消息数量。
|
||||
|
||||
@@ -86,6 +86,10 @@ def test_parse_result_marks_music_torrents(hddolby_spider):
|
||||
"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://api.hddolby.com/api/v1",
|
||||
"https://api.hddolby.com/api/v1/torrent/search"),
|
||||
("https://www.uhddolby.com/", "https://api.hddolby.com/api/v1/",
|
||||
"https://api.hddolby.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"),
|
||||
],
|
||||
@@ -124,3 +128,28 @@ def test_search_honors_resource_api_url(
|
||||
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"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asynchronous", [False, True])
|
||||
def test_search_connection_failure_logs_api_host_without_credentials(monkeypatch, asynchronous):
|
||||
"""
|
||||
同步和异步搜索的连接告警指出 API 主机,避免误报网页故障并隐藏地址中的凭据。
|
||||
"""
|
||||
monkeypatch.setattr(hddolby_module, "get_configured_system_config", lambda: None)
|
||||
spider = HddolbySpider({
|
||||
**_build_indexer(),
|
||||
"domain": "https://www.uhddolby.com/",
|
||||
"api_url": "https://user:secret@api.hddolby.com/api/v1?token=secret",
|
||||
})
|
||||
request = Mock()
|
||||
request.return_value.post_res = AsyncMock(return_value=None) if asynchronous else Mock(return_value=None)
|
||||
monkeypatch.setattr(hddolby_module, "AsyncRequestUtils" if asynchronous else "RequestUtils", request)
|
||||
warning = Mock()
|
||||
monkeypatch.setattr(hddolby_module.logger, "warn", warning)
|
||||
|
||||
result = asyncio.run(spider.async_search("Movie")) if asynchronous else spider.search("Movie")
|
||||
|
||||
assert result == (True, [])
|
||||
warning.assert_called_once_with(
|
||||
"高清杜比 搜索失败,HDDolby API 主机 api.hddolby.com 无法连接,请检查 API 地址及网络代理",
|
||||
)
|
||||
|
||||
@@ -238,10 +238,10 @@ def test_api_probe_honors_resource_endpoint(monkeypatch, protocol, host_key, exp
|
||||
|
||||
|
||||
@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/"])
|
||||
@pytest.mark.parametrize("api_url", ["https://api.hddolby.com/api/v1", "https://api.hddolby.com/api/v1/"])
|
||||
def test_hddolby_probe_uses_migrated_resource_for_both_domains(monkeypatch, domain, api_url):
|
||||
"""
|
||||
旧域专用路由和新域协议路由均按资源配置探测主站 API,并保留认证与连接参数。
|
||||
旧域专用路由和新域协议路由均按资源访问旧域 API,并保留认证与连接参数。
|
||||
"""
|
||||
chain = SiteChain()
|
||||
site = SiteSnapshot(
|
||||
@@ -262,13 +262,54 @@ def test_hddolby_probe_uses_migrated_resource_for_both_domains(monkeypatch, doma
|
||||
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["url"] == "https://api.hddolby.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(
|
||||
("api_url", "host"),
|
||||
[
|
||||
(None, "api.uhddolby.com"),
|
||||
("https://api.hddolby.com/api/v1", "api.hddolby.com"),
|
||||
("https://user:secret@custom-api.test:8443/api?token=secret", "custom-api.test"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("status_code", [None, 405])
|
||||
def test_hddolby_api_failure_identifies_host_without_page_fallback(monkeypatch, api_url, host, status_code):
|
||||
"""
|
||||
接口失败提示真实 API 主机且不泄露凭据,网页可用也不能掩盖 API 搜索不可用。
|
||||
"""
|
||||
chain = SiteChain()
|
||||
site = SiteSnapshot(
|
||||
id=1, name="高清杜比", domain="uhddolby.com", url="https://www.uhddolby.com/", apikey="test-key",
|
||||
)
|
||||
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 = None if status_code is None else Mock(status_code=405, reason="Not Allowed")
|
||||
monkeypatch.setattr("app.chain.site._open_site_response", Mock(return_value=context))
|
||||
generic_test = Mock(return_value=(True, "连接成功"))
|
||||
monkeypatch.setattr(SiteChain, "_SiteChain__test", generic_test)
|
||||
|
||||
state, message = chain.test(site.url)
|
||||
|
||||
assert state is False
|
||||
if status_code is None:
|
||||
assert message == f"HDDolby API 主机 {host} 无法连接,请检查 API 地址及网络代理"
|
||||
else:
|
||||
assert message == f"HDDolby API 主机 {host} 返回 HTTP 405 Not Allowed"
|
||||
assert "secret" not in message
|
||||
assert "无法打开网站" not in message
|
||||
generic_test.assert_not_called()
|
||||
chain.site_repository.fail.assert_called_once_with("uhddolby.com")
|
||||
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