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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<KeystoreStatus | null>(null)
|
||||
const [keystoreBusy, setKeystoreBusy] = useState(false)
|
||||
const keystoreInput = useRef<HTMLInputElement>(null)
|
||||
// Preset-Namen + Empfehlung, beides von der API (siehe PresetUebersicht).
|
||||
const [presetInfo, setPresetInfo] = useState<PresetUebersicht | null>(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<SettingsState> = {}
|
||||
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 (
|
||||
<div className="space-y-1.5">
|
||||
<Select label={label} value={wert} onChange={(e) => handleChange(feld, e.target.value)}>
|
||||
<option value={PRESET_KEINE}>{keineText}</option>
|
||||
{/* Ein gespeicherter Wert, den dieser Worker nicht kennt, wird
|
||||
BENANNT statt verschluckt — genau so fällt eine falsche
|
||||
Bestandseinstellung überhaupt auf. */}
|
||||
{!bekannt && wert && (
|
||||
<option value={wert}>{wert} — gespeichert, dieser Worker kennt es nicht</option>
|
||||
)}
|
||||
{passend.length > 0 && (
|
||||
<optgroup label="Passt zur Auflösung">
|
||||
{passend.map(o => (
|
||||
<option key={o.name} value={o.name}>
|
||||
{o.name}{empfehlung?.gefunden && empfehlung.preset === o.name ? ' ★ empfohlen' : ''}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
{kleiner.length > 0 && (
|
||||
<optgroup label="Bewusst verkleinern — die 4K-Auflösung ist danach weg">
|
||||
{kleiner.map(o => <option key={o.name} value={o.name}>{o.name}</option>)}
|
||||
</optgroup>
|
||||
)}
|
||||
</Select>
|
||||
{empfehlung && empfehlung.preset !== wert && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{empfehlung.gefunden ? (
|
||||
<>
|
||||
Empfohlen wäre{' '}
|
||||
<strong className="text-slate-700 dark:text-slate-300">
|
||||
{empfehlung.preset === PRESET_KEINE ? 'nicht komprimieren' : empfehlung.preset}
|
||||
</strong>
|
||||
{' — '}{empfehlung.grund}
|
||||
</>
|
||||
) : empfehlung.grund}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
*/}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Ein Preset je Quelle — Rippy erkennt den Disc-Typ selbst und wählt passend aus.
|
||||
<strong className="text-slate-700 dark:text-slate-300"> Eine 4K-UHD gehört auf ein
|
||||
2160p-Preset</strong>, sonst rechnet HandBrake sie auf 1080p herunter und die
|
||||
4K-Auflösung ist weg.
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Ein Preset je Quelle — Rippy erkennt den Disc-Typ selbst und wählt passend aus.
|
||||
<strong className="text-slate-700 dark:text-slate-300"> Eine 4K-UHD gehört auf ein
|
||||
2160p-Preset</strong>, sonst rechnet HandBrake sie auf 1080p herunter und die
|
||||
4K-Auflösung ist weg.
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={bestePresetsUebernehmen}
|
||||
disabled={!presetInfo}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
Bestes wählen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Preset für 4K-UHD"
|
||||
value={settings.transcodePresetUhd}
|
||||
onChange={(e) => handleChange('transcodePresetUhd', e.target.value)}
|
||||
>
|
||||
<option value={PRESET_KEINE}>Nicht komprimieren — 4K verlustfrei behalten</option>
|
||||
<option value="H.265 MKV 2160p60 4K">H.265 MKV 2160p60 4K — 4K erhalten</option>
|
||||
<option value="HQ 2160p60 4K HEVC Surround">HQ 2160p60 4K HEVC Surround — 4K, mehr Qualität</option>
|
||||
<option value="Super HQ 2160p60 4K HEVC Surround">Super HQ 2160p60 4K HEVC Surround — 4K, größer</option>
|
||||
<option value="H.265 MKV 1080p30">H.265 MKV 1080p30 — bewusst auf 1080p verkleinern</option>
|
||||
</Select>
|
||||
{presetInfo && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{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.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{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() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label="Preset für Blu-ray"
|
||||
value={settings.transcodePresetBluray}
|
||||
onChange={(e) => handleChange('transcodePresetBluray', e.target.value)}
|
||||
>
|
||||
<option value={PRESET_KEINE}>Nicht komprimieren — verlustfrei behalten</option>
|
||||
<option value="H.265 MKV 1080p30">H.265 MKV 1080p30 — klein & modern (empfohlen)</option>
|
||||
<option value="HQ 1080p30 Surround">HQ 1080p30 Surround — H.264, maximale Kompatibilität</option>
|
||||
<option value="Super HQ 1080p30 Surround">Super HQ 1080p30 Surround — größer, beste Qualität</option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
label="Preset für DVD"
|
||||
value={settings.transcodePresetDvd}
|
||||
onChange={(e) => handleChange('transcodePresetDvd', e.target.value)}
|
||||
>
|
||||
<option value={PRESET_KEINE}>Nicht komprimieren — verlustfrei behalten</option>
|
||||
<option value="H.265 MKV 576p25">H.265 MKV 576p25 — PAL-DVD, native Auflösung (empfohlen)</option>
|
||||
<option value="H.265 MKV 480p30">H.265 MKV 480p30 — NTSC-DVD, native Auflösung</option>
|
||||
<option value="HQ 576p25 Surround">HQ 576p25 Surround — H.264, maximale Kompatibilität</option>
|
||||
<option value="H.265 MKV 1080p30">H.265 MKV 1080p30 — hochskalieren (bringt nichts)</option>
|
||||
</Select>
|
||||
{presetAuswahl('bluray', 'Preset für Blu-ray',
|
||||
'Nicht komprimieren — verlustfrei behalten')}
|
||||
{presetAuswahl('dvd', 'Preset für DVD',
|
||||
'Nicht komprimieren — verlustfrei behalten')}
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
„Nicht komprimieren" heißt: Der Rip bleibt so, wie MakeMKV ihn
|
||||
|
||||
Reference in New Issue
Block a user