import { useState, useEffect, useRef } from 'react' import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv, KeyRound, Upload, Trash2, Download } 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' import { PRESET_KEINE, schwacheEncoderCpu, simdWarnung } from '../lib/encoder' interface SettingsState { tmdbApiKey: string tvdbApiKey: string omdbApiKey: string outputDir: string movieDir: string seriesDir: string musicDir: string mainFeatureOnly: boolean autoEject: boolean autoRipStart: boolean notificationWebhook: string transcodeEnabled: boolean // transcodePreset bleibt als Rückfall für Bestandsinstallationen bestehen — // solange die drei Felder darunter nicht gespeichert sind, gilt weiter dieser // Wert (siehe preset_fuer() in docker/worker/ripping.py). transcodePreset: string transcodePresetDvd: string transcodePresetBluray: string transcodePresetUhd: 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', mainFeatureOnly: false, autoEject: true, autoRipStart: false, notificationWebhook: '', transcodeEnabled: true, transcodePreset: 'H.265 MKV 1080p30', transcodePresetDvd: 'H.265 MKV 576p25', transcodePresetBluray: 'H.265 MKV 1080p30', transcodePresetUhd: 'H.265 MKV 2160p60 4K', keepOriginal: false, mediaServer: 'none', jellyfinUrl: '', jellyfinApiKey: '', workDir: '', makemkvAppKey: '', } type SettingsTab = 'ripping' | 'verarbeitung' | 'worker' | 'speicherziele' | 'apis' | 'benachrichtigungen' | 'system' interface WorkerInfo { name: string encoders: string[] // keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg // in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich // rippt, zählt, deshalb steht die Angabe pro Worker und nicht global. // cpu_kerne/cpu_simd: Rechenleistung fürs Software-Encodieren. Ohne diese // Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der // Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne). // handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst. info?: { makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string, cpu_modell?: string, cpu_kerne?: string, cpu_simd?: string, handbrake_encoder?: string, } } // Antwort von GET/POST/DELETE /system/keydb (Feldnamen exakt wie die API sie liefert). interface KeydbStatus { vorhanden: boolean pfad: string groesse_bytes: number eintraege: number geaendert: string } // Ein Ablageziel aus GET /storage-targets (Feldnamen exakt wie die API liefert). interface StorageZiel { name: string path: string is_mount: boolean free_gb: number | null } // Antwort von GET/POST /system/keystore — MakeMKVs eigener Schlüsselvorrat. interface KeystoreStatus { vorhanden: boolean pfad: string groesse_bytes: number schluessel: number geaendert: string } // Ein AACS-Dump aus GET /system/aacs-dumps. interface AacsDump { name: string groesse_bytes: number geaendert: string } interface SystemInfo { api_version: string plaetze: { name: string, frei_gb: number, gesamt_gb: number }[] workers: WorkerInfo[] makemkv_key_ui: boolean webhook_gesetzt: boolean } // Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB groß. function bytesLesbar(bytes: number): string { if (!bytes) return '0 B' if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB` if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB` return `${bytes} B` } // Die API liefert ISO-8601 in UTC (oder einen leeren String). Ohne Zonen-Kennung // würde der Browser den Zeitstempel als Ortszeit lesen und die Uhrzeit um den // Zonen-Versatz verschieben — deshalb notfalls ein Z anhaengen. function zeitLesbar(iso: string): string { if (!iso) return 'unbekannt' const mitZone = /[Zz]$|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z` const d = new Date(mitZone) return isNaN(d.getTime()) ? 'unbekannt' : d.toLocaleString('de-DE') } 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) // KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine // Einstellungen — deshalb bewusst NICHT in SettingsState: der globale // Speichern-Knopf postet dieses Objekt komplett und würde sie mitschleifen. const [keydb, setKeydb] = useState(null) const [keydbBusy, setKeydbBusy] = useState(false) const [dumps, setDumps] = useState([]) const keydbInput = useRef(null) // Ablageziele unter /app/media inkl. eingehängter Freigaben — speist die // Auswahl des Arbeitsverzeichnisses (vorher musste man den Pfad tippen). const [ziele, setZiele] = useState([]) const [keystore, setKeystore] = useState(null) const [keystoreBusy, setKeystoreBusy] = useState(false) const keystoreInput = useRef(null) 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) } } // Status der KEYDB.cfg + Liste der AACS-Dumps frisch holen. Wird beim Laden // der Seite und nach jeder Aktion aufgerufen — die API ist die Wahrheit, // nicht das, was wir gerade hochgeschickt haben. const keydbLaden = () => { api.get('/system/keydb').then(r => setKeydb(r.data)).catch(() => setKeydb(null)) api.get('/system/keystore').then(r => setKeystore(r.data)).catch(() => setKeystore(null)) api.get('/system/aacs-dumps').then(r => setDumps(r.data?.dumps || [])).catch(() => setDumps([])) // Die Worker-Plaketten oben kommen aus /capabilities und stammen aus dem // letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen // widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung. // Ganz sofort ist sie trotzdem nicht: der Worker meldet erst beim // nächsten Herzschlag neu. Genau so steht es auch im Erklaertext. api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([])) } const keydbHochladen = async (datei: File) => { setKeydbBusy(true) try { // Bewusst als JSON-Text und nicht als Multipart-Upload: der API fehlt // python-multipart, ein File()-Endpunkt würde sie beim Import killen. const inhalt = await datei.text() await api.post('/system/keydb', { inhalt }) keydbLaden() toast('success', 'KEYDB.cfg gespeichert — sie wirkt ab dem nächsten Rip.') } catch (e: any) { toast('error', e?.response?.data?.detail || 'Hochladen fehlgeschlagen — ist das wirklich eine KEYDB.cfg, und läuft die API?') } finally { setKeydbBusy(false) } } const keystoreHochladen = async (datei: File) => { setKeystoreBusy(true) try { // Die Datei wandert als roher Anfrage-Körper zur API — _private_data.tar // ist binär, JSON oder Base64 wäre nur unnötiger Ballast, und Multipart // kann die API nicht (kein python-multipart). await api.post('/system/keystore', datei, { headers: { 'Content-Type': 'application/octet-stream' }, }) keydbLaden() toast('success', 'Schlüsselspeicher übernommen — wirkt ab dem nächsten Rip.') } catch (e: any) { toast('error', e?.response?.data?.detail || 'Übernahme fehlgeschlagen — ist das wirklich die Datei _private_data.tar, und läuft die API?') } finally { setKeystoreBusy(false) } } const keydbEntfernen = async () => { setKeydbBusy(true) try { await api.delete('/system/keydb') keydbLaden() toast('success', 'KEYDB.cfg entfernt — Rippy nutzt jetzt keine Schlüsseldatei mehr.') } catch (e: any) { toast('error', e?.response?.data?.detail || 'Entfernen fehlgeschlagen — läuft die API?') } finally { setKeydbBusy(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)) api.get('/system/keydb') .then(r => setKeydb(r.data)) .catch(() => setKeydb(null)) api.get('/system/keystore') .then(r => setKeystore(r.data)) .catch(() => setKeystore(null)) api.get('/storage-targets') .then(r => setZiele(Array.isArray(r.data) ? r.data : [])) .catch(() => setZiele([])) api.get('/system/aacs-dumps') .then(r => setDumps(r.data?.dumps || [])) .catch(() => setDumps([])) }, []) 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

{/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen. Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe build_abcde_cmd) und es gibt auch keine Oberfläche, um eine Teilmenge zu wählen — eine Audio-CD wurde also immer vollständig gerippt, egal wie der Schalter stand. Statt einer Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}

💿 Eine Audio-CD wird immer vollständig gerippt — alle Tracks als FLAC, mit MusicBrainz-Tags.

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} ))} {(w.info?.cpu_kerne || w.info?.cpu_simd) && (

{w.info?.cpu_modell ? `${w.info.cpu_modell} · ` : ''} {w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''} {w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''} {w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}

)} {simdWarnung(w.info?.cpu_simd) && (

{simdWarnung(w.info?.cpu_simd)}

)} {w.info?.handbrake_encoder && (

HandBrake meldet: {w.info.handbrake_encoder}

)}
)) )}
handleChange('transcodeEnabled', v)} />
{/* Ein Preset JE DISC-TYP (25.07.2026). Vorher lief alles durch dasselbe Preset — eine 4K-UHD wurde damit auf 1080p heruntergerechnet und eine DVD auf 1080p hochskaliert. Preset-Namen stammen aus `HandBrakeCLI --preset-list` im Worker-Image (HandBrake 1.6.1), nicht aus dem Kopf. */}

Ein Preset je Quelle — Rippy erkennt den Disc-Typ selbst und wählt passend aus. Eine 4K-UHD gehört auf ein 2160p-Preset, sonst rechnet HandBrake sie auf 1080p herunter und die 4K-Auflösung ist weg.

{/* Die Warnung, die den 50-Stunden-Lauf verhindert haette. Sie steht nur da, wenn die Maschine sie WIRKLICH braucht - gemessen, nicht vermutet (worker/caps.py meldet cpu_simd). */} {settings.transcodePresetUhd !== PRESET_KEINE && settings.transcodePresetUhd.includes('2160') && schwacheEncoderCpu(workers) && (

⚠️ Keiner deiner Worker kann AVX2 — 4K in H.265 dauert damit ein bis zwei Tage pro Film (gemessen, nicht geschätzt). Besser „Nicht komprimieren" wählen und die verlustfreie Datei behalten, oder einen Worker mit Hardware-Encoder anschließen.

)}

„Nicht komprimieren" heißt: Der Rip bleibt so, wie MakeMKV ihn gesichert hat — beste Qualität, aber 20–100 GB je Film. Sinnvoll für 4K, wenn die Ablage groß genug ist.

handleChange('keepOriginal', v)} />
{/* Auswahl statt Eintippen (Befund 25.07.2026): Das Feld war ein freies Textfeld — man musste den Container-Pfad KENNEN (/app/media/…), um eine Netzwerk-Freigabe zu treffen. Genau daran ist es gescheitert, und der 74-GB-Rohschnitt landete auf der VM-Platte, bis sie voll war. Die Ziele kommen aus /storage-targets — dieselbe Liste wie bei den Speicherzielen, inklusive freiem Platz. */}

Der Standard — beim „Rippen starten" kannst du für jede Disc etwas anderes wählen. Läuft die Vollautomatik, fragt dich niemand: dann gilt genau dieser Wert. Wichtig für 4K-UHD: Rohdaten sind bis 100 GB groß und passen selten auf die Container-Platte. Am besten dieselbe Freigabe wie das Ziel — dann muss Rippy die Rohdatei am Ende nur umhängen statt sie zu kopieren. {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} )} {/* Nur das Verzeichnis des rippenden Workers zählt — deshalb je Worker. Die Zahl der Disc-Schluessel ist die entscheidende Angabe für 4K-UHD: 0 heißt, dass JEDE unbekannte UHD-Disc scheitert. */} {w.info?.schluessel && w.info.schluessel !== '0' && w.info.schluessel !== 'unbekannt' && ( {w.info.schluessel} Disc-Schlüssel )} {w.info?.schluessel === '0' && ( keine Disc-Schlüssel — 4K-UHD scheitert )} {w.info?.keydb === 'ja' && ( + KEYDB.cfg )}
)) )} {/* Bis 25.07.2026 stand hier, MakeMKV-Updates braechten die neueste Disc-Schluessel-Datenbank mit. Nachgemessen: falsch — MakeMKV liefert gar keine Schluessel mit, es holt sie zur Laufzeit. Und die Linux-Version holt sie nie (kein einziger Verbindungsversuch, auf beiden Maschinen verglichen). Deshalb der Schlüsselspeicher unten. */}

MakeMKV lässt sich aktuell halten (Update-Check unten; Version über MAKEMKV_VERSION + Rebuild) — neue Versionen bringen vor allem Laufwerks-Unterstützung und Fehlerbehebungen. Disc-Schlüssel für 4K-UHD kommen NICHT aus einem Update — die holt MakeMKV zur Laufzeit, und die Linux-Version tut das nie. Deshalb der Block „Disc-Schlüssel für 4K-UHD" weiter unten. {' '}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”)" /> {/* Schlüsselspeicher — der Hauptweg für 4K-UHD (Befund 25.07.2026, auf beiden Maschinen gemessen): makemkvcon holt Disc-Schluessel unter Linux NIE selbst, die Windows-Version tut es (Meldung 3338, Verbindung nach 185.84.108.20:443). Deshalb wird der Vorrat hier von Hand hereingereicht. Frueher stand an dieser Stelle die These, MakeMKVs Schluessel-Kanal sei abgeschaltet — das war falsch. */}

Disc-Schlüssel für 4K-UHD

{ const datei = e.target.files?.[0] e.target.value = '' if (datei) keystoreHochladen(datei) }} />
{keystore?.vorhanden && keystore.schluessel > 0 ? (

{keystore.schluessel} Disc-Schlüssel vorhanden.

{bytesLesbar(keystore.groesse_bytes)} · zuletzt geändert {zeitLesbar(keystore.geaendert)}

) : (

Kein einziger Disc-Schlüssel vorhanden.

DVDs und normale Blu-rays laufen trotzdem. 4K-UHD-Discs scheitern dagegen mit „The volume key is unknown for this disc" — solange hier nichts liegt, jede einzelne.

)}

Warum das nötig ist: MakeMKV holt sich diese Schlüssel eigentlich selbst aus dem Netz. Die Linux-Version tut das nicht — am 25.07.2026 nachgemessen: sie baut dabei nicht eine einzige Verbindung auf. Die Windows-Version schon. Ein MakeMKV-Update ändert daran nichts, es ist kein Fehler in Rippy.

So füllst du den Vorrat: MakeMKV auf einem Windows-PC installieren (gleicher Beta-Key), das Laufwerk dort anstecken und die Disc einmal öffnen. MakeMKV lädt die Schlüssel dabei nach. Danach unter Preferences → General das „MakeMKV data directory" nachschlagen, die Datei _private_data.tar daraus hier hochladen — fertig. Für neue Discs von Zeit zu Zeit wiederholen.

Das ist der Zwischenspeicher deiner eigenen MakeMKV-Installation, über MakeMKVs offiziellen Weg mit deiner eigenen Lizenz geholt. Rippy liefert keine Schlüssel mit und lädt keine herunter.

{/* KEYDB.cfg — der NOTNAGEL, nicht der Hauptweg. Sie hilft bei Pressungen, die auch MakeMKV selbst nicht kennt. Der Regelfall läuft über den Schlüsselspeicher im Block darüber. Dateiname GROSS geschrieben, unter Linux case-sensitiv. Rippy stellt nur den Platz bereit. */}

KEYDB.cfg (Notnagel)

{/* Versteckter Datei-Dialog: der Inhalt wird im Browser gelesen und als JSON gepostet. Kein Multipart — der API fehlt python-multipart. */} { const datei = e.target.files?.[0] // Wert leeren, damit dieselbe Datei erneut gewählt werden kann. e.target.value = '' if (datei) keydbHochladen(datei) }} /> {keydb?.vorhanden && ( )}
{keydb?.vorhanden ? (

Eine KEYDB.cfg liegt bereit.

{bytesLesbar(keydb.groesse_bytes)} · {keydb.eintraege} Zeilen mit Disc-Kennung · zuletzt geändert {zeitLesbar(keydb.geaendert)}

{keydb.pfad}

) : (

Keine KEYDB.cfg hinterlegt.

Das ist normal und meistens auch nicht nötig — der Regelfall läuft über den Schlüsselspeicher oben.

)}

Die KEYDB.cfg ist eine reine Textdatei mit Disc-Schlüsseln. Sie ist der Notnagel für den Fall, dass eine Pressung selbst über den Schlüsselspeicher nicht aufgeht — also auch MakeMKV sie nicht kennt. Zuerst immer den Weg darüber versuchen.

Wichtig: Rippy liefert keine Schlüssel mit, lädt keine herunter und verteilt keine. Rippy stellt nur den Platz für eine Datei bereit, die du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt. Die Zahl oben ist genau das: die gezählten Zeilen mit einer Disc-Kennung in deiner Datei — keine von MakeMKV bestätigte Anzahl brauchbarer Schlüssel.

Eine hochgeladene Datei ersetzt die bisherige und wirkt ab dem nächsten Rip — laufende Jobs bleiben unberührt, ein Neustart ist nicht nötig. Die Plakette am Worker weiter oben folgt erst mit dessen nächstem Herzschlag (etwa eine Minute) — dieser Kasten hier ist sofort aktuell.

{/* AACS-Dumps: MakeMKV legt sie bei einer unbekannten UHD-Disc ab (Meldung 3332). Seit das Datenverzeichnis persistent gemountet ist, überleben sie den Container-Neustart — vorher waren sie nach jedem Rip weg. */}

AACS-Dumps

{dumps.length === 0 ? (

Noch kein Dump vorhanden. MakeMKV legt hier automatisch einen ab, sobald eine 4K-UHD-Disc an einem unbekannten Schlüssel scheitert.

) : ( )}

Ein AACS-Dump ist das, was das Laufwerk von der Disc gelesen hat, bevor der Schlüssel fehlte. Er enthält keinen Schlüssel und nützt dir allein nichts — aber du kannst ihn herunterladen und im MakeMKV-Forum im Bereich Ultra HD Blu-ray einreichen. Dort können Leute daraus den Schlüssel für deine Pressung ermitteln; den trägst du dann in deine KEYDB.cfg ein.

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. {/* 25.07.2026 richtiggestellt: ein Update bringt KEINE Disc-Schluessel mit. */} Neue Versionen bringen Laufwerks-Unterstützung und Fehlerbehebungen — aber keine Disc-Schlüssel: die kommen aus dem Schlüsselspeicher.

) : ✓ 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' && (
)}
) }