feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse

Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie
veröffentlicht wurde):

Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in
kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge,
Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts,
Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts.

Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/,
uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte),
einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/
(ErstEinrichtung, PosterHintergrund).

Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs);
LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown
statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update;
Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die
Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown;
Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen
Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange
pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag;
Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem
Poster-Grid.

Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung,
fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit
29 Schritten inklusive Erst-Einrichtung (zweiter Start mit
RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-02 17:51:15 +02:00
co-authored by Claude Fable 5.1
parent 6678c81592
commit 3dc6798c79
67 changed files with 6961 additions and 3950 deletions
@@ -0,0 +1,137 @@
// Einstellungen (§ 6.7): Das Fenster liest und schreibt NUR über den Kern;
// erlaubt sind genau die Schlüssel hier. Dazu die Pfad-Regeln: Ablage
// (Fertiges — beim Commander der Server) und seit 5.5.0 der Arbeitsordner
// (Roh-Dateien und Kompression — lokal, schnell; Vorgabe: unter der Ablage,
// also wie bisher).
import { join } from 'node:path'
import { AUFBEWAHRUNG_SCHLUESSEL } from '../ablage/rohaufraeumen'
import { einstellung, type KernKontext } from '../kontext'
export const ERLAUBTE_EINSTELLUNGEN: ReadonlySet<string> = new Set([
'ablage',
'arbeitsordner',
'transcodeEnabled',
'transcodePresetDvd',
'transcodePresetBluray',
'transcodePresetUhd',
'audioSprachen',
'untertitelSprachen',
'mediaServer',
'mediaServerUrl',
'mediaServerApiKey',
'tmdbApiKey',
'omdbApiKey',
'makemkvAppKey',
'updateUrl',
'setup.done',
'autoAuswurf',
'werkzeug.makemkv',
'werkzeug.handbrake',
'werkzeug.flac',
AUFBEWAHRUNG_SCHLUESSEL,
// 5.5.0
'automatik',
'automatikSekunden',
'benachrichtigungen',
'discordWebhook',
'telegramToken',
'telegramChatId',
])
/** Die Schlüssel, die Rip und Kompression lesen (Pipeline/Ablage). Das
* frühere „Allgemeine Preset" (transcodePreset) ist seit 5.5.0 raus —
* Commander-Wunsch: je Disc-Typ reicht, ein unsichtbarer Übersteuerer
* verwirrt nur. */
const PIPELINE_EINSTELLUNGEN = [
'transcodeEnabled',
'transcodePresetDvd',
'transcodePresetBluray',
'transcodePresetUhd',
'audioSprachen',
'untertitelSprachen',
'mediaServer',
'mediaServerUrl',
'mediaServerApiKey',
] as const
export const AUTOMATIK_SEKUNDEN_VORGABE = 30
export class EinstellungsDienst {
constructor(private readonly kontext: KernKontext) {}
lesen(schluessel: string, vorgabe = ''): string {
return einstellung(this.kontext, schluessel, vorgabe)
}
/** Wurzel der Ablage — Fertiges, Musik, ISO. */
ablageOrdner(): string {
const eingestellt = this.lesen('ablage')
if (eingestellt.length > 0) return eingestellt
return join(process.env['USERPROFILE'] ?? '.', 'Videos', 'Rippy')
}
/** Ein eigener Arbeitsordner — '' wenn keiner gesetzt ist. */
arbeitsordnerEigen(): string {
return this.lesen('arbeitsordner').trim()
}
/** Wo gearbeitet wird: der eigene Arbeitsordner, sonst die Ablage. */
arbeitsordner(): string {
const eigen = this.arbeitsordnerEigen()
return eigen.length > 0 ? eigen : this.ablageOrdner()
}
/** Wo die Roh-Ordner liegen. */
rohWurzel(): string {
return join(this.arbeitsordner(), 'roh')
}
/** Was Pipeline und Ablage brauchen. */
fuerPipeline(): Record<string, string> {
const werte: Record<string, string> = {}
for (const schluessel of PIPELINE_EINSTELLUNGEN) {
const wert = this.kontext.db?.einstellungLesen(schluessel)
if (wert !== null && wert !== undefined) werte[schluessel] = wert
}
return werte
}
automatikAn(): boolean {
return this.lesen('automatik', 'an') !== 'aus'
}
automatikSekunden(): number {
const n = Number(this.lesen('automatikSekunden', String(AUTOMATIK_SEKUNDEN_VORGABE)))
return Number.isInteger(n) && n >= 5 && n <= 600 ? n : AUTOMATIK_SEKUNDEN_VORGABE
}
/** Alle erlaubten Werte ans Fenster — AUCH ans Haupt (Update-Adresse, § 6.8). */
senden(): void {
const werte: Record<string, string> = { ablage: this.ablageOrdner() }
for (const schluessel of ERLAUBTE_EINSTELLUNGEN) {
const wert = this.kontext.db?.einstellungLesen(schluessel)
if (wert !== null && wert !== undefined) werte[schluessel] = wert
}
this.kontext.anHauptUndFenster({ art: 'einstellungen', werte })
}
/** Schreiben — nur erlaubte Schlüssel, nur mit Datenbank. Liefert, ob es klappte. */
setzen(schluessel: string, wert: string): boolean {
if (!ERLAUBTE_EINSTELLUNGEN.has(schluessel)) {
this.kontext.fehler(`Unbekannter Einstellungs-Schlüssel: ${schluessel}`)
return false
}
if (this.kontext.db === null) {
this.kontext.fehler('Einstellung nicht gespeichert — die Datenbank ist nicht da.')
return false
}
this.kontext.db.einstellungSchreiben(schluessel, wert)
this.kontext.protokoll.schreiben(`Einstellung ${schluessel} gesetzt${istGeheim(schluessel) ? '' : `: ${wert.slice(0, 120)}`}`)
this.senden()
return true
}
}
function istGeheim(schluessel: string): boolean {
return /key|token|webhook|passwort/i.test(schluessel)
}