"""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 }, }