import { FormEvent, useEffect, useState } from 'react' import { Activity, CheckCircle2, Cpu, Database, Eye, EyeOff, Globe, Loader2, Lock, Save, Server, Sparkles, XCircle, } from 'lucide-react' import toast from 'react-hot-toast' import { adminAPI } from '../api/admin' import { libraryAPI } from '../api/library' import type { Library, Setting } from '../types' import { adultSettingsGroup } from './settingsGroupAccess' const POPULAR_METATUBE_PROVIDERS = [ { id: '', label: '自动 (全部)' }, { id: 'javdb', label: 'JavDB' }, { id: 'javbus', label: 'JavBus' }, { id: 'dmm', label: 'DMM / FANZA' }, { id: 'fc2', label: 'FC2' }, { id: 'gfriends', label: 'GFriends' }, { id: 'airav', label: 'AirAV' }, { id: 'heyzo', label: 'HEYZO' }, { id: 'mgs', label: 'MGS' }, ] export function AdultSettingsPanel() { const [values, setValues] = useState>({}) const [dirty, setDirty] = useState>(new Set()) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [libraries, setLibraries] = useState([]) const [showToken, setShowToken] = useState(false) // Test connection state const [testingMT, setTestingMT] = useState(false) const [mtTestResult, setMtTestResult] = useState<{ success?: boolean latency_ms?: number providers?: string[] error?: string } | null>(null) const [testingBuiltin, setTestingBuiltin] = useState(false) const [builtinTestResult, setBuiltinTestResult] = useState<{ success?: boolean latency_ms?: number message?: string error?: string } | null>(null) const refresh = async () => { setLoading(true) try { const [all, libs] = await Promise.all([ adminAPI.listSettings(), libraryAPI.list({ includeHidden: true }).catch(() => [] as Library[]), ]) const idx: Record = {} for (const item of adultSettingsGroup.items) { if (item.defaultValue !== undefined) { idx[item.key] = item.defaultValue } } for (const s of all as Setting[]) { if (s.key.startsWith('adult.')) { idx[s.key] = s.value } } setValues(idx) setLibraries(libs as Library[]) setDirty(new Set()) } finally { setLoading(false) } } useEffect(() => { refresh().catch(() => undefined) }, []) const setVal = (key: string, value: string) => { setValues((prev) => ({ ...prev, [key]: value })) setDirty((prev) => new Set(prev).add(key)) } const toggleVal = (key: string) => { const current = values[key] === 'true' || values[key] === '1' || values[key] === 'on' setVal(key, current ? 'false' : 'true') } const isChecked = (key: string, def = false) => { const val = values[key] if (val === undefined) return def return val === 'true' || val === '1' || val === 'on' } const onSave = async (e?: FormEvent) => { if (e) e.preventDefault() if (dirty.size === 0) return setSaving(true) try { for (const key of dirty) { await adminAPI.updateSetting(key, values[key] ?? '') } toast.success(`已保存 ${dirty.size} 项配置`) setDirty(new Set()) } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '保存失败' toast.error(msg) } finally { setSaving(false) } } const runMetaTubeTest = async () => { setTestingMT(true) setMtTestResult(null) try { const res = await adminAPI.testAdultScraper({ engine: 'metatube', server_url: values['adult.scraper.metatube_server'] || 'http://127.0.0.1:7700', token: values['adult.scraper.metatube_token'] || '', }) setMtTestResult(res) if (res.success) { toast.success(`MetaTube 连接成功 (${res.latency_ms ?? 0}ms)`) } else { toast.error(res.error || 'MetaTube 连接失败') } } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? (err as Error).message ?? '测试失败' setMtTestResult({ success: false, error: msg }) toast.error(msg) } finally { setTestingMT(false) } } const runBuiltinTest = async () => { setTestingBuiltin(true) setBuiltinTestResult(null) try { const res = await adminAPI.testAdultScraper({ engine: 'builtin', javdb_url: values['adult.scraper.builtin_javdb_url'] || 'https://javdb.com', javbus_url: values['adult.scraper.builtin_javbus_url'] || '', cookie: values['adult.scraper.builtin_cookie'] || '', }) setBuiltinTestResult(res) if (res.success) { toast.success(`内置刮削源连接成功 (${res.latency_ms ?? 0}ms)`) } else { toast.error(res.error || '内置源连接失败') } } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? (err as Error).message ?? '测试失败' setBuiltinTestResult({ success: false, error: msg }) toast.error(msg) } finally { setTestingBuiltin(false) } } const selectedLibraryIDs = parseLibraryIDs(values['adult.library_ids'] || '[]') const toggleLibrary = (id: string, checked: boolean) => { const next = checked ? Array.from(new Set([...selectedLibraryIDs, id])) : selectedLibraryIDs.filter((item) => item !== id) setVal('adult.library_ids', JSON.stringify(next)) } const engine = values['adult.scraper.engine'] || 'builtin' if (loading) { return (
) } return (
{/* 1. 全局访问与隔离卡片 */}

访问与媒体库隔离控制

设置全局成人内容显示规则、指定成人影视库与访问 PIN 保护

{/* 主开关 */}
{/* PIN 保护 */}
{isChecked('adult.require_pin', false) && (
setVal('adult.pin', e.target.value)} />
)}
{/* 成人媒体库绑定 */}
指定成人媒体库 指定哪些媒体库属于成人分类。指定后,搜索、分类及 Emby/Jellyfin 协议都会统一应用隔离与过滤规则。
{libraries.length === 0 && (
暂无媒体库
)} {libraries.map((lib) => { const checked = selectedLibraryIDs.includes(lib.id) return ( ) })}
{/* 2. 番号刮削引擎配置卡片 */}

番号刮削配置模块

支持切换本项目内置番号刮削源或接入 MetaTube 独立服务端,进行精准影视与女优元数据抓取

{/* 引擎模式选择 */}
{/* 3. MetaTube 专属配置模块 */} {(engine === 'metatube' || engine === 'auto') && (
MetaTube 后端配置
MetaTube 官方文档
setVal('adult.scraper.metatube_server', e.target.value)} />

独立部署的 MetaTube Server 地址,如 http://127.0.0.1:7700 或内网 Docker IP

setVal('adult.scraper.metatube_token', e.target.value)} />

MetaTube 服务端配置的 TOKEN 环境变量

{/* Provider 选择器 */}
{POPULAR_METATUBE_PROVIDERS.map((p) => { const active = (values['adult.scraper.metatube_provider'] ?? '') === p.id return ( ) })}
{/* 测试 MetaTube 连接按钮 */}
{mtTestResult && (
{mtTestResult.success ? ( 连接成功 ({mtTestResult.latency_ms}ms) {mtTestResult.providers && mtTestResult.providers.length > 0 && ( · 支持 {mtTestResult.providers.length} 个源 )} ) : ( {mtTestResult.error} )}
)}
)} {/* 4. 本地内置刮削源专属配置 */} {(engine === 'builtin' || engine === 'auto') && (
内置多源刮削配置
setVal('adult.scraper.builtin_javdb_url', e.target.value)} />
setVal('adult.scraper.builtin_cookie', e.target.value)} />