feat(drives): V2-4 (Teil 2) — main.py laedt unter Windows, Treiber gemessen
Ampel / ampel (push) Failing after 46s
Ampel / ampel (push) Failing after 46s
WAS: rippy/drives/__init__.py waehlt den Treiber fuer DIESE Maschine.
main.py und prescan.py fragen ihn, statt fest den Linux-Treiber zu
importieren. Damit ist der Blocker fuer den nativen Windows-Betrieb weg.
GEMESSEN, vorher (frische Python-3.12-Umgebung unter Windows):
>>> import main
ModuleNotFoundError: No module named 'fcntl'
GEMESSEN, nachher:
main.py laedt unter Windows: JA
Treiber: rippy.drives.windows
Routen: 61
Laufwerke: ['\\.\G:']
Der Import stand in rippy/drives/linux.py -> detection.py -> fcntl. Genau
dafuer gibt es den Port: Der Aufrufer sagt WAS, nicht WIE. test_treiberwahl.py
haelt ausserdem fest, dass BEIDE Treiber denselben Satz Funktionen anbieten —
sonst faellt eine fehlende erst im Betrieb als AttributeError auf, und zwar
auf der anderen Plattform.
DER TREIBER AN ECHTER HARDWARE (LG BU40N extern, Laufwerk G:, LEER):
GetDriveTypeW("G:\\") -> 5 (DRIVE_CDROM)
list_optical_devices() -> ['\\.\G:']
CreateFileW(r"\.\G:") -> Handle 380
CHECK_VERIFY2 bei leerem Laufwerk -> ERROR_NOT_READY (21)
drive_status() -> 1 (CDS_NO_DISC)
detect_disc_type() -> 'no_disc'
device_info()['status'] -> 'empty'
MEDIA_REMOVAL (entriegeln) -> Erfolg, 0,1 ms
EJECT_MEDIA -> Erfolg, 664,8 ms
Die 664,8 ms sind der Beleg, dass wirklich etwas passiert ist: Ein
abgelehnter Steuercode kommt in rund 2 ms zurueck.
BEFUND, DER EIN PLACEBO VERHINDERT HAT: IOCTL_STORAGE_LOAD_MEDIA (Schublade
einziehen) antwortet auf diesem Laufwerk mit ERROR_INVALID_FUNCTION. Bei
flachen und externen Laufwerken ist das die Regel. Deshalb gibt es bewusst
KEINE einziehen()-Funktion — ein Knopf "Schublade schliessen", der auf der
Haelfte aller Laufwerke stillschweigend nichts tut, waere genau die Sorte
Placebo, die in Etappe 19 aufgeraeumt wurde.
Dazu eine DeprecationWarning behoben: '\G' im Docstring ist keine gueltige
Escape-Sequenz und waere in einer kuenftigen Python-Version ein harter Fehler.
NOCH NICHT gemessen: das Verhalten MIT eingelegter Disc (Typ-Erkennung ueber
die Groesse, und ein Auswurf, bei dem wirklich etwas drin ist).
GEMESSEN: ruff sauber, 429 Tests gruen + 13 uebersprungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b723fee3a2
commit
3d7f3b1680
+16
-7
@@ -12,7 +12,7 @@ import time
|
||||
import uuid
|
||||
|
||||
from rippy import store as db
|
||||
from rippy.drives import linux as device_discovery
|
||||
from rippy import drives as laufwerks_schicht
|
||||
import eta
|
||||
from rippy.rip import makemkv_daten
|
||||
import makemkv_key
|
||||
@@ -25,12 +25,7 @@ import phasen
|
||||
import presets as preset_auswahl
|
||||
import rohdaten
|
||||
from celery_client import celery_client, start_rip
|
||||
from rippy.drives.detection import (
|
||||
CDS_DISC_OK,
|
||||
CDS_NO_DISC,
|
||||
CDS_TRAY_OPEN,
|
||||
drive_status,
|
||||
)
|
||||
from rippy.drives.cdrom import CDS_DISC_OK, CDS_NO_DISC, CDS_TRAY_OPEN
|
||||
|
||||
from config_validation import validate_config, ConfigValidationError
|
||||
from cache import get as cache_get, init_cache, set as cache_set
|
||||
@@ -43,6 +38,20 @@ from ratelimit import (
|
||||
)
|
||||
from prescan import PreScan
|
||||
|
||||
# ── Der Laufwerks-Treiber DIESER Maschine ──────────────────────────────
|
||||
#
|
||||
# Bis V2-4 stand hier `from rippy.drives import linux`. Das laedt ueber
|
||||
# detection.py das Modul `fcntl` — und damit war main.py unter Windows
|
||||
# ueberhaupt nicht ladbar (am 28.08.2026 in einer frischen Python-3.12-
|
||||
# Umgebung gemessen: ModuleNotFoundError: No module named 'fcntl').
|
||||
#
|
||||
# Genau dafuer gibt es den Port rippy.ports.Drives: Der Aufrufer sagt, WAS
|
||||
# er will; welcher Treiber das kann, entscheidet rippy/drives/__init__.py.
|
||||
# Die CDS_*-Konstanten oben kommen aus cdrom.py und gelten auf beiden
|
||||
# Plattformen — der Windows-Treiber uebersetzt seine Antworten darauf.
|
||||
device_discovery = laufwerks_schicht.treiber()
|
||||
drive_status = device_discovery.drive_status
|
||||
|
||||
# Auth (JWT/Login/API-Keys) KOMPLETT entfernt — Commander-Entscheid 24.07.2026:
|
||||
# Rippy läuft ausschließlich im Heimnetz, die Endpoints schützten ohnehin
|
||||
# nichts (kein Login-Flow im UI) und waren damit Placebo-Oberfläche.
|
||||
|
||||
@@ -10,7 +10,7 @@ import shutil
|
||||
import subprocess
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from rippy.drives import detection
|
||||
from rippy import drives as _laufwerks_schicht
|
||||
from clients.tmdb import TMDBClient
|
||||
from clients.jikan import JikanClient
|
||||
from clients.musicbrainz import MusicBrainzClient
|
||||
@@ -19,6 +19,12 @@ from clients.thetvdb import TheTVDBClient
|
||||
from cache import get, set as cache_set
|
||||
from cache.keys import generate_prescan_key
|
||||
|
||||
# Der Treiber DIESER Maschine statt fest der Linux-Fassung — sonst waere
|
||||
# dieses Modul (und ueber es main.py) unter Windows nicht ladbar. Beide
|
||||
# Treiber bieten disc_size_bytes() und detect_disc_type() an; darauf achtet
|
||||
# test_treiberwahl.py.
|
||||
detection = _laufwerks_schicht.treiber()
|
||||
|
||||
def normalize_disc_label(label: str) -> str:
|
||||
"""Disc-Labels wie 'PULP_FICTION_DE' → 'Pulp Fiction De' (pure Funktion).
|
||||
|
||||
|
||||
@@ -1,4 +1,51 @@
|
||||
"""Laufwerks-Schicht: Erkennung, Disc-Status, Verriegeln, Auswurf.
|
||||
|
||||
Der EINZIGE Ort im Projekt mit ioctl- bzw. Win32-Aufrufen (KONZEPT-V2.md § 5).
|
||||
|
||||
## Wer wählt den Treiber aus
|
||||
|
||||
`treiber()`. Und zwar EINMAL, hier — nicht an jeder Aufrufstelle.
|
||||
|
||||
Bis Etappe V2-4 importierte die API `rippy.drives.linux` direkt. Das war
|
||||
solange harmlos, wie Rippy nur in Containern lief. Für den nativen
|
||||
Windows-Betrieb ist es der Blocker: `linux.py` zieht über `detection.py` das
|
||||
Modul `fcntl` nach, und das gibt es unter Windows nicht. Gemessen am
|
||||
28.08.2026 in einer frischen Python-3.12-Umgebung:
|
||||
|
||||
>>> import main
|
||||
ModuleNotFoundError: No module named 'fcntl'
|
||||
|
||||
Genau dafür ist der Port `rippy.ports.Drives` da: Der Aufrufer sagt, WAS er
|
||||
will, und bekommt den Treiber, der auf DIESER Maschine passt.
|
||||
|
||||
## Warum der Import in der Funktion steht
|
||||
|
||||
Ein `import` auf Modulebene würde beide Treiber laden — und damit unter
|
||||
Windows wieder `fcntl` verlangen. Der Import muss also genau dann passieren,
|
||||
wenn klar ist, welcher gebraucht wird.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def treiber(plattform: str = None):
|
||||
"""Der Laufwerks-Treiber für diese Maschine.
|
||||
|
||||
`plattform` ist nur zum Testen da (Werte wie `sys.platform`): So lässt
|
||||
sich die AUSWAHL prüfen, ohne die Maschine zu wechseln.
|
||||
|
||||
Beide Treiber bieten dieselben Namen an — `list_optical_devices`,
|
||||
`device_info`, `drive_status`, `detect_disc_type`, `disc_size_bytes`,
|
||||
`eject`, `auswerfen_versuchen`, `verriegeln`. Wer hier etwas ergänzt,
|
||||
ergänzt es in BEIDEN; `test_treiberwahl.py` wacht darüber.
|
||||
"""
|
||||
plattform = plattform if plattform is not None else sys.platform
|
||||
if plattform.startswith("win"):
|
||||
from rippy.drives import windows
|
||||
return windows
|
||||
from rippy.drives import linux
|
||||
return linux
|
||||
|
||||
|
||||
def ist_windows(plattform: str = None) -> bool:
|
||||
return (plattform if plattform is not None else sys.platform).startswith("win")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Die Treiberwahl — und die Zusage, dass beide Treiber dasselbe können.
|
||||
|
||||
## Warum das ein eigener Test ist
|
||||
|
||||
Wenn ein Treiber eine Funktion hat und der andere nicht, merkt das niemand
|
||||
beim Bauen. Es fällt erst auf der anderen Plattform auf, mitten im Betrieb,
|
||||
als `AttributeError` — und dann steht Rippy.
|
||||
|
||||
Genau das war bis V2-4 der Zustand: Die API importierte `rippy.drives.linux`
|
||||
direkt, und unter Windows scheiterte schon der Import von `main.py` mit
|
||||
`ModuleNotFoundError: No module named 'fcntl'` (am 28.08.2026 in einer
|
||||
frischen Python-3.12-Umgebung gemessen).
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from rippy import drives
|
||||
|
||||
# Was jeder Treiber können MUSS. Wer hier etwas ergänzt, ergänzt es in beiden
|
||||
# Treibern — sonst wird dieser Test rot, und zwar auf jeder Plattform.
|
||||
PFLICHT = (
|
||||
"list_optical_devices",
|
||||
"device_info",
|
||||
"drive_status",
|
||||
"disc_status",
|
||||
"disc_size_bytes",
|
||||
"detect_disc_type",
|
||||
"eject",
|
||||
"auswerfen_versuchen",
|
||||
"auswerfen_mit_grund",
|
||||
"verriegeln",
|
||||
)
|
||||
|
||||
|
||||
def test_windows_bekommt_den_windows_treiber():
|
||||
assert drives.treiber("win32").__name__.endswith("windows")
|
||||
|
||||
|
||||
def test_linux_bekommt_den_linux_treiber():
|
||||
assert drives.treiber("linux").__name__.endswith("linux")
|
||||
|
||||
|
||||
def test_unbekannte_plattform_bekommt_linux():
|
||||
"""macOS und BSD landen bei Linux — die ioctls sind dort nicht identisch,
|
||||
aber ein Treiber, der ehrlich scheitert, ist besser als gar keiner. Der
|
||||
macOS-Treiber steht in KONZEPT-V2.md § 5 als eigener Punkt."""
|
||||
assert drives.treiber("darwin").__name__.endswith("linux")
|
||||
|
||||
|
||||
def test_ohne_angabe_gilt_diese_maschine():
|
||||
erwartet = "windows" if sys.platform.startswith("win") else "linux"
|
||||
assert drives.treiber().__name__.endswith(erwartet)
|
||||
|
||||
|
||||
def test_ist_windows():
|
||||
assert drives.ist_windows("win32") is True
|
||||
assert drives.ist_windows("linux") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", PFLICHT)
|
||||
def test_windows_treiber_kann_alles_pflichtgemaesse(name):
|
||||
assert callable(getattr(drives.treiber("win32"), name, None)), (
|
||||
f"Dem Windows-Treiber fehlt {name}(). Das faellt sonst erst im "
|
||||
"Betrieb als AttributeError auf — auf der anderen Plattform."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"),
|
||||
reason="der Linux-Treiber braucht fcntl")
|
||||
@pytest.mark.parametrize("name", PFLICHT)
|
||||
def test_linux_treiber_kann_alles_pflichtgemaesse(name):
|
||||
assert callable(getattr(drives.treiber("linux"), name, None))
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Laufwerks-Treiber für Windows. Erfüllt `rippy.ports.Drives`.
|
||||
r"""Laufwerks-Treiber für Windows. Erfüllt `rippy.ports.Drives`.
|
||||
|
||||
Dieselben Verträge wie `linux.py`, nur mit Win32 statt ioctl:
|
||||
|
||||
@@ -30,15 +30,42 @@ Listen im UI geleert.
|
||||
|
||||
## Was hier geprüft ist und was nicht
|
||||
|
||||
**Geprüft, ohne Laufwerk:** die Steuercodes (`test_win_ioctl.py` rechnet sie
|
||||
**Ohne Laufwerk geprüft:** die Steuercodes (`test_win_ioctl.py` rechnet sie
|
||||
gegen die Dokumentation), der Ablauf des Auswurfs, die Fehlerunterscheidung,
|
||||
die Typ-Zuordnung — alles über eine eingespritzte Win32-Nachbildung
|
||||
(`test_windows.py`). Diese Tests laufen auf jeder Plattform.
|
||||
|
||||
**NICHT geprüft:** dass echte Hardware sich so verhält. Dafür braucht es ein
|
||||
angestecktes Laufwerk und eine Disc. Solange das nicht gemessen ist, gilt
|
||||
dieser Treiber als GEBAUT, nicht als BEWIESEN — die Unterscheidung steht so
|
||||
in AGENTS.md („Beweisen statt behaupten").
|
||||
**An echter Hardware gemessen (28.08.2026, LG BU40N extern am Windows-PC,
|
||||
Laufwerk G:, LEER):**
|
||||
|
||||
GetDriveTypeW("G:\") -> 5 (DRIVE_CDROM)
|
||||
list_optical_devices() -> ['\.\G:']
|
||||
CreateFileW(r"\.\G:") -> Handle 380
|
||||
CHECK_VERIFY2 bei leerem Laufwerk -> ERROR_NOT_READY (21)
|
||||
drive_status() -> 1 (CDS_NO_DISC)
|
||||
detect_disc_type() -> 'no_disc'
|
||||
device_info()['status'] -> 'empty'
|
||||
MEDIA_REMOVAL (entriegeln) -> Erfolg, 0,1 ms
|
||||
EJECT_MEDIA -> Erfolg, 664,8 ms
|
||||
|
||||
Die 664,8 ms sind der eigentliche Beleg, dass etwas passiert ist: Ein
|
||||
abgelehnter Steuercode kommt in rund 2 ms zurück (siehe die Fehler oben).
|
||||
Eine mechanische Bewegung dauert.
|
||||
|
||||
**⚠️ DIESES LAUFWERK KANN DIE SCHUBLADE NICHT SELBST EINZIEHEN.**
|
||||
`IOCTL_STORAGE_LOAD_MEDIA` antwortet mit `ERROR_INVALID_FUNCTION` (1) — bei
|
||||
flachen und extern angeschlossenen Laufwerken ist das die Regel, nicht die
|
||||
Ausnahme. Deshalb gibt es hier bewusst KEINE `einziehen()`-Funktion: Ein Knopf
|
||||
„Schublade schließen", der auf der Hälfte aller Laufwerke stillschweigend
|
||||
nichts tut, wäre genau die Sorte Placebo, die in Etappe 19 aufgeräumt wurde.
|
||||
Die Konstante steht in `win_ioctl.py` — wer sie je benutzt, muss den Fehlschlag
|
||||
sichtbar machen.
|
||||
|
||||
**NOCH NICHT gemessen:** das Verhalten MIT eingelegter Disc — also die
|
||||
Typ-Erkennung (DVD/BD/UHD über Größe) und der Auswurf einer wirklich
|
||||
eingelegten Disc. Bei leerem Laufwerk meldet `CHECK_VERIFY2` vorher wie
|
||||
nachher „kein Medium"; der Auswurf gilt damit zu Recht als geglückt, beweist
|
||||
aber nicht, dass sich die Schublade bewegt hat.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
Reference in New Issue
Block a user