mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-04-13 09:59:45 +08:00
feat(rss): add connection status tracking and display
Track RSS feed reachability during refresh cycles. Each feed now stores connection_status (healthy/error), last_checked_at, and last_error. The RSS management page shows a green "Connected" tag for healthy feeds and a red "Error" tag with tooltip for failed feeds. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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`,避免大数组的深层响应式代理
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -200,9 +200,11 @@
|
||||
}
|
||||
},
|
||||
"rss": {
|
||||
"connected": "Connected",
|
||||
"delete": "Delete",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"error": "Error",
|
||||
"name": "Name",
|
||||
"selectbox": "Select",
|
||||
"status": "Status",
|
||||
|
||||
@@ -200,9 +200,11 @@
|
||||
}
|
||||
},
|
||||
"rss": {
|
||||
"connected": "已连接",
|
||||
"delete": "删除",
|
||||
"disable": "禁用",
|
||||
"enable": "启用",
|
||||
"error": "错误",
|
||||
"name": "名称",
|
||||
"selectbox": "选择",
|
||||
"status": "状态",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="tsx" setup>
|
||||
import { NDataTable, type DataTableColumns } from 'naive-ui';
|
||||
import { NDataTable, NTooltip, type DataTableColumns } from 'naive-ui';
|
||||
import type { RSS } from '#/rss';
|
||||
|
||||
definePage({
|
||||
@@ -49,6 +49,17 @@ const rssColumns = computed<DataTableColumns<RSS>>(() => [
|
||||
<div flex="~ justify-end gap-x-8">
|
||||
{rss.parser && <ab-tag type="primary" title={rss.parser} />}
|
||||
{rss.aggregate && <ab-tag type="primary" title="aggregate" />}
|
||||
{rss.connection_status === 'healthy' && (
|
||||
<ab-tag type="active" title={t('rss.connected')} />
|
||||
)}
|
||||
{rss.connection_status === 'error' && (
|
||||
<NTooltip>
|
||||
{{
|
||||
trigger: () => <ab-tag type="warn" title={t('rss.error')} />,
|
||||
default: () => rss.last_error || 'Unknown error',
|
||||
}}
|
||||
</NTooltip>
|
||||
)}
|
||||
{rss.enabled ? (
|
||||
<ab-tag type="active" title="active" />
|
||||
) : (
|
||||
@@ -85,6 +96,17 @@ const rssRowKey = (row: RSS) => row.id;
|
||||
<div class="rss-card-tags">
|
||||
<ab-tag v-if="item.parser" type="primary" :title="item.parser" />
|
||||
<ab-tag v-if="item.aggregate" type="primary" title="aggregate" />
|
||||
<ab-tag
|
||||
v-if="item.connection_status === 'healthy'"
|
||||
type="active"
|
||||
:title="$t('rss.connected')"
|
||||
/>
|
||||
<NTooltip v-if="item.connection_status === 'error'">
|
||||
<template #trigger>
|
||||
<ab-tag type="warn" :title="$t('rss.error')" />
|
||||
</template>
|
||||
{{ item.last_error || 'Unknown error' }}
|
||||
</NTooltip>
|
||||
<ab-tag
|
||||
:type="item.enabled ? 'active' : 'inactive'"
|
||||
:title="item.enabled ? 'active' : 'inactive'"
|
||||
|
||||
@@ -5,6 +5,9 @@ export interface RSS {
|
||||
aggregate: boolean;
|
||||
parser: string;
|
||||
enabled: boolean;
|
||||
connection_status: string | null;
|
||||
last_checked_at: string | null;
|
||||
last_error: string | null;
|
||||
}
|
||||
|
||||
export const rssTemplate: RSS = {
|
||||
@@ -14,4 +17,7 @@ export const rssTemplate: RSS = {
|
||||
aggregate: false,
|
||||
parser: '',
|
||||
enabled: false,
|
||||
connection_status: null,
|
||||
last_checked_at: null,
|
||||
last_error: null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user