Ampel / ampel (push) Failing after 48s
WAS: Der Ablauf ist aus tasks.py heraus (ablauf.py, ohne Celery), die
LocalQueue wird bedient, ein Laeufer arbeitet Auftraege im selben Prozess
ab, und Rippy meldet sich mit gemessenen Faehigkeiten selbst als Arbeiter.
WARUM: "Rippy fuer Windows soll standalone funktionieren" (Commander). Bis
hierher konnte die Windows-App alles ANZEIGEN und nichts TUN — ein Rip waere
eingereiht worden und fuer immer liegengeblieben, weil niemand ihn holt.
DIE TRENNUNG: rip_disc hing an GENAU DREI Celery-Stellen in 234 Zeilen —
self.update_state, _transcode_queue, transcode_files.apply_async. Alle drei
sind Fragen der ZUSTELLUNG, nicht des Ablaufs. Sie sind jetzt Rueckrufe:
tasks.py reicht die Celery-Fassung herein, standalone.py die lokale. OHNE
Rueckruf komprimiert derselbe Prozess weiter — genau das, was ein
Ein-Prozess-Rippy braucht. Der Ablauf selbst ist Zeile fuer Zeile derselbe;
der Docker-Betrieb merkt vom Umbau nichts (Task-Namen, Argumente, Queues
unveraendert).
EINE ZUSTELL-STELLE statt drei: celery_client.abschicken() bedient alle
Auftragsarten. Vorher rief jede Stelle send_task selbst auf — der
Standalone-Betrieb haette an drei Stellen umgebogen werden muessen, beim
naechsten Auftragstyp an einer vierten.
WEITERER BLOCKER GEFUNDEN: ablauf.py holte detect_disc_type fest aus dem
LINUX-Treiber, in einem try/except. Unter Windows waere es damit IMMER None
gewesen und Rippen "hart verriegelt" — Rippy haette alles angezeigt und
nichts gerippt, ohne dass irgendwo ein Fehler stuende. Jetzt fragt es den
Treiber-Port.
WERKZEUGE: ripping.py und caps.py suchten nur im PATH. Auf dem Commander-PC
gemessen, vorher/nachher:
vorher check_makemkv_installed() -> False (obwohl installiert)
erkenne_encoder() -> nur CPU
nachher MakeMKV 1.18.4 C:\Program Files (x86)\MakeMKV\makemkvcon64.exe
HandBrake 1.11.2 ueber die API geholt, in 2,7 s
Encoder cpu-x264, cpu-x265, cpu-av1, VCE, VCE-AV1
107 Presets, Ryzen 7 9700X, 16 Kerne, avx512f
VCE ist die Hardwarebeschleunigung der Radeon — die hat Rippy auf diesem
Rechner vorher nie gesehen, weil es HandBrake gar nicht fand.
NEUE ROUTEN: GET /system/werkzeuge (was liegt wo, in welcher Fassung, gibt
es Neueres) und POST /system/werkzeuge/{name}/holen. HandBrake kommt
vollautomatisch von GitHub. MakeMKV wird NICHT mitgeliefert — Rippy laedt
die offizielle Datei und startet sie (Black-Box-Trennung, KONZEPT.md § 6).
makemkv.com antwortete beim Bauen mit HTTP 525; das wird im Klartext
gemeldet, und eine selbst geholte Datei bleibt moeglich.
HERZSCHLAG: /capabilities las die workers-Tabelle, die bisher nur der
Celery-Herzschlag fuellte. Im Standalone-Betrieb stand dort "0 Worker" und
die Encoder-Auswahl im UI blieb LEER — auf einem Rechner, der alles kann.
Jetzt meldet sich der Prozess selbst, mit dem, was caps.py MISST.
GEMESSEN, aus der fertigen EXE (29,0 MB):
bereit nach 1 s, keine Fehler im Log
Werkzeuge: beide gefunden, mit Version und Pfad
Worker: 1 (TobisNicerPC), 5 Encoder, 107 Presets
Die ganze Kette ist als Test festgehalten (test_kette.py): zustellen ->
einreihen -> Laeufer -> ablauf -> Job endet in einem EHRLICHEN Zustand.
Ohne Laufwerk geprueft, und das ist der wichtigere Fall: Ein Rip auf ein
totes Geraet muss zuegig scheitern, nicht auf "pending" haengenbleiben.
GEMESSEN: ruff sauber, 512 Tests gruen + 15 uebersprungen (vorher 489).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""Tests für die Phasen-Erkennung — was darf der „Neu"-Knopf anbieten?"""
|
|
|
|
import json
|
|
|
|
import phasen
|
|
|
|
|
|
def test_laufender_job_bietet_nichts():
|
|
assert phasen.retry_art({"status": "running"}) == phasen.NICHTS
|
|
assert phasen.retry_art({"status": "completed"}) == phasen.NICHTS
|
|
|
|
|
|
def test_toter_transcode_bietet_komprimieren():
|
|
job = {"status": "failed", "meta": json.dumps({"rip_fertig": True})}
|
|
assert phasen.retry_art(job) == phasen.NEU_KOMPRIMIEREN
|
|
|
|
|
|
def test_toter_rip_bietet_rippen():
|
|
"""Der Vorfall vom 26.07.2026: 5,1 GB von 40 GB. Komprimieren wäre falsch."""
|
|
job = {"status": "failed", "meta": json.dumps({"rip_fertig": False, "year": 1988})}
|
|
assert phasen.retry_art(job) == phasen.NEU_RIPPEN
|
|
|
|
|
|
def test_bestandsjob_ohne_marke_ist_unklar():
|
|
"""Jobs von VOR dieser Änderung dürfen nicht geraten werden."""
|
|
assert phasen.retry_art({"status": "failed", "meta": None}) == phasen.UNKLAR
|
|
assert phasen.retry_art({"status": "failed"}) == phasen.UNKLAR
|
|
assert phasen.retry_art(
|
|
{"status": "failed", "meta": json.dumps({"year": 1988})}
|
|
) == phasen.UNKLAR
|
|
|
|
|
|
def test_kaputtes_json_ist_unklar_statt_absturz():
|
|
"""/jobs darf an einer krummen meta-Zeile nicht scheitern."""
|
|
assert phasen.retry_art({"status": "failed", "meta": "{kein json"}) == phasen.UNKLAR
|
|
assert phasen.retry_art({"status": "failed", "meta": "[1,2]"}) == phasen.UNKLAR
|
|
assert phasen.retry_art({"status": "failed", "meta": 7}) == phasen.UNKLAR
|
|
|
|
|
|
def test_marke_wird_auch_als_dict_gelesen():
|
|
"""Der Detail-Endpunkt hat die Metadaten schon geparst."""
|
|
job = {"status": "failed", "meta": {"rip_fertig": True}}
|
|
assert phasen.retry_art(job) == phasen.NEU_KOMPRIMIEREN
|
|
|
|
|
|
def test_der_schluessel_heisst_im_worker_genauso():
|
|
"""Zwei Container, kein geteiltes Paket — die Marke muss zusammenpassen.
|
|
|
|
Ohne diese Prüfung ist ein Tippfehler in einer der beiden Dateien lautlos:
|
|
Der Worker schreibt `rip_fertig`, die API liest `ripFertig`, und JEDER Job
|
|
wäre für immer „unklar". Genau diese Sorte Auseinanderdriften hat die
|
|
Zombie-Erkennung ein Release lang blind gemacht („running" vs. „ripping").
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
quelle = (
|
|
# Seit V2-4 steht der Ablauf in ablauf.py; tasks.py ist nur noch
|
|
# die Celery-Huelle. Dieser Waechter muss dorthin schauen, wo der
|
|
# Code WIRKLICH steht — sonst prueft er eine leere Datei und ist
|
|
# gruen, ohne etwas zu beweisen.
|
|
pathlib.Path(__file__).resolve().parents[1] / "worker" / "ablauf.py"
|
|
).read_text(encoding="utf-8")
|
|
# Der Worker schreibt die Marke über eine Konstante — deren Wert muss hier
|
|
# ankommen.
|
|
treffer = re.search(r'^RIP_FERTIG\s*=\s*"([^"]+)"', quelle, re.MULTILINE)
|
|
assert treffer, "worker/tasks.py definiert RIP_FERTIG nicht mehr"
|
|
assert treffer.group(1) == phasen.RIP_FERTIG
|