Files
rippy/docker/worker/caps.py
HitonabiandClaude Opus 5 d3e86d2641 fix(windows): Der Schalter, den es nicht gibt, und vierzehn weitere Funde
Commander: „Kompression fehlgeschlagen bei Spartacus … _t01.mkv: HandBrake
endete mit Code 0" — 16,5 GB fertiger Rohschnitt, und die Kompression war in
derselben Sekunde vorbei, in der sie begann.

Nachgestellt mit genau der Befehlszeile, die Rippy baute:

    unknown option (--audio-codec)
    HandBrake has exited.        $? = 0

Den Schalter `--audio-codec` gibt es bei HandBrakeCLI nicht; er heisst
`-E` / `--aencoder`. Ein unbekannter Schalter ist fuer HandBrake kein Fehler,
der Rueckgabewert ist 0. Der Test dazu forderte den falschen Namen sogar ein.

Daraus wurde ein Rundgang durch den Windows-Pfad. Alles unten ist gemessen,
nichts vermutet (Regel D).

## Die Kompression

1. `--aencoder` statt `--audio-codec`. Am mitgelieferten HandBrakeCLI 1.11.2
   gemessen, mit einem 5-Sekunden-Encode auf der echten Roh-Datei bestaetigt.

2. HandBrakes letzte Zeilen werden aufgehoben (12 gepuffert, 4 in der
   Meldung) und `unknown option (...)` wird als eigener Fall erkannt, VOR
   allen anderen. Vorher wurde jede Zeile weggeworfen, die kein Fortschritt
   war — bei Rueckgabewert 0 blieb damit keine Auskunft uebrig. Die geratene
   Zeile „Meist ist der Zielordner nicht beschreibbar" ist raus; sie war
   falsch und hat die Suche in die falsche Richtung geschickt.

3. Ein `ue`-Umlaut im Pfad toetete die Kompression. HandBrake schreibt zwei
   Kodierungen in denselben Strom (derselbe Pfad einmal UTF-8, einmal CP850).
   In CP850 ist das Byte 0x81, und das ist in cp1252 — was `text=True` auf
   deutschem Windows waehlt — undefiniert:

       UnicodeDecodeError: charmap codec can't decode byte 0x81

   Neu: `rip/handbrake_aufruf.py` mit `HB_LESEN`, benutzt von ripping.py und
   caps.py. Bewusst nicht binaer wie bei makemkvcon: HandBrake trennt
   Fortschrittszeilen mit CR, im Binaermodus waere der Balken weg.

## Die Rohdaten

4. `rohdaten.kandidaten` machte aus dem Arbeitsordner `F:\` ein `F:` und
   verband damit weiter. Das ist unter Windows der aktuelle Ordner auf
   Laufwerk F, nicht dessen Wurzel — 16,5 GB waren unsichtbar, und der
   Wiederholen-Dialog bot nur „Neu rippen" an. Die Falle steht woertlich im
   Kopf von `pfade.verbinden`.

5. Gesucht wurde unter der heutigen Einstellung statt unter der Wahl DIESES
   Rips (`meta["work_dir"]`). Genau dafuer wurde rohdaten.py am 26.07.
   gebaut; repariert wurde damals die Kandidatenliste, nicht der Aufrufer.
   Neu: `_arbeitsverzeichnis_des_jobs`, benutzt an vier Stellen.

6. Zwei Speicher fuer dieselben Ordner: Die Oberflaeche schreibt
   `outputDir`/`workDir` in die Datenbank, `betrieb` liest `storage.*` aus
   der Konfigurationsdatei, und die schreibt niemand. Gemessen: eingestellt
   `E:\Rippy`, angezeigt `C:\Users\...\Videos\Rippy`. Neu:
   `betrieb.mit_einstellungen`.

## Das Laufwerk

7. `device_info` fing den OSError ab und lieferte „unknown" ohne den Grund.
   Nach einem Rip mit Lesefehlern beantwortete das Laufwerk keine
   Medien-Abfragen mehr (Win32-Fehler 1), die Geraete-Auskunft aber schon —
   im UI stand eine volle Laufwerkskarte, kein Rip startbar, und im
   Protokoll das laengst veraltete „Disc erkannt". Neu: `ZUGRIFFS_GRUENDE`,
   ein Feld `grund` im Laufwerks-Eintrag und eine Protokollzeile je Wechsel.
   Eine fehlgeschlagene Disc-Erkennung wird ebenfalls protokolliert.

8. Der Linux-Treiber nannte ein unzugaengliches Laufwerk „empty", waehrend
   Windows richtig „unknown" sagt. Angeglichen, samt Feld-Paritaet.

9. `CreateFileW`, `DeviceIoControl` und `CloseHandle` hatten weder `restype`
   noch `argtypes` — 32-Bit-`c_int` fuer einen 64-Bit-HANDLE, in beide
   Richtungen. Mit `restype` aendert sich der Fehlerwert von -1 auf
   0xFFFFFFFFFFFFFFFF; die Pruefung deckt jetzt beides ab. Am echten
   Laufwerk gegengeprueft, Fehlerpfad eingeschlossen.

10. Der Vor-Scan lief bei JEDER eingelegten Disc ein `makemkvcon info` mit
    120 s Zeitgrenze — 20 bis 120 Sekunden „Disc wird gelesen". Frueher war
    das schnell, weil der Zweig unter Windows nie lief (`shutil.which`,
    repariert am 28.08.). Das Ergebnis landete allein in `toc["tracks"]`,
    das niemand liest: Der Rip-Dialog holt seine Liste ueber
    `/devices/{id}/scan-tracks`, wenn sie gebraucht wird. Entfernt.

## Notbremsen

11. `_frei_bytes` suchte den naechsten vorhandenen Ordner selbst.
    `os.path.dirname("Q:\\")` gibt sich selbst zurueck — ein
    Arbeitsverzeichnis auf einer abgezogenen Platte haette den Job vor dem
    Rip stumm haengen lassen. Benutzt jetzt
    `pfade.naechster_vorhandener`, das den Abbruch seit V2-1 hat.

12. `naechster_vorhandener` haelt Laufwerks- und UNC-Wurzeln jetzt absolut.

13. `aufraeum_skript` baut sein `rmdir /s /q` aus `InstallLocation` in der
    Registry. Waere das eine Laufwerks-Wurzel, loeschte die Deinstallation
    das Laufwerk. Nicht beobachtet, aber nicht wiedergutzumachen — der
    Loeschbefehl bleibt in dem Fall weg.

## Lesefehler

MakeMKV sicherte 1 von 2 Titeln, endete mit 0, und Rippy schrieb „Rip
fertig". Jetzt gibt es eine Warnung, auch wenn der Rip als Erfolg endet, und
die MSG-Nummer steht im Protokoll: MakeMKVs Texte sind uebersetzt, die
Nummern nicht.

## Aus der Gegenprobe am laufenden Rippy

Die erste Fassung dieses Standes war installiert, als der Commander meldete:
„nun oeffnen sich diverse fenster im hintergrund, gehen ganz kurz auf und dann
wieder zu. Das laufwerk hoert auch einfach auf zu lesen." Beides Altlasten,
die erst durch die neue Protokollzeile sichtbar wurden.

14. Prozesserzeugung mitgeschnitten:

        14:54:40  timeout.exe          timeout 4 ls -d C:\Users\...\d7ee6c06-...
        14:54:40  WindowsTerminal.exe

    `rohdaten.pruefen` fragt mit `timeout N ls -d`, ob es ein Verzeichnis
    gibt. Unter Linux ist das richtig (os.path.isdir kann an einem toten
    CIFS-Mount im Kernel haengen, ein Kindprozess laesst sich abbrechen).
    Unter Windows ist es dreifach falsch: timeout.exe gibt es dort, kennt
    aber weder `ls` noch `-d`; sie braucht eine Konsole, und die reisst
    Windows auf; und ihr Rueckgabewert ist nie 0, die Antwort lautete also
    „weg" fuer JEDES Verzeichnis. Rohdaten waren unter Windows
    grundsaetzlich unsichtbar. Neu: `nativ_nachsehen()`. Die zwei
    gleichartigen Aufrufe in mounts.py bekommen dieselbe Absicherung.

15. Der Waechter fragte das Laufwerk alle drei Sekunden ab — auch mitten im
    Rip, also drei CreateFileW plus IOCTLs auf ein Geraet, das makemkvcon
    gerade liest:

        12:49:52  bluray-Rip gestartet
        12:50:09  [watcher] Laufwerk G: beantwortet keine Medien-Abfragen
        12:50:12  MSG 2003 SCSI-Fehler ILLEGAL REQUEST:INVALID FIELD IN CDB
        12:50:12  makemkvcon endete mit Code 11

    `_auto_prescan` haelt sich seit dem 29.08.2026 an die Regel „waehrend
    eines Rips wird nicht gescannt"; die Laufwerksabfrage tat es nicht.
    Jetzt gilt in der Zeit der letzte bekannte Stand.

## Zwei Tests, die gelogen haben

* `assert "--audio-codec" in cmd` schrieb den Fehler fest.
* `lambda: {}` als Doppelgaenger fuer `get_settings(key, bei_fehler_leer)`
  brach, sobald ein Aufrufer einen Parameter benutzte — und zeigte dann auf
  den Code statt auf sich selbst.

977 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 15:11:00 +02:00

480 lines
19 KiB
Python

"""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 subprocess
from rippy.platform.winlauf import OHNE_FENSTER
from rippy.rip.handbrake_aufruf import HB_LESEN
from rippy.tools import katalog as werkzeuge
def _extern(werte=None, container=None) -> str:
"""Läuft Rippy woanders als dieser Worker? „ja"/„nein" (einspritzbar).
Drei Fälle, und nur einer davon ist „ja":
* **Im Container** — der Worker ist Teil von Rippy. Nein.
* **Nativ und eigenständig** (Windows-App) — dieser Prozess IST Rippy.
Nein. Es gibt hier weder Container-Pfade noch eine Freigabe, über die
etwas zu übersetzen wäre.
* **Nativ und verteilt** — ein Worker auf einem anderen Rechner, der sich
bei einem Docker-Rippy meldet. Ja; er braucht RIPPY_PATH_MAP.
"""
from rippy import betrieb, config
if container is None:
container = betrieb.im_container()
if container:
return "nein"
if werte is None:
try:
werte = config.laden()
except Exception: # noqa: BLE001
werte = {}
return "ja" if betrieb.modus(werte) == "verteilt" else "nein"
def _hb() -> str:
"""Pfad zu HandBrakeCLI — "" wenn es nicht da ist.
Bis V2-4 stand hier `shutil.which("HandBrakeCLI")`. Das findet unter
Windows nichts, auch wenn HandBrake installiert ist (Program Files statt
PATH). Die Folge waere gewesen: Rippy meldet "keine Encoder gefunden" auf
einem Rechner, auf dem alles da ist — und die Encoder-Auswahl im UI bliebe
leer, ohne dass jemand den Grund saehe.
"""
return werkzeuge.finden("handbrake")
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 <string>`
(AGENTS Regel D — im Worker-Image am 25.07.2026 gemessen, nicht geraten):
-e, --encoder <string> Select video encoder:
svt_av1
x264
x265
...
--encoder-preset <string>
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
# Hardware-Encoder-Familien, wie HandBrake sie benennt → Backend-Kennung.
# Reihenfolge = Anzeige-Reihenfolge; AV1 zuerst, weil es das modernste ist.
HW_FAMILIEN = (
("nvenc", "nvenc"), # NVIDIA
("qsv", "qsv"), # Intel QuickSync
("vce", "vce"), # AMD VCE/VCN
("vaapi", "vaapi"), # generisch (Linux)
)
def leite_backends_ab(hb_encoder) -> list:
"""Übersetzt die HandBrake-Encoder-Namen in die Backends, die das UI zeigt.
## HandBrakes Liste IST die Auskunft — keine zweite Meinung nötig
Frühere Fassung (25.07.2026) verlangte zusätzlich ein Gerät: `/dev/dri`
für VAAPI/QSV/VCE bzw. `nvidia-smi` für NVENC. Das war doppelt falsch:
1. **Unter Windows gibt es beides nicht.** Der Commander-PC (RX 9070 XT)
meldete deshalb nur CPU-Encoder, obwohl HandBrakes Windows-Build
`vce_*`, `nvenc_*` und `qsv_*` beherrscht. Ein Bug, der genau den
Anwendungsfall traf, für den externe Worker gedacht sind.
2. **Die Prüfung war überflüssig.** HandBrake probiert Hardware-Encoder
beim Start selbst an und listet nur, was es wirklich benutzen kann. Auf
der Rippy-VM belegt: `--version` meldet „qsv: not available on this
system" und „Cannot load libnvidia-encode.so.1" — und genau diese
Encoder fehlen dann in `--help`. Wer die Liste hat, hat die Antwort.
Bleibt die eine Regel, die weiter gilt: **ohne HandBrake gibt es keine
Encoder.** Vorher standen `cpu-x264`/`cpu-x265` fest verdrahtet drin, und
ein reiner Rip-Worker behauptete damit, komprimieren zu können.
Hardware wird jetzt nach Familie unterschieden (nvenc/qsv/vce/vaapi) statt
alles in „vaapi" zu werfen — sonst kann das UI nicht sagen, welches Preset
passt, und AMD-Karten liefen unter einem Intel-Namen.
"""
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")
for kennzeichen, backend in HW_FAMILIEN:
treffer = [n for n in namen if kennzeichen in n]
if not treffer:
continue
gefunden.append(backend)
# AV1 in Hardware ist die derzeit beste Kombination aus Tempo und
# Dateigröße — das muss sichtbar sein, nicht in „nvenc" verschwinden.
if any("av1" in n for n in treffer):
gefunden.append(f"{backend}-av1")
return gefunden
def parse_preset_liste(text: str) -> list:
"""Preset-NAMEN, die DIESES HandBrake wirklich kennt.
Quelle: `HandBrakeCLI --preset-list`, am 26.07.2026 im Worker-Image
gemessen (HandBrake 1.6.1) — AGENTS Regel D, nicht aus dem Kopf:
General/
Very Fast 2160p60 4K AV1
AV1 video (up to 2160p60) and AAC stereo audio, in an MP4
container.
Hardware/
H.265 VCN 2160p 4K
Die Struktur ist die Einrückung: Kategorien ohne Einrückung (mit „/" am
Ende), Preset-Namen mit GENAU vier Leerzeichen, Beschreibungen mit acht.
HandBrakes Logzeilen („[10:26:05] qsv: not available on this system") und
Meldungen wie „Cannot load libnvidia-encode.so.1" haben keine Einrückung
und fallen damit von selbst weg.
⚠️ Wichtige Richtigstellung zum SAVEPOINT v3.16: Dort steht, die Namen der
HARDWARE-Presets seien auf der Rippy-VM „nicht ermittelbar", weil deren
HandBrake keinen Hardware-Encoder hat. Gemessen ist das falsch — die
Kategorie `Hardware/` steht vollständig in der Liste (VCN, NVENC, QSV, MF).
HandBrake trennt beides: `--preset-list` nennt alle mitgelieferten Presets,
`--help` nur die Encoder, die diese Maschine wirklich benutzen kann. Für
„welches Preset gibt es" ist die Preset-Liste die Quelle, für „läuft das
hier" die Encoder-Liste. Deshalb werden beide gemeldet.
"""
presets = []
for zeile in (text or "").splitlines():
if not zeile.strip():
continue
einrueckung = len(zeile) - len(zeile.lstrip(" "))
if einrueckung == 4:
presets.append(zeile.strip())
return presets
def hole_handbrake_presets() -> list:
"""`HandBrakeCLI --preset-list` einmal abrufen (leer, wenn nicht installiert)."""
if not _hb():
return []
try:
aus = subprocess.run(
[_hb(), "--preset-list"],
capture_output=True, timeout=30, **HB_LESEN,
creationflags=OHNE_FENSTER,
)
return parse_preset_liste((aus.stdout or "") + (aus.stderr or ""))
except (OSError, subprocess.TimeoutExpired):
return []
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
# Merkmals-Nummern für IsProcessorFeaturePresent (Win32, winnt.h).
# Absteigend nach Stärke — der erste Treffer ist die höchste Stufe.
WIN_MERKMALE = (
("avx512f", 41), # PF_AVX512F_INSTRUCTIONS_AVAILABLE
("avx2", 40), # PF_AVX2_INSTRUCTIONS_AVAILABLE
("avx", 39), # PF_AVX_INSTRUCTIONS_AVAILABLE
("sse4_2", 38), # PF_SSE4_2_INSTRUCTIONS_AVAILABLE
)
def _simd_windows() -> str:
"""Höchste Vektor-Befehlsstufe unter Windows.
Es gibt dort kein /proc/cpuinfo, deshalb meldete der Windows-Worker bis
25.07.2026 „Vektorbefehle unbekannt" — gerade auf der Maschine, die das
Encodieren übernehmen soll, also genau da, wo die Angabe zählt.
Gefragt wird `IsProcessorFeaturePresent` aus kernel32 (Win32-API, in
winnt.h dokumentiert). Kein Zusatzpaket, kein Registry-Gefummel, keine
Fremdprozesse.
"""
try:
import ctypes
kernel32 = ctypes.windll.kernel32
except (ImportError, AttributeError, OSError):
return "unbekannt"
for name, nummer in WIN_MERKMALE:
try:
if kernel32.IsProcessorFeaturePresent(nummer):
return name
except OSError:
return "unbekannt"
return "unbekannt"
def _modell_windows() -> str:
"""Lesbarer CPU-Name unter Windows.
platform.processor() liefert dort nur „AMD64 Family 26 Model 68 …" — die
Registry kennt den Namen, den auch der Aufkleber trägt.
"""
try:
import winreg
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
r"HARDWARE\DESCRIPTION\System\CentralProcessor\0",
) as schluessel:
name, _ = winreg.QueryValueEx(schluessel, "ProcessorNameString")
return " ".join(str(name).split())
except (ImportError, OSError):
return ""
def cpu_merkmale() -> tuple:
"""(Modell, SIMD-Stufe, Kernzahl) — plattformübergreifend, nie leer."""
modell, flags, kerne = _lies_cpuinfo()
simd = beste_simd(flags)
if simd == "unbekannt" and platform.system() == "Windows":
simd = _simd_windows()
if not modell:
modell = _modell_windows() or platform.processor() or "unbekannt"
return modell, simd, (kerne or os.cpu_count() or 0)
def hole_handbrake_hilfe() -> str:
"""`HandBrakeCLI --help` einmal abrufen (leer, wenn nicht installiert)."""
if not _hb():
return ""
try:
aus = subprocess.run(
[_hb(), "--help"], capture_output=True, timeout=30,
creationflags=OHNE_FENSTER, **HB_LESEN
)
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.
Gefragt wird ausschließlich HandBrake selbst — es probiert Hardware beim
Start an und listet nur Nutzbares. Eine zusätzliche Geräteprüfung wäre
plattformabhängig (und war unter Windows falsch), siehe leite_backends_ab.
"""
return leite_backends_ab(parse_encoder_liste(hole_handbrake_hilfe()))
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, simd, kerne = cpu_merkmale()
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,
"cpu_kerne": str(kerne),
"cpu_simd": simd,
# Läuft dieser Worker INNERHALB des Rippy-Containers oder auf einer
# fremden Maschine? Entscheidend, weil ein externer Worker die
# Container-Pfade (/app/media, /app/temp) nur über eine Freigabe plus
# RIPPY_PATH_MAP erreicht. Das UI kann damit VOR dem Rip warnen, statt
# den Nutzer eine Stunde rippen zu lassen (Vorfall 25.07.2026).
#
# ⚠️ Hier stand `os.path.isdir("/app")` (Befund 29.08.2026). Im Image
# stimmt das. Auf einem Windows-PC gibt es `/app` nicht — und damit
# hielt sich der eigenständige Windows-Rippy für einen FREMDEN Worker.
# Folge: Das UI warnte vor fehlender Pfad-Übersetzung auf einer
# Maschine, auf der es weder Container noch Freigabe gibt.
#
# „Extern" heißt: Rippy läuft woanders als dieser Worker. In der
# eigenständigen Installation IST dieser Worker Rippy — dort ist die
# Frage gegenstandslos. Deshalb entscheidet der Betrieb, nicht ein
# Ordnername.
"extern": _extern(),
# Ist die Pfad-Übersetzung gesetzt? Ohne sie kann ein externer Worker
# grundsätzlich nicht komprimieren.
"pfad_map": os.getenv("RIPPY_PATH_MAP", ""),
}
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)
# Die Preset-NAMEN dieses HandBrake. Damit hört das Raten auf: Das UI bietet
# genau an, was der jeweilige Worker kennt — die Namen unterscheiden sich
# zwischen HandBrake-Versionen, und ein erfundener Name lässt die Kompression
# scheitern (Commander-Anforderung „immer das Beste auswählen" braucht eine
# echte Liste, nicht eine geratene).
presets = hole_handbrake_presets()
if presets:
info["presets"] = presets
if werkzeuge.finden("makemkv"):
info["makemkv"] = os.getenv("MAKEMKV_VERSION") or "installiert"
if _hb():
try:
aus = subprocess.run(
[_hb(), "--version"],
capture_output=True, timeout=15, **HB_LESEN,
creationflags=OHNE_FENSTER,
)
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:
from rippy import store as db
ui_key = (db.get_settings(bei_fehler_leer=True).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:
from rippy.rip import makemkv_daten
daten_dir = makemkv_daten.DATEN_DIR
except Exception:
daten_dir = ""
# ⚠️ `shutil.which` und `os.path.ismount` sind hier BEIDE Linux-Annahmen
# (Befund 29.08.2026): Unter Windows liegt makemkvcon in „Programme" und
# nicht im PATH, und ein normaler Ordner ist kein Mount. Beide Prüfungen
# waren dort also immer falsch — die Schlüssel-Auskunft blieb dauerhaft
# „unbekannt", obwohl MakeMKV samt Datenverzeichnis da war.
#
# Der Mount-Test bleibt für den Container: Dort teilen sich Rippy und ein
# reiner Encoder-Worker dasselbe Image, aber nur einer bekommt den Mount.
# Nativ zählt stattdessen, ob der Ordner überhaupt existiert.
from rippy import betrieb
_im_container = betrieb.im_container()
daten_da = bool(daten_dir) and (os.path.ismount(daten_dir) if _im_container
else os.path.isdir(daten_dir))
if werkzeuge.finden("makemkv") and daten_da:
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"
# Version für den Windows-Worker (aus version.txt) oder Linux-Worker (aus Env)
try:
ver_pfad = os.path.join(os.path.dirname(__file__), "version.txt")
if os.path.isfile(ver_pfad):
with open(ver_pfad, "r", encoding="utf-8") as f:
info["rippy_version"] = f.read().strip()
else:
info["rippy_version"] = os.getenv("RIPPY_VERSION", "dev")
except Exception:
info["rippy_version"] = "unbekannt"
return info