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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b6a93aa726
commit
d3e86d2641
+81
-2
@@ -38,6 +38,7 @@ Verwechslungsgefahr gibt es dabei nicht: Roh-Verzeichnisse heißen exakt wie die
|
||||
Job-ID (vollständige UUID), fertige Ablagen heißen `Titel (Jahr) [kurz-id]`.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from rippy import pfade as _pfade
|
||||
@@ -48,6 +49,23 @@ RAW_STANDARD = "/app/temp/raw"
|
||||
MEDIA_ROOT = "/app/media"
|
||||
|
||||
|
||||
def _ui_einstellungen() -> dict:
|
||||
"""Was in der Oberflaeche eingestellt ist — leer, wenn die Datenbank
|
||||
gerade nicht antwortet.
|
||||
|
||||
Bewusst gekapselt und abgesichert: `wurzeln()` wird auch aus der
|
||||
Jobliste heraus aufgerufen, die alle vier Sekunden laeuft. Sie darf an
|
||||
einer klemmenden Datenbank nicht scheitern — dann gilt eben die
|
||||
Vorgabe, wie bisher.
|
||||
"""
|
||||
try:
|
||||
from rippy import store as db
|
||||
|
||||
return db.get_settings(bei_fehler_leer=True) or {}
|
||||
except Exception: # noqa: BLE001
|
||||
return {}
|
||||
|
||||
|
||||
def wurzeln(werte=None) -> tuple:
|
||||
"""`(roh_standard, medien_wurzel, frei)` fuer DIESEN Betrieb.
|
||||
|
||||
@@ -70,6 +88,11 @@ def wurzeln(werte=None) -> tuple:
|
||||
werte = config.laden()
|
||||
except Exception: # noqa: BLE001
|
||||
werte = {}
|
||||
# Dieselbe Bruecke wie in /system/info: Die Oberflaeche legt
|
||||
# ihre Orte in der Datenbank ab, nicht in der Datei. Ohne sie
|
||||
# suchte die Rohdaten-Suche unter der Vorgabe statt unter dem,
|
||||
# was eingestellt ist (Befund 30.08.2026).
|
||||
werte = betrieb.mit_einstellungen(werte, _ui_einstellungen())
|
||||
return (betrieb.arbeits_vorgabe(werte) or RAW_STANDARD,
|
||||
betrieb.medien_wurzel(werte) or MEDIA_ROOT,
|
||||
betrieb.frei_blaettern(werte))
|
||||
@@ -99,10 +122,29 @@ def kandidaten(job_id: str, work_dir: str, media_unterordner,
|
||||
from rippy import pfade
|
||||
|
||||
orte = [pfade.verbinden(roh, job_id)]
|
||||
wahl = (work_dir or "").strip().rstrip("/\\")
|
||||
# Zum VERGLEICHEN ohne Schluss-Trenner, zum VERBINDEN mit.
|
||||
#
|
||||
# ⚠️ Befund 30.08.2026, am Rechner des Commanders nachgerechnet:
|
||||
# Hier stand `wahl = (work_dir or "").strip().rstrip("/\\")`, und mit
|
||||
# dieser einen abgestreiften Zeichenkette wurde dann auch VERBUNDEN.
|
||||
# Sein Arbeitsordner ist `F:\` — ein Laufwerks-Stammverzeichnis:
|
||||
#
|
||||
# "F:\\".rstrip("/\\") -> "F:"
|
||||
# verbinden("F:", job_id) -> "F:1aa41fef-…" isdir: False
|
||||
# verbinden("F:\\", job_id) -> "F:\1aa41fef-…" isdir: True
|
||||
#
|
||||
# `F:` ohne Trenner heisst unter Windows „der aktuelle Ordner auf
|
||||
# Laufwerk F", nicht die Wurzel — die Falle steht woertlich im Kopf von
|
||||
# `pfade.verbinden`, und diese Zeile ist hineingetreten. Folge: 16,5 GB
|
||||
# Rohschnitt unsichtbar, und der Wiederholen-Dialog bot nur „Neu
|
||||
# rippen" an — Stunden am beschaedigten Datentraeger fuer nichts.
|
||||
wahl = (work_dir or "").strip()
|
||||
vergleich = wahl.rstrip("/\\")
|
||||
grenze = (medien or "").rstrip("/\\")
|
||||
# Nativ zaehlt jede Wahl — dort liegt der Arbeitsordner oft auf einem
|
||||
# ganz anderen Laufwerk und damit unter gar keiner Wurzel.
|
||||
if wahl and (frei or wahl == medien or wahl.startswith(medien + "/")):
|
||||
if vergleich and (frei or vergleich == grenze
|
||||
or vergleich.startswith(grenze + "/")):
|
||||
orte.append(pfade.verbinden(wahl, job_id))
|
||||
for name in media_unterordner or []:
|
||||
if name:
|
||||
@@ -115,6 +157,16 @@ def kandidaten(job_id: str, work_dir: str, media_unterordner,
|
||||
return eindeutig
|
||||
|
||||
|
||||
def nativ_nachsehen() -> bool:
|
||||
"""Darf direkt nachgesehen werden, statt einen Prozess dafuer zu starten?
|
||||
|
||||
Eigene Funktion, damit beide Zweige ueberall pruefbar sind — dieselbe
|
||||
Regel wie bei `betrieb.im_container`. Die Begruendung steht in
|
||||
`pruefen`.
|
||||
"""
|
||||
return os.name == "nt"
|
||||
|
||||
|
||||
def pruefen(pfad: str, laufen=None) -> str:
|
||||
"""Gibt es dieses Verzeichnis? „da" | „weg" | „unklar" — mit HARTER Zeitgrenze.
|
||||
|
||||
@@ -131,6 +183,33 @@ def pruefen(pfad: str, laufen=None) -> str:
|
||||
"""
|
||||
if not pfad:
|
||||
return "weg"
|
||||
if laufen is None and nativ_nachsehen():
|
||||
# ## Warum Windows hier NICHT den Umweg ueber einen Prozess geht
|
||||
#
|
||||
# ⚠️ Befund 30.08.2026, an der laufenden Instanz beobachtet:
|
||||
#
|
||||
# 14:54:40 timeout.exe timeout 4 ls -d C:\\...\\d7ee6c06-...
|
||||
# 14:54:40 WindowsTerminal.exe
|
||||
#
|
||||
# Der Commander: „nun oeffnen sich diverse fenster im hintergrund,
|
||||
# gehen ganz kurz auf und dann wieder zu."
|
||||
#
|
||||
# `timeout` und `ls` sind Linux-Befehle. Unter Windows GIBT es eine
|
||||
# `timeout.exe` — sie wartet nur Sekunden ab und kennt weder `ls`
|
||||
# noch `-d`. Sie braucht aber eine Konsole, und die reisst Windows
|
||||
# dann auf. Dreifach falsch also: ein Fenster bei jedem Durchlauf,
|
||||
# ein Prozess fuer nichts, und ein Rueckgabewert ungleich 0 — also
|
||||
# die Antwort „weg" fuer JEDES Verzeichnis. Rohdaten waren damit
|
||||
# unter Windows grundsaetzlich unsichtbar.
|
||||
#
|
||||
# Der Grund fuer den Umweg gilt hier nicht: Der Kernel-Hang im
|
||||
# Zustand D (siehe `verzeichnis_da`) ist eine Linux-Eigenheit. Ein
|
||||
# totes Netzlaufwerk laesst `os.path.isdir` unter Windows mit einem
|
||||
# Fehler zurueckkommen, nicht unabbrechbar haengen.
|
||||
try:
|
||||
return "da" if os.path.isdir(pfad) else "weg"
|
||||
except OSError:
|
||||
return "unklar"
|
||||
starten = laufen or subprocess.run
|
||||
try:
|
||||
ergebnis = starten(
|
||||
|
||||
Reference in New Issue
Block a user