feat(bausteine): Preset-Liste, Pfad-Vorschlag und Restzeit - alles gemessen

Vier neue reine Bausteine, jeder mit Tests. Sie beantworten Fragen, die Rippy
bisher geraten oder gar nicht gestellt hat.

1. caps.parse_preset_liste - die Preset-NAMEN, die das HandBrake DIESES Workers
   wirklich kennt (`--preset-list`, Format im Worker-Image gemessen). Damit
   endet das Raten: die Namen unterscheiden sich je HandBrake-Version, und ein
   erfundener Name laesst die Kompression scheitern.
   Richtigstellung zum SAVEPOINT v3.16: Dort galt es als unmoeglich, die
   Hardware-Preset-Namen auf der Rippy-VM zu ermitteln, weil dort kein
   Hardware-Encoder laeuft. Gemessen ist das falsch - die Kategorie `Hardware/`
   steht vollstaendig in der Liste (VCN, NVENC, QSV, MF). HandBrake trennt
   zwei Fragen: --preset-list nennt alle Presets, --help nur die nutzbaren
   Encoder. Zwei Fragen, zwei Quellen.

2. mounts.pfad_map_vorschlag/pfad_map_zeile - der fehlende Anschluss fuer
   RIPPY_PATH_MAP. Geraten werden muss dafuer nichts: Rippy hat die Freigabe
   selbst eingehaengt und kennt ihre Quelle (//host/share). Mountpunkt plus
   Quelle IST das Mapping. NFS gibt bewusst "" - Windows-Schreibweise ist nicht
   ableitbar (AGENTS Regel D).
   Der Test fand dabei sofort die dokumentierte Windows-Falle: os.path.join
   baute `/app/media\rippy` in einen Container-Pfad, das Mapping waere still
   wirkungslos geblieben. _mountpoint nutzt jetzt posixpath.

3. presets.empfehlung - "immer das Beste" (Commander-Anforderung) ohne
   Punktesystem: fuer jede Lage eine feste Reihenfolge echter Namen, genommen
   wird der erste, den der Worker kennt. Hardware nur, wenn die Familie
   wirklich gemeldet ist (vce -> Preset heisst VCN, sonst liefe eine AMD-Karte
   unter dem Intel-Namen). vaapi und MF werden nie empfohlen: fuer vaapi gibt
   es kein Preset, bei MF ist die Nutzbarkeit nicht ablesbar.

4. eta - Restzeit aus dem gemessenen Fortschritt. Messreihe je Phase (Rip und
   Kompression haben nichts miteinander zu tun), Stillstand verlaengert die
   Schaetzung, und unter zwei Messwerten oder 60 Sekunden Spanne gibt es
   ehrlich keine Aussage. Test mit den echten 25.07.-Werten: 1,44 % in 29 min
   landet bei "noch ca. 1 Tag 23 h" - genau die Angabe, deren Fehlen den
   50-Stunden-Lauf unsichtbar machte.

Dazu: HandBrakes "Invalid preset <Name>" wird uebersetzt. Vorher stand im UI
nur "HandBrake endete mit Code 3" - dass der Preset-NAME das Problem ist, war
daraus nicht zu erraten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 12:57:51 +02:00
parent a468e4b6a4
commit 4195854bf8
10 changed files with 1151 additions and 2 deletions
+245
View File
@@ -0,0 +1,245 @@
"""Welches HandBrake-Preset passt zu welcher Disc — und was ist das BESTE.
Commander-Anforderung (26.07.2026): *„Bei den Presets soll IMMER das Beste
ausgewählt werden"* und *„wenn der Worker AV1 oder noch besseres kann, immer
diesem empfehlen"*.
## Warum das hier liegt und nicht im UI
Die Preset-Namen standen bis dahin fest verdrahtet im UI (drei Listen in
Settings.tsx, eine vierte im Wizard). Zwei Probleme: die Namen unterscheiden
sich zwischen HandBrake-Versionen, und ein erfundener Name lässt die Kompression
scheitern — das Projekt hat genau das zweimal teuer bezahlt (AGENTS Regel D).
Jetzt meldet jeder Worker seine echte Liste (`worker/caps.py`,
`HandBrakeCLI --preset-list`), und die Auswahl entsteht hier: als reine
Funktionen, die die CI-Ampel prüft. Das UI zeigt nur noch an.
## Es wird nicht bewertet, sondern gestaffelt
Kein Punktesystem. Für jede Lage gibt es eine feste Reihenfolge von Namen, und
genommen wird der erste, den der Worker WIRKLICH kennt. Jeder Name unten ist am
26.07.2026 aus `--preset-list` des Worker-Images abgenommen (HandBrake 1.6.1).
Ein Punktesystem hätte über Namen geurteilt, die es vielleicht nicht gibt.
## Zwei Quellen, zwei Fragen
- `--preset-list` sagt, welche Presets es GIBT. Die Kategorie `Hardware/` steht
dort auch auf Maschinen ohne Hardware-Encoder.
- `--help` sagt, welche Encoder LAUFEN.
Deshalb wird ein Hardware-Preset nur empfohlen, wenn ein Worker die passende
Familie wirklich meldet. (Richtigstellung zum SAVEPOINT v3.16: dort galt es als
unmöglich, die Hardware-Preset-Namen auf der VM zu ermitteln — die Messung sagt
das Gegenteil, siehe caps.parse_preset_liste.)
"""
# Reservewert: „diesen Disc-Typ NICHT komprimieren". Gleichlautend in
# worker/ripping.PRESET_KEINE und ui/src/lib/encoder.ts — es gibt kein
# geteiltes Paket zwischen den drei Seiten.
PRESET_KEINE = "keine"
# Auflösungs-Token je Disc-Typ, so wie HandBrake sie in die Preset-Namen
# schreibt. Damit bietet die 4K-Auswahl keine 1080p-Presets als Normalfall an —
# genau dieser Griff hat in v3.12 eine 4K-UHD auf 1080p heruntergerechnet.
AUFLOESUNG_JE_TYP = {
"uhd": ("2160p",),
"bluray": ("1080p",),
"dvd": ("576p", "480p"),
}
# Hardware-Familie im ENCODER-Namen (aus `--help`) → Kürzel im PRESET-Namen.
# Gemessen: die Presets heißen „H.265 VCN 2160p 4K", der Encoder dazu aber
# `vce_h265`. Ohne diese Zuordnung bekäme eine AMD-Karte kein AMD-Preset.
# Reihenfolge = Vorzug, wenn eine Maschine mehrere Familien meldet.
HW_PRESET_KUERZEL = (
("qsv", "QSV"), # Intel QuickSync — in 1.6.1 die einzige mit AV1-Preset
("nvenc", "NVENC"), # NVIDIA
("vce", "VCN"), # AMD (Encoder heißt vce, Preset heißt VCN)
)
# Absichtlich NICHT dabei:
# - `vaapi`: HandBrake 1.6.1 liefert kein VAAPI-Preset mit (Kategorie
# `Hardware/` kennt nur QSV/NVENC/VCN/MF). Ein VAAPI-Worker fällt daher auf
# Software zurück, statt einen Namen zu bekommen, den es nicht gibt.
# - `MF` (Media Foundation): die Presets existieren, aber ob diese Maschine sie
# nutzen kann, ist aus der Encoder-Liste nicht ablesbar. Empfohlen wird MF
# deshalb nie; in der Auswahlliste steht es trotzdem.
# Vektorbefehls-Stufen, mit denen Software-Encoding brauchbar schnell ist.
# Gleichlautend in ui/src/lib/encoder.ts (SIMD_SCHNELL).
SIMD_SCHNELL = ("avx512f", "avx2")
# Rückfall, wenn KEIN Worker eine Preset-Liste meldet (alter Worker-Stand, oder
# gar kein Worker online). Alle vier Namen sind im Worker-Image gegengeprüft.
RUECKFALL_PRESETS = (
"H.265 MKV 2160p60 4K",
"H.265 MKV 1080p30",
"H.265 MKV 576p25",
"H.265 MKV 480p30",
"HQ 2160p60 4K HEVC Surround",
"HQ 1080p30 Surround",
"HQ 576p25 Surround",
"Super HQ 2160p60 4K HEVC Surround",
"Super HQ 1080p30 Surround",
)
def verfuegbare_presets(workers) -> list:
"""Alle Preset-Namen, die die gemeldeten Worker kennen (Vereinigung).
Vereinigung und nicht Schnittmenge: Die Kompression kann gezielt an EINEN
Worker geroutet werden (worker_direct), es muss also nicht jeder alles
können. Ob der gewählte Worker das Preset kennt, entscheidet er selbst —
und meldet es als Klartext-Fehler.
"""
gefunden = []
for w in workers or []:
for name in ((w.get("info") or {}).get("presets") or []):
if name and name not in gefunden:
gefunden.append(name)
return gefunden or list(RUECKFALL_PRESETS)
def hardware_kuerzel(workers) -> list:
"""Preset-Kürzel der Hardware-Encoder, die WIRKLICH gemeldet sind."""
vorhanden = set()
for w in workers or []:
for encoder in (w.get("encoders") or []):
vorhanden.add(str(encoder).lower())
kuerzel = []
for familie, kurz in HW_PRESET_KUERZEL:
if any(e == familie or e.startswith(familie + "-") for e in vorhanden):
kuerzel.append(kurz)
return kuerzel
def simd_schnell(workers) -> bool:
"""Kann mindestens ein Worker Software-Encoding brauchbar schnell?
Bei unbekannter Stufe wird NICHT geraten (ein Windows-Worker ohne die
Erkennung aus v3.16 meldet `unbekannt`) — dann zählt er hier nicht mit,
bremst aber auch niemanden aus.
"""
for w in workers or []:
if ((w.get("info") or {}).get("cpu_simd") or "") in SIMD_SCHNELL:
return True
return False
def _kandidaten(disc_type: str, kuerzel: list, schnell: bool) -> list:
"""Namens-Staffel für diese Lage — bester Kandidat zuerst.
Die Software-Zweige sind bewusst dieselben, die der Wizard seit v3.15
vorschlägt (dort schon geprüft): schwache CPU → 4K verlustfrei behalten und
H.264 für den Rest, starke CPU → H.265 durchgehend. Neu ist nur, dass
Hardware-Presets davor kommen.
"""
liste = []
# 1. Hardware zuerst — sie ist um Größenordnungen schneller. Auf der
# Rippy-VM brauchte Software-4K gemessene 28-55 Stunden pro Film.
for kurz in kuerzel:
if disc_type == "uhd":
liste += [f"AV1 {kurz} 2160p 4K", f"H.265 {kurz} 2160p 4K"]
elif disc_type == "bluray":
liste += [f"H.265 {kurz} 1080p"]
# Für DVD-Auflösungen liefert HandBrake 1.6.1 keine Hardware-Presets —
# eine DVD ist auch in Software in Minuten fertig.
# 2. Software.
if disc_type == "uhd":
liste += ["H.265 MKV 2160p60 4K"] if schnell else [PRESET_KEINE]
elif disc_type == "bluray":
liste += ["H.265 MKV 1080p30"] if schnell else ["HQ 1080p30 Surround"]
elif disc_type == "dvd":
liste += ["H.265 MKV 576p25"] if schnell else ["HQ 576p25 Surround"]
return liste
def _grund(preset: str, disc_type: str, kuerzel: list, schnell: bool) -> str:
"""Ein Satz, WARUM das die Empfehlung ist — der Commander liest das."""
if preset == PRESET_KEINE:
return (
"Keiner der gemeldeten Worker hat AVX2 oder einen Hardware-Encoder. "
"4K in Software dauert auf so einer Maschine gemessene 28-55 Stunden "
"pro Film — die verlustfreie Datei zu behalten ist hier die ehrliche "
"Wahl (kostet 20-100 GB)."
)
if any(f" {k} " in f" {preset} " for k in kuerzel):
return (
f"Nutzt den gemeldeten Hardware-Encoder ({', '.join(kuerzel)}) — "
"um Größenordnungen schneller als die CPU. Software-x265 wäre bei "
"gleicher Dateigröße etwas sauberer, dauert aber Stunden bis Tage."
)
if schnell:
return (
"H.265 in Software: kleinste Dateien bei sehr guter Qualität. Ein "
"Worker mit AVX2 ist dafür schnell genug (gemessen)."
)
return (
"H.264 statt H.265: ohne AVX2 ist H.265 sehr langsam. Die Datei wird "
"etwas größer, der Encode dafür um ein Mehrfaches schneller."
)
def empfehlung(disc_type: str, workers) -> dict:
"""Bestes Preset für diesen Disc-Typ — nur Namen, die es wirklich gibt.
Rückgabe: {"preset": str, "grund": str, "gefunden": bool}. `gefunden` ist
False, wenn keiner der bekannten Namen in der Liste des Workers steht (z. B.
eine HandBrake-Version mit anderer Benennung) — dann muss der Nutzer selbst
wählen, und das UI sagt es ihm, statt still etwas Falsches einzustellen.
"""
vorhanden = verfuegbare_presets(workers)
kuerzel = hardware_kuerzel(workers)
schnell = simd_schnell(workers)
for kandidat in _kandidaten(disc_type, kuerzel, schnell):
if kandidat == PRESET_KEINE or kandidat in vorhanden:
return {
"preset": kandidat,
"grund": _grund(kandidat, disc_type, kuerzel, schnell),
"gefunden": True,
}
return {
"preset": "",
"grund": (
"Dieses HandBrake kennt keines der Presets, die Rippy vorschlagen "
"kann — die Namen unterscheiden sich zwischen HandBrake-Versionen. "
"Bitte unten selbst eines aus der Liste des Workers wählen."
),
"gefunden": False,
}
def auswahl(disc_type: str, workers) -> list:
"""Die Presets, die für diesen Disc-Typ zur Wahl stehen — sortiert.
Gefiltert auf die passende Auflösung: Bei 4K sollen keine 1080p-Presets als
Normalfall in der Liste stehen, sonst passiert wieder, was in v3.12 passiert
ist (4K-Rip auf 1080p heruntergerechnet, weil ein globales Preset galt).
Das bewusste Verkleinern bleibt möglich — es steht am Ende, mit Hinweis.
"""
vorhanden = verfuegbare_presets(workers)
tokens = AUFLOESUNG_JE_TYP.get(disc_type, ())
passend = [p for p in vorhanden if any(t in p for t in tokens)]
liste = [{"name": p, "verkleinert": False} for p in sorted(passend)]
if disc_type == "uhd":
# Bewusstes Verkleinern auf 1080p — eigene Gruppe, damit niemand aus
# Versehen dort landet.
kleiner = [p for p in vorhanden if "1080p" in p]
liste += [{"name": p, "verkleinert": True} for p in sorted(kleiner)]
return liste
def uebersicht(workers) -> dict:
"""Alles, was das UI für die Preset-Auswahl braucht — in einem Aufruf."""
typen = ("uhd", "bluray", "dvd")
return {
"quelle": "worker" if verfuegbare_presets(workers) != list(RUECKFALL_PRESETS)
else "rueckfall",
"hardware": hardware_kuerzel(workers),
"simd_schnell": simd_schnell(workers),
"typen": {
t: {"empfehlung": empfehlung(t, workers), "auswahl": auswahl(t, workers)}
for t in typen
},
}