mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 21:06:38 +08:00
feat(site): 补全 Site 模型 11 个缺失字段并同步前端类型
- 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 验证 ✅
This commit is contained in:
@@ -97,9 +97,10 @@ func reprobeHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
c.Status(http.StatusNoContent)
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -15,9 +15,27 @@ type Site struct {
|
|||||||
Cookie string `gorm:"type:text" json:"-"` // AES 加密
|
Cookie string `gorm:"type:text" json:"-"` // AES 加密
|
||||||
APIKey string `gorm:"type:text" json:"-"` // AES 加密
|
APIKey string `gorm:"type:text" json:"-"` // AES 加密
|
||||||
AuthHeader 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"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
IsDefault bool `gorm:"default:false" json:"is_default"`
|
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"`
|
LastError string `gorm:"size:1024" json:"last_error"`
|
||||||
LastCheckAt *time.Time `json:"last_check_at"`
|
LastCheckAt *time.Time `json:"last_check_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,14 +115,25 @@ func (s *SiteService) TestConnection(ctx context.Context, id string) (bool, stri
|
|||||||
var ok bool
|
var ok bool
|
||||||
var msg string
|
var msg string
|
||||||
switch {
|
switch {
|
||||||
case resp.StatusCode == 200:
|
case resp.StatusCode >= 200 && resp.StatusCode < 300:
|
||||||
ok, msg = true, "连接成功"
|
ok, msg = true, "连接成功 ("+resp.Status+")"
|
||||||
case resp.StatusCode == 403:
|
case resp.StatusCode == 301 || resp.StatusCode == 302 || resp.StatusCode == 307 || resp.StatusCode == 308:
|
||||||
ok, msg = false, "认证失败 (HTTP 403)"
|
// 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:
|
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:
|
default:
|
||||||
ok, msg = resp.StatusCode < 400, "HTTP "+resp.Status
|
ok, msg = resp.StatusCode >= 400 && resp.StatusCode < 500, resp.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
loginStatus := "ok"
|
loginStatus := "ok"
|
||||||
|
|||||||
+1
-8
@@ -96,9 +96,6 @@ const AssistantChatPage = lazy(() =>
|
|||||||
const DownloadClientsPage = lazy(() =>
|
const DownloadClientsPage = lazy(() =>
|
||||||
import('./pages/DownloadClientsPage').then((m) => ({ default: m.DownloadClientsPage })),
|
import('./pages/DownloadClientsPage').then((m) => ({ default: m.DownloadClientsPage })),
|
||||||
)
|
)
|
||||||
const LicensePage = lazy(() =>
|
|
||||||
import('./pages/LicensePage').then((m) => ({ default: m.LicensePage })),
|
|
||||||
)
|
|
||||||
const StorageConfigPage = lazy(() =>
|
const StorageConfigPage = lazy(() =>
|
||||||
import('./pages/StorageConfigPage').then((m) => ({ default: m.StorageConfigPage })),
|
import('./pages/StorageConfigPage').then((m) => ({ default: m.StorageConfigPage })),
|
||||||
)
|
)
|
||||||
@@ -246,11 +243,7 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="license"
|
path="license"
|
||||||
element={
|
element={<Navigate to="/admin" replace />}
|
||||||
<RequireAdmin>
|
|
||||||
<LicensePage />
|
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="storage-config"
|
path="storage-config"
|
||||||
|
|||||||
+6
-29
@@ -1,27 +1,8 @@
|
|||||||
import { api } from './client'
|
import { api } from './client'
|
||||||
|
|
||||||
// ─── TypeScript interfaces ──────────────────────────────────────────────
|
// ─── TypeScript interfaces ──────────────────────────────────────────────
|
||||||
|
// Note: the canonical Site type lives in ../types/index.ts
|
||||||
export interface Site {
|
// The backend uses json:"url" (not base_url) and json:"type" (not site_type).
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
base_url: string
|
|
||||||
site_type: string
|
|
||||||
auth_type: string
|
|
||||||
cookie?: string
|
|
||||||
api_key?: string
|
|
||||||
auth_header?: string
|
|
||||||
user_agent?: string
|
|
||||||
rss_url?: string
|
|
||||||
timeout: number
|
|
||||||
priority: number
|
|
||||||
use_proxy: boolean
|
|
||||||
enabled: boolean
|
|
||||||
login_status: string
|
|
||||||
downloader?: string
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SiteSearchResult {
|
export interface SiteSearchResult {
|
||||||
site_name: string
|
site_name: string
|
||||||
@@ -37,19 +18,15 @@ export interface SiteSearchResult {
|
|||||||
|
|
||||||
export interface CreateSiteInput {
|
export interface CreateSiteInput {
|
||||||
name: string
|
name: string
|
||||||
base_url: string
|
url: string
|
||||||
site_type?: string
|
type?: string
|
||||||
auth_type?: string
|
auth_type?: string
|
||||||
cookie?: string
|
cookie?: string
|
||||||
api_key?: string
|
api_key?: string
|
||||||
auth_header?: string
|
auth_header?: string
|
||||||
user_agent?: string
|
|
||||||
rss_url?: string
|
|
||||||
timeout?: number
|
|
||||||
priority?: number
|
|
||||||
use_proxy?: boolean
|
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
downloader?: string
|
is_default?: boolean
|
||||||
|
extra?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── API client ─────────────────────────────────────────────────────────
|
// ─── API client ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
Home,
|
Home,
|
||||||
GalleryHorizontalEnd,
|
GalleryHorizontalEnd,
|
||||||
KeySquare,
|
|
||||||
Link2,
|
Link2,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
ListMusic,
|
ListMusic,
|
||||||
@@ -137,7 +136,6 @@ export function Layout() {
|
|||||||
<SidebarLink to="/duplicates" icon={<Copy size={18} />} label="重复文件" />
|
<SidebarLink to="/duplicates" icon={<Copy size={18} />} label="重复文件" />
|
||||||
<SidebarLink to="/strm" icon={<Link2 size={18} />} label="STRM 管理" />
|
<SidebarLink to="/strm" icon={<Link2 size={18} />} label="STRM 管理" />
|
||||||
<SidebarLink to="/tools" icon={<Wrench size={18} />} label="运维工具" />
|
<SidebarLink to="/tools" icon={<Wrench size={18} />} label="运维工具" />
|
||||||
<SidebarLink to="/license" icon={<KeySquare size={18} />} label="许可证" />
|
|
||||||
<SidebarLink to="/assistant" icon={<MessageSquare size={18} />} label="AI 对话" />
|
<SidebarLink to="/assistant" icon={<MessageSquare size={18} />} label="AI 对话" />
|
||||||
<SidebarLink to="/settings" icon={<Sliders size={18} />} label="系统设置" />
|
<SidebarLink to="/settings" icon={<Sliders size={18} />} label="系统设置" />
|
||||||
<SidebarLink to="/recycle" icon={<Trash2 size={18} />} label="回收站" />
|
<SidebarLink to="/recycle" icon={<Trash2 size={18} />} label="回收站" />
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import type { Library, User } from '../types'
|
|||||||
import { APIConfigsPanel } from '../components/APIConfigsPanel'
|
import { APIConfigsPanel } from '../components/APIConfigsPanel'
|
||||||
|
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
const [tab, setTab] = useState<'library' | 'users' | 'settings' | 'api'>('library')
|
const [tab, setTab] = useState<'library' | 'users' | 'api'>('library')
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'library' as const, label: '媒体库' },
|
{ key: 'library' as const, label: '媒体库' },
|
||||||
{ key: 'users' as const, label: '用户' },
|
{ key: 'users' as const, label: '用户' },
|
||||||
{ key: 'api' as const, label: '外部API' },
|
{ key: 'api' as const, label: '外部API' },
|
||||||
{ key: 'settings' as const, label: '系统设置' },
|
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -38,7 +37,6 @@ export function AdminPage() {
|
|||||||
{tab === 'library' && <LibraryPanel />}
|
{tab === 'library' && <LibraryPanel />}
|
||||||
{tab === 'users' && <UsersPanel />}
|
{tab === 'users' && <UsersPanel />}
|
||||||
{tab === 'api' && <APIConfigsPanel />}
|
{tab === 'api' && <APIConfigsPanel />}
|
||||||
{tab === 'settings' && <SettingsPanel />}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -191,65 +189,3 @@ function UsersPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<form onSubmit={onSave} className="glass-panel grid gap-3 md:grid-cols-3">
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="input-base"
|
|
||||||
placeholder="键 (如 tmdb_api_key)"
|
|
||||||
value={k}
|
|
||||||
onChange={(e) => setK(e.target.value)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="input-base"
|
|
||||||
placeholder="值"
|
|
||||||
value={v}
|
|
||||||
onChange={(e) => setV(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button type="submit" className="neon-button">
|
|
||||||
保存
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div className="glass-panel">
|
|
||||||
<table className="w-full text-left text-sm">
|
|
||||||
<thead className="text-xs uppercase tracking-wider text-slate-500">
|
|
||||||
<tr>
|
|
||||||
<th className="py-2">键</th>
|
|
||||||
<th>值</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{items.map((it) => (
|
|
||||||
<tr key={it.key} className="border-t border-white/5">
|
|
||||||
<td className="py-2 text-white">{it.key}</td>
|
|
||||||
<td className="text-slate-300">{it.value}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -78,9 +78,20 @@ export function MediaDetailPage() {
|
|||||||
|
|
||||||
const reprobe = async () => {
|
const reprobe = async () => {
|
||||||
if (!media) return
|
if (!media) return
|
||||||
await api.post(`/media/${media.id}/probe`)
|
try {
|
||||||
toast.success('已重新探测')
|
const r = await api.post(`/media/${media.id}/probe`)
|
||||||
await refresh()
|
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 () => {
|
const exportNFO = async () => {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { FormEvent, useEffect, useState } from 'react'
|
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 toast from 'react-hot-toast'
|
||||||
|
|
||||||
import { AppFooter } from '../components/AppFooter'
|
import { AppFooter } from '../components/AppFooter'
|
||||||
import { adminAPI } from '../api/admin'
|
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 /
|
// SettingsPage replaces the Vue SettingsView's General / Organize /
|
||||||
// Scrape / Adult tabs. The Go backend stores settings as a single
|
// Scrape / Adult tabs. The Go backend stores settings as a single
|
||||||
@@ -33,7 +34,7 @@ const GROUPS: SettingGroup[] = [
|
|||||||
{
|
{
|
||||||
key: 'general',
|
key: 'general',
|
||||||
label: '常规',
|
label: '常规',
|
||||||
description: 'TMDb / 转码引擎默认参数',
|
description: '语言 / 转码引擎参数(API 密钥请在管理后台 → 外部API 配置)',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'tmdb.language',
|
key: 'tmdb.language',
|
||||||
@@ -88,13 +89,20 @@ const GROUPS: SettingGroup[] = [
|
|||||||
{
|
{
|
||||||
key: 'organize',
|
key: 'organize',
|
||||||
label: '整理 & 刮削',
|
label: '整理 & 刮削',
|
||||||
description: '媒体文件命名 + 自动刮削开关',
|
description: '媒体文件命名 + 自动刮削 + 整理目标',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'organize.auto',
|
key: 'organize.auto',
|
||||||
label: '入库时自动整理',
|
label: '入库时自动整理',
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'organize.target_dir',
|
||||||
|
label: '整理目标目录',
|
||||||
|
type: 'text',
|
||||||
|
hint: '留空则默认整理到各媒体库对应路径(见下方参考)',
|
||||||
|
placeholder: '/mnt/media/organized',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'organize.movie_format',
|
key: 'organize.movie_format',
|
||||||
label: '电影命名格式',
|
label: '电影命名格式',
|
||||||
@@ -179,16 +187,21 @@ export function SettingsPage() {
|
|||||||
const [dirty, setDirty] = useState<Set<string>>(new Set())
|
const [dirty, setDirty] = useState<Set<string>>(new Set())
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [libraries, setLibraries] = useState<Library[]>([])
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const all = await adminAPI.listSettings()
|
const [all, libs] = await Promise.all([
|
||||||
|
adminAPI.listSettings(),
|
||||||
|
libraryAPI.list().catch(() => [] as Library[]),
|
||||||
|
])
|
||||||
const idx: Record<string, string> = {}
|
const idx: Record<string, string> = {}
|
||||||
for (const s of all as Setting[]) {
|
for (const s of all as Setting[]) {
|
||||||
if (ALL_KEYS.has(s.key)) idx[s.key] = s.value
|
if (ALL_KEYS.has(s.key)) idx[s.key] = s.value
|
||||||
}
|
}
|
||||||
setValues(idx)
|
setValues(idx)
|
||||||
|
setLibraries(libs as Library[])
|
||||||
setDirty(new Set())
|
setDirty(new Set())
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -235,7 +248,7 @@ export function SettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="font-display text-3xl font-bold text-white">系统设置</h1>
|
<h1 className="font-display text-3xl font-bold text-white">系统设置</h1>
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-slate-400">
|
||||||
按分组编辑 TMDb / 整理 / 刮削 / 下载器等关键配置
|
按分组编辑转码 / 整理 / 刮削 / 下载器等关键配置
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,6 +303,44 @@ export function SettingsPage() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 整理 tab 时显示各媒体库默认路径 */}
|
||||||
|
{!loading && activeGroup === 'organize' && libraries.length > 0 && (
|
||||||
|
<div className="glass-panel">
|
||||||
|
<div className="mb-3 flex items-center gap-2 text-sm text-slate-300">
|
||||||
|
<FolderOpen size={16} className="text-primary-400" />
|
||||||
|
<span>默认整理路径参考(未设目标目录时按媒体库归类)</span>
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-left text-sm">
|
||||||
|
<thead className="text-xs uppercase tracking-wider text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th className="py-2">媒体库</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>路径</th>
|
||||||
|
<th>整理后示例</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{libraries.map((lib) => (
|
||||||
|
<tr key={lib.id} className="border-t border-white/5">
|
||||||
|
<td className="py-2 font-medium text-white">{lib.name}</td>
|
||||||
|
<td className="text-slate-400">
|
||||||
|
{lib.type === 'movie' ? '电影' : lib.type === 'tv' ? '电视剧' : lib.type === 'anime' ? '动漫' : '音乐'}
|
||||||
|
</td>
|
||||||
|
<td className="font-mono text-xs text-slate-400">{lib.path}</td>
|
||||||
|
<td className="font-mono text-[11px] text-slate-500">
|
||||||
|
{lib.type === 'movie'
|
||||||
|
? `${lib.path}/片名 (2024)/片名 (2024).mkv`
|
||||||
|
: lib.type === 'tv' || lib.type === 'anime'
|
||||||
|
? `${lib.path}/剧名/Season 01/剧名 - S01E01.mkv`
|
||||||
|
: lib.path}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<AppFooter className="mt-6" />
|
<AppFooter className="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,10 +161,14 @@ export function SitesPage() {
|
|||||||
const handleTest = async (id: string) => {
|
const handleTest = async (id: string) => {
|
||||||
setTestingId(id)
|
setTestingId(id)
|
||||||
try {
|
try {
|
||||||
await sitesAPI.test(id)
|
const res = await sitesAPI.test(id)
|
||||||
toast.success('连接测试成功')
|
const msg = res?.message || '连接测试成功'
|
||||||
} catch {
|
toast.success(msg)
|
||||||
toast.error('连接测试失败')
|
} catch (err: unknown) {
|
||||||
|
const msg =
|
||||||
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
||||||
|
'连接测试失败'
|
||||||
|
toast.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setTestingId(null)
|
setTestingId(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,6 +323,24 @@ export interface Site {
|
|||||||
cookie?: string // decrypted only in detail view
|
cookie?: string // decrypted only in detail view
|
||||||
api_key?: string // decrypted only in detail view
|
api_key?: string // decrypted only in detail view
|
||||||
auth_header?: 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
|
enabled: boolean
|
||||||
is_default: boolean
|
is_default: boolean
|
||||||
extra?: string
|
extra?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user