4195854bf8
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>
246 lines
10 KiB
Python
246 lines
10 KiB
Python
"""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
|
|
},
|
|
}
|