mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
fix(indexer): fetch TNode current user before requiring an id (#6895)
This commit is contained in:
@@ -47,6 +47,8 @@ class SiteParserBase(metaclass=ABCMeta):
|
||||
schema = None
|
||||
# 请求模式 cookie/apikey
|
||||
request_mode = "cookie"
|
||||
# 详情页默认依赖用户 ID;当前会话接口可在配置页面时关闭此前置校验。
|
||||
_user_detail_requires_userid = True
|
||||
|
||||
def __init__(self, site_name: str,
|
||||
url: str,
|
||||
@@ -227,7 +229,7 @@ class SiteParserBase(metaclass=ABCMeta):
|
||||
self._parse_user_base_info(self._index_html)
|
||||
# 解析用户详细信息
|
||||
if self._user_detail_page:
|
||||
if not self._has_valid_userid():
|
||||
if self._user_detail_requires_userid and not self._has_valid_userid():
|
||||
logger.warn(f"{self._site_name} 未解析到有效用户 ID,跳过用户详情页请求")
|
||||
else:
|
||||
self._parse_user_detail_info(
|
||||
|
||||
@@ -9,16 +9,21 @@ from app.foundation import temporal as time_tools
|
||||
|
||||
|
||||
class TNodeSiteUserInfo(SiteParserBase):
|
||||
"""通过当前会话 API 获取 TNode 站点用户资料与做种信息。"""
|
||||
|
||||
schema = SiteSchema.TNode
|
||||
|
||||
def _parse_site_page(self, html_text: str):
|
||||
"""提取 CSRF 令牌,并配置用于取得用户 ID 的当前会话接口。"""
|
||||
html_text = self._prepare_html_text(html_text)
|
||||
|
||||
# <meta name="x-csrf-token" content="fd169876a7b4846f3a7a16fcd5cccf8d">
|
||||
csrf_token = re.search(r'<meta name="x-csrf-token" content="(.+?)">', html_text)
|
||||
if csrf_token:
|
||||
self._addition_headers = {'X-CSRF-TOKEN': csrf_token.group(1)}
|
||||
# 当前用户接口依赖 Cookie 会话,不需要预先知道用户 ID。
|
||||
self._user_detail_page = "api/user/getMainInfo"
|
||||
self._user_detail_requires_userid = False
|
||||
self._torrent_seeding_page = "api/user/listTorrentActivity?id=&type=seeding&page=1&size=20000"
|
||||
|
||||
def _parse_logged_in(self, html_text):
|
||||
@@ -31,12 +36,15 @@ class TNodeSiteUserInfo(SiteParserBase):
|
||||
return True
|
||||
|
||||
def _parse_user_base_info(self, html_text: str):
|
||||
"""首页不提供用户 ID,身份信息需在详情接口返回后补齐。"""
|
||||
self.username = self.userid
|
||||
|
||||
def _parse_user_traffic_info(self, html_text: str):
|
||||
"""流量信息已由当前用户接口返回,无需单独解析。"""
|
||||
pass
|
||||
|
||||
def _parse_user_detail_info(self, html_text: str):
|
||||
"""从当前用户接口一次解析身份、资料、流量及未读消息数量。"""
|
||||
try:
|
||||
detail = json.loads(html_text)
|
||||
except json.JSONDecodeError:
|
||||
@@ -98,6 +106,7 @@ class TNodeSiteUserInfo(SiteParserBase):
|
||||
return next_page
|
||||
|
||||
def _parse_message_unread_links(self, html_text: str, msg_links: list) -> Optional[str]:
|
||||
"""当前仅支持 API 返回的未读数量,不解析消息链接。"""
|
||||
return None
|
||||
|
||||
def _parse_message_content(self, html_text):
|
||||
|
||||
152
tests/test_tnode_parser.py
Normal file
152
tests/test_tnode_parser.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""TNode 用户信息刷新顺序与失败保护回归测试。"""
|
||||
|
||||
import json
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from requests import Response
|
||||
|
||||
from app.adapters.network.http import RequestUtils
|
||||
from app.modules.indexer.parser.tnode import TNodeSiteUserInfo
|
||||
from app.runtime.config import settings
|
||||
|
||||
SITE_URL = "https://tnode.example/"
|
||||
PROFILE_URL = f"{SITE_URL}api/user/getMainInfo"
|
||||
SEEDING_URL = f"{SITE_URL}api/user/listTorrentActivity?id=&type=seeding&page=1&size=20000"
|
||||
HOME_HTML = '<html><head><meta name="x-csrf-token" content="test-csrf"></head></html>'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_info() -> dict:
|
||||
"""构造不含真实账号信息的 TNode 用户资料。"""
|
||||
return {
|
||||
"id": 42,
|
||||
"username": "test-user",
|
||||
"class": {"name": "Power User"},
|
||||
"regTime": "2023-01-02 03:04:05",
|
||||
"upload": 4096,
|
||||
"download": 1024,
|
||||
"bonus": 12.5,
|
||||
"unreadAdmin": 1,
|
||||
"unreadInbox": 2,
|
||||
"unreadSystem": 3,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pages(user_info: dict) -> dict[str, str]:
|
||||
"""提供完整解析流程所需的离线首页、资料和做种响应。"""
|
||||
return {
|
||||
SITE_URL: HOME_HTML,
|
||||
PROFILE_URL: json.dumps({"status": 200, "data": user_info}),
|
||||
f"{SITE_URL}index.php": HOME_HTML,
|
||||
SEEDING_URL: json.dumps({
|
||||
"status": 200,
|
||||
"data": {"torrents": [{"size": 1024, "seeding": 2}, {"size": 2048, "seeding": 3}]},
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser(monkeypatch: pytest.MonkeyPatch, pages: dict[str, str]) -> TNodeSiteUserInfo:
|
||||
"""只替换 HTTP 边界,保留真实解析、请求头合并和会话关闭行为。"""
|
||||
client = Mock(spec=RequestUtils)
|
||||
|
||||
def get_response(url: str, **_kwargs) -> Response:
|
||||
"""拒绝未声明的请求,并返回离线 HTTP 响应。"""
|
||||
response = Response()
|
||||
response.status_code = 200
|
||||
response.encoding = "utf-8"
|
||||
response._content = pages[url].encode("utf-8")
|
||||
return response
|
||||
|
||||
client.get_res.side_effect = get_response
|
||||
monkeypatch.setattr(settings, "SITE_MESSAGE", False)
|
||||
return TNodeSiteUserInfo(
|
||||
site_name="TNode",
|
||||
url=SITE_URL,
|
||||
site_cookie="session=test-cookie",
|
||||
apikey=None,
|
||||
token=None,
|
||||
request_utils=client,
|
||||
)
|
||||
|
||||
|
||||
def test_tnode_fetches_current_user_before_seeding(parser: TNodeSiteUserInfo) -> None:
|
||||
"""空用户 ID 时仍应获取当前会话资料,之后继续刷新做种数据。"""
|
||||
assert parser.userid is None
|
||||
|
||||
parser.parse()
|
||||
|
||||
assert parser.userid == 42
|
||||
assert parser.username == "test-user"
|
||||
assert parser.user_level == "Power User"
|
||||
assert parser.join_at == "2023-01-02 03:04:05"
|
||||
assert parser.upload == 4096
|
||||
assert parser.download == 1024
|
||||
assert parser.ratio == 4
|
||||
assert parser.bonus == 12.5
|
||||
assert parser.message_unread == 6
|
||||
assert parser.seeding == 2
|
||||
assert parser.seeding_size == 3072
|
||||
assert parser.seeding_info == [[2, 1024], [3, 2048]]
|
||||
|
||||
calls = parser._request_utils.get_res.call_args_list
|
||||
urls = [call.kwargs["url"] for call in calls]
|
||||
assert urls.count(PROFILE_URL) == 1
|
||||
assert urls.index(PROFILE_URL) < urls.index(SEEDING_URL)
|
||||
for call in calls:
|
||||
if call.kwargs["url"] in (PROFILE_URL, SEEDING_URL):
|
||||
assert call.kwargs["headers"]["X-CSRF-TOKEN"] == "test-csrf"
|
||||
assert call.kwargs["cookies"] == "session=test-cookie"
|
||||
parser._request_utils.close.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", ["", "<html>Login</html>", '{"status": 401}'])
|
||||
def test_tnode_failed_profile_skips_seeding(
|
||||
parser: TNodeSiteUserInfo, pages: dict[str, str], payload: str,
|
||||
) -> None:
|
||||
"""资料为空、非 JSON 或业务失败时,不应继续请求做种接口。"""
|
||||
pages[PROFILE_URL] = payload
|
||||
|
||||
parser.parse()
|
||||
|
||||
urls = [call.kwargs["url"] for call in parser._request_utils.get_res.call_args_list]
|
||||
assert urls.count(PROFILE_URL) == 1
|
||||
assert SEEDING_URL not in urls
|
||||
assert parser.userid is None
|
||||
assert parser.username is None
|
||||
assert parser.seeding == 0
|
||||
parser._request_utils.close.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("userid", [None, "", " ", 0, "0"])
|
||||
def test_tnode_invalid_userid_skips_seeding(
|
||||
parser: TNodeSiteUserInfo, pages: dict[str, str], user_info: dict, userid,
|
||||
) -> None:
|
||||
"""即使资料接口成功,缺失或无效的用户 ID 仍应阻止做种请求。"""
|
||||
user_info["id"] = userid
|
||||
pages[PROFILE_URL] = json.dumps({"status": 200, "data": user_info})
|
||||
|
||||
parser.parse()
|
||||
|
||||
urls = [call.kwargs["url"] for call in parser._request_utils.get_res.call_args_list]
|
||||
assert urls.count(PROFILE_URL) == 1
|
||||
assert SEEDING_URL not in urls
|
||||
assert not parser._has_valid_userid()
|
||||
assert parser.seeding == 0
|
||||
parser._request_utils.close.assert_called_once_with()
|
||||
|
||||
|
||||
def test_tnode_missing_csrf_skips_user_requests(parser: TNodeSiteUserInfo, pages: dict[str, str]) -> None:
|
||||
"""首页缺少 CSRF 时,不应请求未经配置的资料或做种地址。"""
|
||||
pages[SITE_URL] = "<html>Login</html>"
|
||||
|
||||
parser.parse()
|
||||
|
||||
urls = [call.kwargs["url"] for call in parser._request_utils.get_res.call_args_list]
|
||||
assert urls == [SITE_URL, f"{SITE_URL}index.php"]
|
||||
assert parser.userid is None
|
||||
assert parser.seeding == 0
|
||||
parser._request_utils.close.assert_called_once_with()
|
||||
Reference in New Issue
Block a user