"""Tray-Symbol für den NATIVEN Windows-Worker (nicht im Docker-Image genutzt). Zeigt den Worker-Status unten rechts neben der Uhr und bietet: Start/Stopp, Rippy im Browser öffnen, Log anzeigen, Beenden. Gestartet wird es über start-tray.bat (setzt die Verbindungs-Umgebung und ruft pythonw.exe auf — kein Konsolenfenster). Der Worker-Prozess (celery, Queue transcode) läuft als Kind-Prozess und stoppt mit dem Tray. Abhängigkeiten pystray + Pillow installiert NUR der Windows-Installer (install.ps1) — das Docker-Image bleibt davon unberührt. """ import os import subprocess import sys import webbrowser try: import pystray from PIL import Image, ImageDraw except ImportError: print("Tray braucht pystray + Pillow — install.ps1 erneut ausführen.") sys.exit(1) BASIS = os.path.dirname(os.path.abspath(__file__)) RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "") WORKER_NAME = os.getenv("WORKER_NAME", "windows-worker") def _log_pfad() -> str: """Wohin das Worker-Log geschrieben wird. NICHT ins Programmverzeichnis (Befund 25.07.2026, Commander-Wunsch): Der Worker wird jetzt standardmäßig nach `C:\\Program Files\\Rippy Worker` installiert, und dort darf ein normaler Benutzer nicht schreiben. Ein Log neben dem Programm hätte den Worker beim Starten scheitern lassen — genau die Art Fehler, die man erst merkt, wenn nichts passiert. Deshalb ins Benutzerprofil (`%LOCALAPPDATA%\\Rippy Worker`), wie es sich für veränderliche Daten gehört. Nur falls das nicht klappt, bleibt das Programmverzeichnis der Rückfall — bei einer Installation ins Benutzerprofil (frei wählbar) ist es ohnehin beschreibbar. """ basis_daten = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") or "" if basis_daten: ordner = os.path.join(basis_daten, "Rippy Worker") try: os.makedirs(ordner, exist_ok=True) return os.path.join(ordner, "worker.log") except OSError: pass return os.path.join(BASIS, "worker.log") LOG_PFAD = _log_pfad() prozess = None def disc_bild(laeuft: bool) -> Image.Image: """Disc-Symbol im Rippy-Look: Indigo wenn der Worker läuft, grau sonst.""" bild = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) zeichner = ImageDraw.Draw(bild) farbe = (99, 102, 241, 255) if laeuft else (125, 132, 145, 255) zeichner.ellipse([4, 4, 60, 60], fill=farbe) zeichner.ellipse([24, 24, 40, 40], fill=(255, 255, 255, 255)) return bild def worker_laeuft() -> bool: return prozess is not None and prozess.poll() is None def worker_starten(icon=None, item=None): global prozess if worker_laeuft(): return celery = os.path.join(BASIS, "venv", "Scripts", "celery.exe") log = open(LOG_PFAD, "a", encoding="utf-8", errors="replace") prozess = subprocess.Popen( [celery, "-A", "celery_app", "worker", "--loglevel=info", "-Q", "transcode", "--pool=solo", "-n", f"{WORKER_NAME}@%h"], cwd=BASIS, stdout=log, stderr=subprocess.STDOUT, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) if icon: icon.icon = disc_bild(True) icon.update_menu() def worker_stoppen(icon=None, item=None): global prozess if worker_laeuft(): prozess.terminate() try: prozess.wait(15) except subprocess.TimeoutExpired: prozess.kill() prozess = None if icon: icon.icon = disc_bild(False) icon.update_menu() def umschalten(icon, item): if worker_laeuft(): worker_stoppen(icon) else: worker_starten(icon) def rippy_oeffnen(icon, item): if RIPPY_HOST: webbrowser.open(f"http://{RIPPY_HOST}") def log_anzeigen(icon, item): if os.path.exists(LOG_PFAD): os.startfile(LOG_PFAD) # noqa: S606 — bewusst: Standard-Editor des Nutzers def beenden(icon, item): worker_stoppen() icon.stop() menue = pystray.Menu( pystray.MenuItem( lambda item: f'Rippy-Worker "{WORKER_NAME}": {"läuft" if worker_laeuft() else "gestoppt"}', None, enabled=False, ), pystray.Menu.SEPARATOR, pystray.MenuItem( lambda item: "Worker stoppen" if worker_laeuft() else "Worker starten", umschalten, ), pystray.MenuItem("Rippy öffnen", rippy_oeffnen), pystray.MenuItem("Log anzeigen", log_anzeigen), pystray.Menu.SEPARATOR, pystray.MenuItem("Beenden (stoppt den Worker)", beenden), ) if __name__ == "__main__": tray = pystray.Icon("rippy-worker", disc_bild(False), "Rippy Worker", menue) worker_starten() tray.icon = disc_bild(worker_laeuft()) tray.run()