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:
co-authored by
Claude Opus 5
parent
f6f0a4ccb6
commit
f0f719ca12
+4
-1
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user