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>
480 lines
19 KiB
Python
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
|