diff --git a/app/modules/indexer/parser/__init__.py b/app/modules/indexer/parser/__init__.py
index 80893652c..da566f3d9 100644
--- a/app/modules/indexer/parser/__init__.py
+++ b/app/modules/indexer/parser/__init__.py
@@ -98,9 +98,9 @@ class SiteParserBase(metaclass=ABCMeta):
self.join_at = None
self.bonus = 0.0
- # 流量信息
- self.upload = 0.0
- self.download = 0.0
+ # 上传/下载量按整数字节计,分享率为浮点数
+ self.upload = 0
+ self.download = 0
self.ratio = 0.0
# 做种信息
diff --git a/app/modules/indexer/parser/nexus_php.py b/app/modules/indexer/parser/nexus_php.py
index ac635fec7..58694cb3b 100644
--- a/app/modules/indexer/parser/nexus_php.py
+++ b/app/modules/indexer/parser/nexus_php.py
@@ -650,9 +650,42 @@ class NexusPhpSiteUserInfo(SiteParserBase):
return message_head_text, message_date_text, message_content_text
def _fixup_traffic_info(self, html):
- """从详情页补充首页缺失的魔力值。"""
+ """从详情表格补读为零的流量、分享率和魔力值,非零值保持不变。"""
+ if not self.upload:
+ upload_text = self._traffic_table_value(html, r"上[传傳]量?")
+ if upload_text is not None:
+ self.upload = self.num_filesize(upload_text)
+ if not self.download:
+ download_text = self._traffic_table_value(html, r"下[载載]量?")
+ if download_text is not None:
+ self.download = self.num_filesize(download_text)
+ if not self.ratio:
+ ratio_text = self._traffic_table_value(html, "分享率")
+ if ratio_text is not None:
+ ratio = text_tools.parse_float(ratio_text)
+ else:
+ ratio = round(self.upload / self.download, 3) if self.download else 0.0
+ self.ratio = ratio
+
# fixup bonus
if not self.bonus:
bonus_text = html.xpath('//tr/td[text()="魔力值" or text()="猫粮"]/following-sibling::td[1]/text()')
if bonus_text:
self.bonus = text_tools.parse_float(bonus_text[0].strip())
+
+ @staticmethod
+ def _traffic_table_value(html: Any, field: str) -> Optional[str]:
+ """按单元格字段名读取数值,支持同格或相邻列布局,排除“实际”流量。"""
+ for cell in html.xpath('//tr/td[not(.//td)]'):
+ cell_text = " ".join(cell.xpath("string(.)").split())
+ field_match = re.fullmatch(rf"{field}\s*[::]?\s*(.*)", cell_text)
+ if not field_match:
+ continue
+ value = field_match.group(1).strip()
+ if not value:
+ siblings = cell.xpath('following-sibling::td[1]')
+ value = " ".join(siblings[0].xpath("string(.)").split()) if siblings else ""
+ value_match = re.fullmatch(r"([\d,.]+(?:\s*[KMGTPI]i?B)?)(?:\s*[((].*)?", value, re.I)
+ if value_match:
+ return value_match.group(1)
+ return None
diff --git a/tests/fixtures/depthstudio_userdetails.html b/tests/fixtures/depthstudio_userdetails.html
new file mode 100644
index 000000000..dbc63ffa4
--- /dev/null
+++ b/tests/fixtures/depthstudio_userdetails.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+ 分享率
+ 4.393
+
+
+
+
+ 上传量
+
+ 323.04 GB
+
+
+
+
+
+ 下载量
+
+ 73.54 GB
+
+
+
+
+
+
+ |
+ 分享率: 4.392
+ (实际分享率:0.045)
+ |
+
+
+ | 上传量: 323.04 GB |
+ 下载量: 73.54 GB |
+
+
+ | 实际上传量: 317.04 GB |
+ 实际下载量: 6.838 TB |
+ 实际上传/下载量 (仅用于记录, 不参与分享率计算) |
+
+
+
+
diff --git a/tests/test_discuz_parser.py b/tests/test_discuz_parser.py
index 942a89d3a..7ba7c06e6 100644
--- a/tests/test_discuz_parser.py
+++ b/tests/test_discuz_parser.py
@@ -19,10 +19,13 @@ def parser() -> Iterator[DiscuzUserInfo]:
instance.close()
-def test_discuz_traffic_defaults_are_floats(parser: DiscuzUserInfo) -> None:
- for value in (parser.upload, parser.download, parser.ratio):
- assert value == 0.0
- assert isinstance(value, float)
+def test_discuz_traffic_defaults_use_integer_bytes(parser: DiscuzUserInfo) -> None:
+ """上传/下载量使用整数字节,分享率保持浮点数。"""
+ for value in (parser.upload, parser.download):
+ assert value == 0
+ assert type(value) is int
+ assert parser.ratio == 0.0
+ assert isinstance(parser.ratio, float)
@pytest.mark.parametrize("include_menu", [True, False])
diff --git a/tests/test_nexus_php_parser.py b/tests/test_nexus_php_parser.py
index d707ce659..495c8c3fd 100644
--- a/tests/test_nexus_php_parser.py
+++ b/tests/test_nexus_php_parser.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
+from pathlib import Path
from urllib.parse import parse_qs, urlsplit
+import pytest
from lxml import etree
from app.modules.indexer.parser.nexus_php import NexusPhpSiteUserInfo
@@ -19,6 +21,121 @@ def _build_parser() -> NexusPhpSiteUserInfo:
)
+def test_nexus_php_depthstudio_traffic_falls_back_to_details():
+ """首页解析失败后从详情表格补齐流量,不依赖卡片类名或实际流量。"""
+ html_text = (Path(__file__).parent / "fixtures" / "depthstudio_userdetails.html").read_text(encoding="utf-8")
+ html = etree.HTML(html_text)
+ homepage = etree.tostring(html.xpath("//div")[0], encoding="unicode")
+ homepage = homepage.replace("info-stat", "other-style")
+ parser = _build_parser()
+
+ parser._parse_user_base_info(homepage)
+
+ assert parser.upload == 0
+ assert parser.download == 0
+
+ parser._parse_user_detail_info(html_text)
+
+ assert parser.upload == round(323.04 * 1024 ** 3)
+ assert parser.download == round(73.54 * 1024 ** 3)
+ assert parser.ratio == 4.393
+
+
+def test_nexus_php_detail_traffic_preserves_nonzero_values():
+ """详情兜底不能覆盖已有的非零流量和分享率。"""
+ parser = _build_parser()
+ parser._parse_user_base_info(
+ "上传量: 1 GB 下载量: 2 GB 分享率: 0.5"
+ )
+ original = parser.upload, parser.download, parser.ratio
+
+ parser._parse_user_detail_info(
+ '| 上传量: 323.04 GB | '
+ '下载量: 73.54 GB |
'
+ '| 分享率: 4.392 |
'
+ )
+
+ assert (parser.upload, parser.download, parser.ratio) == original
+
+
+@pytest.mark.parametrize("homepage, expected_upload, expected_download, expected_ratio", [
+ ("上传量: 1 GB", 1024 ** 3, round(73.54 * 1024 ** 3), 4.392),
+ ("下载量: 0 B 分享率: 0", round(323.04 * 1024 ** 3), round(73.54 * 1024 ** 3), 4.392),
+ ("上传量: 0 B 下载量: 0 B 分享率: 0", round(323.04 * 1024 ** 3), round(73.54 * 1024 ** 3), 4.392),
+ ("上传量: 1 GB 下载量: 2 GB 分享率: 0", 1024 ** 3, 2 * 1024 ** 3, 4.392),
+])
+def test_nexus_php_detail_traffic_fills_zero_fields(
+ homepage, expected_upload, expected_download, expected_ratio
+):
+ """零值字段允许从详情补读,分享率优先取详情表格的显示值。"""
+ parser = _build_parser()
+ parser._parse_user_base_info(f"{homepage}")
+ detail = (Path(__file__).parent / "fixtures" / "depthstudio_userdetails.html").read_text(encoding="utf-8")
+
+ parser._parse_user_detail_info(detail)
+
+ assert parser.upload == expected_upload
+ assert parser.download == expected_download
+ assert parser.ratio == expected_ratio
+
+
+def test_nexus_php_zero_ratio_uses_final_traffic_without_displayed_ratio():
+ """详情未显示分享率时,按补读后的上传/下载量重新计算零分享率。"""
+ parser = _build_parser()
+ parser._parse_user_base_info("上传量: 0 B 下载量: 0 B 分享率: 0")
+
+ parser._parse_user_detail_info(
+ ''
+ )
+
+ assert parser.upload == 8 * 1024 ** 3
+ assert parser.download == 2 * 1024 ** 3
+ assert parser.ratio == 4.0
+
+
+@pytest.mark.parametrize("table, expected_upload, expected_download, expected_ratio", [
+ ('| 上传量: 1.5 TB | '
+ '下载量: 512 GB |
',
+ round(1.5 * 1024 ** 4), 512 * 1024 ** 3, 3.0),
+ ('| 上傳量 | 1,024 GB |
| 下載量 | 512 GB |
'
+ '| 分享率 | 2.001 |
',
+ 1024 ** 4, 512 * 1024 ** 3, 2.001),
+ ('| 实际上传量: 1 TB | '
+ '实际下载量: 1 TB |
', 0, 0, 0.0),
+ ('| 上传量 | 0 B |
| 下载量 | 0 B |
', 0, 0, 0.0),
+ ('| 上传量 | 未知 |
| 下载量 | 未知 |
', 0, 0, 0.0),
+])
+def test_nexus_php_detail_traffic_table(table, expected_upload, expected_download, expected_ratio):
+ """按表格字段识别流量,支持嵌套单位和分列布局,并排除实际流量。"""
+ parser = _build_parser()
+ parser._parse_user_base_info("")
+
+ parser._parse_user_detail_info(f"")
+
+ assert parser.upload == expected_upload
+ assert parser.download == expected_download
+ assert parser.ratio == expected_ratio
+
+
+@pytest.mark.parametrize("download, ratio_text, expected_ratio", [
+ ("512 GB", "分享率: 3.001", 3.001),
+ ("512 GB", "", 3.0),
+ ("0 B", "", 0.0),
+])
+def test_nexus_php_legacy_traffic(download, ratio_text, expected_ratio):
+ """保留传统页面解析、分享率计算及零下载量行为。"""
+ parser = _build_parser()
+
+ parser._parse_user_traffic_info(
+ f"上传量: 1.5 TB 下载量: {download} {ratio_text}"
+ )
+
+ assert parser.upload == round(1.5 * 1024 ** 4)
+ assert parser.download == (512 * 1024 ** 3 if download == "512 GB" else 0)
+ assert parser.ratio == expected_ratio
+
+
def test_nexus_php_seeding_next_page_stops_when_userid_missing():
"""
userid 未识别且下一页也缺少 userid 时应停止翻页而不是抛出异常。