refactor(core): V2-1 — die vier Ports, ein Store, ein Laufwerks-Treiber
Ampel / ampel (push) Successful in 40s
Ampel / ampel (push) Successful in 40s
WAS: rippy/ports.py beschreibt Store/Queue/Bus/Drives als Protocol. Zwei
weitere Doppelungen sind zusammengelegt: db.py (API+Worker) wird
rippy/store, und der Auswurf (api/devices.py + ripping.wirf_disc_aus)
wird rippy/drives/linux. Verhalten unveraendert.
WARUM: Drei Betriebsarten tragen nur, wenn ein Modus die Auswahl der
Treiber hinter vier Nahtstellen ist statt ein eigener Codestand
(KONZEPT-V2.md §1). Diese Etappe zieht die Nahtstellen ein, ohne schon
einen zweiten Treiber zu haben — die kommen in V2-2 (SQLite/LocalQueue)
und V2-4 (Windows).
DIE UNANGENEHMERE DOPPELUNG WAR DER AUSWURF: Er stand zweimal da, mit
UNTERSCHIEDLICHEN Vertraegen — devices.eject wirft OSError, ripping.
wirf_disc_aus gibt False zurueck und wirft nie. Beides ist richtig fuer
seine Seite (Browser-Meldung gegen "ein Rip stirbt nicht an einer
klemmenden Schublade"). Jetzt liegt EINE Mechanik darunter
(auswerfen_mit_grund) und beide Vertraege unveraendert darueber.
Die API-Fassung war ausserdem NIE getestet — jetzt schon, inklusive
"reicht ENOENT/EPERM unveraendert weiter".
get_settings hatte den einzigen echten Verhaltensunterschied der beiden
db.py: die Worker-Fassung schluckte jeden Fehler und gab {} zurueck.
Nicht still entschieden, sondern sichtbar gemacht — der Parameter
bei_fehler_leer steht jetzt in der Signatur, mit der offenen Frage im
Docstring. {} heisst fuer den Aufrufer "nichts gesetzt", nicht "konnte
nicht nachsehen"; das ist dieselbe Klasse wie catch(() => []) im alten
UI. Zu entscheiden in V2-2.
ZWEITER BEINAHE-FEHLER DIESER ETAPPE: linux.py importierte detection
auf Modulebene — und das zieht fcntl. Damit waere ripping.py und ueber
es der NATIVE WINDOWS-WORKER nicht mehr ladbar gewesen. Diesmal haben
die Tests es sofort gefangen (4 Sammelfehler). Behoben an der Wurzel:
Konstanten und die reine classify() leben jetzt in drives/cdrom.py,
ganz ohne fcntl. Nebengewinn — die classify-Tests liefen bisher NUR in
der Ampel ("erst nach dem Push bewiesen") und laufen jetzt ueberall.
Ausserdem: .dockerignore-Testmuster brauchen **, sonst greifen sie nur
in der obersten Ebene. Im laufenden Container nachgezaehlt: 30 test_*.py
lagen in den Images.
GEMESSEN: ruff sauber, 301 Tests gruen + 1 uebersprungen (vorher 290;
+4 neue eject-Tests, +7 classify-Tests die jetzt lokal laufen). Kein
Modul liegt mehr doppelt im Repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
edfe0d4313
commit
dd1d0b7365
+19
-105
@@ -17,6 +17,25 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# Auswurf und ioctl-Konstanten leben seit V2-1 im gemeinsamen Treiber
|
||||
# (rippy/drives/linux.py). Vorher stand derselbe Ablauf ZWEIMAL im Repo —
|
||||
# hier und in docker/api/devices.py, mit unterschiedlichen Vertraegen.
|
||||
#
|
||||
# Die Namen werden hier weiter angeboten, weil tasks.py und die Tests sie so
|
||||
# kennen; `wirf_disc_aus` ist der Worker-Vertrag (gibt False zurueck, wirft nie).
|
||||
from rippy.drives.linux import ( # noqa: F401
|
||||
AUSWURF_WARTEN_SEKUNDEN,
|
||||
CDROM_DRIVE_STATUS,
|
||||
CDROM_LOCKDOOR,
|
||||
CDROMCLOSETRAY,
|
||||
CDROMEJECT,
|
||||
CDS_DISC_OK,
|
||||
CDS_DRIVE_NOT_READY,
|
||||
CDS_NO_DISC,
|
||||
CDS_TRAY_OPEN,
|
||||
_auswurf_geglueckt,
|
||||
)
|
||||
from rippy.drives.linux import auswerfen_versuchen as wirf_disc_aus # noqa: F401
|
||||
from winlauf import OHNE_FENSTER
|
||||
|
||||
RIP_OUTPUT_DIR = os.getenv("RIP_OUTPUT_DIR", "/app/media")
|
||||
@@ -27,111 +46,6 @@ class RipAbbruch(Exception):
|
||||
Nutzer den Job abgebrochen hat (Status 'canceling' in der DB)."""
|
||||
|
||||
|
||||
# include/uapi/linux/cdrom.h — dieselben ioctls wie in api/devices.py
|
||||
CDROMEJECT = 0x5309
|
||||
CDROM_LOCKDOOR = 0x5329 # 1 = Tür verriegeln, 0 = entriegeln
|
||||
CDROM_DRIVE_STATUS = 0x5326
|
||||
CDROMCLOSETRAY = 0x5319
|
||||
|
||||
# Antworten von CDROM_DRIVE_STATUS (cdrom.h)
|
||||
CDS_NO_DISC = 1
|
||||
CDS_TRAY_OPEN = 2
|
||||
CDS_DRIVE_NOT_READY = 3
|
||||
CDS_DISC_OK = 4
|
||||
|
||||
# Wie lange auf die Schublade gewartet wird. Ein Laufwerk braucht dafür ein
|
||||
# bis zwei Sekunden; fünf sind reichlich und blockieren nichts Wichtiges.
|
||||
AUSWURF_WARTEN_SEKUNDEN = 5
|
||||
|
||||
|
||||
def _auswurf_geglueckt(status: int) -> bool:
|
||||
"""Ist die Disc nach dem Auswurf wirklich draußen? (pure Funktion)
|
||||
|
||||
Sowohl „Schublade offen" als auch „kein Datenträger" zählen: Ein
|
||||
Slot-Laufwerk hat keine Schublade und meldet nach dem Auswerfen CDS_NO_DISC.
|
||||
"""
|
||||
return status in (CDS_TRAY_OPEN, CDS_NO_DISC)
|
||||
|
||||
|
||||
def wirf_disc_aus(device_path: str, ioctl_fn=None, oeffnen=None,
|
||||
schliessen=None, warten=None) -> bool:
|
||||
"""Wirft die Disc aus und PRÜFT, ob sie draußen ist. Wirft NIE.
|
||||
|
||||
## Warum `CDROMEJECT` allein nicht genügt (Befund 26.07.2026, gemessen)
|
||||
|
||||
Der Commander meldete: „Der Button gibt es in den Settings, aber es passiert
|
||||
nicht, das Laufwerk geht nicht auf." Am laufenden System nachgestellt:
|
||||
|
||||
wirf_disc_aus("/dev/sr0") → True
|
||||
CDROM_DRIVE_STATUS danach → 4 (Disc drin)
|
||||
|
||||
Das ioctl wird also **angenommen und tut nichts**. Ursache: MakeMKV
|
||||
verriegelt während des Rips die Laufwerkstür (`CDROM_LOCKDOOR 1`) und
|
||||
entriegelt sie nicht wieder. Ein verriegeltes Laufwerk quittiert den Auswurf
|
||||
trotzdem mit Erfolg. Deshalb macht das Werkzeug `eject` immer beides:
|
||||
erst entriegeln, dann auswerfen. Gegenprobe an derselben Disc:
|
||||
|
||||
CDROM_LOCKDOOR 0 + CDROMEJECT → Status 2 (SCHUBLADE OFFEN)
|
||||
|
||||
## Und deshalb wird das Ergebnis geprüft, nicht geglaubt
|
||||
|
||||
Genau diese Sorte Fehler ist zweimal durchgerutscht: In v3.14 stand hier
|
||||
„Auswurf tat nichts, jetzt entscheidet die Einstellung" — die Einstellung
|
||||
wurde danach wirklich gelesen, nur ausgeworfen wurde weiterhin nicht, und im
|
||||
Log stand „Disc ausgeworfen". Ein Rückgabewert eines ioctls beweist nichts;
|
||||
gefragt wird jetzt das Laufwerk.
|
||||
|
||||
Bewusst hier und nicht in detection.py: das Modul ist ein byteweiser
|
||||
Zwilling der API-Kopie. `fcntl` gibt es nur unter Linux — der native
|
||||
Windows-Worker lädt ripping.py ebenfalls, rippt dort aber nie.
|
||||
|
||||
Die drei Parameter sind nur zum Testen einspritzbar (kein echtes Laufwerk).
|
||||
"""
|
||||
if ioctl_fn is None:
|
||||
try:
|
||||
from fcntl import ioctl
|
||||
except ImportError: # Windows — dieser Worker rippt nie
|
||||
return False
|
||||
ioctl_fn = ioctl
|
||||
if warten is None:
|
||||
import time
|
||||
warten = time.sleep
|
||||
oeffnen = oeffnen or (lambda p: os.open(p, os.O_RDONLY | os.O_NONBLOCK))
|
||||
schliessen = schliessen or os.close
|
||||
|
||||
try:
|
||||
fd = oeffnen(device_path)
|
||||
except OSError:
|
||||
return False
|
||||
try:
|
||||
# Entriegeln ist der entscheidende Schritt. Scheitert er, wird der
|
||||
# Auswurf trotzdem versucht — bei einem nicht verriegelten Laufwerk
|
||||
# (oder einem, das das ioctl nicht kennt) klappt er ohnehin.
|
||||
try:
|
||||
ioctl_fn(fd, CDROM_LOCKDOOR, 0)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
ioctl_fn(fd, CDROMEJECT, 0)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# Nachsehen statt hoffen: Die Schublade braucht ein bis zwei Sekunden.
|
||||
for _ in range(AUSWURF_WARTEN_SEKUNDEN):
|
||||
try:
|
||||
if _auswurf_geglueckt(ioctl_fn(fd, CDROM_DRIVE_STATUS, 0)):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
warten(1)
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
schliessen(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def check_makemkv_installed() -> bool:
|
||||
"""Prüft, ob makemkvcon installiert ist."""
|
||||
return shutil.which("makemkvcon") is not None
|
||||
|
||||
Reference in New Issue
Block a user