// Die Einstellungs-Karten (5.5.0, je Karte eine Funktion): Ablage, // Kompression, Roh-Dateien, Automatik, Metadaten, MakeMKV, Medienserver, // Benachrichtigung, Allgemein, Update, Werkzeuge. Alle schreiben über // einstellungSetzen in die EINE Quelle (§ 6.7). import { useState } from 'react' import type { GeraeteInfo, HauptStatus, VorgangEintrag } from '../../gemeinsam/nachrichten' import { presetEmpfehlung } from '../../gemeinsam/preset-empfehlung' import { Badge, FELD_KLASSE, Feld, HinterlegtPunkt, KNOPF_LEISE, LAUFENDE_PHASEN, PfadFeld, Schalter, SektionsKarte, TYP_NAMEN, datumKurz, gb, gbKurz, } from '../bausteine' import { benachrichtigungTesten, einstellungSetzen, fremdenOrdnerLoeschen, kompressionAbbrechen, makemkvUpdate, rohAufraeumen, schluesselPruefen, vorgaengeLaden, vorgangNeuKomprimieren, vorgangRohLoeschen, type Verbindung, } from '../kernverbindung' import { phasenTon } from '../uebersicht/VerlaufKarte' import { SprachAuswahl } from './SprachAuswahl' const setzen = einstellungSetzen export function AblageKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen const speicher = verbindung.speicher return ( setzen('ablage', w)} /> {speicher !== null && speicher.ablage.ok && (

{gbKurz(speicher.ablage.freiBytes)} frei von {gbKurz(speicher.ablage.gesamtBytes)}

)} setzen('arbeitsordner', w)} /> {speicher !== null && speicher.getrennt && speicher.arbeit.ok && (

{gbKurz(speicher.arbeit.freiBytes)} frei von {gbKurz(speicher.arbeit.gesamtBytes)} — eine Blu-ray braucht rund 50 GB.

)}

So legt Rippy ab:

├─ Filme\Akira (1988)\ — Film, Poster und Infodatei beieinander

├─ Serien\Spartacus\Season 01\ — Folgen, wie Jellyfin sie erwartet

├─ Musik\ — Audio-CDs als FLAC, benannt nach MusicBrainz

├─ ISO\ — unerkannte Discs als 1:1-Abbild

└─ roh\ — im Arbeitsordner: verlustfreies Roh-Material, aufgeräumt nach der Regel unter „Roh-Dateien"

setzen('autoAuswurf', an ? 'true' : 'false')} label="Disc nach dem Rip auswerfen — die nächste kann rein, während die Kompression läuft" />
) } function PresetWahl({ name, schluessel, wert, presets, empfehlung, }: { name: string schluessel: string wert: string presets: string[] empfehlung: string }) { return ( ) } export function KompressionKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen const presets = verbindung.werkzeuge?.presets ?? [] const backends = verbindung.werkzeuge?.backends ?? [] const empfehlungFuer = (typ: string): string => presetEmpfehlung(typ, backends, presets) return ( {verbindung.werkzeuge !== null && verbindung.werkzeuge.handbrake.length === 0 && (

⚠ HandBrakeCLI fehlt — Rippen geht, Komprimieren nicht (siehe Werkzeuge).

)} setzen('transcodeEnabled', an ? 'true' : 'false')} label="Nach dem Rip automatisch komprimieren" /> {backends.length > 0 && (
GEMESSENE ENCODER: {backends.map((backend) => ( ))}
)}

„automatisch" nimmt die Empfehlung aus der gemessenen Liste deines HandBrake — auf dieser Maschine die Grafikkarte, wo es sie gibt. Eine eigene Wahl schlägt die Empfehlung immer.

setzen('audioSprachen', w)} /> setzen('untertitelSprachen', w)} />
) } export function AutomatikKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen const an = werte['automatik'] !== 'aus' return ( setzen('automatik', a ? 'an' : 'aus')} label="Automatik: bei sicherer Erkennung (ab 90 %) von selbst rippen" />

Der Countdown steht in der Laufwerks-Kachel, mit „Stopp" und „Jetzt". Eine Disc, die schon einmal gerippt wurde, startet nie von selbst.

) } export function RohDateienKarte({ verbindung, aktivFuer }: { verbindung: Verbindung; aktivFuer?: boolean }) { const werte = verbindung.einstellungen const v = verbindung.vorgaenge const [presetWahl, setPresetWahl] = useState>({}) const [alleZeigen, setAlleZeigen] = useState(aktivFuer === true) const presets = verbindung.werkzeuge?.presets ?? [] const regelWert = werte['rohAufbewahrung'] ?? '7' const phasenText: Record = { rippt: 'rippt', wartet: 'wartet auf Kompression', komprimiert: 'komprimiert', 'legt-ab': 'legt ab', fertig: 'fertig', fehler: 'Fehler', abgebrochen: 'abgebrochen', } const liste = v?.vorgaenge ?? [] const sichtbar = alleZeigen ? liste : liste.slice(0, 12) return (
{v === null ? (

Belegung wird gemessen …

) : ( <>

Belegung jetzt: {gb(v.rohGesamtBytes)} in{' '} {liste.filter((e) => !e.rohGeloescht).length + v.fremde.length} Roh-Ordnern

{v.regel}

Unkomprimierte Rips (z. B. 4K verlustfrei) liegen im Zielordner, nicht hier — sie sind nie betroffen.

)}
{liste.length === 0 && v !== null &&

Noch kein Vorgang — der erste Rip legt hier einen Eintrag an.

}
{sichtbar.map((e) => { const ruhig = e.phase === 'fertig' || e.phase === 'fehler' || e.phase === 'abgebrochen' return (
{e.titel} {e.jahr !== null && ({e.jahr})} {(TYP_NAMEN[e.typ as GeraeteInfo['typ']] ?? e.typ).toUpperCase()} · {datumKurz(e.begonnen)} {e.titelZahl > 0 && ` · ${e.titelZahl} Titel`} {e.preset.length > 0 && ` · ${e.preset}`}

Roh: {e.rohGeloescht ? 'gelöscht' : gb(e.rohBytes)} {e.faelligAm.length > 0 && ` · fällig ${datumKurz(e.faelligAm)}`} {' · '} Fertig:{' '} {e.phase === 'fertig' ? ( {e.fertigBytes > 0 ? `${gb(e.fertigBytes)} · ` : ''} {e.zielOrdner} {!e.komprimiert && ' (verlustfrei, ohne Kompression)'} ) : ( '—' )}

{e.fehler.length > 0 &&

⚠ {e.fehler}

}
{e.neuKomprimierbar && ( <> )} {!e.rohGeloescht && ruhig && ( )} {e.phase === 'fertig' && e.zielOrdner.length > 0 && ( )} {(e.phase === 'wartet' || e.phase === 'komprimiert' || e.phase === 'legt-ab') && ( )}
) })}
{liste.length > sichtbar.length && ( )} {v !== null && v.fremde.length > 0 && (

ORDNER OHNE VORGANG

Aus der Zeit vor 5.4.0 oder von Hand hineingelegt — Rippy kennt sie nicht und löscht sie deshalb NIE von selbst.

{v.fremde.map((f) => (
{f.ordner} · {gb(f.bytes)}
))}
)}
) } export function MetadatenKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen return (
setzen('tmdbApiKey', w)} />

www.themoviedb.org → Einstellungen → API

setzen('omdbApiKey', w)} />

www.omdbapi.com/apikey.aspx

Ohne Schlüssel rippt Rippy trotzdem — die Filme heißen dann wie das Disc-Label, und „Ändern …" an der Laufwerks-Kachel bleibt ohne Suche.

) } export function MakemkvKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen const s = verbindung.schluessel const b = verbindung.beschaffung return (
{s === null ? (

Schlüssel wird geprüft …

) : ( <>

{s.angenommen === true ? 'SCHLÜSSEL ANGENOMMEN' : s.weiterBetrieb ? 'BETA-MODUS — RIPS LAUFEN' : 'AUSFALL'} {s.version.length > 0 && MakeMKV {s.version}}

{s.text}

zuletzt geprüft: {s.geholtAm.slice(0, 16).replace('T', ' ')} {s.gueltigBis.length > 0 && ` · Key gültig bis ${s.gueltigBis}`}

)}
{s !== null && s.angenommen === false && ( )}
{b !== null && (

{b.text}

{b.laeuft && b.anteil !== null && (
)}
)} setzen('makemkvAppKey', w)} /> ) } export function MedienserverKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen return (
setzen('mediaServerUrl', w)} /> setzen('mediaServerApiKey', w)} />

Bei Plex benennt Rippy nur nach dessen Regeln — angestoßen wird dort nichts. Die Ordnerstruktur aus „Ablage" passt für alle vier.

) } export function BenachrichtigungKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen return ( setzen('benachrichtigungen', an ? 'an' : 'aus')} label="Benachrichtigungen verschicken" /> setzen('discordWebhook', w)} />
setzen('telegramToken', w)} /> setzen('telegramChatId', w)} />
) } export function AllgemeinKarte({ verbindung, haupt }: { verbindung: Verbindung; haupt: HauptStatus | null }) { const werte = verbindung.einstellungen const kern = verbindung.kern const db = kern?.datenbank ?? null const dbText = db === null ? '—' : db.ok ? db.pfad : `FEHLER — ${db.fehler}` return ( void window.rippy.autostartSetzen(an)} label="Rippy mit Windows starten (Autostart im Benutzerkonto — kein Dienst, keine Adminrechte)" />
{(werte['arbeitsordner'] ?? '').length > 0 && ( )}

Rippy v{haupt?.version ?? '…'} {kern !== null && ` · Kern: Node ${kern.nodeVersion} (PID ${kern.pid})`} {haupt !== null && ` · Leine ${haupt.leine.ok ? 'gesetzt' : 'NICHT gesetzt'}`} {verbindung.pongLatenzMs !== null && ` · Antwortzeit ${verbindung.pongLatenzMs} ms`}

Datenbank: {dbText}

Protokoll: {haupt?.protokollOrdner ?? '—'}

) } export function UpdateKarte({ verbindung, haupt }: { verbindung: Verbindung; haupt: HauptStatus | null }) { // § 6.8: Der Prüf-Knopf ist während eines Rips aus — nicht, weil er // nicht könnte, sondern weil Rippy dann bewusst nicht sucht. const ripLaeuftGerade = Object.values(verbindung.rips).some((r) => LAUFENDE_PHASEN.includes(r.phase)) return ( {/* Das Feld „Update-Adresse" ist am 01.09.2026 auf Zuruf des Commanders verschwunden: Updates kommen immer vom Gitea, ein Eingabefeld dafür war nur Ballast. Der Einstellungs-Schlüssel `updateUrl` gilt weiterhin — wer ihn wirklich braucht, setzt ihn in der Datenbank, und § 6.8 prüft ihn wie zuvor (nur https://). */} {haupt !== null && (

Stand: {haupt.update.text}

{haupt.update.letzteErfolgreichePruefung.length > 0 &&

letzte erfolgreiche Prüfung: {haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}

} {haupt.update.neueVersion.length > 0 &&

Version {haupt.update.neueVersion} gefunden — die Karte in der Übersicht zeigt, was neu ist.

}
)}
) } function WerkzeugZeile({ name, pfad, fehlt, pflicht, schluessel, wert, }: { name: string pfad: string | null fehlt: string pflicht: boolean schluessel: string wert: string }) { return (

0 ? 'text-gruen' : pflicht ? 'text-rot' : 'text-warn'}`}> {name}: {pfad === null ? 'wird gesucht …' : pfad.length > 0 ? pfad : fehlt}

setzen(schluessel, w)} />
) } export function WerkzeugeKarte({ verbindung }: { verbindung: Verbindung }) { const werte = verbindung.einstellungen const backends = verbindung.werkzeuge?.backends ?? [] return ( {verbindung.werkzeuge !== null && (

HandBrake gemessen: {verbindung.werkzeuge.presets.length} Presets · Encoder: {backends.length > 0 ? backends.join(', ') : '—'}

)}
) }