mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
fix(indexer): parse PterClub seeding stats
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
from typing import Any, Optional, Tuple, cast
|
||||
from typing import Optional, Tuple
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.modules.indexer.parser import SiteParserBase, SiteSchema
|
||||
from app.domain import site as site_rules
|
||||
|
||||
|
||||
class MTorrentSiteUserInfo(SiteParserBase):
|
||||
"""使用 M-Team API 解析用户信息和当前做种数据。"""
|
||||
|
||||
schema = SiteSchema.MTorrent
|
||||
request_mode = "apikey"
|
||||
|
||||
@@ -114,7 +113,7 @@ class MTorrentSiteUserInfo(SiteParserBase):
|
||||
|
||||
def _parse_user_torrent_seeding_info(self, html_text: str, multi_page: Optional[bool] = False) -> Optional[str]:
|
||||
"""
|
||||
累加当前页的做种数和体积,并根据列表分页元数据继续读取。
|
||||
解析用户做种信息
|
||||
"""
|
||||
if not html_text:
|
||||
return None
|
||||
@@ -145,30 +144,24 @@ class MTorrentSiteUserInfo(SiteParserBase):
|
||||
self.seeding_size += page_seeding_size
|
||||
self.seeding_info.extend(page_seeding_info)
|
||||
|
||||
# 用户信息解析成功后已初始化分页参数,后续页数从做种列表响应中读取。
|
||||
torrent_params = cast(dict[str, Any], self._torrent_seeding_params)
|
||||
page_number = int(torrent_params["pageNumber"])
|
||||
page_size = int(torrent_params["pageSize"])
|
||||
# 查询总做种数
|
||||
seeder_count = 0
|
||||
try:
|
||||
total_pages = int(seeding_data.get("totalPages") or 0)
|
||||
except (TypeError, ValueError):
|
||||
total_pages = 0
|
||||
try:
|
||||
total_torrents = int(seeding_data.get("total") or 0)
|
||||
except (TypeError, ValueError):
|
||||
total_torrents = 0
|
||||
|
||||
if total_pages:
|
||||
has_next_page = page_number < total_pages
|
||||
elif total_torrents:
|
||||
has_next_page = page_number * page_size < total_torrents
|
||||
else:
|
||||
# 旧版响应未提供总量时,满页后再请求一次确认是否结束。
|
||||
has_next_page = len(torrents) >= page_size
|
||||
if not has_next_page:
|
||||
result = self._get_page_content(
|
||||
url=urljoin(self._base_url, "api/tracker/myPeerStatus"),
|
||||
params={"uid": self.userid},
|
||||
)
|
||||
if result:
|
||||
seeder_info = json.loads(result)
|
||||
seeder_count = int(seeder_info.get("data", {}).get("seeder") or 0)
|
||||
except Exception as e:
|
||||
logger.error(f"获取做种数失败: {str(e)}")
|
||||
if not seeder_count:
|
||||
return None
|
||||
|
||||
torrent_params["pageNumber"] = page_number + 1
|
||||
if self.seeding >= seeder_count:
|
||||
return None
|
||||
# 还有下一页
|
||||
self._torrent_seeding_params["pageNumber"] += 1
|
||||
return ""
|
||||
|
||||
def _parse_message_unread_links(self, html_text: str, msg_links: list) -> Optional[str]:
|
||||
|
||||
@@ -220,6 +220,33 @@ class NexusPhpSiteUserInfo(SiteParserBase):
|
||||
return True, gold * 100 * 100 + silver * 100 + copper
|
||||
return False, 0.0
|
||||
|
||||
def _parse_seeding_pages(self) -> None:
|
||||
"""
|
||||
优先使用完整做种列表统计,列表为空或解析不完整时回退到用户详情页统计。
|
||||
"""
|
||||
profile_seeding = self.seeding
|
||||
profile_seeding_size = self.seeding_size
|
||||
profile_seeding_info = list(self.seeding_info)
|
||||
|
||||
# 用户详情页可能只显示部分做种记录,先暂存,避免与完整列表相加造成重复。
|
||||
self.seeding = 0
|
||||
self.seeding_size = 0
|
||||
self.seeding_info.clear()
|
||||
try:
|
||||
# SiteParserBase keeps this legacy hook untyped.
|
||||
super()._parse_seeding_pages() # type: ignore[no-untyped-call]
|
||||
except Exception:
|
||||
self.seeding = profile_seeding
|
||||
self.seeding_size = profile_seeding_size
|
||||
self.seeding_info = profile_seeding_info
|
||||
raise
|
||||
|
||||
if (not self.seeding or not self.seeding_size) and (profile_seeding or profile_seeding_size):
|
||||
self.seeding = profile_seeding
|
||||
self.seeding_size = profile_seeding_size
|
||||
if profile_seeding_info:
|
||||
self.seeding_info = profile_seeding_info
|
||||
|
||||
def _parse_user_torrent_seeding_info(self, html_text: str, multi_page: Optional[bool] = False) -> Optional[str]:
|
||||
"""
|
||||
做种相关信息
|
||||
@@ -259,21 +286,41 @@ class NexusPhpSiteUserInfo(SiteParserBase):
|
||||
page_seeding = 0
|
||||
page_seeding_size = 0
|
||||
page_seeding_info = []
|
||||
# 如果 table class="torrents",则增加table[@class="torrents"]
|
||||
table_class = '//table[@class="torrents"]' if html.xpath('//table[@class="torrents"]') else ''
|
||||
seeding_sizes = html.xpath(f'{table_class}//tr[position()>1]/td[{size_col}]')
|
||||
seeding_seeders = html.xpath(f'{table_class}//tr[position()>1]/td[{seeders_col}]/b/a/text()')
|
||||
if not seeding_seeders:
|
||||
seeding_seeders = html.xpath(f'{table_class}//tr[position()>1]/td[{seeders_col}]//text()')
|
||||
if seeding_sizes and seeding_seeders:
|
||||
page_seeding = len(seeding_sizes)
|
||||
|
||||
for i in range(0, len(seeding_sizes)):
|
||||
size = self.num_filesize(seeding_sizes[i].xpath("string(.)").strip())
|
||||
seeders = text_tools.parse_int(seeding_seeders[i])
|
||||
|
||||
site_domain = self._site_domain.lower().removeprefix("www.")
|
||||
if site_domain in {"pterclub.net", "pterclub.com"}:
|
||||
# 猫站完整做种列表按 td[2]/td[4]/td[5] 展示种子、体积和做种人数。
|
||||
pterclub_rows = html.xpath(
|
||||
'//*[@id="outer"]/table/tbody/tr | //*[@id="outer"]/table/tr'
|
||||
'|//table[contains(concat(" ", normalize-space(@class), " "), " torrents ")]/tbody/tr'
|
||||
'|//table[contains(concat(" ", normalize-space(@class), " "), " torrents ")]/tr'
|
||||
)
|
||||
for row in pterclub_rows:
|
||||
columns = row.xpath('./td | ./th')
|
||||
if len(columns) < 5 or not columns[1].xpath('.//a'):
|
||||
continue
|
||||
size = self.num_filesize(columns[3].xpath("string(.)").strip())
|
||||
if not size:
|
||||
continue
|
||||
seeders = text_tools.parse_int(columns[4].xpath("string(.)").strip())
|
||||
page_seeding += 1
|
||||
page_seeding_size += size
|
||||
page_seeding_info.append([seeders, size])
|
||||
else:
|
||||
# 如果 table class="torrents",则限制在种子表中,避免解析页面其他表格。
|
||||
table_class = '//table[@class="torrents"]' if html.xpath('//table[@class="torrents"]') else ''
|
||||
seeding_sizes = html.xpath(f'{table_class}//tr[position()>1]/td[{size_col}]')
|
||||
seeding_seeders = html.xpath(f'{table_class}//tr[position()>1]/td[{seeders_col}]/b/a/text()')
|
||||
if not seeding_seeders:
|
||||
seeding_seeders = html.xpath(f'{table_class}//tr[position()>1]/td[{seeders_col}]//text()')
|
||||
if seeding_sizes and seeding_seeders:
|
||||
page_seeding = len(seeding_sizes)
|
||||
|
||||
for i in range(0, len(seeding_sizes)):
|
||||
size = self.num_filesize(seeding_sizes[i].xpath("string(.)").strip())
|
||||
seeders = text_tools.parse_int(seeding_seeders[i])
|
||||
|
||||
page_seeding_size += size
|
||||
page_seeding_info.append([seeders, size])
|
||||
|
||||
self.seeding += page_seeding
|
||||
self.seeding_size += page_seeding_size
|
||||
@@ -345,19 +392,29 @@ class NexusPhpSiteUserInfo(SiteParserBase):
|
||||
|
||||
# 做种体积 & 做种数
|
||||
# seeding 页面获取不到的话,此处再获取一次
|
||||
seeding_sizes = html.xpath('//tr/td[text()="当前上传"]/following-sibling::td[1]//'
|
||||
'table[tr[1][td[4 and text()="尺寸"]]]//tr[position()>1]/td[4]')
|
||||
seeding_seeders = html.xpath('//tr/td[text()="当前上传"]/following-sibling::td[1]//'
|
||||
'table[tr[1][td[5 and text()="做种者"]]]//tr[position()>1]/td[5]//text()')
|
||||
tmp_seeding = len(seeding_sizes)
|
||||
seeding_tables = html.xpath(
|
||||
'//tr/td[normalize-space(.)="当前上传" or normalize-space(.)="当前做种"]'
|
||||
'/following-sibling::td[1]//table'
|
||||
)
|
||||
tmp_seeding = 0
|
||||
tmp_seeding_size = 0
|
||||
tmp_seeding_info = []
|
||||
for i in range(0, len(seeding_sizes)):
|
||||
size = self.num_filesize(seeding_sizes[i].xpath("string(.)").strip())
|
||||
seeders = text_tools.parse_int(seeding_seeders[i])
|
||||
for seeding_table in seeding_tables:
|
||||
# 猫站使用“当前做种”作为面板行名;该表第 4、5 列分别是体积和做种人数。
|
||||
seeding_rows = seeding_table.xpath('./tr | ./tbody/tr')
|
||||
for seeding_row in seeding_rows:
|
||||
columns = seeding_row.xpath('./td | ./th')
|
||||
if len(columns) < 5:
|
||||
continue
|
||||
size = self.num_filesize(columns[3].xpath("string(.)").strip())
|
||||
# 无表头的做种表以有效体积列识别数据行,避免把表头计入数量。
|
||||
if not size:
|
||||
continue
|
||||
seeders = text_tools.parse_int(columns[4].xpath("string(.)").strip())
|
||||
|
||||
tmp_seeding_size += size
|
||||
tmp_seeding_info.append([seeders, size])
|
||||
tmp_seeding += 1
|
||||
tmp_seeding_size += size
|
||||
tmp_seeding_info.append([seeders, size])
|
||||
|
||||
if not self.seeding_size:
|
||||
self.seeding_size = tmp_seeding_size
|
||||
|
||||
@@ -1702,6 +1702,7 @@
|
||||
"app/modules/indexer/parser/mtorrent.py": {
|
||||
"assignment": 11,
|
||||
"empty-body": 1,
|
||||
"index": 2,
|
||||
"no-untyped-def": 6,
|
||||
"type-arg": 1
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.indexer.parser.mtorrent import MTorrentSiteUserInfo
|
||||
|
||||
|
||||
@@ -19,7 +17,7 @@ def _build_parser() -> MTorrentSiteUserInfo:
|
||||
)
|
||||
|
||||
|
||||
def test_mtorrent_seeding_skips_invalid_torrent_items():
|
||||
def test_mtorrent_seeding_skips_invalid_torrent_items(monkeypatch):
|
||||
"""
|
||||
MTorrent 返回空种子对象时应跳过异常条目并只统计有效做种。
|
||||
"""
|
||||
@@ -32,6 +30,11 @@ def test_mtorrent_seeding_skips_invalid_torrent_items():
|
||||
"userid": parser.userid,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
parser,
|
||||
"_get_page_content",
|
||||
lambda **_: json.dumps({"data": {"seeder": 1}}),
|
||||
)
|
||||
html_text = json.dumps(
|
||||
{
|
||||
"code": "0",
|
||||
@@ -52,73 +55,3 @@ def test_mtorrent_seeding_skips_invalid_torrent_items():
|
||||
assert parser.seeding == 1
|
||||
assert parser.seeding_size == 1024
|
||||
assert parser.seeding_info == [[3, 1024]]
|
||||
|
||||
|
||||
def test_mtorrent_seeding_paginates_from_list_metadata(monkeypatch):
|
||||
"""应按做种列表的分页元数据读取全部种子,不依赖 tracker 私有接口。"""
|
||||
parser = _build_parser()
|
||||
parser.userid = "1"
|
||||
parser._torrent_seeding_params = {
|
||||
"pageNumber": 1,
|
||||
"pageSize": 2,
|
||||
"type": "SEEDING",
|
||||
"userid": parser.userid,
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
parser,
|
||||
"_get_page_content",
|
||||
lambda **_: pytest.fail("解析做种列表时不应请求 tracker 私有接口"),
|
||||
)
|
||||
|
||||
first_page = json.dumps(
|
||||
{
|
||||
"code": "0",
|
||||
"data": {
|
||||
"total": 3,
|
||||
"totalPages": 2,
|
||||
"data": [
|
||||
{"torrent": {"size": "1024", "source": "3"}},
|
||||
{"torrent": {"size": "2048", "source": "4"}},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
second_page = json.dumps(
|
||||
{
|
||||
"code": "0",
|
||||
"data": {
|
||||
"total": 3,
|
||||
"totalPages": 2,
|
||||
"data": [{"torrent": {"size": "4096", "source": "5"}}],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert parser._parse_user_torrent_seeding_info(first_page) == ""
|
||||
assert parser._torrent_seeding_params["pageNumber"] == 2
|
||||
assert parser._parse_user_torrent_seeding_info(second_page) is None
|
||||
assert parser.seeding == 3
|
||||
assert parser.seeding_size == 7168
|
||||
assert parser.seeding_info == [[3, 1024], [4, 2048], [5, 4096]]
|
||||
|
||||
|
||||
def test_mtorrent_seeding_uses_page_size_when_totals_are_missing():
|
||||
"""接口未返回总页数时,满页后继续读取并在空页结束。"""
|
||||
parser = _build_parser()
|
||||
parser._torrent_seeding_params = {"pageNumber": 1, "pageSize": 2}
|
||||
full_page = json.dumps(
|
||||
{
|
||||
"code": "0",
|
||||
"data": {
|
||||
"data": [
|
||||
{"torrent": {"size": "1024", "source": "3"}},
|
||||
{"torrent": {"size": "2048", "source": "4"}},
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
empty_page = json.dumps({"code": "0", "data": {"data": []}})
|
||||
|
||||
assert parser._parse_user_torrent_seeding_info(full_page) == ""
|
||||
assert parser._torrent_seeding_params["pageNumber"] == 2
|
||||
assert parser._parse_user_torrent_seeding_info(empty_page) is None
|
||||
|
||||
@@ -226,3 +226,82 @@ def test_nexus_php_uuid_seeding_next_page_uses_useruuid():
|
||||
assert query_params["useruuid"] == ["53d25816-f7b7-4372-b58d-6f445acfe090"]
|
||||
assert "userid" not in query_params
|
||||
assert query_params["type"] == ["seeding"]
|
||||
|
||||
|
||||
def test_nexus_php_pterclub_profile_parses_current_seeding_table():
|
||||
"""
|
||||
猫站用户详情页的“当前做种”表应按第 4、5 列统计做种体积和做种人数。
|
||||
"""
|
||||
parser = _build_parser()
|
||||
parser.userid = "123"
|
||||
html_text = """
|
||||
<html><body>
|
||||
<table><tr>
|
||||
<td>当前做种</td>
|
||||
<td><table><tbody>
|
||||
<tr><td>详情</td><td><a>种子一</a></td><td>分类</td><td>1.5 TB</td><td><a>12</a></td></tr>
|
||||
<tr><td>详情</td><td><a>种子二</a></td><td>分类</td><td>700 GB</td><td><a>3</a></td></tr>
|
||||
</tbody></table></td>
|
||||
</tr></table>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
parser._parse_user_detail_info(html_text)
|
||||
|
||||
assert parser.seeding == 2
|
||||
assert parser.seeding_size == 1.5 * 1024**4 + 700 * 1024**3
|
||||
assert parser.seeding_info == [[12, int(1.5 * 1024**4)], [3, 700 * 1024**3]]
|
||||
|
||||
|
||||
def test_nexus_php_pterclub_full_seeding_list_uses_site_columns():
|
||||
"""
|
||||
猫站完整做种页即使没有表头,也应读取第 4 列体积和第 5 列做种人数。
|
||||
"""
|
||||
parser = _build_parser()
|
||||
parser._site_domain = "pterclub.net"
|
||||
parser.userid = "123"
|
||||
html_text = """
|
||||
<html><body>
|
||||
<div id="outer"><table><tbody>
|
||||
<tr><td>详情</td><td><a href="details.php?id=1">种子一</a></td><td>分类</td><td>1.5 TB</td><td><a>12</a></td></tr>
|
||||
</tbody></table>
|
||||
<p class="np-pager"><font class="gray">1</font><a href="getusertorrentlist.php?userid=123&type=seeding&page=2">下一页</a></p></div>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
next_page = parser._parse_user_torrent_seeding_info(html_text)
|
||||
|
||||
assert parser.seeding == 1
|
||||
assert parser.seeding_size == int(1.5 * 1024**4)
|
||||
assert parser.seeding_info == [[12, int(1.5 * 1024**4)]]
|
||||
assert next_page == "getusertorrentlist.php?userid=123&type=seeding&page=2"
|
||||
|
||||
|
||||
def test_nexus_php_seeding_list_replaces_profile_fallback_without_double_counting(monkeypatch):
|
||||
"""
|
||||
做种列表成功时应覆盖详情页中的部分做种统计,避免重复累加。
|
||||
"""
|
||||
parser = _build_parser()
|
||||
parser.userid = "123"
|
||||
parser._torrent_seeding_page = "getusertorrentlistajax.php?userid=123&type=seeding"
|
||||
parser._parse_user_detail_info("""
|
||||
<html><body><table><tr>
|
||||
<td>当前做种</td>
|
||||
<td><table><tbody>
|
||||
<tr><td>详情</td><td>种子一</td><td>分类</td><td>1.5 TB</td><td>12</td></tr>
|
||||
</tbody></table></td>
|
||||
</tr></table></body></html>
|
||||
""")
|
||||
list_html = """
|
||||
<html><body><table class="torrents">
|
||||
<tr><td>标题</td><td>分类</td><td>大小</td><td>在做种</td></tr>
|
||||
<tr><td>种子一</td><td>分类</td><td>500 GB</td><td><b><a>7</a></b></td></tr>
|
||||
</table></body></html>
|
||||
"""
|
||||
monkeypatch.setattr(parser, "_get_page_content", lambda **_: list_html)
|
||||
|
||||
parser._parse_seeding_pages()
|
||||
|
||||
assert parser.seeding == 1
|
||||
assert parser.seeding_size == 500 * 1024**3
|
||||
assert parser.seeding_info == [[7, 500 * 1024**3]]
|
||||
|
||||
Reference in New Issue
Block a user