import { useState, useEffect } from 'react' import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' import StorageMounts from '../components/StorageMounts' import WorkerVerwaltung from '../components/WorkerVerwaltung' import { PageHeader } from '../components/ui/PageHeader' import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { Input, Select, Toggle } from '../components/ui/Input' import { MEDIA_SERVER_OPTIONEN } from '../lib/mediaServer' import { ENCODER_BADGES } from '../lib/design' interface SettingsState { tmdbApiKey: string tvdbApiKey: string omdbApiKey: string outputDir: string movieDir: string seriesDir: string musicDir: string ripAllTracks: boolean mainFeatureOnly: boolean autoEject: boolean autoRipStart: boolean notificationWebhook: string transcodeEnabled: boolean transcodePreset: string keepOriginal: boolean mediaServer: string jellyfinUrl: string jellyfinApiKey: string workDir: string makemkvAppKey: string } const defaultSettings: SettingsState = { tmdbApiKey: '', tvdbApiKey: '', omdbApiKey: '', outputDir: '/app/media', movieDir: 'movies', seriesDir: 'series', musicDir: 'music', ripAllTracks: true, mainFeatureOnly: false, autoEject: true, autoRipStart: false, notificationWebhook: '', transcodeEnabled: true, transcodePreset: 'H.265 MKV 1080p30', keepOriginal: false, mediaServer: 'none', jellyfinUrl: '', jellyfinApiKey: '', workDir: '', makemkvAppKey: '', } type SettingsTab = 'ripping' | 'verarbeitung' | 'worker' | 'speicherziele' | 'apis' | 'benachrichtigungen' | 'system' interface WorkerInfo { name: string encoders: string[] info?: { makemkv?: string, handbrake?: string, makemkv_key?: string } } interface SystemInfo { api_version: string plaetze: { name: string, frei_gb: number, gesamt_gb: number }[] workers: WorkerInfo[] makemkv_key_ui: boolean webhook_gesetzt: boolean } export default function SettingsPage() { const [settings, setSettings] = useState(defaultSettings) const [activeTab, setActiveTab] = useState('ripping') const [workers, setWorkers] = useState([]) const [systemInfo, setSystemInfo] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [testBusy, setTestBusy] = useState(false) const [quellenStatus, setQuellenStatus] = useState | null>(null) const [quellenBusy, setQuellenBusy] = useState(false) const [updates, setUpdates] = useState | null>(null) const [updatesBusy, setUpdatesBusy] = useState(false) const { toast } = useToast() const updatesPruefen = async () => { setUpdatesBusy(true) try { const r = await api.get('/system/updates') setUpdates(r.data) if (r.data?.makemkv?.update) { toast('info', `MakeMKV ${r.data.makemkv.verfuegbar} ist verfügbar (installiert: ${r.data.makemkv.installiert})`) } else { toast('success', 'Update-Check abgeschlossen') } } catch { toast('error', 'Update-Check fehlgeschlagen (makemkv.com/GitHub nicht erreichbar?)') } finally { setUpdatesBusy(false) } } const quellenPruefen = async () => { setQuellenBusy(true) try { const r = await api.get('/metadata/status') setQuellenStatus(r.data) const kaputt = Object.entries(r.data).filter(([, v]) => v === 'fehler') if (kaputt.length === 0) { toast('success', 'Metadaten-Quellen geprüft — alles in Ordnung') } else { toast('error', `Quelle mit Problem: ${kaputt.map(([k]) => k.toUpperCase()).join(', ')}`) } } catch { toast('error', 'Prüfung fehlgeschlagen — läuft die API?') } finally { setQuellenBusy(false) } } useEffect(() => { api.get('/capabilities') .then(r => setWorkers(r.data.workers || [])) .catch(() => setWorkers([])) api.get('/system/info') .then(r => setSystemInfo(r.data)) .catch(() => setSystemInfo(null)) }, []) const webhookTesten = async () => { if (!settings.notificationWebhook) return setTestBusy(true) try { await api.post('/notifications/test', { url: settings.notificationWebhook }) toast('success', 'Test-Nachricht gesendet — prüfe deinen Kanal!') } catch (e: any) { toast('error', e?.response?.data?.detail || 'Test fehlgeschlagen') } finally { setTestBusy(false) } } useEffect(() => { const loadSettings = async () => { try { const response = await api.get('/settings') setSettings({ ...defaultSettings, ...response.data }) } catch { setSettings(defaultSettings) } finally { setLoading(false) } } loadSettings() }, []) const handleChange = (field: keyof SettingsState, value: any) => { setSettings(prev => ({ ...prev, [field]: value })) setSaved(false) } const handleSave = async () => { setSaving(true) try { await api.post('/settings', settings) setSaved(true) toast('success', 'Einstellungen gespeichert') setTimeout(() => setSaved(false), 3000) } catch { toast('error', 'Speichern fehlgeschlagen — läuft die API?') } finally { setSaving(false) } } if (loading) { return (
) } const tabs: { id: SettingsTab; label: string; icon: any }[] = [ { id: 'ripping', label: 'Ripping', icon: Disc }, { id: 'verarbeitung', label: 'Verarbeitung', icon: Cpu }, { id: 'worker', label: 'Worker', icon: Cpu }, { id: 'speicherziele', label: 'Speicherziele', icon: HardDrive }, { id: 'apis', label: 'APIs', icon: Globe }, { id: 'benachrichtigungen', label: 'Benachrichtigungen', icon: AlertCircle }, { id: 'system', label: 'System', icon: Wrench }, ] return (
{saved && (
Einstellungen erfolgreich gespeichert!
)} {/* Navigation Tabs */}
{tabs.map((tab) => { const isActive = activeTab === tab.id return ( ) })}
{/* Tab Content */} {/* Ripping Settings */} {activeTab === 'ripping' && (

Ripping-Einstellungen

📀 Video — DVD · Blu-ray · 4K UHD

handleChange('autoRipStart', v)} />
handleChange('mainFeatureOnly', v)} />

🌐 Sprachen & Untertitel: MakeMKV sichert grundsätzlich ALLE Ton-Spuren und Untertitel verlustfrei.

🎵 Audio-CD

handleChange('ripAllTracks', v)} />

Allgemein

handleChange('autoEject', v)} />
{/* Media-Server Integration */}

Media-Server (Jellyfin & Co.)

Rippy benennt fertige Rips als „Titel (Jahr)" und legt NFO + Poster dazu.

{MEDIA_SERVER_OPTIONEN.map(o => ( ))}

{MEDIA_SERVER_OPTIONEN.find(o => o.wert === settings.mediaServer)?.hinweis}

{(settings.mediaServer === 'jellyfin' || settings.mediaServer === 'emby') && (

Bibliotheks-Refresh: Server-URL + API-Key für automatischen Scan nach dem Rip.

handleChange('jellyfinUrl', e.target.value)} placeholder="http://192.168.178.50:8096" />
handleChange('jellyfinApiKey', e.target.value)} placeholder="API-Key" />
)}
)} {/* Verarbeitung */} {activeTab === 'verarbeitung' && (

Verarbeitung (Kompression)

MakeMKV rippt verlustfrei, HandBrake komprimiert danach auf Arbeitsgröße.

Verfügbare Encoder

{workers.length === 0 ? (

Kein Worker gemeldet.

) : ( workers.map(w => (
{w.name}: {w.encoders.map(e => ( {ENCODER_BADGES[e]?.label || e} ))}
)) )}
handleChange('transcodeEnabled', v)} />
handleChange('keepOriginal', v)} />
handleChange('workDir', e.target.value)} placeholder="leer = /app/temp (Container-Platte)" />

Wichtig für 4K-UHD: Rohdaten sind bis 100 GB groß. {systemInfo?.plaetze?.length ? ( <> Aktuell frei: {systemInfo.plaetze.map(p => `${p.name}: ${p.frei_gb} GB`).join(' · ')} ) : null}

)} {/* Worker */} {activeTab === 'worker' && (

Encoding-Worker

)} {/* Speicherziele */} {activeTab === 'speicherziele' && (

Speicherziele

Schnellwahl beim Rippen

Name der Ordner im „Rippen starten"-Dialog (🎥/📺/🎵).

{([['movieDir', '🎥 Filme'], ['seriesDir', '📺 Serien'], ['musicDir', '🎵 Musik']] as const).map(([feld, label]) => ( handleChange(feld, e.target.value)} /> ))}
)} {/* APIs */} {activeTab === 'apis' && (

API-Schlüssel

{quellenStatus ? (
{Object.entries(quellenStatus).map(([quelle, status]) => ( {quelle.toUpperCase()}: {status === 'ok' ? '✓ funktioniert' : status === 'kein_key' ? 'kein Key' : '✗ Fehler'} ))}
) : ( Prüft live gegen TMDB/OMDb — nach dem Speichern eines neuen Keys hier klicken. )}
handleChange('tmdbApiKey', e.target.value)} placeholder="Dein TMDB API Key" /> handleChange('tvdbApiKey', e.target.value)} placeholder="Dein TVDb API Key" /> handleChange('omdbApiKey', e.target.value)} placeholder="Dein OMDb API Key" />
)} {/* Benachrichtigungen */} {activeTab === 'benachrichtigungen' && (

Webhook-Benachrichtigungen

Rippy meldet sich, sobald ein Job fertig ist, fehlschlägt oder abgebrochen wird.

💬 Discord (einfachster Weg)

Kanal-Zahnrad → Integrationen → Webhooks → „Neuer Webhook" → URL kopieren.

📱 ntfy (Push aufs Handy)

Thema abonnieren und https://ntfy.sh/mein-thema eintragen.

handleChange('notificationWebhook', e.target.value)} placeholder="https://discord.com/api/webhooks/… oder https://ntfy.sh/mein-thema" />
)} {/* System */} {activeTab === 'system' && (

System & Kern-Werkzeuge

Installierte Werkzeuge

{!systemInfo || systemInfo.workers.length === 0 ? (

Noch kein Worker gemeldet.

) : ( systemInfo.workers.map(w => (
{w.name} {w.info?.makemkv && ( MakeMKV {w.info.makemkv} )} {w.info?.handbrake && ( HandBrake {w.info.handbrake} )}
)) )}

MakeMKV lässt sich aktuell halten (Update-Check unten; Version über MAKEMKV_VERSION + Rebuild) — wichtig wegen der Disc-Schlüssel-Datenbank. {' '}HandBrake im eingebauten Docker-Worker ist bewusst die stabile Debian-Version — für die Kompression völlig ausreichend und wird nicht separat aktualisiert. Native Worker (Windows) holen dagegen automatisch die neueste HandBrake-Version.

handleChange('makemkvAppKey', e.target.value)} placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)" />

Update-Check

{updates ? (
{/* MakeMKV = aktualisierbar → hier ist ein Update ein echtes To-do */}
MakeMKV: installiert {updates.makemkv?.installiert || '—'}, neueste {updates.makemkv?.verfuegbar || 'unbekannt'} {updates.makemkv?.update ? (

⚠️ Update verfügbar — in der .env MAKEMKV_VERSION={updates.makemkv.verfuegbar} setzen, dann auf der Rippy-Maschine docker compose build worker && docker compose up -d worker. Neue Versionen bringen auch die neueste Disc-Schlüssel-Datenbank mit.

) : ✓ aktuell}
{/* HandBrake im Docker = bewusst Debian-stabil → KEIN To-do, nur Info */}
HandBrake (Docker-Worker): {updates.handbrake?.installiert || '—'} — stabile Debian-Version, bewusst so, kein Update nötig

Neueste offizielle Version wäre {updates.handbrake?.verfuegbar || 'unbekannt'} — die nutzen die nativen Worker (Windows) automatisch. Der Docker-Worker bleibt bei der Debian-Version (für die Kompression identisch, robustere Builds).

) : (

Prüft gegen makemkv.com und die HandBrake-GitHub-Releases (Ergebnis wird 12 h gemerkt).

)}
)} {/* Global Save Button */} {activeTab !== 'worker' && (
)}
) }