fix: ruff linter errors (multiple statements, bare except, unused vars)
Ampel / ampel (push) Failing after 38s

This commit is contained in:
Hitonabi
2026-07-27 17:00:52 +02:00
parent a32a1bb075
commit 061f79ecda
2 changed files with 1159 additions and 870 deletions
+198 -57
View File
@@ -4,9 +4,9 @@ import subprocess
import threading
import urllib.request
import json
import time
import zipfile
def main(page: ft.Page):
page.title = "Rippy Encoding-Worker Installation"
page.window_width = 800
@@ -26,43 +26,77 @@ def main(page: ft.Page):
std_ziel = r"C:\Program Files\Rippy Worker"
# UI Fields
txt_host = ft.TextField(label="LAN-IP der Rippy-Maschine (Docker-Host)", width=400, bgcolor=c_panel, color=c_text)
txt_name = ft.TextField(label="Name dieses Workers (frei wählbar)", width=250, value=os.environ.get("COMPUTERNAME", "worker").lower(), bgcolor=c_panel, color=c_text)
txt_host = ft.TextField(
label="LAN-IP der Rippy-Maschine (Docker-Host)",
width=400,
bgcolor=c_panel,
color=c_text,
)
txt_name = ft.TextField(
label="Name dieses Workers (frei wählbar)",
width=250,
value=os.environ.get("COMPUTERNAME", "worker").lower(),
bgcolor=c_panel,
color=c_text,
)
txt_slots = ft.Dropdown(
label="Kerne (Gleichzeitig)",
width=150,
options=[ft.dropdown.Option(str(i)) for i in range(1, 9)],
value="2" if os.cpu_count() and os.cpu_count() >= 12 else "1",
bgcolor=c_panel,
color=c_text
color=c_text,
)
txt_pfad = ft.TextField(label="Installieren nach", value=std_ziel, expand=True, bgcolor=c_panel, color=c_text)
txt_map = ft.TextField(label="Netzwerk-Freigabe mit den Rohdaten", expand=True, bgcolor=c_panel, color=c_text)
txt_pfad = ft.TextField(
label="Installieren nach",
value=std_ziel,
expand=True,
bgcolor=c_panel,
color=c_text,
)
txt_map = ft.TextField(
label="Netzwerk-Freigabe mit den Rohdaten",
expand=True,
bgcolor=c_panel,
color=c_text,
)
chk_auto = ft.Checkbox(label="Beim Anmelden automatisch starten (Hintergrund)", value=True, fill_color=c_accent)
chk_desktop = ft.Checkbox(label="Verknüpfung auf dem Desktop anlegen", value=True, fill_color=c_accent)
chk_auto = ft.Checkbox(
label="Beim Anmelden automatisch starten (Hintergrund)",
value=True,
fill_color=c_accent,
)
chk_desktop = ft.Checkbox(
label="Verknüpfung auf dem Desktop anlegen", value=True, fill_color=c_accent
)
log_view = ft.ListView(height=150, auto_scroll=True, spacing=2)
def log(msg: str):
log_view.controls.append(ft.Text(msg, color=c_muted, font_family="Consolas", size=12))
log_view.controls.append(
ft.Text(msg, color=c_muted, font_family="Consolas", size=12)
)
try:
page.update()
except:
except Exception:
pass
def check_map(e):
r_host = txt_host.value.strip()
if not r_host:
page.snack_bar = ft.SnackBar(ft.Text("Bitte zuerst die LAN-IP eintragen!"), bgcolor="red")
page.snack_bar = ft.SnackBar(
ft.Text("Bitte zuerst die LAN-IP eintragen!"), bgcolor="red"
)
page.snack_bar.open = True
page.update()
return
try:
log(f"Frage Rippy nach Freigabe ({r_host})...")
with urllib.request.urlopen(f"http://{r_host}/api/worker-setup/pfad-map", timeout=5) as r:
with urllib.request.urlopen(
f"http://{r_host}/api/worker-setup/pfad-map", timeout=5
) as r:
data = json.loads(r.read().decode())
if data.get("hinweis"):
log(data["hinweis"])
@@ -95,11 +129,14 @@ def main(page: ft.Page):
python_exe = None
for cmd in ["py -3", "python"]:
try:
res = subprocess.run(f"{cmd} --version", shell=True, capture_output=True, text=True)
res = subprocess.run(
f"{cmd} --version", shell=True, capture_output=True, text=True
)
if "Python 3." in res.stdout:
python_exe = cmd
break
except: pass
except Exception:
pass
if not python_exe:
log("FEHLER: Python 3.10+ nicht gefunden.")
@@ -113,9 +150,10 @@ def main(page: ft.Page):
try:
os.makedirs(install_dir, exist_ok=True)
test_file = os.path.join(install_dir, ".test")
with open(test_file, "w") as f: f.write("x")
with open(test_file, "w") as f:
f.write("x")
os.remove(test_file)
except Exception as ex:
except Exception:
log(f"FEHLER: Keine Schreibrechte in {install_dir}")
log("Bitte als Administrator starten oder Pfad aendern.")
btn_install.disabled = False
@@ -126,8 +164,10 @@ def main(page: ft.Page):
log(f"Lade Worker-Code von {r_host}...")
try:
zip_path = os.path.join(install_dir, "worker.zip")
urllib.request.urlretrieve(f"http://{r_host}/api/worker-setup/paket", zip_path)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
urllib.request.urlretrieve(
f"http://{r_host}/api/worker-setup/paket", zip_path
)
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
os.remove(zip_path)
except Exception as ex:
@@ -139,27 +179,49 @@ def main(page: ft.Page):
# Setup venv — --clear loescht einen alten venv komplett, damit keine
# veralteten Pakete (z.B. flet 0.86 statt 0.23) ueberleben.
log("Lege Python-Umgebung an (venv)...")
subprocess.run(f"{python_exe} -m venv --clear venv", cwd=install_dir, shell=True)
subprocess.run(
f"{python_exe} -m venv --clear venv", cwd=install_dir, shell=True
)
log("Installiere Abhaengigkeiten (pip)...")
subprocess.run(r"venv\Scripts\python.exe -m pip install --quiet --upgrade pip", cwd=install_dir, shell=True)
subprocess.run(r"venv\Scripts\pip.exe install --quiet -r requirements.txt", cwd=install_dir, shell=True)
subprocess.run(r"venv\Scripts\pip.exe install --quiet requests pillow", cwd=install_dir, shell=True)
subprocess.run(
r"venv\Scripts\python.exe -m pip install --quiet --upgrade pip",
cwd=install_dir,
shell=True,
)
subprocess.run(
r"venv\Scripts\pip.exe install --quiet -r requirements.txt",
cwd=install_dir,
shell=True,
)
subprocess.run(
r"venv\Scripts\pip.exe install --quiet requests pillow",
cwd=install_dir,
shell=True,
)
# Download Handbrake
hb_path = os.path.join(install_dir, "HandBrakeCLI.exe")
if not os.path.exists(hb_path):
log("Lade HandBrakeCLI herunter...")
try:
hb_ver = "1.7.3" # Fallback if github fails
hb_ver = "1.7.3" # Fallback if github fails
try:
with urllib.request.urlopen("https://api.github.com/repos/HandBrake/HandBrake/releases/latest", timeout=5) as r:
with urllib.request.urlopen(
"https://api.github.com/repos/HandBrake/HandBrake/releases/latest",
timeout=5,
) as r:
hb_data = json.loads(r.read().decode())
if hb_data.get("tag_name"): hb_ver = hb_data["tag_name"].lstrip("v")
except: pass
if hb_data.get("tag_name"):
hb_ver = hb_data["tag_name"].lstrip("v")
except Exception:
pass
hb_zip = os.path.join(install_dir, "hb.zip")
urllib.request.urlretrieve(f"https://github.com/HandBrake/HandBrake/releases/download/{hb_ver}/HandBrakeCLI-{hb_ver}-win-x86_64.zip", hb_zip)
with zipfile.ZipFile(hb_zip, 'r') as zip_ref:
urllib.request.urlretrieve(
f"https://github.com/HandBrake/HandBrake/releases/download/{hb_ver}/HandBrakeCLI-{hb_ver}-win-x86_64.zip",
hb_zip,
)
with zipfile.ZipFile(hb_zip, "r") as zip_ref:
zip_ref.extractall(install_dir)
os.remove(hb_zip)
except Exception as ex:
@@ -167,14 +229,24 @@ def main(page: ft.Page):
log("Erstelle Start-Skripte...")
slots = txt_slots.value
pool_arg = "--pool=solo" if int(slots) <= 1 else f"--pool=threads --concurrency={slots}"
map_zeile = f"set RIPPY_PATH_MAP={txt_map.value.strip()}\r\n" if txt_map.value.strip() else ""
pool_arg = (
"--pool=solo"
if int(slots) <= 1
else f"--pool=threads --concurrency={slots}"
)
map_zeile = (
f"set RIPPY_PATH_MAP={txt_map.value.strip()}\r\n"
if txt_map.value.strip()
else ""
)
gui_bat = f"@echo off\r\ncd /d \"%~dp0\"\r\nset REDIS_URL=redis://{r_host}:6379/0\r\nset DATABASE_URL=postgresql://rippy:rippy@{r_host}:5432/rippy\r\nset API_URL=http://{r_host}:8000\r\nset WORKER_NAME={w_name}\r\nset RIPPY_TRAY_HOST={r_host}\r\nset RIPPY_SLOTS={slots}\r\nset TZ=UTC\r\n{map_zeile}set PATH=%~dp0;%PATH%\r\nstart \"\" venv\\Scripts\\pythonw.exe gui.py"
with open(os.path.join(install_dir, "start-gui.bat"), "w") as f: f.write(gui_bat)
gui_bat = f'@echo off\r\ncd /d "%~dp0"\r\nset REDIS_URL=redis://{r_host}:6379/0\r\nset DATABASE_URL=postgresql://rippy:rippy@{r_host}:5432/rippy\r\nset API_URL=http://{r_host}:8000\r\nset WORKER_NAME={w_name}\r\nset RIPPY_TRAY_HOST={r_host}\r\nset RIPPY_SLOTS={slots}\r\nset TZ=UTC\r\n{map_zeile}set PATH=%~dp0;%PATH%\r\nstart "" venv\\Scripts\\pythonw.exe gui.py'
with open(os.path.join(install_dir, "start-gui.bat"), "w") as f:
f.write(gui_bat)
work_bat = f"@echo off\r\ncd /d \"%~dp0\"\r\nset REDIS_URL=redis://{r_host}:6379/0\r\nset DATABASE_URL=postgresql://rippy:rippy@{r_host}:5432/rippy\r\nset API_URL=http://{r_host}:8000\r\nset WORKER_NAME={w_name}\r\nset RIPPY_SLOTS={slots}\r\nset TZ=UTC\r\n{map_zeile}set PATH=%~dp0;%PATH%\r\nvenv\\Scripts\\celery.exe -A celery_app worker --loglevel=info -Q transcode {pool_arg} -n {w_name}@%%h"
with open(os.path.join(install_dir, "start-worker.bat"), "w") as f: f.write(work_bat)
work_bat = f'@echo off\r\ncd /d "%~dp0"\r\nset REDIS_URL=redis://{r_host}:6379/0\r\nset DATABASE_URL=postgresql://rippy:rippy@{r_host}:5432/rippy\r\nset API_URL=http://{r_host}:8000\r\nset WORKER_NAME={w_name}\r\nset RIPPY_SLOTS={slots}\r\nset TZ=UTC\r\n{map_zeile}set PATH=%~dp0;%PATH%\r\nvenv\\Scripts\\celery.exe -A celery_app worker --loglevel=info -Q transcode {pool_arg} -n {w_name}@%%h'
with open(os.path.join(install_dir, "start-worker.bat"), "w") as f:
f.write(work_bat)
# Uninstaller
uninstall_ps1 = f"""param([switch]$Force)
@@ -193,34 +265,50 @@ schtasks /delete /tn "RippyWorker" /f *>$null
Start-Process cmd -ArgumentList "/c ping 127.0.0.1 -n 3 >nul & rmdir /s /q `"$PSScriptRoot`"" -WindowStyle Hidden
if (-not $Force) {{ [System.Windows.Forms.MessageBox]::Show("Rippy-Worker entfernt.", "Rippy Worker") }}
"""
with open(os.path.join(install_dir, "uninstall.ps1"), "w", encoding="utf-8") as f: f.write(uninstall_ps1)
with open(
os.path.join(install_dir, "uninstall.ps1"), "w", encoding="utf-8"
) as f:
f.write(uninstall_ps1)
# Autostart
if chk_auto.value:
log("Richte Autostart (Task Scheduler) ein...")
bat_path = os.path.join(install_dir, "start-gui.bat")
subprocess.run(f'schtasks /create /tn "RippyWorker" /tr "\\"{bat_path}\\"" /sc ONLOGON /rl HIGHEST /f', shell=True, capture_output=True)
subprocess.run(
f'schtasks /create /tn "RippyWorker" /tr "\\"{bat_path}\\"" /sc ONLOGON /rl HIGHEST /f',
shell=True,
capture_output=True,
)
# Desktop Shortcut — ueber PowerShell/COM, weil win32com im
# PyInstaller-Bundle nicht enthalten ist.
if chk_desktop.value:
try:
public_desktop = os.path.join(os.environ.get("PUBLIC", r"C:\Users\Public"), "Desktop")
public_desktop = os.path.join(
os.environ.get("PUBLIC", r"C:\Users\Public"), "Desktop"
)
user_desktop = os.path.join(os.path.expanduser("~"), "Desktop")
desktop = public_desktop if "Program Files" in install_dir and os.path.isdir(public_desktop) else user_desktop
desktop = (
public_desktop
if "Program Files" in install_dir and os.path.isdir(public_desktop)
else user_desktop
)
lnk_path = os.path.join(desktop, "Rippy Worker.lnk")
bat_target = os.path.join(install_dir, "start-gui.bat")
ico_path = os.path.join(install_dir, "rippy.ico")
ps_cmd = (
f'$ws = New-Object -ComObject WScript.Shell; '
f'$sc = $ws.CreateShortcut(\"{lnk_path}\"); '
f'$sc.TargetPath = \"{bat_target}\"; '
f'$sc.WorkingDirectory = \"{install_dir}\"; '
f'$sc.IconLocation = \"{ico_path}\"; '
f'$sc.WindowStyle = 7; '
f'$sc.Save()'
f"$ws = New-Object -ComObject WScript.Shell; "
f'$sc = $ws.CreateShortcut("{lnk_path}"); '
f'$sc.TargetPath = "{bat_target}"; '
f'$sc.WorkingDirectory = "{install_dir}"; '
f'$sc.IconLocation = "{ico_path}"; '
f"$sc.WindowStyle = 7; "
f"$sc.Save()"
)
subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True,
)
subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True)
log("Desktop-Verknüpfung erstellt.")
except Exception as e:
log(f"Warnung: Shortcut konnte nicht erstellt werden: {e}")
@@ -239,8 +327,23 @@ if (-not $Force) {{ [System.Windows.Forms.MessageBox]::Show("Rippy-Worker entfer
os.startfile(bat_path)
page.window_close()
btn_install = ft.ElevatedButton("Installieren", on_click=do_install, bgcolor=c_accent, color=ft.colors.WHITE, width=200, height=40)
btn_start = ft.ElevatedButton("Worker starten", on_click=start_worker, bgcolor=c_success, color=ft.colors.WHITE, width=200, height=40, visible=False)
btn_install = ft.ElevatedButton(
"Installieren",
on_click=do_install,
bgcolor=c_accent,
color=ft.colors.WHITE,
width=200,
height=40,
)
btn_start = ft.ElevatedButton(
"Worker starten",
on_click=start_worker,
bgcolor=c_success,
color=ft.colors.WHITE,
width=200,
height=40,
visible=False,
)
def get_dir(e: ft.FilePickerResultEvent):
if e.path:
@@ -251,21 +354,59 @@ if (-not $Force) {{ [System.Windows.Forms.MessageBox]::Show("Rippy-Worker entfer
page.overlay.append(dir_picker)
page.add(
ft.Row([
ft.Icon(ft.icons.DISC_FULL, size=40, color=c_accent),
ft.Text("Rippy Encoding-Worker", size=28, weight=ft.FontWeight.BOLD, color=c_text)
]),
ft.Text("Diese Maschine übernimmt die Video-Kompression für Rippy. Gerippt wird weiter auf der Hauptmaschine.", color=c_muted),
ft.Row(
[
ft.Icon(ft.icons.DISC_FULL, size=40, color=c_accent),
ft.Text(
"Rippy Encoding-Worker",
size=28,
weight=ft.FontWeight.BOLD,
color=c_text,
),
]
),
ft.Text(
"Diese Maschine übernimmt die Video-Kompression für Rippy. Gerippt wird weiter auf der Hauptmaschine.",
color=c_muted,
),
ft.Divider(color="#334155"),
ft.Row([txt_host], alignment=ft.MainAxisAlignment.START),
ft.Row([txt_name, txt_slots], alignment=ft.MainAxisAlignment.START),
ft.Row([txt_pfad, ft.ElevatedButton("Durchsuchen ...", on_click=lambda _: dir_picker.get_directory_path("Installationsordner wählen"), bgcolor=c_panel, color=c_text)], alignment=ft.MainAxisAlignment.START),
ft.Row([txt_map, ft.ElevatedButton("Freigabe holen", on_click=check_map, bgcolor=c_panel, color=c_text)], alignment=ft.MainAxisAlignment.START),
ft.Row(
[
txt_pfad,
ft.ElevatedButton(
"Durchsuchen ...",
on_click=lambda _: dir_picker.get_directory_path(
"Installationsordner wählen"
),
bgcolor=c_panel,
color=c_text,
),
],
alignment=ft.MainAxisAlignment.START,
),
ft.Row(
[
txt_map,
ft.ElevatedButton(
"Freigabe holen", on_click=check_map, bgcolor=c_panel, color=c_text
),
],
alignment=ft.MainAxisAlignment.START,
),
chk_auto,
chk_desktop,
ft.Container(content=log_view, bgcolor="#020617", padding=10, border_radius=5, border=ft.border.all(1, "#334155")),
ft.Row([btn_install, btn_start])
ft.Container(
content=log_view,
bgcolor="#020617",
padding=10,
border_radius=5,
border=ft.border.all(1, "#334155"),
),
ft.Row([btn_install, btn_start]),
)
if __name__ == "__main__":
ft.app(target=main)
+242 -94
View File
@@ -1,6 +1,5 @@
import os
import subprocess
import sys
import threading
import time
import urllib.request
@@ -13,6 +12,7 @@ import flet as ft
try:
import pystray
from PIL import Image as PILImage
TRAY_VERFUEGBAR = True
except ImportError:
TRAY_VERFUEGBAR = False
@@ -24,6 +24,7 @@ WORKER_NAME = os.getenv("WORKER_NAME", "windows-worker")
SLOTS = max(1, int(os.getenv("RIPPY_SLOTS", "1") or "1"))
ABFRAGE_TAKT_SEKUNDEN = 4
# Log file path
def _log_pfad() -> str:
basis_daten = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") or ""
@@ -33,6 +34,7 @@ def _log_pfad() -> str:
return os.path.join(ordner, "worker.log")
return os.path.join(BASIS, "worker.log")
LOG_PFAD = _log_pfad()
# State variables
@@ -42,11 +44,13 @@ aktuell = {"job": None, "online": False}
# Windows Standby blocker
_wach = {"an": False}
def wach_halten(an: bool) -> None:
if _wach["an"] == an:
return
try:
import ctypes
ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
flags = (ES_CONTINUOUS | ES_SYSTEM_REQUIRED) if an else ES_CONTINUOUS
@@ -55,6 +59,7 @@ def wach_halten(an: bool) -> None:
except Exception:
pass
# Celery Subprocess Management
def _leser(prozess_ref, datei, log_callback) -> None:
try:
@@ -74,35 +79,53 @@ def _leser(prozess_ref, datei, log_callback) -> None:
except OSError:
pass
def worker_laeuft() -> bool:
return prozess is not None and prozess.poll() is None
def worker_starten(log_callback=None):
global prozess
if worker_laeuft():
return
celery = os.path.join(BASIS, "venv", "Scripts", "celery.exe")
if not os.path.exists(celery):
celery = "celery" # fallback to global if testing outside venv
celery = "celery" # fallback to global if testing outside venv
befehl = [
celery, "-A", "celery_app", "worker", "--loglevel=info",
"-Q", "transcode", "-n", f"{WORKER_NAME}@%h",
celery,
"-A",
"celery_app",
"worker",
"--loglevel=info",
"-Q",
"transcode",
"-n",
f"{WORKER_NAME}@%h",
]
befehl += (["--pool=solo"] if SLOTS == 1 else ["--pool=threads", f"--concurrency={SLOTS}"])
befehl += (
["--pool=solo"] if SLOTS == 1 else ["--pool=threads", f"--concurrency={SLOTS}"]
)
log = open(LOG_PFAD, "a", encoding="utf-8", errors="replace")
prozess = subprocess.Popen(
befehl, cwd=BASIS,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1, errors="replace",
befehl,
cwd=BASIS,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
errors="replace",
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
threading.Thread(
target=_leser, args=(prozess, log, log_callback),
daemon=True, name="log-leser",
target=_leser,
args=(prozess, log, log_callback),
daemon=True,
name="log-leser",
).start()
def worker_stoppen():
global prozess
if worker_laeuft():
@@ -115,6 +138,7 @@ def worker_stoppen():
wach_halten(False)
aktuell["job"] = None
# API Polling
def hole(pfad: str, host: str = None, timeout: int = 5):
ziel = host if host is not None else RIPPY_HOST
@@ -126,13 +150,14 @@ def hole(pfad: str, host: str = None, timeout: int = 5):
except Exception:
return None
def hole_job_und_zustand():
caps = hole("/capabilities")
jobs = hole("/jobs") or []
online = False
w_info = {}
for w in ((caps or {}).get("workers") or []):
for w in (caps or {}).get("workers") or []:
if w.get("name") == WORKER_NAME:
online = bool(w.get("online"))
w_info = w
@@ -149,6 +174,7 @@ def hole_job_und_zustand():
wach_halten(bool(laufend))
return online, laufend, caps, jobs, w_info
def deinstallieren() -> None:
skript = os.path.join(BASIS, "uninstall.ps1")
if not os.path.isfile(skript):
@@ -158,6 +184,7 @@ def deinstallieren() -> None:
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
def _bruecke_bauen():
try:
import db
@@ -172,9 +199,11 @@ def _bruecke_bauen():
except Exception:
return None
def _schluessel_wache(bruecke=None, log_callback=None):
try:
import schluessel
if not schluessel.makemkvcon_pfad():
return
@@ -191,11 +220,14 @@ def _schluessel_wache(bruecke=None, log_callback=None):
stand, _was = schluessel.runde(RIPPY_HOST, stand, melden=melden)
except Exception as e:
if log_callback:
log_callback(f"[schluessel] ERROR Schlüssel-Automatik fehlgeschlagen: {e}\n")
log_callback(
f"[schluessel] ERROR Schlüssel-Automatik fehlgeschlagen: {e}\n"
)
time.sleep(schluessel.TAKT_SEKUNDEN)
except ImportError:
pass
# GUI Application
def main(page: ft.Page):
page.title = f"Rippy Worker — {WORKER_NAME}"
@@ -246,8 +278,9 @@ def main(page: ft.Page):
pystray.Menu.SEPARATOR,
pystray.MenuItem("Beenden", tray_beenden),
)
icon = pystray.Icon("rippy_worker", _tray_icon_bild(),
f"Rippy Worker — {WORKER_NAME}", menu)
icon = pystray.Icon(
"rippy_worker", _tray_icon_bild(), f"Rippy Worker — {WORKER_NAME}", menu
)
tray_icon_ref["icon"] = icon
threading.Thread(target=icon.run, daemon=True, name="tray-icon").start()
@@ -267,14 +300,14 @@ def main(page: ft.Page):
tray_starten()
# Colors
c_bg = "#0f172a"
c_panel = "#1e293b"
c_text = "#e2e8f0"
c_muted = "#94a3b8"
c_accent = "#6366f1" # Indigo
c_success = "#10b981" # Emerald
c_warning = "#f59e0b" # Amber
c_danger = "#f43f5e" # Rose
c_accent = "#6366f1" # Indigo
c_success = "#10b981" # Emerald
c_warning = "#f59e0b" # Amber
c_danger = "#f43f5e" # Rose
# Formatter for job status
def job_text(job: dict) -> str:
@@ -291,12 +324,22 @@ def main(page: ft.Page):
return f"{titel}{status}"
# UI Components
status_text = ft.Text("Verbinde...", color=c_muted, size=18, weight=ft.FontWeight.BOLD)
title_text = ft.Text(f"Rippy Worker: {WORKER_NAME}", size=24, weight=ft.FontWeight.BOLD, color=c_text)
machine_info = ft.Text("System-Infos werden geladen...", color=c_muted, size=13, font_family="Consolas")
status_text = ft.Text(
"Verbinde...", color=c_muted, size=18, weight=ft.FontWeight.BOLD
)
title_text = ft.Text(
f"Rippy Worker: {WORKER_NAME}", size=24, weight=ft.FontWeight.BOLD, color=c_text
)
machine_info = ft.Text(
"System-Infos werden geladen...", color=c_muted, size=13, font_family="Consolas"
)
progress_ring = ft.ProgressRing(width=120, height=120, stroke_width=10, value=0, color=c_accent)
progress_percentage = ft.Text("0%", size=28, weight=ft.FontWeight.BOLD, color=c_text)
progress_ring = ft.ProgressRing(
width=120, height=120, stroke_width=10, value=0, color=c_accent
)
progress_percentage = ft.Text(
"0%", size=28, weight=ft.FontWeight.BOLD, color=c_text
)
job_title = ft.Text("Nichts in Arbeit", size=20, color=c_muted)
eta_text = ft.Text("Bereit", size=14, color=c_muted)
@@ -308,7 +351,7 @@ def main(page: ft.Page):
alignment=ft.alignment.center,
width=120,
height=120,
)
),
],
width=120,
height=120,
@@ -316,32 +359,53 @@ def main(page: ft.Page):
# Live Log Regex and Layout
# [2026-07-26 20:34:00,093: INFO/MainProcess] message...
LOG_REGEX = re.compile(r"^\[.*? (\d{2}:\d{2}:\d{2}),.*?: (INFO|WARNING|ERROR|SUCCESS).*?\] (.*)")
LOG_REGEX = re.compile(
r"^\[.*? (\d{2}:\d{2}:\d{2}),.*?: (INFO|WARNING|ERROR|SUCCESS).*?\] (.*)"
)
log_view = ft.ListView(expand=1, spacing=2, auto_scroll=True)
queue_view = ft.ListView(height=120, spacing=4, auto_scroll=False)
def add_log(zeile):
z = zeile.strip()
if not z: return
if not z:
return
match = LOG_REGEX.match(z)
if match:
zeit, level, msg = match.groups()
color = c_text
if level == "INFO": color = "#38bdf8" # sky-400
elif level == "WARNING": color = "#fbbf24" # amber-400
elif level == "ERROR": color = "#f87171" # red-400
elif level == "SUCCESS": color = "#34d399" # emerald-400
if level == "INFO":
color = "#38bdf8" # sky-400
elif level == "WARNING":
color = "#fbbf24" # amber-400
elif level == "ERROR":
color = "#f87171" # red-400
elif level == "SUCCESS":
color = "#34d399" # emerald-400
row = ft.Row([
ft.Text(zeit, color=c_muted, size=12, font_family="Consolas"),
ft.Text(f"[{level}]", color=color, size=12, font_family="Consolas", weight=ft.FontWeight.BOLD),
ft.Text(msg, color=c_text, size=12, font_family="Consolas", expand=True)
], spacing=10, vertical_alignment=ft.CrossAxisAlignment.START)
row = ft.Row(
[
ft.Text(zeit, color=c_muted, size=12, font_family="Consolas"),
ft.Text(
f"[{level}]",
color=color,
size=12,
font_family="Consolas",
weight=ft.FontWeight.BOLD,
),
ft.Text(
msg, color=c_text, size=12, font_family="Consolas", expand=True
),
],
spacing=10,
vertical_alignment=ft.CrossAxisAlignment.START,
)
log_view.controls.append(row)
else:
log_view.controls.append(ft.Text(z, color=c_muted, size=12, font_family="Consolas"))
log_view.controls.append(
ft.Text(z, color=c_muted, size=12, font_family="Consolas")
)
if len(log_view.controls) > 200:
log_view.controls.pop(0)
@@ -365,7 +429,7 @@ def main(page: ft.Page):
bgcolor="#1e293b" if worker_laeuft() else c_success,
color="white",
disabled=worker_laeuft(),
on_click=toggle_worker
on_click=toggle_worker,
)
def uninstall_click(e):
@@ -379,10 +443,16 @@ def main(page: ft.Page):
dlg = ft.AlertDialog(
title=ft.Text("Deinstallieren?"),
content=ft.Text(f"Worker '{WORKER_NAME}' wirklich entfernen? Das beendet den Prozess und entfernt ihn aus dem Autostart."),
content=ft.Text(
f"Worker '{WORKER_NAME}' wirklich entfernen? Das beendet den Prozess und entfernt ihn aus dem Autostart."
),
actions=[
ft.TextButton("Abbrechen", on_click=close_dlg),
ft.TextButton("Ja, Deinstallieren", on_click=do_uninstall, style=ft.ButtonStyle(color=c_danger)),
ft.TextButton(
"Ja, Deinstallieren",
on_click=do_uninstall,
style=ft.ButtonStyle(color=c_danger),
),
],
actions_alignment=ft.MainAxisAlignment.END,
)
@@ -394,63 +464,98 @@ def main(page: ft.Page):
text="Deinstallieren",
icon=ft.icons.DELETE,
icon_color=c_danger,
on_click=uninstall_click
on_click=uninstall_click,
)
btn_dashboard = ft.TextButton(
text="Rippy Dashboard",
icon=ft.icons.OPEN_IN_BROWSER,
on_click=lambda _: webbrowser.open(f"http://{RIPPY_HOST}") if RIPPY_HOST else None
on_click=lambda _: (
webbrowser.open(f"http://{RIPPY_HOST}") if RIPPY_HOST else None
),
)
btn_logs = ft.TextButton(
text="Log in Rippy",
icon=ft.icons.FORMAT_ALIGN_LEFT,
on_click=lambda _: webbrowser.open(f"http://{RIPPY_HOST}/logs") if RIPPY_HOST else None
on_click=lambda _: (
webbrowser.open(f"http://{RIPPY_HOST}/logs") if RIPPY_HOST else None
),
)
btn_local_log = ft.TextButton(
text="Lokales Log",
icon=ft.icons.INSERT_DRIVE_FILE,
on_click=lambda _: os.startfile(LOG_PFAD) if os.path.exists(LOG_PFAD) else None
on_click=lambda _: os.startfile(LOG_PFAD) if os.path.exists(LOG_PFAD) else None,
)
# Layout
header = ft.Row(
[title_text, ft.Container(expand=True), btn_dashboard, btn_logs, btn_local_log, btn_uninstall],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN
[
title_text,
ft.Container(expand=True),
btn_dashboard,
btn_logs,
btn_local_log,
btn_uninstall,
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
)
active_poster = ft.Image(src="", visible=False, width=64, height=96, border_radius=8, fit=ft.ImageFit.COVER)
active_poster = ft.Image(
src="",
visible=False,
width=64,
height=96,
border_radius=8,
fit=ft.ImageFit.COVER,
)
dashboard_card = ft.Container(
content=ft.Column([
status_text,
machine_info,
ft.Container(height=5),
ft.Row([
active_poster,
progress_stack,
ft.Column([job_title, eta_text], alignment=ft.MainAxisAlignment.CENTER, spacing=10)
], alignment=ft.MainAxisAlignment.START, spacing=30),
ft.Container(height=10),
btn_toggle
]),
content=ft.Column(
[
status_text,
machine_info,
ft.Container(height=5),
ft.Row(
[
active_poster,
progress_stack,
ft.Column(
[job_title, eta_text],
alignment=ft.MainAxisAlignment.CENTER,
spacing=10,
),
],
alignment=ft.MainAxisAlignment.START,
spacing=30,
),
ft.Container(height=10),
btn_toggle,
]
),
bgcolor=c_panel,
border_radius=15,
padding=30,
border=ft.border.all(1, "#334155")
border=ft.border.all(1, "#334155"),
)
queue_card = ft.Container(
content=ft.Column([
ft.Text("Aufgaben (Warteschlange)", size=16, weight=ft.FontWeight.BOLD, color=c_text),
queue_view
]),
content=ft.Column(
[
ft.Text(
"Aufgaben (Warteschlange)",
size=16,
weight=ft.FontWeight.BOLD,
color=c_text,
),
queue_view,
]
),
bgcolor="#020617",
border_radius=10,
padding=15,
border=ft.border.all(1, "#1e293b")
border=ft.border.all(1, "#1e293b"),
)
terminal_card = ft.Container(
@@ -459,7 +564,7 @@ def main(page: ft.Page):
border_radius=10,
padding=15,
expand=True,
border=ft.border.all(1, "#1e293b")
border=ft.border.all(1, "#1e293b"),
)
page.add(
@@ -470,7 +575,7 @@ def main(page: ft.Page):
queue_card,
ft.Container(height=10),
ft.Text("Live Logs", size=16, weight=ft.FontWeight.BOLD, color=c_text),
terminal_card
terminal_card,
)
# Background polling loop
@@ -481,30 +586,75 @@ def main(page: ft.Page):
# Update System Info
info = w_info.get("info") or {}
teile = [t for t in (
info.get("cpu_modell"),
f"{info.get('cpu_kerne')} Kerne" if info.get("cpu_kerne") else "",
info.get("cpu_simd") if info.get("cpu_simd") not in ("", "unbekannt") else "",
", ".join(w_info.get("encoders") or []),
) if t]
machine_info.value = " · ".join(teile) if teile else f"{SLOTS} Aufträge gleichzeitig möglich"
teile = [
t
for t in (
info.get("cpu_modell"),
f"{info.get('cpu_kerne')} Kerne"
if info.get("cpu_kerne")
else "",
info.get("cpu_simd")
if info.get("cpu_simd") not in ("", "unbekannt")
else "",
", ".join(w_info.get("encoders") or []),
)
if t
]
machine_info.value = (
" · ".join(teile)
if teile
else f"{SLOTS} Aufträge gleichzeitig möglich"
)
# Update Queue
queue_view.controls.clear()
if not jobs:
queue_view.controls.append(ft.Text("Keine Aufgaben in Rippy", color=c_muted, font_family="Consolas", size=13))
queue_view.controls.append(
ft.Text(
"Keine Aufgaben in Rippy",
color=c_muted,
font_family="Consolas",
size=13,
)
)
else:
for j in jobs[:8]:
poster = None
if j.get("meta") and j["meta"].get("poster_path"):
p = j["meta"]["poster_path"]
poster = p if p.startswith("http") else f"https://image.tmdb.org/t/p/w342{p}"
poster = (
p
if p.startswith("http")
else f"https://image.tmdb.org/t/p/w342{p}"
)
row_items = []
if poster:
row_items.append(ft.Image(src=poster, width=40, height=60, border_radius=5, fit=ft.ImageFit.COVER))
row_items.append(ft.Text(job_text(j), color=c_muted, font_family="Consolas", size=13, expand=True))
queue_view.controls.append(ft.Row(row_items, alignment=ft.MainAxisAlignment.START, spacing=15))
row_items.append(
ft.Image(
src=poster,
width=40,
height=60,
border_radius=5,
fit=ft.ImageFit.COVER,
)
)
row_items.append(
ft.Text(
job_text(j),
color=c_muted,
font_family="Consolas",
size=13,
expand=True,
)
)
queue_view.controls.append(
ft.Row(
row_items,
alignment=ft.MainAxisAlignment.START,
spacing=15,
)
)
# Update status
if not worker_laeuft():
@@ -540,7 +690,11 @@ def main(page: ft.Page):
poster = None
if job.get("meta") and job["meta"].get("poster_path"):
p = job["meta"]["poster_path"]
poster = p if p.startswith("http") else f"https://image.tmdb.org/t/p/w342{p}"
poster = (
p
if p.startswith("http")
else f"https://image.tmdb.org/t/p/w342{p}"
)
if poster:
active_poster.src = poster
@@ -562,7 +716,7 @@ def main(page: ft.Page):
btn_toggle.bgcolor = "#1e293b"
page.update()
except Exception as e:
except Exception:
pass
time.sleep(ABFRAGE_TAKT_SEKUNDEN)
@@ -580,20 +734,14 @@ def main(page: ft.Page):
pass
threading.Thread(target=poll_loop, daemon=True).start()
threading.Thread(target=_schluessel_wache, args=(_bruecke_bauen(), add_log), daemon=True, name="schluessel-wache").start()
threading.Thread(
target=_schluessel_wache,
args=(_bruecke_bauen(), add_log),
daemon=True,
name="schluessel-wache",
).start()
def worker_stoppen():
"""Celery-Prozess sauber beenden."""
global prozess
if prozess and prozess.poll() is None:
try:
prozess.terminate()
prozess.wait(timeout=5)
except Exception:
try:
prozess.kill()
except Exception:
pass
if __name__ == "__main__":
ft.app(target=main)