diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a7972e..001527ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Backend +### Features + +- RSS 订阅源新增连接状态追踪:每次刷新后记录 `connection_status`(healthy/error)、`last_checked_at` 和 `last_error` +- 新增数据库迁移 v2:为 `rssitem` 表添加连接状态相关字段 + ### Performance - 新增共享 HTTP 客户端连接池,复用 TCP/SSL 连接,减少每次请求的握手开销 @@ -24,6 +29,10 @@ ## Frontend +### Features + +- RSS 管理页面新增连接状态标签:健康时显示绿色「已连接」,错误时显示红色「错误」并通过 tooltip 显示错误详情 + ### Performance - 下载器 store 使用 `shallowRef` 替代 `ref`,避免大数组的深层响应式代理 diff --git a/backend/src/module/database/combine.py b/backend/src/module/database/combine.py index da2aa8a8..63446d28 100644 --- a/backend/src/module/database/combine.py +++ b/backend/src/module/database/combine.py @@ -15,7 +15,7 @@ from .user import UserDatabase logger = logging.getLogger(__name__) # Increment this when adding new migrations to MIGRATIONS list. -CURRENT_SCHEMA_VERSION = 1 +CURRENT_SCHEMA_VERSION = 2 # Each migration is a tuple of (version, description, list of SQL statements). # Migrations are applied in order. A migration at index i brings the schema @@ -26,6 +26,15 @@ MIGRATIONS = [ "add air_weekday column to bangumi", ["ALTER TABLE bangumi ADD COLUMN air_weekday INTEGER"], ), + ( + 2, + "add connection status columns to rssitem", + [ + "ALTER TABLE rssitem ADD COLUMN connection_status TEXT", + "ALTER TABLE rssitem ADD COLUMN last_checked_at TEXT", + "ALTER TABLE rssitem ADD COLUMN last_error TEXT", + ], + ), ] @@ -88,6 +97,10 @@ class Database(Session): columns = [col["name"] for col in inspector.get_columns("bangumi")] if "air_weekday" in columns: needs_run = False + if "rssitem" in tables and version == 2: + columns = [col["name"] for col in inspector.get_columns("rssitem")] + if "connection_status" in columns: + needs_run = False if needs_run: with self.engine.connect() as conn: for stmt in statements: diff --git a/backend/src/module/models/rss.py b/backend/src/module/models/rss.py index 1546db80..8ea51ae8 100644 --- a/backend/src/module/models/rss.py +++ b/backend/src/module/models/rss.py @@ -10,6 +10,9 @@ class RSSItem(SQLModel, table=True): aggregate: bool = Field(False, alias="aggregate") parser: str = Field("mikan", alias="parser") enabled: bool = Field(True, alias="enabled") + connection_status: Optional[str] = Field(None, alias="connection_status") + last_checked_at: Optional[str] = Field(None, alias="last_checked_at") + last_error: Optional[str] = Field(None, alias="last_error") class RSSUpdate(SQLModel): diff --git a/backend/src/module/rss/engine.py b/backend/src/module/rss/engine.py index b79d8e83..15ab75e3 100644 --- a/backend/src/module/rss/engine.py +++ b/backend/src/module/rss/engine.py @@ -1,6 +1,7 @@ import asyncio import logging import re +from datetime import datetime, timezone from typing import Optional from module.database import Database, engine @@ -98,6 +99,16 @@ class RSSEngine(Database): new_torrents = self.torrent.check_new(torrents) return new_torrents + async def _pull_rss_with_status( + self, rss_item: RSSItem + ) -> tuple[list[Torrent], Optional[str]]: + try: + torrents = await self.pull_rss(rss_item) + return torrents, None + except Exception as e: + logger.warning(f"[Engine] Failed to fetch RSS {rss_item.name}: {e}") + return [], str(e) + _filter_cache: dict[str, re.Pattern] = {} def _get_filter_pattern(self, filter_str: str) -> re.Pattern: @@ -127,11 +138,17 @@ class RSSEngine(Database): rss_items = [rss_item] if rss_item else [] # From RSS Items, fetch all torrents concurrently logger.debug(f"[Engine] Get {len(rss_items)} RSS items") - all_torrents = await asyncio.gather( - *[self.pull_rss(rss_item) for rss_item in rss_items] + results = await asyncio.gather( + *[self._pull_rss_with_status(rss_item) for rss_item in rss_items] ) + now = datetime.now(timezone.utc).isoformat() # Process results sequentially (DB operations) - for rss_item, new_torrents in zip(rss_items, all_torrents): + for rss_item, (new_torrents, error) in zip(rss_items, results): + # Update connection status + rss_item.connection_status = "error" if error else "healthy" + rss_item.last_checked_at = now + rss_item.last_error = error + self.add(rss_item) for torrent in new_torrents: matched_data = self.match_torrent(torrent) if matched_data: @@ -140,6 +157,7 @@ class RSSEngine(Database): torrent.downloaded = True # Add all torrents to database self.torrent.add_all(new_torrents) + self.commit() async def download_bangumi(self, bangumi: Bangumi): async with RequestContent() as req: diff --git a/webui/src/i18n/en.json b/webui/src/i18n/en.json index 76f5a381..74fcd1a9 100644 --- a/webui/src/i18n/en.json +++ b/webui/src/i18n/en.json @@ -200,9 +200,11 @@ } }, "rss": { + "connected": "Connected", "delete": "Delete", "disable": "Disable", "enable": "Enable", + "error": "Error", "name": "Name", "selectbox": "Select", "status": "Status", diff --git a/webui/src/i18n/zh-CN.json b/webui/src/i18n/zh-CN.json index 0bc4329a..db6dd941 100644 --- a/webui/src/i18n/zh-CN.json +++ b/webui/src/i18n/zh-CN.json @@ -200,9 +200,11 @@ } }, "rss": { + "connected": "已连接", "delete": "删除", "disable": "禁用", "enable": "启用", + "error": "错误", "name": "名称", "selectbox": "选择", "status": "状态", diff --git a/webui/src/pages/index/rss.vue b/webui/src/pages/index/rss.vue index 2fee0f18..1717605c 100644 --- a/webui/src/pages/index/rss.vue +++ b/webui/src/pages/index/rss.vue @@ -1,5 +1,5 @@