"""Encoder-Fähigkeiten dieses Workers — ehrlich erkannt, nicht behauptet. Jeder Worker meldet beim Start, was auf SEINER Maschine wirklich verfügbar ist (Commander-Anforderung 23.07.: das UI zeigt an, WAS da ist). Ein Remote-GPU-Worker meldet sich hier genauso wie der eingebaute CPU-Worker. """ import os import platform import re import shutil import subprocess HB_ENCODER_KOPF = re.compile(r"^-e,\s*--encoder\b") # Reihenfolge = Anzeige-Reihenfolge im UI. SIMD_STUFEN = ("avx512f", "avx2", "avx", "sse4_2") def parse_encoder_liste(hilfe_text: str) -> list: """Video-Encoder, die DIESES HandBrake wirklich anbietet. Quelle: `HandBrakeCLI --help`, Abschnitt `-e, --encoder ` (AGENTS Regel D — im Worker-Image am 25.07.2026 gemessen, nicht geraten): -e, --encoder Select video encoder: svt_av1 x264 x265 ... --encoder-preset Der Block endet an der nächsten Option — also an der ersten Zeile, deren erstes nicht-leeres Zeichen ein Bindestrich ist. """ encoder = [] im_block = False for zeile in (hilfe_text or "").splitlines(): inhalt = zeile.strip() if not im_block: if HB_ENCODER_KOPF.match(inhalt): im_block = True continue if not inhalt: continue if inhalt.startswith("-"): break encoder.append(inhalt.split()[0]) return encoder def leite_backends_ab(hb_encoder, dri_da: bool, nvidia_da: bool) -> list: """Übersetzt die HandBrake-Encoder-Namen in die Backends, die das UI zeigt. Zwei Ehrlichkeits-Regeln, die vorher fehlten (Befund 25.07.2026): * Ohne HandBrake gibt es KEINE Encoder. Vorher standen `cpu-x264` und `cpu-x265` fest verdrahtet in der Liste („immer dabei") — ein reiner Rip-Worker ohne HandBrake behauptete damit, komprimieren zu können. * Hardware zählt nur, wenn das Gerät da ist UND HandBrake den Encoder überhaupt kennt. Vorher genügte `/dev/dri` — das Worker-Image kann laut eigener Auskunft aber gar keinen Hardware-Encoder. """ namen = {e.lower() for e in (hb_encoder or [])} gefunden = [] if any(n.startswith("x264") for n in namen): gefunden.append("cpu-x264") if any(n.startswith("x265") for n in namen): gefunden.append("cpu-x265") if any(n.startswith("svt_av1") for n in namen): gefunden.append("cpu-av1") if dri_da and any(("qsv" in n) or ("vce" in n) or ("vaapi" in n) for n in namen): gefunden.append("vaapi") if nvidia_da and any("nvenc" in n for n in namen): gefunden.append("nvenc") return gefunden def beste_simd(flags_text: str) -> str: """Höchste Vektor-Befehlsstufe der CPU — entscheidend fürs Software-Encodieren. Befund 25.07.2026: Die Rippy-VM läuft auf dem generischen QEMU-CPU-Modell („QEMU Virtual CPU version 2.5+") und hat deshalb KEIN AVX2, nur sse4_2. x265 lebt von diesen Befehlen; ein 4K-Encode brauchte dort gemessene 28-55 Stunden. Sichtbar war das nirgends — genau diese Lücke schließt die Anzeige. Abhilfe bei Proxmox/KVM: CPU-Typ der VM auf `host` stellen. """ vorhanden = set((flags_text or "").split()) for stufe in SIMD_STUFEN: if stufe in vorhanden: return stufe return "unbekannt" def _lies_cpuinfo() -> tuple: """(Modellname, Flags-Text, Kernzahl) aus /proc/cpuinfo — leer auf Windows.""" modell, flags, kerne = "", "", 0 try: with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as f: for zeile in f: if zeile.startswith("processor"): kerne += 1 elif not modell and zeile.startswith("model name"): modell = zeile.split(":", 1)[-1].strip() elif not flags and zeile.startswith("flags"): flags = zeile.split(":", 1)[-1].strip() except OSError: pass return modell, flags, kerne def hole_handbrake_hilfe() -> str: """`HandBrakeCLI --help` einmal abrufen (leer, wenn nicht installiert).""" if not shutil.which("HandBrakeCLI"): return "" try: aus = subprocess.run( ["HandBrakeCLI", "--help"], capture_output=True, text=True, timeout=30 ) return (aus.stdout or "") + (aus.stderr or "") except (OSError, subprocess.TimeoutExpired): return "" def erkenne_encoder() -> list: """Encoder-Backends, die auf DIESER Maschine wirklich nutzbar sind.""" hb_encoder = parse_encoder_liste(hole_handbrake_hilfe()) dri_da = os.path.exists("/dev/dri/renderD128") nvidia_da = bool( shutil.which("nvidia-smi") or os.path.exists("/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1") ) return leite_backends_ab(hb_encoder, dri_da, nvidia_da) def erkenne_ip() -> str: """Beste erratbare eigene IP (UDP-Route-Trick, KEIN echter Traffic). Ehrlicher Vorbehalt: im Docker-Bridge-Netz ist das die Container-IP — für die Maschinen-Zuordnung ist deshalb WORKER_NAME (Anzeigename in compose/.env) die verlässliche Größe; die IP ist Zusatz-Indiz. """ import socket try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("192.0.2.1", 9)) # TEST-NET — nur Routing, kein Paket nötig return s.getsockname()[0] finally: s.close() except OSError: return "" def werkzeug_versionen() -> dict: """Kern-Werkzeuge dieses Workers — fürs UI (Einstellungen → System). MakeMKV-Version kommt aus dem Build (ENV MAKEMKV_VERSION im Dockerfile) — makemkvcon hat keinen dokumentierten --version-Schalter (usage.txt). HandBrakeCLI --version ist dokumentiert (handbrake.fr/docs, CLI Options) und gibt z.B. "HandBrake 1.6.1" aus. """ import socket modell, flags, kerne = _lies_cpuinfo() info = { # Zuordnung im UI: hostname (für den Celery-Online-Abgleich) + IP "hostname": socket.gethostname(), "ip": erkenne_ip(), # Rechenleistung fürs Software-Encodieren. Ohne diese drei Angaben war # am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf dieser Maschine # Tage statt Stunden braucht (kein AVX2, 4 Kerne). "cpu_modell": modell or (platform.processor() or "unbekannt"), "cpu_kerne": str(kerne or os.cpu_count() or 0), "cpu_simd": beste_simd(flags), } hb_encoder = parse_encoder_liste(hole_handbrake_hilfe()) if hb_encoder: # Die ungefilterte Auskunft von HandBrake — damit im UI nachprüfbar ist, # woher die Backend-Liste kommt. info["handbrake_encoder"] = ",".join(hb_encoder) if shutil.which("makemkvcon"): info["makemkv"] = os.getenv("MAKEMKV_VERSION") or "installiert" if shutil.which("HandBrakeCLI"): try: aus = subprocess.run( ["HandBrakeCLI", "--version"], capture_output=True, text=True, timeout=15, ) treffer = re.search(r"HandBrake\s+([\w.]+)", (aus.stdout or "") + (aus.stderr or "")) info["handbrake"] = treffer.group(1) if treffer else "installiert" except (OSError, subprocess.TimeoutExpired): info["handbrake"] = "installiert" # Woher kommt der MakeMKV-Key? UI-Setting schlägt Env — ehrlich anzeigen. try: import db ui_key = (db.get_settings().get("makemkvAppKey") or "").strip() except Exception: ui_key = "" if ui_key: info["makemkv_key"] = "ui" elif os.getenv("MAKEMKV_APP_KEY"): info["makemkv_key"] = "env" else: info["makemkv_key"] = "keiner" # Sieht DIESER Worker eine KEYDB.cfg? Die API zeigt ihren eigenen Mount — # bei einem Remote-Worker kann das etwas ganz anderes sein, und nur das # Verzeichnis des rippenden Workers zählt (Befund 25.07.2026). # # Gemeldet wird die Angabe NUR, wenn das Datenverzeichnis hier wirklich # eingehängt ist (os.path.ismount). Ein Remote-Transcode-Worker benutzt # dasselbe Image — makemkvcon ist dort also vorhanden und der entrypoint # legt den Ordner an —, er bekommt den Mount aber nicht und rippt nie. # Ohne diese Prüfung trüge er im UI dauerhaft die Warnung "keine # KEYDB.cfg", obwohl ihn das gar nichts angeht. try: import makemkv_daten daten_dir = makemkv_daten.DATEN_DIR except Exception: daten_dir = "" if shutil.which("makemkvcon") and daten_dir and os.path.ismount(daten_dir): try: info["keydb"] = "ja" if makemkv_daten.keydb_status().get("vorhanden") else "nein" except Exception: info["keydb"] = "unbekannt" # Die entscheidende Zahl für 4K-UHD: wie viele Disc-Schlüssel kennt # dieser Worker? 0 heißt, dass jede unbekannte UHD-Disc scheitert — # makemkvcon holt sie unter Linux nie selbst (Befund 25.07.2026). try: info["schluessel"] = str(makemkv_daten.schluesselspeicher_status().get("schluessel", 0)) except Exception: info["schluessel"] = "unbekannt" return info