mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 05:17:23 +08:00
205 lines
7.4 KiB
Python
205 lines
7.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
解析 NexusRabbit 站点的用户资料、流量和做种信息。
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from urllib.parse import urljoin
|
|
|
|
from lxml import etree
|
|
|
|
from app.foundation import size as size_tools
|
|
from app.foundation import temporal as time_tools
|
|
from app.foundation import text as text_tools
|
|
from app.foundation.dom import DomUtils
|
|
from app.modules.indexer.parser import SitePage, SiteParserBase, SiteSchema
|
|
from app.runtime.log import logger
|
|
|
|
|
|
class NexusRabbitSiteUserInfo(SiteParserBase):
|
|
"""
|
|
解析 NexusRabbit 站点的用户资料、流量与消息。
|
|
"""
|
|
schema = SiteSchema.NexusRabbit
|
|
|
|
def _parse_site_page(self, html_text: SitePage) -> None:
|
|
"""
|
|
识别并配置站点数据页面与请求参数。
|
|
"""
|
|
html_text = self._prepare_html_text(html_text)
|
|
|
|
user_detail = re.search(r"user.php\?id=(\d+)", html_text)
|
|
|
|
if not (user_detail and user_detail.group().strip()):
|
|
return
|
|
|
|
self.userid = user_detail.group(1)
|
|
self._user_detail_page = f"user.php?id={self.userid}"
|
|
|
|
self._user_traffic_page = None
|
|
|
|
self._torrent_seeding_page = "api/general"
|
|
self._torrent_seeding_params = {
|
|
"page": 1,
|
|
"limit": 5000000,
|
|
"action": "userTorrentsList",
|
|
"data": {"type": "seeding", "id": int(self.userid)},
|
|
}
|
|
self._torrent_seeding_headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json, text/plain, */*",
|
|
# 必须要加上这一条,不然返回的是空数据
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
}
|
|
|
|
self._user_mail_unread_page = None
|
|
self._sys_mail_unread_page = "api/general"
|
|
self._mail_unread_params = {
|
|
"page": 1,
|
|
"limit": 5000000,
|
|
"action": "getMessageIn",
|
|
}
|
|
self._mail_unread_headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json, text/plain, */*",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
}
|
|
|
|
def _parse_user_torrent_seeding_info(
|
|
self, html_text: str, multi_page: bool = False
|
|
) -> None:
|
|
"""
|
|
从一次完整列表响应更新做种数量、体积和人数分布。
|
|
|
|
:param html_text: 做种列表 API 响应文本
|
|
:param multi_page: 保留基类调用参数,本实现不继续翻页
|
|
"""
|
|
|
|
try:
|
|
torrents = json.loads(html_text).get("data", [])
|
|
except Exception as e:
|
|
logger.error(f"解析做种信息失败: {str(e)}")
|
|
return None
|
|
|
|
seeding_size = 0
|
|
seeding_info = []
|
|
|
|
for torrent in torrents:
|
|
seeders = int(torrent.get("seeders", 0))
|
|
size = size_tools.parse_size(torrent.get("size"))
|
|
seeding_size += size
|
|
seeding_info.append([seeders, size])
|
|
|
|
self.seeding = len(torrents)
|
|
self.seeding_size = seeding_size
|
|
self.seeding_info = seeding_info
|
|
|
|
def _parse_message_unread_links(
|
|
self, html_text: str, msg_links: list[str]
|
|
) -> str | None:
|
|
"""
|
|
收集未读消息链接,返回仍需读取的下一页。
|
|
"""
|
|
unread_ids = []
|
|
try:
|
|
messages = json.loads(html_text).get("data", [])
|
|
except Exception as e:
|
|
logger.error(f"解析未读消息失败: {e}")
|
|
return None
|
|
for msg in messages:
|
|
msg_id, msg_unread = msg.get("id"), msg.get("unread")
|
|
if not (msg_id and msg_unread) or msg_unread == "no":
|
|
continue
|
|
unread_ids.append(msg_id)
|
|
head, date, content = msg.get("subject"), msg.get("added"), msg.get("msg")
|
|
if head and date and content:
|
|
self.message_unread_contents.append((head, date, content))
|
|
self.message_unread = len(unread_ids)
|
|
if unread_ids:
|
|
self._get_page_content(
|
|
url=urljoin(self._base_url, "api/general?loading=true"),
|
|
params={"action": "readMessage", "data": {"ids": unread_ids}},
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json, text/plain, */*",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
},
|
|
)
|
|
return None
|
|
|
|
def _parse_user_base_info(self, html_text: SitePage) -> None:
|
|
"""
|
|
只有奶糖余额才需要在 base 中获取,其它均可以在详情页拿到。
|
|
"""
|
|
html = etree.HTML(html_text)
|
|
try:
|
|
if not DomUtils.has_child_elements(html):
|
|
return
|
|
bonus = html.xpath(
|
|
'//div[contains(text(), "奶糖余额")]/following-sibling::div[1]/text()'
|
|
)
|
|
if bonus:
|
|
self.bonus = text_tools.parse_float(bonus[0].strip())
|
|
finally:
|
|
if html is not None:
|
|
del html
|
|
|
|
def _parse_user_detail_info(self, html_text: str) -> None:
|
|
"""
|
|
解析用户等级、注册时间及其他资料。
|
|
"""
|
|
html = etree.HTML(html_text)
|
|
try:
|
|
if not DomUtils.has_child_elements(html):
|
|
return
|
|
# 缩小一下查找范围,所有的信息都在这个 div 里
|
|
user_info = html.xpath('//div[contains(@class, "layui-hares-user-info-right")]')
|
|
if not user_info:
|
|
return
|
|
user_info = user_info[0]
|
|
# 用户名
|
|
if username := user_info.xpath(
|
|
'.//span[contains(text(), "用户名")]/a/span/text()'
|
|
):
|
|
self.username = username[0].strip()
|
|
# 等级
|
|
if user_level := user_info.xpath('.//span[contains(text(), "等级")]/b/text()'):
|
|
self.user_level = user_level[0].strip()
|
|
# 加入日期
|
|
if join_date := user_info.xpath('.//span[contains(text(), "注册日期")]/text()'):
|
|
join_date = join_date[0].strip().split("\r")[0].removeprefix("注册日期:")
|
|
self.join_at = time_tools.normalize_datetime(join_date)
|
|
# 上传量
|
|
if upload := user_info.xpath('.//span[contains(text(), "上传量")]/text()'):
|
|
self.upload = size_tools.parse_size(
|
|
upload[0].strip().removeprefix("上传量:")
|
|
)
|
|
# 下载量
|
|
if download := user_info.xpath('.//span[contains(text(), "下载量")]/text()'):
|
|
self.download = size_tools.parse_size(
|
|
download[0].strip().removeprefix("下载量:")
|
|
)
|
|
# 分享率
|
|
if ratio := user_info.xpath('.//span[contains(text(), "分享率")]/em/text()'):
|
|
self.ratio = text_tools.parse_float(ratio[0].strip())
|
|
finally:
|
|
if html is not None:
|
|
del html
|
|
|
|
def _parse_message_content(self, html_text: str) -> None:
|
|
"""
|
|
消息正文已在消息列表接口中写入实例,此入口不再解析。
|
|
|
|
:param html_text: 未使用的消息详情文本
|
|
"""
|
|
pass
|
|
|
|
def _parse_user_traffic_info(self, html_text: str) -> None:
|
|
"""
|
|
解析用户的上传,下载,分享率等信息,已经在 _parse_user_detail_info 内实现,重载防止 abstractmethod 报错。
|
|
|
|
:param html_text: 当前页面或 API 响应文本
|
|
"""
|
|
pass
|