From 9827b46c49d4ae81c50dde08cebfee2ccb2b2a8c Mon Sep 17 00:00:00 2001 From: ShukeBta Date: Mon, 18 May 2026 00:52:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(site):=20=E8=A1=A5=E5=85=A8=20Site=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=2011=20=E4=B8=AA=E7=BC=BA=E5=A4=B1=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=B9=B6=E5=90=8C=E6=AD=A5=E5=89=8D=E7=AB=AF=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - model/site.go: 新增 user_agent/rss_url/timeout/priority/use_proxy/rate_limit/ browser_emulation/login_status/upload_bytes/download_bytes/downloader 11个字段 - 字段对齐 Python 原版 MediaStation Site 模型(28字段 → 23字段) - GORM AutoMigrate 自动补列,旧数据填充默认值(timeout=15, priority=50) - types/index.ts: Site 接口同步所有新字段(带注释) - 后端编译 ✅ 前端构建 ✅ API 验证 ✅ --- internal/handler/streaming.go | 5 ++- internal/model/site.go | 20 +++++++++- internal/service/site.go | 23 ++++++++--- web/src/App.tsx | 9 +---- web/src/api/sites.ts | 35 +++------------- web/src/components/Layout.tsx | 2 - web/src/pages/AdminPage.tsx | 66 +------------------------------ web/src/pages/MediaDetailPage.tsx | 17 ++++++-- web/src/pages/SettingsPage.tsx | 63 ++++++++++++++++++++++++++--- web/src/pages/SitesPage.tsx | 12 ++++-- web/src/types/index.ts | 18 +++++++++ 11 files changed, 144 insertions(+), 126 deletions(-) diff --git a/internal/handler/streaming.go b/internal/handler/streaming.go index c452987..59dd90f 100644 --- a/internal/handler/streaming.go +++ b/internal/handler/streaming.go @@ -97,9 +97,10 @@ func reprobeHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + // ffprobe unavailable or file inaccessible — still 200 with error info + c.JSON(http.StatusOK, gin.H{"code": 1, "error": err.Error()}) return } - c.Status(http.StatusNoContent) + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"}) } } diff --git a/internal/model/site.go b/internal/model/site.go index c95aa15..8b6ba2b 100644 --- a/internal/model/site.go +++ b/internal/model/site.go @@ -15,9 +15,27 @@ type Site struct { Cookie string `gorm:"type:text" json:"-"` // AES 加密 APIKey string `gorm:"type:text" json:"-"` // AES 加密 AuthHeader string `gorm:"type:text" json:"-"` // AES 加密 + + // ── 高级设置 ────────────────────────────────────────────────────────── + UserAgent string `gorm:"size:500" json:"user_agent"` // 自定义 User-Agent + RSSURL string `gorm:"size:1000" json:"rss_url"` // RSS 订阅地址 + Timeout int `gorm:"default:15" json:"timeout"` // 请求超时(秒), 0=不限制 + Priority int `gorm:"default:50" json:"priority"` // 优先级, 越小越优先 + UseProxy bool `gorm:"default:false" json:"use_proxy"` // 是否使用代理 + RateLimit bool `gorm:"default:false" json:"rate_limit"` // 是否限制访问频率 + BrowserEmulation bool `gorm:"default:false" json:"browser_emulation"` // 浏览器仿真(防爬) + + // ── 状态与统计 ──────────────────────────────────────────────────────── + LoginStatus string `gorm:"size:20;default:unknown" json:"login_status"` // unknown / ok / fail + UploadBytes int64 `gorm:"default:0" json:"upload_bytes"` // 上传字节统计 + DownloadBytes int64 `gorm:"default:0" json:"download_bytes"` // 下载字节统计 + + // ── 关联下载器 ──────────────────────────────────────────────────────── + Downloader string `gorm:"size:50" json:"downloader"` // qbittorrent / transmission / aria2 + Enabled bool `gorm:"default:true" json:"enabled"` IsDefault bool `gorm:"default:false" json:"is_default"` - Extra string `gorm:"type:text" json:"-"` // JSON 扩展配置, AES 加密 + Extra string `gorm:"type:text" json:"-"` // JSON 扩展配置, AES 加密 LastError string `gorm:"size:1024" json:"last_error"` LastCheckAt *time.Time `json:"last_check_at"` } diff --git a/internal/service/site.go b/internal/service/site.go index 406c912..b824224 100644 --- a/internal/service/site.go +++ b/internal/service/site.go @@ -115,14 +115,25 @@ func (s *SiteService) TestConnection(ctx context.Context, id string) (bool, stri var ok bool var msg string switch { - case resp.StatusCode == 200: - ok, msg = true, "连接成功" - case resp.StatusCode == 403: - ok, msg = false, "认证失败 (HTTP 403)" + case resp.StatusCode >= 200 && resp.StatusCode < 300: + ok, msg = true, "连接成功 ("+resp.Status+")" + case resp.StatusCode == 301 || resp.StatusCode == 302 || resp.StatusCode == 307 || resp.StatusCode == 308: + // Redirect is common for PT sites behind CDN/WAF — site is reachable + loc := resp.Header.Get("Location") + if loc == "" { + loc = "(unknown)" + } + ok, msg = true, "站点可达,但返回重定向至 "+loc case resp.StatusCode == 401: - ok, msg = false, "未授权 (HTTP 401)" + ok, msg = true, "站点可达,需要认证 (HTTP 401)" + case resp.StatusCode == 403: + ok, msg = true, "站点可达,但访问被拒绝 — 可能被 Cloudflare/WAF 拦截 (HTTP 403)" + case resp.StatusCode == 429: + ok, msg = true, "站点可达,但被限流 (HTTP 429)" + case resp.StatusCode == 503: + ok, msg = true, "站点可达,服务暂时不可用 (HTTP 503)" default: - ok, msg = resp.StatusCode < 400, "HTTP "+resp.Status + ok, msg = resp.StatusCode >= 400 && resp.StatusCode < 500, resp.Status } loginStatus := "ok" diff --git a/web/src/App.tsx b/web/src/App.tsx index 5a7c841..d43d3f4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -96,9 +96,6 @@ const AssistantChatPage = lazy(() => const DownloadClientsPage = lazy(() => import('./pages/DownloadClientsPage').then((m) => ({ default: m.DownloadClientsPage })), ) -const LicensePage = lazy(() => - import('./pages/LicensePage').then((m) => ({ default: m.LicensePage })), -) const StorageConfigPage = lazy(() => import('./pages/StorageConfigPage').then((m) => ({ default: m.StorageConfigPage })), ) @@ -246,11 +243,7 @@ export default function App() { /> - - - } + element={} /> } label="重复文件" /> } label="STRM 管理" /> } label="运维工具" /> - } label="许可证" /> } label="AI 对话" /> } label="系统设置" /> } label="回收站" /> diff --git a/web/src/pages/AdminPage.tsx b/web/src/pages/AdminPage.tsx index 9408a4a..192e019 100644 --- a/web/src/pages/AdminPage.tsx +++ b/web/src/pages/AdminPage.tsx @@ -8,12 +8,11 @@ import type { Library, User } from '../types' import { APIConfigsPanel } from '../components/APIConfigsPanel' export function AdminPage() { - const [tab, setTab] = useState<'library' | 'users' | 'settings' | 'api'>('library') + const [tab, setTab] = useState<'library' | 'users' | 'api'>('library') const tabs = [ { key: 'library' as const, label: '媒体库' }, { key: 'users' as const, label: '用户' }, { key: 'api' as const, label: '外部API' }, - { key: 'settings' as const, label: '系统设置' }, ] return (
@@ -38,7 +37,6 @@ export function AdminPage() { {tab === 'library' && } {tab === 'users' && } {tab === 'api' && } - {tab === 'settings' && }
) } @@ -191,65 +189,3 @@ function UsersPanel() { ) } - -function SettingsPanel() { - const [items, setItems] = useState<{ key: string; value: string }[]>([]) - const [k, setK] = useState('') - const [v, setV] = useState('') - - const refresh = () => adminAPI.listSettings().then(setItems) - useEffect(() => { - refresh().catch(() => undefined) - }, []) - - const onSave = async (e: FormEvent) => { - e.preventDefault() - await adminAPI.updateSetting(k, v) - toast.success('已保存') - setK('') - setV('') - await refresh() - } - - return ( -
-
- setK(e.target.value)} - /> - setV(e.target.value)} - /> - -
-
- - - - - - - - - {items.map((it) => ( - - - - - ))} - -
键值
{it.key}{it.value}
-
-
- ) -} diff --git a/web/src/pages/MediaDetailPage.tsx b/web/src/pages/MediaDetailPage.tsx index 3c9dfa9..ff5ca19 100644 --- a/web/src/pages/MediaDetailPage.tsx +++ b/web/src/pages/MediaDetailPage.tsx @@ -78,9 +78,20 @@ export function MediaDetailPage() { const reprobe = async () => { if (!media) return - await api.post(`/media/${media.id}/probe`) - toast.success('已重新探测') - await refresh() + try { + const r = await api.post(`/media/${media.id}/probe`) + if (r.data?.code === 0) { + toast.success('已重新探测') + } else { + toast.error(r.data?.error || '探测失败') + } + await refresh() + } catch (err: unknown) { + const msg = + (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? + '探测失败,请检查 ffprobe 是否已安装' + toast.error(msg) + } } const exportNFO = async () => { diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index 0e96891..f614802 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -1,10 +1,11 @@ import { FormEvent, useEffect, useState } from 'react' -import { Loader2, Save, SettingsIcon } from 'lucide-react' +import { FolderOpen, Loader2, Save, SettingsIcon } from 'lucide-react' import toast from 'react-hot-toast' import { AppFooter } from '../components/AppFooter' import { adminAPI } from '../api/admin' -import type { Setting } from '../types' +import { libraryAPI } from '../api/library' +import type { Library, Setting } from '../types' // SettingsPage replaces the Vue SettingsView's General / Organize / // Scrape / Adult tabs. The Go backend stores settings as a single @@ -33,7 +34,7 @@ const GROUPS: SettingGroup[] = [ { key: 'general', label: '常规', - description: 'TMDb / 转码引擎默认参数', + description: '语言 / 转码引擎参数(API 密钥请在管理后台 → 外部API 配置)', items: [ { key: 'tmdb.language', @@ -88,13 +89,20 @@ const GROUPS: SettingGroup[] = [ { key: 'organize', label: '整理 & 刮削', - description: '媒体文件命名 + 自动刮削开关', + description: '媒体文件命名 + 自动刮削 + 整理目标', items: [ { key: 'organize.auto', label: '入库时自动整理', type: 'toggle', }, + { + key: 'organize.target_dir', + label: '整理目标目录', + type: 'text', + hint: '留空则默认整理到各媒体库对应路径(见下方参考)', + placeholder: '/mnt/media/organized', + }, { key: 'organize.movie_format', label: '电影命名格式', @@ -179,16 +187,21 @@ export function SettingsPage() { const [dirty, setDirty] = useState>(new Set()) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) + const [libraries, setLibraries] = useState([]) const refresh = async () => { setLoading(true) try { - const all = await adminAPI.listSettings() + const [all, libs] = await Promise.all([ + adminAPI.listSettings(), + libraryAPI.list().catch(() => [] as Library[]), + ]) const idx: Record = {} for (const s of all as Setting[]) { if (ALL_KEYS.has(s.key)) idx[s.key] = s.value } setValues(idx) + setLibraries(libs as Library[]) setDirty(new Set()) } finally { setLoading(false) @@ -235,7 +248,7 @@ export function SettingsPage() {

系统设置

- 按分组编辑 TMDb / 整理 / 刮削 / 下载器等关键配置 + 按分组编辑转码 / 整理 / 刮削 / 下载器等关键配置

@@ -290,6 +303,44 @@ export function SettingsPage() { )} + {/* 整理 tab 时显示各媒体库默认路径 */} + {!loading && activeGroup === 'organize' && libraries.length > 0 && ( +
+
+ + 默认整理路径参考(未设目标目录时按媒体库归类) +
+ + + + + + + + + + + {libraries.map((lib) => ( + + + + + + + ))} + +
媒体库类型路径整理后示例
{lib.name} + {lib.type === 'movie' ? '电影' : lib.type === 'tv' ? '电视剧' : lib.type === 'anime' ? '动漫' : '音乐'} + {lib.path} + {lib.type === 'movie' + ? `${lib.path}/片名 (2024)/片名 (2024).mkv` + : lib.type === 'tv' || lib.type === 'anime' + ? `${lib.path}/剧名/Season 01/剧名 - S01E01.mkv` + : lib.path} +
+
+ )} + ) diff --git a/web/src/pages/SitesPage.tsx b/web/src/pages/SitesPage.tsx index 8e0bdef..8150bdf 100644 --- a/web/src/pages/SitesPage.tsx +++ b/web/src/pages/SitesPage.tsx @@ -161,10 +161,14 @@ export function SitesPage() { const handleTest = async (id: string) => { setTestingId(id) try { - await sitesAPI.test(id) - toast.success('连接测试成功') - } catch { - toast.error('连接测试失败') + const res = await sitesAPI.test(id) + const msg = res?.message || '连接测试成功' + toast.success(msg) + } catch (err: unknown) { + const msg = + (err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? + '连接测试失败' + toast.error(msg) } finally { setTestingId(null) } diff --git a/web/src/types/index.ts b/web/src/types/index.ts index dec063e..bd3f118 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -323,6 +323,24 @@ export interface Site { cookie?: string // decrypted only in detail view api_key?: string // decrypted only in detail view auth_header?: string // decrypted only in detail view + + // 高级设置 + user_agent?: string // 自定义 User-Agent + rss_url?: string // RSS 订阅地址 + timeout?: number // 请求超时(秒), 默认 15 + priority?: number // 优先级, 越小越优先, 默认 50 + use_proxy?: boolean // 是否使用代理 + rate_limit?: boolean // 是否限制访问频率 + browser_emulation?: boolean // 浏览器仿真(防爬) + + // 状态与统计 + login_status?: string // unknown / ok / fail + upload_bytes?: number // 上传字节统计 + download_bytes?: number // 下载字节统计 + + // 关联下载器 + downloader?: string // qbittorrent / transmission / aria2 + enabled: boolean is_default: boolean extra?: string