From 638edc6a9f7b2eed73ecc9600e844dbc1033c662 Mon Sep 17 00:00:00 2001
From: Hitonabi
Date: Sun, 26 Jul 2026 12:58:42 +0200
Subject: [PATCH] feat(ui): Presets vom Worker, Warnung vor unerreichbaren
Pfaden, Browser weg
Vier Commander-Punkte auf einmal, alle am selben Ort.
Presets (Punkt 1 + 3b): Die drei Auswahllisten in Einstellungen kommen jetzt
vom Worker statt aus dieser Datei, gefiltert auf die passende Aufloesung - die
4K-Liste bietet keine 1080p-Presets mehr als Normalfall an (genau dieser Griff
rechnete in v3.12 eine 4K-UHD auf 1080p herunter; bewusstes Verkleinern steht
jetzt in einer eigenen, benannten Gruppe). Knopf "Bestes waehlen" uebernimmt die
Empfehlung fuer alle drei Disc-Typen, und unter jeder Liste steht, WARUM. Ein
gespeicherter Wert, den der Worker nicht kennt, wird benannt statt verschluckt -
so faellt eine falsche Bestandseinstellung ueberhaupt auf.
Der Wizard entscheidet nicht mehr selbst, sondern nimmt dieselbe Empfehlung.
Damit gibt es nur noch EINE Stelle mit dieser Logik, und die hat Tests.
Warnung vor dem Start (Punkt 6 des Savepoints): Waehlt man einen externen
Encoder, der Quelle oder Ziel nicht erreicht, steht das JETZT im Rip-Dialog -
nicht erst nach einer Stunde Rip. Geprueft wird mit derselben Regel, die
pfad_lokal() im Worker anwendet. Bei "Automatisch" wird genannt, welcher Worker
die Aufgabe kaputtmachen koennte: die geteilte Queue nimmt den ersten freien.
Datei-Browser (Punkt 7): "Warum wird hier der Datei Browser noch angezeigt - das
ist doch quatsch." Er ist nicht wirklich redundant (nur ueber ihn geht ein
freier Zielordner fuer EINEN Rip), aber er ueberschrieb die Schnellwahl
stillschweigend. Jetzt eingeklappt - wer ihn aufklappt, entscheidet bewusst, und
/browse wird erst dann geholt. Nebenbefund: browseFiles wurde geladen und NIE
angezeigt, samt ungenutztem File-Icon - beides raus.
Co-Authored-By: Claude Opus 5
---
docker/ui/src/components/FirstRunWizard.tsx | 68 +++++--
docker/ui/src/components/RipTargetModal.tsx | 135 +++++++++----
docker/ui/src/lib/encoder.ts | 85 ++++++++
docker/ui/src/pages/Settings.tsx | 205 ++++++++++++++++----
4 files changed, 393 insertions(+), 100 deletions(-)
diff --git a/docker/ui/src/components/FirstRunWizard.tsx b/docker/ui/src/components/FirstRunWizard.tsx
index ac157b0..31e364d 100644
--- a/docker/ui/src/components/FirstRunWizard.tsx
+++ b/docker/ui/src/components/FirstRunWizard.tsx
@@ -37,24 +37,32 @@ const ENCODER_LABELS: Record = {
// braucht roh rund 40 GB, eine 4K-UHD bis 100 GB.
const PLATZ_WARNUNG_GB = 60
-/** Was beim Abschliessen gespeichert wird — abhängig davon, was die Maschine kann. */
-function presetsFuer(schwach: boolean) {
- if (schwach) {
- // Gemessen auf einer CPU ohne AVX2: 4K-HEVC = 28-55 h pro Film. Also 4K
- // verlustfrei behalten und für den Rest H.264 nehmen (deutlich schneller
- // als H.265, dafür etwas größer).
- return {
- transcodePreset: 'HQ 1080p30 Surround',
- transcodePresetBluray: 'HQ 1080p30 Surround',
- transcodePresetDvd: 'HQ 576p25 Surround',
- transcodePresetUhd: PRESET_KEINE,
- }
+// Antwort von GET /presets — dieselbe Quelle, die auch die Einstellungen-Seite
+// benutzt. Bis 26.07.2026 entschied der Wizard hier selbst und mit fest
+// verdrahteten Namen; jetzt kommt beides aus api/presets.py, wo es Tests hat.
+interface PresetUebersicht {
+ quelle: 'worker' | 'rueckfall'
+ hardware: string[]
+ typen: Record<'uhd' | 'bluray' | 'dvd', {
+ empfehlung: { preset: string, grund: string, gefunden: boolean }
+ }>
+}
+
+/** Was beim Abschliessen gespeichert wird — die Empfehlung der API. */
+function presetsFuer(uebersicht: PresetUebersicht | null) {
+ const nimm = (typ: 'uhd' | 'bluray' | 'dvd') => {
+ const e = uebersicht?.typen?.[typ]?.empfehlung
+ return e?.gefunden ? e.preset : ''
}
+ const uhd = nimm('uhd')
+ const bluray = nimm('bluray')
+ const dvd = nimm('dvd')
+ // Leere Werte gar nicht schreiben: dann bleibt der Bestandswert stehen,
+ // statt dass HandBrake später ein leeres --preset bekommt.
return {
- transcodePreset: 'H.265 MKV 1080p30',
- transcodePresetBluray: 'H.265 MKV 1080p30',
- transcodePresetDvd: 'H.265 MKV 576p25',
- transcodePresetUhd: 'H.265 MKV 2160p60 4K',
+ ...(bluray ? { transcodePreset: bluray, transcodePresetBluray: bluray } : {}),
+ ...(dvd ? { transcodePresetDvd: dvd } : {}),
+ ...(uhd ? { transcodePresetUhd: uhd } : {}),
}
}
@@ -66,6 +74,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
const [workers, setWorkers] = useState([])
const [geraete, setGeraete] = useState([])
const [plaetze, setPlaetze] = useState([])
+ const [presetInfo, setPresetInfo] = useState(null)
const [geladen, setGeladen] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
@@ -77,15 +86,17 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
useEffect(() => {
let laeuft = true
const laden = async () => {
- const [caps, devs, info] = await Promise.allSettled([
+ const [caps, devs, info, pres] = await Promise.allSettled([
api.get('/capabilities'),
api.get('/devices'),
api.get('/system/info'),
+ api.get('/presets'),
])
if (!laeuft) return
if (caps.status === 'fulfilled') setWorkers(caps.value.data?.workers || [])
if (devs.status === 'fulfilled') setGeraete(devs.value.data || [])
if (info.status === 'fulfilled') setPlaetze(info.value.data?.plaetze || [])
+ if (pres.status === 'fulfilled') setPresetInfo(pres.value.data || null)
setGeladen(true)
}
laden()
@@ -94,7 +105,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
}, [])
const schwach = schwacheEncoderCpu(workers)
- const presets = presetsFuer(schwach)
+ const presets = presetsFuer(presetInfo)
const encoderWorker = workers.filter(w => w.encoders.length > 0)
const platz = plaetze.length > 0 ? Math.min(...plaetze.map(p => p.frei_gb)) : null
@@ -306,6 +317,10 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
{' '}dauern. Deshalb wird eingestellt:
Alles einzeln änderbar unter Einstellungen → Verarbeitung.
diff --git a/docker/ui/src/components/RipTargetModal.tsx b/docker/ui/src/components/RipTargetModal.tsx
index be8a591..9bee217 100644
--- a/docker/ui/src/components/RipTargetModal.tsx
+++ b/docker/ui/src/components/RipTargetModal.tsx
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react'
-import { Folder, FolderOpen, File, ArrowUp, CheckCircle, Film, Tv, Music, Cpu, HardDrive } from 'lucide-react'
+import { Folder, FolderOpen, ArrowUp, CheckCircle, Film, Tv, Music, Cpu, HardDrive, ChevronRight, ChevronDown } from 'lucide-react'
import { api } from '../lib/api'
import { Modal } from './ui/Modal'
import { Button } from './ui/Button'
import { Input, Select } from './ui/Input'
+import { automatikWarnung, externWarnung } from '../lib/encoder'
interface TargetConfig {
id: string
@@ -35,6 +36,10 @@ interface WorkerWahl {
node: string | null
online?: boolean
encoders: string[]
+ // extern: 'ja' = läuft AUSSERHALB des Rippy-Containers und erreicht die
+ // Container-Pfade nur über eine Freigabe plus pfad_map. Der Worker meldet
+ // beides selbst (worker/caps.py).
+ info?: { extern?: string, pfad_map?: string }
}
const ENCODER_KURZ: Record = {
@@ -120,8 +125,16 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
const [browsePath, setBrowsePath] = useState('/app/media')
const [browseParent, setBrowseParent] = useState(null)
const [browseDirs, setBrowseDirs] = useState([])
- const [browseFiles, setBrowseFiles] = useState<{ name: string, size_mb: number | null }[]>([])
const [customPath, setCustomPath] = useState('')
+ // Der Ordner-Browser ist EINGEKLAPPT (Commander 26.07.2026: „Warum wird hier
+ // der Datei Browser noch angezeigt — das ist doch quatsch"). Er ist nicht
+ // wirklich redundant: nur über ihn lässt sich für DIESEN einen Rip ein
+ // beliebiger Zielordner wählen. Aber der Normalfall ist die Schnellwahl
+ // darüber, und der Browser überschrieb sie stillschweigend, sobald man auf
+ // „Diesen Ordner nutzen" klickte. Jetzt muss man ihn aufklappen — dann ist
+ // die Wahl bewusst. Nebenbefund beim Aufräumen: `browseFiles` wurde geladen
+ // und NIE angezeigt (samt ungenutztem File-Icon) — beides entfernt.
+ const [browserOffen, setBrowserOffen] = useState(false)
// Arbeitsverzeichnis dieses Rips (Commander-Wunsch 25.07.2026: hier wählbar,
// nicht global vorgegeben). '' = der Wert aus den Einstellungen, der auch
// bei Vollautomatik-Rips gilt, weil dort niemand gefragt wird.
@@ -141,6 +154,7 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
setGewaehlt(new Set())
setEncoderNode('')
setArbeitsDir('')
+ setBrowserOffen(false)
api.get('/storage-targets')
.then(r => setArbeitsZiele(Array.isArray(r.data) ? r.data : []))
.catch(() => setArbeitsZiele([]))
@@ -159,7 +173,8 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
{ id: '3', name: 'Musik', path: `${basis}/${s.musicDir || 'music'}`, type: 'music', isActive: true },
])
}).catch(() => {})
- laden('/app/media')
+ // /browse wird erst beim Aufklappen geholt — der Dialog braucht es im
+ // Normalfall gar nicht.
}, [isOpen])
const laden = async (pfad: string) => {
@@ -168,13 +183,28 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
setBrowsePath(r.data.path)
setBrowseParent(r.data.parent)
setBrowseDirs(r.data.dirs)
- setBrowseFiles(r.data.files || [])
} catch {
setBrowseDirs([])
- setBrowseFiles([])
}
}
+ // Der effektive Zielpfad dieses Rips — Schnellwahl oder eigener Ordner.
+ const zielPfad = customPath || targets.find(t => t.type === selectedType)?.path || ''
+ // Das effektive Arbeitsverzeichnis: Wahl für diesen Rip → Einstellung →
+ // leer (= Container-Platte /app/temp, für externe Worker unerreichbar).
+ const arbeitsPfadEffektiv = arbeitsDir || standardArbeitsDir
+ // Warnung VOR dem Start, wenn der gewählte externe Encoder die Pfade nicht
+ // erreicht (Punkt 6 des Savepoints v3.16). Am 26.07.2026 fiel genau das erst
+ // NACH dem Rip auf, weil nur der Worker selbst prüfte.
+ const gewaehlterWorker = workers.find(w => w.node === encoderNode)
+ const pfadWarnung = selectedType === 'music'
+ ? null
+ : encoderNode
+ ? externWarnung(gewaehlterWorker, zielPfad, arbeitsPfadEffektiv)
+ // „Automatisch": die geteilte Queue nimmt den ersten freien Worker — auch
+ // einen, der die Pfade nicht erreicht.
+ : automatikWarnung(workers, zielPfad, arbeitsPfadEffektiv)
+
const handleSave = () => {
const optionen: RipOptionen = { mainFeatureOnly: nurHauptfilm }
if (selectedType === 'series' && serienName.trim()) {
@@ -325,6 +355,16 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
)}
+ {/* Die Warnung, die am 26.07.2026 gefehlt hat: Der gewählte externe
+ Encoder nahm die Aufgabe an und lehnte sie 182 ms später ab, weil er
+ die Container-Pfade nicht sieht. Sichtbar war das erst NACH dem Rip. */}
+ {pfadWarnung && (
+
diff --git a/docker/ui/src/lib/encoder.ts b/docker/ui/src/lib/encoder.ts
index 4e301ca..ee59a52 100644
--- a/docker/ui/src/lib/encoder.ts
+++ b/docker/ui/src/lib/encoder.ts
@@ -33,6 +33,91 @@ interface WorkerFuerBewertung {
info?: { cpu_simd?: string }
}
+// --- Erreicht ein EXTERNER Worker die Pfade? ---------------------------------
+//
+// Befund 26.07.2026, live am Job 95afdc89: Das Routing an den Windows-PC des
+// Commanders funktionierte einwandfrei — der Worker nahm die Aufgabe an und
+// lehnte sie 182 ms später ab, weil `/app/media/...` Pfade INNERHALB des
+// Rippy-Containers sind. Ohne RIPPY_PATH_MAP sieht ein externer Worker sie nie.
+// Auffallen durfte das nicht erst nach einer Stunde Rip, sondern hier.
+
+interface WorkerFuerPfadPruefung {
+ name: string
+ node?: string | null
+ info?: { extern?: string, pfad_map?: string }
+}
+
+/**
+ * Deckt `RIPPY_PATH_MAP` diesen Container-Pfad ab? Gleiche Regel wie
+ * `pfad_lokal()` im Worker (docker/worker/tasks.py): Paare `quelle=ziel`,
+ * getrennt durch `;`, Treffer per Präfix.
+ */
+export function pfadAbgedeckt(pfad: string, mapping: string): boolean {
+ if (!pfad || !mapping) return false
+ return mapping.split(';').some(paar => {
+ const quelle = paar.split('=', 1)[0]
+ return !!quelle && pfad.startsWith(quelle)
+ })
+}
+
+/**
+ * Warnt, BEVOR gerippt wird, wenn der gewählte Encoder die Pfade nicht
+ * erreichen kann. null = alles in Ordnung (oder der Worker läuft im Container,
+ * dann gibt es nichts zu übersetzen).
+ *
+ * `arbeitsPfad` leer heißt: Container-Standard /app/temp/raw — ein Docker-Volume,
+ * an das ein externer Worker grundsätzlich nicht herankommt.
+ */
+export function externWarnung(
+ worker: WorkerFuerPfadPruefung | undefined,
+ zielPfad: string,
+ arbeitsPfad: string,
+): string | null {
+ if (!worker || worker.info?.extern !== 'ja') return null
+ const mapping = (worker.info?.pfad_map || '').trim()
+ if (!mapping) {
+ return `„${worker.name}" läuft auf einer anderen Maschine und hat keine `
+ + 'Pfad-Übersetzung (RIPPY_PATH_MAP). Er kann die Rohdaten deshalb nicht '
+ + 'finden und würde sofort abbrechen. Abhilfe: den Windows-Installer erneut '
+ + 'ausführen — er holt den Wert jetzt selbst von Rippy. Bis dahin besser '
+ + '„Automatisch" wählen.'
+ }
+ if (!arbeitsPfad) {
+ return `„${worker.name}" arbeitet auf einer anderen Maschine. Das `
+ + 'Arbeitsverzeichnis steht auf der Container-Platte (/app/temp) — die ist '
+ + 'von außen nicht erreichbar. Oben eine Netzwerk-Freigabe als '
+ + 'Arbeitsverzeichnis wählen.'
+ }
+ const fehlt: string[] = []
+ if (!pfadAbgedeckt(arbeitsPfad, mapping)) fehlt.push(`Rohdaten (${arbeitsPfad})`)
+ if (!pfadAbgedeckt(zielPfad, mapping)) fehlt.push(`Ziel (${zielPfad})`)
+ if (fehlt.length === 0) return null
+ return `„${worker.name}" erreicht ${fehlt.join(' und ')} nicht — seine `
+ + `Übersetzung deckt nur „${mapping}" ab. Beides muss auf einer Freigabe `
+ + 'liegen, die diese Maschine sieht, sonst scheitert die Kompression '
+ + '(die Rohdaten bleiben dabei erhalten).'
+}
+
+/**
+ * Dasselbe Risiko bei „Automatisch": dort nimmt die geteilte Queue den ersten
+ * freien Worker — auch einen externen, der die Pfade nicht erreicht. Nennt die
+ * betroffenen Namen, damit man gezielt einen anderen wählt.
+ */
+export function automatikWarnung(
+ workers: WorkerFuerPfadPruefung[] | undefined,
+ zielPfad: string,
+ arbeitsPfad: string,
+): string | null {
+ const betroffen = (workers || [])
+ .filter(w => externWarnung(w, zielPfad, arbeitsPfad) !== null)
+ .map(w => w.name)
+ if (betroffen.length === 0) return null
+ return `Bei „Automatisch" nimmt die Aufgabe der erste freie Worker — das kann `
+ + `${betroffen.join(', ')} sein, und dort scheitert sie an den Pfaden. `
+ + 'Wähle oben gezielt eine Maschine, die die Freigabe erreicht — oder rüste '
+ + 'die Pfad-Übersetzung nach (Windows-Installer erneut ausführen).'
+}
+
/**
* Kann von den gemeldeten Workern KEINER 4K in Software brauchbar schnell
* encodieren? Wahr nur, wenn das auch belegt ist: mindestens ein Worker hat
diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx
index 78bd373..2963c2b 100644
--- a/docker/ui/src/pages/Settings.tsx
+++ b/docker/ui/src/pages/Settings.tsx
@@ -127,6 +127,37 @@ interface SystemInfo {
webhook_gesetzt: boolean
}
+// Antwort von GET /presets — die Preset-Namen kommen aus
+// `HandBrakeCLI --preset-list` des jeweiligen Workers, die Empfehlung samt
+// Begründung aus api/presets.py (dort getestet). Bis 26.07.2026 standen die
+// Namen fest verdrahtet in dieser Datei; sie unterscheiden sich aber zwischen
+// HandBrake-Versionen, und ein Name, den das HandBrake des Workers nicht kennt,
+// lässt die Kompression scheitern.
+interface PresetEmpfehlung {
+ preset: string
+ grund: string
+ gefunden: boolean
+}
+
+interface PresetTyp {
+ empfehlung: PresetEmpfehlung
+ auswahl: { name: string, verkleinert: boolean }[]
+}
+
+interface PresetUebersicht {
+ quelle: 'worker' | 'rueckfall'
+ hardware: string[]
+ simd_schnell: boolean
+ typen: Record<'uhd' | 'bluray' | 'dvd', PresetTyp>
+}
+
+// Welches Einstellungs-Feld gehört zu welchem Disc-Typ.
+const PRESET_FELD: Record<'uhd' | 'bluray' | 'dvd', keyof SettingsState> = {
+ uhd: 'transcodePresetUhd',
+ bluray: 'transcodePresetBluray',
+ dvd: 'transcodePresetDvd',
+}
+
// Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB groß.
function bytesLesbar(bytes: number): string {
if (!bytes) return '0 B'
@@ -171,6 +202,8 @@ export default function SettingsPage() {
const [keystore, setKeystore] = useState(null)
const [keystoreBusy, setKeystoreBusy] = useState(false)
const keystoreInput = useRef(null)
+ // Preset-Namen + Empfehlung, beides von der API (siehe PresetUebersicht).
+ const [presetInfo, setPresetInfo] = useState(null)
const { toast } = useToast()
@@ -291,8 +324,90 @@ export default function SettingsPage() {
api.get('/system/aacs-dumps')
.then(r => setDumps(r.data?.dumps || []))
.catch(() => setDumps([]))
+ api.get('/presets')
+ .then(r => setPresetInfo(r.data))
+ .catch(() => setPresetInfo(null))
}, [])
+ // „Bestes automatisch wählen" (Commander-Anforderung 26.07.2026). Setzt alle
+ // drei Disc-Typen auf die Empfehlung der API — und nur solche, die wirklich
+ // gefunden wurden, damit nie ein Name landet, den HandBrake nicht kennt.
+ const bestePresetsUebernehmen = () => {
+ if (!presetInfo) return
+ const neu: Partial = {}
+ const uebernommen: string[] = []
+ for (const typ of ['uhd', 'bluray', 'dvd'] as const) {
+ const e = presetInfo.typen[typ]?.empfehlung
+ if (!e?.gefunden || !e.preset) continue
+ neu[PRESET_FELD[typ]] = e.preset as any
+ uebernommen.push(`${typ.toUpperCase()}: ${e.preset === PRESET_KEINE ? 'nicht komprimieren' : e.preset}`)
+ }
+ if (uebernommen.length === 0) {
+ toast('error', 'Kein Preset übernommen — die Worker melden keine bekannten Namen.')
+ return
+ }
+ setSettings(prev => ({ ...prev, ...neu }))
+ setSaved(false)
+ toast('success', `Übernommen — ${uebernommen.join(' · ')}. Noch speichern!`)
+ }
+
+ // Eine Preset-Auswahl je Disc-Typ. Die Optionen kommen vom WORKER (siehe
+ // PresetUebersicht), nicht aus dieser Datei. Der aktuell gespeicherte Wert
+ // steht immer mit drin — sonst würde eine Bestandseinstellung beim Öffnen der
+ // Seite still auf etwas anderes springen.
+ const presetAuswahl = (
+ typ: 'uhd' | 'bluray' | 'dvd', label: string, keineText: string,
+ ) => {
+ const feld = PRESET_FELD[typ]
+ const wert = (settings[feld] as string) || ''
+ const daten = presetInfo?.typen?.[typ]
+ const empfehlung = daten?.empfehlung
+ const passend = (daten?.auswahl || []).filter(o => !o.verkleinert)
+ const kleiner = (daten?.auswahl || []).filter(o => o.verkleinert)
+ const bekannt = wert === PRESET_KEINE
+ || (daten?.auswahl || []).some(o => o.name === wert)
+ return (
+
+ )
+ }
+
const webhookTesten = async () => {
if (!settings.notificationWebhook) return
setTestBusy(true)
@@ -637,28 +752,54 @@ export default function SettingsPage() {
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.
+
+ Seit 26.07.2026 kommen die NAMEN vom Worker selbst
+ (`HandBrakeCLI --preset-list`, siehe worker/caps.py) und die
+ Empfehlung von GET /presets. Vorher standen sie hier fest
+ verdrahtet — sie unterscheiden sich aber je HandBrake-Version,
+ und ein Name, den das HandBrake des Workers nicht kennt, lässt
+ die Kompression scheitern.
*/}
-
- 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.
-
+
+
+ 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.
+
+
+
-
+ {presetInfo && (
+
+ {presetInfo.quelle === 'worker' ? (
+ <>
+ Die Liste kommt von deinen Workern selbst.
+ {presetInfo.hardware.length > 0
+ ? ` Hardware-Encoder gemeldet: ${presetInfo.hardware.join(', ')} — damit geht 4K in Minuten statt Tagen.`
+ : presetInfo.simd_schnell
+ ? ' Kein Hardware-Encoder, aber ein Worker mit AVX2 — Software-H.265 ist damit schnell genug.'
+ : ' Kein Hardware-Encoder und kein AVX2 — hier ist Software-4K keine Option (gemessen: 28–55 h je Film).'}
+ >
+ ) : (
+ <>
+ Kein Worker meldet gerade eine Preset-Liste (alter Worker-Stand oder
+ keiner online) — angezeigt wird deshalb eine geprüfte Standardauswahl.
+ >
+ )}
+
+ )}
+
+ {presetAuswahl('uhd', 'Preset für 4K-UHD',
+ 'Nicht komprimieren — 4K verlustfrei behalten')}
{/* 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). */}
@@ -673,28 +814,10 @@ export default function SettingsPage() {
)}
-
-
-
+ {presetAuswahl('bluray', 'Preset für Blu-ray',
+ 'Nicht komprimieren — verlustfrei behalten')}
+ {presetAuswahl('dvd', 'Preset für DVD',
+ 'Nicht komprimieren — verlustfrei behalten')}
„Nicht komprimieren" heißt: Der Rip bleibt so, wie MakeMKV ihn