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>
203 lines
7.8 KiB
Python
203 lines
7.8 KiB
Python
"""Baut RippySetup.exe — eine Datei, die installiert, laeuft und sich entfernt.
|
|
|
|
## Aufruf
|
|
|
|
python packaging/windows/build.py [--ausgabe ORDNER] [--version 2.0.0]
|
|
|
|
Voraussetzungen (in der Bau-Umgebung, nicht beim Nutzer):
|
|
* Python 3.12 mit den API-Abhaengigkeiten (docker/api/requirements.txt)
|
|
* pyinstaller, pystray, pillow
|
|
* Das UI muss gebaut sein: cd docker/ui && npm run build
|
|
|
|
## Warum die Versions-Ressource nicht optional ist
|
|
|
|
Ohne sie steht im Taskmanager unter „Beschreibung" nichts, und in den
|
|
Datei-Eigenschaften fehlt jede Angabe zu Herausgeber und Version. Eine EXE
|
|
ohne diese Angaben sieht aus wie etwas, das man nicht ausfuehren sollte —
|
|
und Windows SmartScreen sieht das genauso.
|
|
|
|
Der Commander hat ausdruecklich einen Eintrag im Taskmanager gewuenscht.
|
|
Der Prozessname kommt vom Dateinamen (`Rippy.exe`), die BESCHREIBUNG
|
|
daneben aus dieser Ressource.
|
|
|
|
## Was mit hinein muss
|
|
|
|
docker/api/ die API-Module (main.py und Nachbarn)
|
|
docker/ui/dist/ die gebaute Weboberflaeche
|
|
src/rippy/ der Kern (kommt ueber den Import automatisch mit)
|
|
rippy.ico Symbol fuer Tray, Verknuepfung und Programmliste
|
|
|
|
`docker/api` liegt im Paket unter `api/`, `docker/ui/dist` unter `ui/` —
|
|
genau dort sucht `rippy/daemon.py` sie, wenn `sys._MEIPASS` gesetzt ist.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
REPO = os.path.dirname(os.path.dirname(HIER))
|
|
|
|
VERSION_VORLAGE = """
|
|
VSVersionInfo(
|
|
ffi=FixedFileInfo(
|
|
filevers=({v0}, {v1}, {v2}, 0),
|
|
prodvers=({v0}, {v1}, {v2}, 0),
|
|
mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0,
|
|
date=(0, 0)
|
|
),
|
|
kids=[
|
|
StringFileInfo([
|
|
StringTable(u'040704b0', [
|
|
StringStruct(u'CompanyName', u'Rippy'),
|
|
StringStruct(u'FileDescription', u'Rippy — automatisches Ripping'),
|
|
StringStruct(u'FileVersion', u'{version}'),
|
|
StringStruct(u'InternalName', u'Rippy'),
|
|
StringStruct(u'OriginalFilename', u'Rippy.exe'),
|
|
StringStruct(u'ProductName', u'Rippy'),
|
|
StringStruct(u'ProductVersion', u'{version}')])
|
|
]),
|
|
# 0x0407 = Deutsch, 0x04b0 = Unicode. Ohne den passenden Eintrag zeigt
|
|
# Windows die Angaben je nach Systemsprache gar nicht an.
|
|
VarFileInfo([VarStruct(u'Translation', [1031, 1200])])
|
|
]
|
|
)
|
|
"""
|
|
|
|
|
|
def versionsdatei(version: str, ordner: str) -> str:
|
|
teile = (version.split("-")[0].split(".") + ["0", "0", "0"])[:3]
|
|
pfad = os.path.join(ordner, "version_info.txt")
|
|
with open(pfad, "w", encoding="utf-8") as f:
|
|
f.write(VERSION_VORLAGE.format(
|
|
version=version, v0=int(teile[0]), v1=int(teile[1]), v2=int(teile[2])))
|
|
return pfad
|
|
|
|
|
|
def pruefen() -> list:
|
|
"""Alles da? Erst pruefen, dann bauen — wie install.sh es macht.
|
|
|
|
Ein Bau, der auf halber Strecke abbricht, hinterlaesst eine EXE, die
|
|
aussieht wie fertig und es nicht ist.
|
|
"""
|
|
maengel = []
|
|
if sys.platform != "win32":
|
|
maengel.append("Eine Windows-.exe laesst sich nur unter Windows bauen.")
|
|
if not os.path.isfile(os.path.join(REPO, "docker", "api", "main.py")):
|
|
maengel.append("docker/api/main.py fehlt.")
|
|
if not os.path.isfile(os.path.join(REPO, "docker", "worker", "ablauf.py")):
|
|
maengel.append("docker/worker/ablauf.py fehlt — ohne den Ablauf kann "
|
|
"die App nicht rippen.")
|
|
if not os.path.isdir(os.path.join(REPO, "docker", "ui", "dist")):
|
|
maengel.append(
|
|
"docker/ui/dist fehlt — die Oberflaeche ist nicht gebaut. "
|
|
"Abhilfe: cd docker/ui && npm ci && npm run build")
|
|
if not os.path.isfile(os.path.join(REPO, "deploy", "worker-windows", "rippy.ico")):
|
|
maengel.append("deploy/worker-windows/rippy.ico fehlt.")
|
|
try:
|
|
import PyInstaller # noqa: F401
|
|
except ImportError:
|
|
maengel.append("pyinstaller fehlt (pip install pyinstaller).")
|
|
for modul, paket in (("pystray", "pystray"), ("PIL", "pillow")):
|
|
try:
|
|
__import__(modul)
|
|
except ImportError:
|
|
maengel.append(f"{modul} fehlt (pip install {paket}) — ohne das kein Tray-Symbol.")
|
|
return maengel
|
|
|
|
|
|
def bauen(ausgabe: str, version: str) -> str:
|
|
arbeit = os.path.join(ausgabe, "_bau")
|
|
os.makedirs(arbeit, exist_ok=True)
|
|
|
|
trenner = ";" # Windows-Trennzeichen fuer --add-data
|
|
befehl = [
|
|
sys.executable, "-m", "PyInstaller",
|
|
"--noconfirm", "--clean",
|
|
"--onefile",
|
|
# --windowed waere schoener (kein Konsolenfenster), macht aber jede
|
|
# Ausgabe unsichtbar — auch Fehlermeldungen beim Installieren. Der
|
|
# Installer laeuft deshalb MIT Konsole; der Dienst startet spaeter
|
|
# ohne Fenster (CREATE_NO_WINDOW in winlauf.py).
|
|
"--console",
|
|
"--name", "RippySetup",
|
|
"--icon", os.path.join(REPO, "deploy", "worker-windows", "rippy.ico"),
|
|
"--version-file", versionsdatei(version, arbeit),
|
|
"--distpath", ausgabe,
|
|
"--workpath", os.path.join(arbeit, "work"),
|
|
"--specpath", arbeit,
|
|
"--paths", os.path.join(REPO, "src"),
|
|
"--paths", os.path.join(REPO, "docker", "api"),
|
|
"--paths", os.path.join(REPO, "docker", "worker"),
|
|
"--add-data", os.path.join(REPO, "docker", "api") + trenner + "api",
|
|
# Die Worker-Module — ohne sie kann Rippy anzeigen, aber nicht rippen.
|
|
# ablauf.py, ripping.py, medien.py, caps.py und Nachbarn.
|
|
"--add-data", os.path.join(REPO, "docker", "worker") + trenner + "worker",
|
|
"--add-data", os.path.join(REPO, "docker", "ui", "dist") + trenner + "ui",
|
|
"--add-data", os.path.join(REPO, "deploy", "worker-windows", "rippy.ico") + trenner + ".",
|
|
# Was PyInstaller nicht von allein findet: dynamisch importierte Module.
|
|
"--hidden-import", "uvicorn.logging",
|
|
"--hidden-import", "uvicorn.loops.auto",
|
|
"--hidden-import", "uvicorn.protocols.http.auto",
|
|
"--hidden-import", "uvicorn.protocols.websockets.auto",
|
|
"--hidden-import", "uvicorn.lifespan.on",
|
|
"--hidden-import", "rippy.drives.windows",
|
|
"--hidden-import", "pystray._win32",
|
|
# Nicht mitnehmen: Der Windows-Treiber braucht kein psycopg2/celery,
|
|
# und beides zieht Ballast nach.
|
|
"--exclude-module", "psycopg2",
|
|
"--exclude-module", "tkinter",
|
|
os.path.join(REPO, "packaging", "windows", "einstieg.py"),
|
|
]
|
|
|
|
print("PyInstaller wird aufgerufen …\n")
|
|
ergebnis = subprocess.run(befehl, cwd=REPO)
|
|
if ergebnis.returncode != 0:
|
|
raise SystemExit("PyInstaller ist gescheitert (Code %d)." % ergebnis.returncode)
|
|
|
|
exe = os.path.join(ausgabe, "RippySetup.exe")
|
|
if not os.path.isfile(exe):
|
|
raise SystemExit("PyInstaller meldete Erfolg, aber %s gibt es nicht." % exe)
|
|
return exe
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
p = argparse.ArgumentParser(description="Baut RippySetup.exe")
|
|
p.add_argument("--ausgabe", default=os.path.join(REPO, "dist", "windows"))
|
|
p.add_argument("--version", default=os.environ.get("RIPPY_VERSION", "2.0.0"))
|
|
p.add_argument("--nur-pruefen", action="store_true")
|
|
args = p.parse_args(argv)
|
|
|
|
print("Rippy — Windows-Bau")
|
|
print(" Repo %s" % REPO)
|
|
print(" Ausgabe %s" % args.ausgabe)
|
|
print(" Version %s" % args.version)
|
|
print()
|
|
|
|
maengel = pruefen()
|
|
if maengel:
|
|
print("Der Bau kann so nicht laufen:")
|
|
for m in maengel:
|
|
print(" X %s" % m)
|
|
return 1
|
|
print("Alle Voraussetzungen erfuellt.")
|
|
if args.nur_pruefen:
|
|
return 0
|
|
|
|
os.makedirs(args.ausgabe, exist_ok=True)
|
|
exe = bauen(args.ausgabe, args.version)
|
|
|
|
groesse = os.path.getsize(exe) / 1024**2
|
|
print()
|
|
print("Fertig: %s (%.1f MB)" % (exe, groesse))
|
|
|
|
arbeit = os.path.join(args.ausgabe, "_bau")
|
|
shutil.rmtree(arbeit, ignore_errors=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|