mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 12:24:39 +08:00
fix(types): govern Zhixing user data contract
This commit is contained in:
@@ -41,7 +41,7 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
soup = BeautifulSoup(html_text, 'html.parser')
|
||||
return bool(soup.find(id='um'))
|
||||
|
||||
def _parse_user_base_info(self, html_text: SitePage):
|
||||
def _parse_user_base_info(self, html_text: SitePage) -> None:
|
||||
"""
|
||||
解析用户基本信息,这里把_parse_user_traffic_info和_parse_user_detail_info合并到这里
|
||||
"""
|
||||
@@ -49,7 +49,7 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
return None
|
||||
soup = BeautifulSoup(html_text, 'html.parser')
|
||||
details_tabs = soup.find_all('div', class_='user-details-tabs')
|
||||
info_dict = {}
|
||||
info_dict: dict[str, str] = {}
|
||||
for tab in details_tabs:
|
||||
for p in tab.find_all('p'):
|
||||
text = p.text.strip()
|
||||
@@ -70,9 +70,9 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
self.userid = info_dict.get('UID')
|
||||
self.username = info_dict.get('用户名')
|
||||
self.user_level = info_dict.get('用户组')
|
||||
self.join_at = time_tools.normalize_datetime(info_dict.get('注册时间')) if '注册时间' in info_dict else None
|
||||
self.join_at = time_tools.normalize_datetime(info_dict['注册时间']) if '注册时间' in info_dict else None
|
||||
|
||||
def num_filesize_safe(s: str):
|
||||
def num_filesize_safe(s: Optional[str]) -> int:
|
||||
"""为不带单位的数字补充字节单位,空值保持零容量。"""
|
||||
if s:
|
||||
s = s.strip()
|
||||
@@ -80,15 +80,15 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
s += ' B'
|
||||
return self.num_filesize(s) if s else 0
|
||||
|
||||
self.upload = num_filesize_safe(info_dict.get('上传流量')) if '上传流量' in info_dict else 0
|
||||
self.download = num_filesize_safe(info_dict.get('下载流量')) if '下载流量' in info_dict else 0
|
||||
self.ratio = float(info_dict.get('共享率')) if '共享率' in info_dict else 0
|
||||
self.bonus = float(info_dict.get('保种积分')) if '保种积分' in info_dict else 0.0
|
||||
self.upload = num_filesize_safe(info_dict['上传流量']) if '上传流量' in info_dict else 0
|
||||
self.download = num_filesize_safe(info_dict['下载流量']) if '下载流量' in info_dict else 0
|
||||
self.ratio = float(info_dict['共享率']) if '共享率' in info_dict else 0
|
||||
self.bonus = float(info_dict['保种积分']) if '保种积分' in info_dict else 0.0
|
||||
self.message_unread = 0 # 暂无消息解析
|
||||
|
||||
# Temporarily set seeding from basic, will override or fallback later
|
||||
self.seeding = int(info_dict.get('当前保种数量')) if '当前保种数量' in info_dict else 0
|
||||
self.seeding_size = num_filesize_safe(info_dict.get('当前保种容量')) if '当前保种容量' in info_dict else 0
|
||||
self.seeding = int(info_dict['当前保种数量']) if '当前保种数量' in info_dict else 0
|
||||
self.seeding_size = num_filesize_safe(info_dict['当前保种容量']) if '当前保种容量' in info_dict else 0
|
||||
|
||||
def _parse_user_traffic_info(self, html_text: str) -> None:
|
||||
"""该站点在其他解析入口处理此数据,本入口不追加操作。"""
|
||||
@@ -130,7 +130,7 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
"""
|
||||
pass
|
||||
|
||||
def parse(self):
|
||||
def parse(self) -> None:
|
||||
"""
|
||||
解析站点信息
|
||||
"""
|
||||
@@ -166,7 +166,7 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
page_num += 1
|
||||
# Fallback to basic if no seeding found from pages
|
||||
if self.seeding == 0 and hasattr(self, '_basic_info'):
|
||||
def num_filesize_safe(s: str):
|
||||
def num_filesize_safe(s: Optional[str]) -> int:
|
||||
"""为不带单位的数字补充字节单位,空值保持零容量。"""
|
||||
if s:
|
||||
s = s.strip()
|
||||
@@ -176,17 +176,8 @@ class ZhixingSiteUserInfo(SiteParserBase):
|
||||
self.seeding = int(self._basic_info.get('当前保种数量', 0))
|
||||
self.seeding_size = num_filesize_safe(self._basic_info.get('当前保种容量', ''))
|
||||
|
||||
# 🔑 最终对外统一转字符串,避免 join 报错
|
||||
# 文本字段归一为空字符串;流量、数量和比例保留基类约定的数值类型。
|
||||
self.userid = str(self.userid or "")
|
||||
self.username = str(self.username or "")
|
||||
self.user_level = str(self.user_level or "")
|
||||
self.join_at = str(self.join_at or "")
|
||||
|
||||
self.upload = str(self.upload or 0)
|
||||
self.download = str(self.download or 0)
|
||||
self.ratio = str(self.ratio or 0)
|
||||
self.bonus = str(self.bonus or 0.0)
|
||||
self.message_unread = str(self.message_unread or 0)
|
||||
|
||||
self.seeding = str(self.seeding or 0)
|
||||
self.seeding_size = str(self.seeding_size or 0)
|
||||
|
||||
1
mypy.ini
1
mypy.ini
@@ -7,6 +7,7 @@ follow_imports = skip
|
||||
show_error_codes = True
|
||||
pretty = True
|
||||
files =
|
||||
app/modules/indexer/parser/zhixing.py,
|
||||
app/chain/transfer/format.py,
|
||||
app/application/formatting.py,
|
||||
app/modules/transmission/__init__.py,
|
||||
|
||||
@@ -1157,11 +1157,6 @@
|
||||
"arg-type": 2,
|
||||
"type-arg": 1
|
||||
},
|
||||
"app/modules/indexer/parser/zhixing.py": {
|
||||
"arg-type": 7,
|
||||
"assignment": 7,
|
||||
"no-untyped-def": 4
|
||||
},
|
||||
"app/modules/indexer/spider/__init__.py": {
|
||||
"arg-type": 12,
|
||||
"assignment": 3,
|
||||
|
||||
52
tests/test_zhixing_numeric_contract.py
Normal file
52
tests/test_zhixing_numeric_contract.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""知行解析器的数值属性与对外 Schema 兼容合同。"""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.indexer.parser import SiteParserBase
|
||||
from app.modules.indexer.parser.zhixing import ZhixingSiteUserInfo
|
||||
from app.schemas.site import SiteUserData
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seeding_page,seeding,seeding_size", [
|
||||
("", 3, 2 * 1024 ** 3),
|
||||
('<table><tr id="t1"><td class="r"><a>1 GiB</a></td></tr></table>', 1, 1024 ** 3),
|
||||
])
|
||||
def test_numeric_properties_preserve_site_schema_and_seeding_fallback(
|
||||
monkeypatch, seeding_page, seeding, seeding_size,
|
||||
):
|
||||
"""离线解析保留页面统计和详情回退,属性采用基类数值类型,API 数值不变。"""
|
||||
parser = ZhixingSiteUserInfo(
|
||||
site_name="offline", url="https://zhixing.invalid/", site_cookie="", apikey=None, token=None,
|
||||
request_utils=Mock(),
|
||||
)
|
||||
monkeypatch.setattr(SiteParserBase, "parse", lambda _self: None)
|
||||
parser._parse_site_page("")
|
||||
parser._index_html = '<a href="/user/42/">offline</a>'
|
||||
basic = """<div class="user-details-tabs">
|
||||
<p>UID:42</p><p>用户名:offline</p><p>用户组:Power User</p>
|
||||
<p>上传流量:2048</p><p>下载流量:1024</p><p>共享率:2</p><p>保种积分:1.5</p>
|
||||
<p>当前保种数量:3</p><p>当前保种容量:2 GiB</p>
|
||||
</div>"""
|
||||
pages = {
|
||||
"https://zhixing.invalid/user/42/": basic,
|
||||
"https://zhixing.invalid/user/42/seeding/p1": seeding_page,
|
||||
"https://zhixing.invalid/user/42/seeding/p2": "",
|
||||
}
|
||||
reader = Mock(side_effect=lambda *, url: pages[url])
|
||||
monkeypatch.setattr(parser, "_get_page_content", reader)
|
||||
parser.parse()
|
||||
|
||||
expected = {
|
||||
"upload": 2048, "download": 1024, "ratio": 2.0, "bonus": 1.5,
|
||||
"message_unread": 0, "seeding": seeding, "seeding_size": seeding_size,
|
||||
}
|
||||
public_data = SiteUserData.model_validate(parser).model_dump(include=set(expected))
|
||||
assert public_data == expected
|
||||
assert parser.userid == "42"
|
||||
assert parser.username == "offline"
|
||||
assert parser.user_level == "Power User"
|
||||
for field, value in expected.items():
|
||||
assert type(getattr(parser, field)) is type(value), field
|
||||
assert reader.call_count == (3 if seeding_page else 2)
|
||||
Reference in New Issue
Block a user