fix(windows): Zweiter Rip startete waehrend der Kompression ins leere Laufwerk

Der Commander meldete einen Rip-Fehlschlag „bei der Komprimierung", obwohl
der Rip laengst durch war:

    makemkvcon endete mit Code 11 — letzte Meldung:
    Das Öffnen der Disk schlug fehl  — keine MKV-Datei entstanden

Auffaellig war, was FEHLTE: kein „Ursache:". Waere der Geraetepfad schuld
gewesen, stuende dort MSG 2024. Bleibt: kein Datentraeger im Laufwerk.

Der Ablauf dahinter:
  1. Rip fertig → Rippy wirft die Disc aus (Standardeinstellung)
  2. Status wird auf `transcoding` gesetzt — ab hier sagte `has_active_job`
     NEIN, das Laufwerk sei frei; es kennt nur pending/running
  3. Die Disc-Wache sieht beim Auswurf einen Statuswechsel → „eingelegt"
  4. Die Vollautomatik startet einen ZWEITEN Rip — auf ein Laufwerk, dessen
     Schublade gerade herausfaehrt

Der zweite Rip lief in ein leeres Laufwerk. Auf dem Bildschirm sah das aus,
als sei die Kompression gescheitert — sie lief ungestoert weiter.

Drei Aenderungen:

* `store.job_offen` — der weitere Riegel (pending/running/transcoding/
  canceling) fuer die Vollautomatik. `has_active_job` bleibt unveraendert:
  Das fragt „haelt gerade jemand das Laufwerk?", und waehrend der Kompression
  tut das niemand — ein Auswurf von Hand bleibt erlaubt.
* `ripping.disc_fehlt` — vor dem makemkvcon-Start nachsehen, ob ueberhaupt
  eine Disc drin liegt. Statt zwei Minuten Warten und Code 11 gibt es einen
  Satz, den man versteht. Ein FEHLGESCHLAGENER Blick verweigert nichts:
  „ich weiss es nicht" darf nie zu „es geht nicht" werden.
* MSG 5010 in KRITISCHE_CODES — MakeMKVs Sammelmeldung sagt fuer sich nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 20:36:59 +02:00
co-authored by Claude Opus 5
parent f6f0a4ccb6
commit f0f719ca12
6 changed files with 215 additions and 1 deletions
+4 -1
View File
@@ -245,7 +245,10 @@ async def _auto_rip_wenn_aktiviert(pfad: str):
einstellungen = await asyncio.to_thread(db.get_settings)
if not einstellungen.get("autoRipStart"):
return
if await asyncio.to_thread(db.has_active_job, pfad):
# `job_offen`, nicht `has_active_job`: Waehrend der Kompression ist das
# Laufwerk zwar frei, der Vorgang aber nicht durch — und die Disc wurde
# soeben ausgeworfen. Herleitung in store.job_offen.
if await asyncio.to_thread(db.job_offen, pfad):
return
disc = DISC_CACHE.get(pfad) or {}
if disc.get("_laeuft"):
+46
View File
@@ -834,3 +834,49 @@ def test_der_job_start_raeumt_eine_haengende_erkennung_weg():
quelle = inspect.getsource(main)
assert 'DISC_CACHE.pop(device_path, None)' in quelle, \
"beim Job-Start muss eine haengende Erkennungs-Marke weg"
# ── Kein zweiter Rip waehrend der Kompression (Befund 29.08.2026) ───────
#
# Der Commander: „Der Rip an sich war bereits fertig, bei der komprimierung
# passiert das" — Fehlertext: „Das Öffnen der Disk schlug fehl".
#
# Ablauf dahinter: Rip fertig → Disc ausgeworfen → Status `transcoding`.
# `has_active_job` kennt nur pending/running und meldete „Laufwerk frei".
# Die Disc-Wache sah den Auswurf als Statuswechsel, die Vollautomatik startete
# einen ZWEITEN Rip — in eine Schublade, die gerade herausfuhr.
def test_automatik_startet_nicht_waehrend_der_kompression(monkeypatch):
import asyncio
import main
gestartet = []
monkeypatch.setattr(main.db, "get_settings", lambda: {"autoRipStart": True})
monkeypatch.setattr(main.db, "job_offen", lambda p: True) # Job komprimiert
monkeypatch.setattr(main.db, "has_active_job", lambda p: False) # Laufwerk frei
monkeypatch.setattr(main, "start_rip", lambda *a, **k: gestartet.append(a))
main.DISC_CACHE[r"\.\G:"] = {"title": "Akira"}
asyncio.run(main._auto_rip_wenn_aktiviert(r"\.\G:"))
assert gestartet == [], "waehrend der Kompression faengt die Automatik nichts Neues an"
def test_job_offen_zaehlt_die_kompression_mit():
from rippy.store import JOB_OFFEN
assert "transcoding" in JOB_OFFEN, "die Kompression gehoert zum Vorgang"
assert "canceling" in JOB_OFFEN, "ein Abbruch ist auch noch nicht durch"
assert "completed" not in JOB_OFFEN and "failed" not in JOB_OFFEN
def test_auswurf_bleibt_waehrend_der_kompression_erlaubt():
"""Waechter gegen einen zu breiten Riegel: `has_active_job` fragt „haelt
jemand das Laufwerk?" — waehrend der Kompression tut das niemand, die Disc
darf raus. Nur die AUTOMATIK haelt sich zurueck."""
import inspect
from rippy import store
assert "transcoding" not in inspect.getsource(store.has_active_job)
+46
View File
@@ -780,6 +780,48 @@ def write_abcde_config(output_dir: str) -> str:
return tmp.name
def disc_fehlt(device_path: str, zustand=None) -> str:
"""Liegt ueberhaupt eine Disc drin? Klartext-Grund oder "" (alles gut).
## Warum vorher nachgesehen wird (Befund 29.08.2026)
Der Commander bekam:
makemkvcon endete mit Code 11 — letzte Meldung:
Das Öffnen der Disk schlug fehl — keine MKV-Datei entstanden
Auffaellig war, was FEHLTE: kein „Ursache:". Waere der Geraetepfad schuld
gewesen, stuende dort MSG 2024. Bleibt: kein Datentraeger im Laufwerk.
Nach einem fertigen Rip wirft Rippy die Disc aus — ein zweiter Versuch
trifft dann ein leeres Laufwerk, laeuft zwei Minuten in makemkvcon hinein
und meldet einen Satz, aus dem niemand das schliessen kann.
Nachsehen kostet Millisekunden und erspart genau diesen Satz.
⚠️ Ein FEHLGESCHLAGENER Blick ist kein Grund, den Rip zu verweigern. Wer
das Laufwerk gerade nicht lesen kann, weiss nicht, dass es leer ist — und
„ich weiss es nicht" darf nie zu „es geht nicht" werden.
"""
if zustand is None:
try:
from rippy import drives as _schicht
zustand = _schicht.treiber().drive_status
except Exception: # noqa: BLE001
return ""
try:
stand = zustand(device_path)
except Exception: # noqa: BLE001
return ""
if stand == CDS_NO_DISC:
return ("Kein Datentraeger im Laufwerk %s. Nach einem fertigen Rip "
"wirft Rippy die Disc aus — fuer einen neuen Rip muss sie "
"wieder hinein." % device_path)
if stand == CDS_TRAY_OPEN:
return "Die Schublade von %s ist offen." % device_path
return ""
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all",
log_cb=None) -> dict:
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt.
@@ -794,6 +836,10 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
if not check_makemkv_installed():
return {"status": "error", "error": "makemkvcon ist nicht installiert"}
fehlt = disc_fehlt(device_path)
if fehlt:
return {"status": "error", "error": fehlt}
os.makedirs(output_dir, exist_ok=True)
try:
+67
View File
@@ -519,3 +519,70 @@ def test_handbrake_kommando_mit_sprachauswahl():
assert cmd[cmd.index("--audio-lang-list") + 1] == "deu,jpn"
assert cmd[cmd.index("--subtitle-lang-list") + 1] == "deu"
assert "--first-audio" in cmd and "--all-subtitles" in cmd
# ── Erst nachsehen, dann rippen (Befund 29.08.2026) ─────────────────────
#
# Commander: „makemkvcon endete mit Code 11 — letzte Meldung: Das Öffnen der
# Disk schlug fehl — keine MKV-Datei entstanden"
#
# Auffaellig war, was FEHLTE: kein „Ursache:". Waere der Geraetepfad schuld
# gewesen, stuende dort MSG 2024. Bleibt: kein Datentraeger. Nach einem
# fertigen Rip wirft Rippy die Disc aus — ein zweiter Versuch trifft dann ein
# leeres Laufwerk, laeuft zwei Minuten in makemkvcon hinein und meldet einen
# Satz, aus dem niemand das schliessen kann.
def test_ein_leeres_laufwerk_wird_vorher_erkannt():
import ripping
from rippy.drives.linux import CDS_NO_DISC
grund = ripping.disc_fehlt("G:", zustand=lambda p: CDS_NO_DISC)
assert "Kein Datentraeger" in grund
assert "wirft Rippy die Disc aus" in grund, "der Grund muss erklaert sein"
def test_offene_schublade_auch():
import ripping
from rippy.drives.linux import CDS_TRAY_OPEN
assert "Schublade" in ripping.disc_fehlt("G:", zustand=lambda p: CDS_TRAY_OPEN)
def test_mit_disc_wird_nicht_gemeckert():
import ripping
from rippy.drives.linux import CDS_DISC_OK
assert ripping.disc_fehlt("G:", zustand=lambda p: CDS_DISC_OK) == ""
def test_ein_fehlgeschlagener_blick_verweigert_den_rip_NICHT():
"""„Ich weiss es nicht" darf nie zu „es geht nicht" werden. Wer das
Laufwerk gerade nicht lesen kann, weiss nicht, dass es leer ist."""
import ripping
def wirft(p):
raise OSError(5, "Zugriff verweigert")
assert ripping.disc_fehlt("G:", zustand=wirft) == ""
def test_run_makemkv_sieht_vorher_nach():
"""Waechter: Die Pruefung muss VOR dem Prozessstart stehen, sonst laeuft
man weiterhin zwei Minuten ins Leere."""
import inspect
import ripping
quelle = inspect.getsource(ripping.run_makemkv)
vor_popen = quelle.index("subprocess.Popen")
assert "disc_fehlt(" in quelle[:vor_popen], \
"die Disc-Pruefung gehoert vor den makemkvcon-Start"
def test_die_sammelmeldung_5010_wird_erklaert():
"""5010 ist MakeMKVs „ging nicht" und sagt fuer sich nichts."""
from rippy.rip.makemkv_aufruf import KRITISCHE_CODES
assert 5010 in KRITISCHE_CODES
assert "Disc" in KRITISCHE_CODES[5010]