fix: deduplicate SunnyPT site messages

This commit is contained in:
jxxghp
2026-07-23 14:12:41 +08:00
parent 3dde94be0f
commit 297cd04fbc
5 changed files with 98 additions and 20 deletions
+33 -17
View File
@@ -78,23 +78,7 @@ class SiteChain(ChainBase):
eventmanager.send_event(EventType.SiteRefreshed, { eventmanager.send_event(EventType.SiteRefreshed, {
"site_id": site.get("id") "site_id": site.get("id")
}) })
# 发送站点消息 self._post_site_messages(site=site, userdata=userdata)
if userdata.message_unread:
if userdata.message_unread_contents and len(userdata.message_unread_contents) > 0:
for head, date, content in userdata.message_unread_contents:
msg_title = f"【站点 {site.get('name')} 消息】"
msg_text = f"时间:{date}\n标题:{head}\n内容:\n{content}"
self.post_message(Notification(
mtype=NotificationType.SiteMessage,
title=msg_title, text=msg_text, link=site.get("url")
))
else:
self.post_message(Notification(
mtype=NotificationType.SiteMessage,
title=f"站点 {site.get('name')} 收到 "
f"{userdata.message_unread} 条新消息,请登陆查看",
link=site.get("url")
))
# 低分享率警告 # 低分享率警告
if userdata.ratio and float(userdata.ratio) < 1 and not bool( if userdata.ratio and float(userdata.ratio) < 1 and not bool(
re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)): re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)):
@@ -105,6 +89,38 @@ class SiteChain(ChainBase):
)) ))
return userdata return userdata
def _post_site_messages(self, site: dict, userdata: SiteUserData) -> None:
"""
发送站点未读消息,并按解析器提供的来源标识做持久化去重。
:param site: 站点索引配置
:param userdata: 本次刷新的站点用户数据
"""
if not userdata.message_unread:
return
if not userdata.message_unread_contents:
self.post_message(Notification(
mtype=NotificationType.SiteMessage,
title=f"站点 {site.get('name')} 收到 "
f"{userdata.message_unread} 条新消息,请登陆查看",
link=site.get("url")
))
return
for message in userdata.message_unread_contents:
head, date, content, *metadata = message
message_source = metadata[0] if metadata else None
if message_source and self.messageoper.exists_by_source(message_source):
continue
msg_title = f"【站点 {site.get('name')} 消息】"
msg_text = f"时间:{date}\n标题:{head}\n内容:\n{content}"
self.post_message(Notification(
source=message_source,
mtype=NotificationType.SiteMessage,
title=msg_title,
text=msg_text,
link=site.get("url")
))
def refresh_userdatas( def refresh_userdatas(
self, self,
progress_callback: Optional[Callable[..., None]] = None, progress_callback: Optional[Callable[..., None]] = None,
+9
View File
@@ -105,6 +105,15 @@ class MessageOper(DbOper):
""" """
return Message.list_by_page(self._db, page, count) return Message.list_by_page(self._db, page, count)
def exists_by_source(self, source: str) -> bool:
"""
判断指定来源标识的消息记录是否存在。
:param source: 消息来源唯一标识
:return: 是否存在匹配记录
"""
return Message.exists_by_source(self._db, source)
async def async_list_by_page( async def async_list_by_page(
self, page: Optional[int] = 1, count: Optional[int] = 30 self, page: Optional[int] = 1, count: Optional[int] = 30
) -> list[Message]: ) -> list[Message]:
+12
View File
@@ -62,6 +62,18 @@ class Message(Base):
.all() .all()
) )
@classmethod
@db_query
def exists_by_source(cls, db: Session, source: str) -> bool:
"""
判断指定来源标识的消息记录是否存在。
:param db: 数据库会话
:param source: 消息来源唯一标识
:return: 是否存在匹配记录
"""
return db.query(cls.id).filter(cls.source == source).first() is not None
@classmethod @classmethod
@async_db_query @async_db_query
async def async_list_by_page( async def async_list_by_page(
+5 -1
View File
@@ -123,8 +123,12 @@ class SunnyPTSiteUserInfo(SiteParserBase):
title = message.get("title") title = message.get("title")
content = message.get("content") content = message.get("content")
created_at = StringUtils.unify_datetime_str(message.get("created_at")) created_at = StringUtils.unify_datetime_str(message.get("created_at"))
message_id = message.get("id")
if title and content and created_at: if title and content and created_at:
self.message_unread_contents.append((title, created_at, content)) message_source = f"sunnypt-message:{message_id}" if message_id is not None else None
self.message_unread_contents.append(
(title, created_at, content, message_source)
)
return "next" if messages_data.get("has_more") else None return "next" if messages_data.get("has_more") else None
def _parse_user_traffic_info(self, html_text: str) -> None: def _parse_user_traffic_info(self, html_text: str) -> None:
+39 -2
View File
@@ -9,10 +9,11 @@ from app.chain.download import DownloadChain
from app.chain.site import SiteChain from app.chain.site import SiteChain
from app.core.config import settings from app.core.config import settings
from app.core.context import TorrentInfo from app.core.context import TorrentInfo
from app.db.message_oper import MessageOper
from app.modules.indexer import IndexerModule from app.modules.indexer import IndexerModule
from app.modules.indexer.parser.sunnypt import SunnyPTSiteUserInfo from app.modules.indexer.parser.sunnypt import SunnyPTSiteUserInfo
from app.modules.indexer.spider.sunnypt import SunnyPTSpider from app.modules.indexer.spider.sunnypt import SunnyPTSpider
from app.schemas import MediaType from app.schemas import MediaType, NotificationType
class _FakeResponse: class _FakeResponse:
@@ -286,11 +287,47 @@ def test_sunnypt_user_parser_reads_profile_and_messages_without_marking_read(mon
assert parser.leeching_size == 10737418240 assert parser.leeching_size == 10737418240
assert parser.message_unread == 1 assert parser.message_unread == 1
assert parser.message_unread_contents == [ assert parser.message_unread_contents == [
("种子审核通过", "2026-07-21 16:30:00", "你发布的种子已审核通过。") (
"种子审核通过",
"2026-07-21 16:30:00",
"你发布的种子已审核通过。",
"sunnypt-message:9001",
)
] ]
assert not any(url.endswith("/read") or url.endswith("/read-all") for url in requested_urls) assert not any(url.endswith("/read") or url.endswith("/read-all") for url in requested_urls)
def test_site_messages_are_deduplicated_by_persisted_source(monkeypatch):
"""站点消息应使用解析器保留的消息 ID 来源标识做持久化去重。"""
duplicate_source = "sunnypt-message:9001-dedup-test"
MessageOper().add(
source=duplicate_source,
mtype=NotificationType.SiteMessage,
title="existing",
text="existing",
)
sent_messages = []
chain = object.__new__(SiteChain)
chain.messageoper = MessageOper()
monkeypatch.setattr(chain, "post_message", sent_messages.append)
userdata = SimpleNamespace(
message_unread=2,
message_unread_contents=[
("重复消息", "2026-07-21 16:30:00", "旧内容", duplicate_source),
("新消息", "2026-07-22 16:30:00", "新内容", "sunnypt-message:9002-dedup-test"),
],
)
chain._post_site_messages(
site={"name": "Sunny", "url": "https://sunnypt.top/"},
userdata=userdata,
)
assert len(sent_messages) == 1
assert sent_messages[0].source == "sunnypt-message:9002-dedup-test"
assert sent_messages[0].title == "【站点 Sunny 消息】"
def test_indexer_module_dispatches_sunnypt_search(monkeypatch): def test_indexer_module_dispatches_sunnypt_search(monkeypatch):
"""IndexerModule 应把 SunnyPT 同步搜索参数交给专用 API Spider。""" """IndexerModule 应把 SunnyPT 同步搜索参数交给专用 API Spider。"""
captured = {} captured = {}