760 lines
24 KiB
Python
760 lines
24 KiB
Python
import verwaltung
|
||
import os
|
||
import subprocess
|
||
import threading
|
||
import time
|
||
import urllib.request
|
||
import json
|
||
import webbrowser
|
||
import re
|
||
import flet as ft
|
||
|
||
# Tray-Icon: pystray + PIL fuer das Icon-Bild
|
||
try:
|
||
import pystray
|
||
from PIL import Image as PILImage
|
||
|
||
TRAY_VERFUEGBAR = True
|
||
except ImportError:
|
||
TRAY_VERFUEGBAR = False
|
||
|
||
# Environment configuration
|
||
BASIS = os.path.dirname(os.path.abspath(__file__))
|
||
RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "") or os.getenv("RIPPY_HOST", "")
|
||
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 ""
|
||
if basis_daten:
|
||
ordner = os.path.join(basis_daten, "Rippy Worker")
|
||
os.makedirs(ordner, exist_ok=True)
|
||
return os.path.join(ordner, "worker.log")
|
||
return os.path.join(BASIS, "worker.log")
|
||
|
||
|
||
LOG_PFAD = _log_pfad()
|
||
|
||
# State variables
|
||
prozess = None
|
||
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
|
||
ctypes.windll.kernel32.SetThreadExecutionState(flags)
|
||
_wach["an"] = an
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
# Celery Subprocess Management
|
||
def _leser(prozess_ref, datei, log_callback) -> None:
|
||
try:
|
||
for zeile in prozess_ref.stdout:
|
||
try:
|
||
datei.write(zeile)
|
||
datei.flush()
|
||
except OSError:
|
||
pass
|
||
if log_callback:
|
||
log_callback(zeile)
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
try:
|
||
datei.close()
|
||
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
|
||
|
||
befehl = [
|
||
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}"]
|
||
)
|
||
|
||
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",
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
)
|
||
threading.Thread(
|
||
target=_leser,
|
||
args=(prozess, log, log_callback),
|
||
daemon=True,
|
||
name="log-leser",
|
||
).start()
|
||
|
||
|
||
def worker_stoppen():
|
||
global prozess
|
||
if worker_laeuft():
|
||
prozess.terminate()
|
||
try:
|
||
prozess.wait(15)
|
||
except subprocess.TimeoutExpired:
|
||
prozess.kill()
|
||
prozess = None
|
||
wach_halten(False)
|
||
aktuell["job"] = None
|
||
|
||
|
||
# API Polling
|
||
def old_hole(pfad: str, host: str = None, timeout: int = 5):
|
||
ziel = host if host is not None else RIPPY_HOST
|
||
if not ziel:
|
||
return None
|
||
try:
|
||
with urllib.request.urlopen(f"http://{ziel}/api{pfad}", timeout=timeout) as a:
|
||
return json.load(a)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def hole_job_und_zustand():
|
||
caps = verwaltung.hole("/capabilities")
|
||
jobs = verwaltung.hole("/jobs") or []
|
||
|
||
online = False
|
||
w_info = {}
|
||
for w in (caps or {}).get("workers") or []:
|
||
if w.get("name") == WORKER_NAME:
|
||
online = bool(w.get("online"))
|
||
w_info = w
|
||
break
|
||
|
||
laufend = None
|
||
for job in jobs if isinstance(jobs, list) else []:
|
||
if job.get("status") in ("transcoding", "processing", "running"):
|
||
laufend = job
|
||
break
|
||
|
||
aktuell["online"] = online
|
||
aktuell["job"] = laufend
|
||
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):
|
||
return
|
||
subprocess.Popen(
|
||
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", skript],
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
)
|
||
|
||
|
||
def _bruecke_bauen():
|
||
try:
|
||
import db
|
||
import logbruecke
|
||
|
||
db.init_db()
|
||
return logbruecke.Bruecke(
|
||
WORKER_NAME,
|
||
schreiber=lambda level, quelle, text: db.add_log(level, quelle, text),
|
||
jetzt=time.monotonic,
|
||
)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _schluessel_wache(bruecke=None, log_callback=None):
|
||
try:
|
||
import schluessel
|
||
|
||
if not schluessel.makemkvcon_pfad():
|
||
return
|
||
|
||
def melden(level, text):
|
||
msg = f"[schluessel] {level.upper()} {text}"
|
||
if bruecke:
|
||
bruecke.zeile(msg)
|
||
if log_callback:
|
||
log_callback(msg + "\n")
|
||
|
||
stand = None
|
||
while True:
|
||
try:
|
||
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"
|
||
)
|
||
time.sleep(schluessel.TAKT_SEKUNDEN)
|
||
except ImportError:
|
||
pass
|
||
|
||
|
||
# GUI Application
|
||
def main(page: ft.Page):
|
||
page.title = f"Rippy Worker — {WORKER_NAME}"
|
||
page.window.width = 900
|
||
page.window.height = 700
|
||
page.bgcolor = "#0f172a" # Rippy Slate-900 background
|
||
page.padding = 30
|
||
page.theme_mode = ft.ThemeMode.DARK
|
||
page.fonts = {
|
||
"Inter": "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||
}
|
||
page.theme = ft.Theme(font_family="Inter")
|
||
|
||
# --- Tray-Icon -----------------------------------------------------------
|
||
tray_icon_ref = {"icon": None}
|
||
|
||
def _tray_icon_bild():
|
||
"""Lade rippy.ico oder erzeuge ein Fallback-Bild."""
|
||
ico_pfad = os.path.join(BASIS, "rippy.ico")
|
||
if os.path.isfile(ico_pfad):
|
||
try:
|
||
return PILImage.open(ico_pfad)
|
||
except Exception:
|
||
pass
|
||
# Fallback: kleines lila Quadrat
|
||
img = PILImage.new("RGB", (64, 64), (99, 102, 241))
|
||
return img
|
||
|
||
def tray_zeigen(icon, item):
|
||
"""Tray-Menue: Fenster wieder anzeigen."""
|
||
page.window.visible = True
|
||
page.window.minimized = False
|
||
page.update()
|
||
|
||
def tray_beenden(icon, item):
|
||
"""Tray-Menue: App komplett beenden."""
|
||
if tray_icon_ref["icon"]:
|
||
tray_icon_ref["icon"].stop()
|
||
worker_stoppen()
|
||
page.window.destroy()
|
||
|
||
def tray_starten():
|
||
"""Erstellt und startet das System-Tray-Icon."""
|
||
if not TRAY_VERFUEGBAR:
|
||
return
|
||
menu = pystray.Menu(
|
||
pystray.MenuItem("Rippy Worker anzeigen", tray_zeigen, default=True),
|
||
pystray.Menu.SEPARATOR,
|
||
pystray.MenuItem("Beenden", tray_beenden),
|
||
)
|
||
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()
|
||
|
||
def on_window_event(e):
|
||
"""Fenster-Schliessen minimiert ins Tray statt die App zu beenden."""
|
||
if e.data == "close":
|
||
if TRAY_VERFUEGBAR and tray_icon_ref["icon"]:
|
||
page.window.visible = False
|
||
page.update()
|
||
else:
|
||
# Kein Tray verfuegbar — wirklich beenden
|
||
worker_stoppen()
|
||
page.window.destroy()
|
||
|
||
page.window.prevent_close = True
|
||
page.window.on_event = on_window_event
|
||
tray_starten()
|
||
|
||
# Colors
|
||
|
||
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
|
||
|
||
# Formatter for job status
|
||
def old_job_text(job: dict) -> str:
|
||
titel = (job.get("title") or (job.get("id") or "")[:8]) or "?"
|
||
status = {
|
||
"transcoding": "komprimiert",
|
||
"processing": "rippt",
|
||
"running": "rippt",
|
||
"pending": "wartet",
|
||
"completed": "fertig",
|
||
"failed": "Fehler",
|
||
"canceling": "bricht ab",
|
||
}.get(job.get("status") or "", job.get("status") or "?")
|
||
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"
|
||
)
|
||
|
||
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)
|
||
|
||
progress_stack = ft.Stack(
|
||
[
|
||
progress_ring,
|
||
ft.Container(
|
||
content=progress_percentage,
|
||
alignment=ft.alignment.center,
|
||
width=120,
|
||
height=120,
|
||
),
|
||
],
|
||
width=120,
|
||
height=120,
|
||
)
|
||
|
||
# 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_view = ft.ListView(expand=1, spacing=0, 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
|
||
|
||
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
|
||
|
||
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,
|
||
),
|
||
msg, color=c_text, size=12, font_family="Consolas", expand=True, selectable=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", selectable=True)
|
||
)
|
||
|
||
if len(log_view.controls) > 200:
|
||
log_view.controls.pop(0)
|
||
try:
|
||
page.update()
|
||
except Exception:
|
||
pass
|
||
|
||
def toggle_worker(e):
|
||
if not worker_laeuft():
|
||
worker_starten(add_log)
|
||
btn_toggle.visible = False
|
||
page.update()
|
||
|
||
btn_toggle = ft.ElevatedButton(
|
||
text="Worker läuft" if worker_laeuft() else "Worker Starten",
|
||
icon=ft.icons.CHECK if worker_laeuft() else ft.icons.PLAY_ARROW,
|
||
bgcolor="#1e293b" if worker_laeuft() else c_success,
|
||
color="white",
|
||
disabled=worker_laeuft(),
|
||
on_click=toggle_worker,
|
||
)
|
||
|
||
def uninstall_click(e):
|
||
def close_dlg(e):
|
||
dlg.open = False
|
||
page.update()
|
||
|
||
def do_uninstall(e):
|
||
deinstallieren()
|
||
page.window.close()
|
||
|
||
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."
|
||
),
|
||
actions=[
|
||
ft.TextButton("Abbrechen", on_click=close_dlg),
|
||
ft.TextButton(
|
||
"Ja, Deinstallieren",
|
||
on_click=do_uninstall,
|
||
style=ft.ButtonStyle(color=c_danger),
|
||
),
|
||
],
|
||
actions_alignment=ft.MainAxisAlignment.END,
|
||
)
|
||
page.overlay.append(dlg)
|
||
dlg.open = True
|
||
page.update()
|
||
|
||
btn_uninstall = ft.TextButton(
|
||
text="Deinstallieren",
|
||
icon=ft.icons.DELETE,
|
||
icon_color=c_danger,
|
||
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
|
||
),
|
||
)
|
||
|
||
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
|
||
),
|
||
)
|
||
|
||
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,
|
||
)
|
||
|
||
# Layout
|
||
header = ft.Row(
|
||
[
|
||
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=72,
|
||
height=108,
|
||
border_radius=10,
|
||
fit=ft.ImageFit.COVER,
|
||
)
|
||
active_poster_container = ft.Container(
|
||
content=active_poster,
|
||
border=ft.border.all(1, c_warning),
|
||
border_radius=10,
|
||
shadow=ft.BoxShadow(spread_radius=1, blur_radius=5, color=ft.colors.with_opacity(0.3, ft.colors.BLACK)),
|
||
visible=False
|
||
)
|
||
|
||
dashboard_card = ft.Container(
|
||
content=ft.Column(
|
||
[
|
||
status_text,
|
||
machine_info,
|
||
ft.Container(height=5),
|
||
ft.Row(
|
||
[
|
||
active_poster_container,
|
||
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"),
|
||
)
|
||
|
||
queue_card = ft.Container(
|
||
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"),
|
||
)
|
||
|
||
terminal_card = ft.Container(
|
||
content=log_view,
|
||
bgcolor="#020617",
|
||
border_radius=10,
|
||
padding=15,
|
||
expand=True,
|
||
border=ft.border.all(1, "#1e293b"),
|
||
)
|
||
|
||
page.add(
|
||
header,
|
||
ft.Container(height=10),
|
||
dashboard_card,
|
||
ft.Container(height=10),
|
||
queue_card,
|
||
ft.Container(height=10),
|
||
ft.Text("Live Logs", size=16, weight=ft.FontWeight.BOLD, color=c_text),
|
||
terminal_card,
|
||
)
|
||
|
||
# Background polling loop
|
||
def poll_loop():
|
||
while True:
|
||
try:
|
||
online, job, caps, jobs, w_info = hole_job_und_zustand()
|
||
|
||
# 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"
|
||
)
|
||
|
||
# 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,
|
||
)
|
||
)
|
||
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}"
|
||
)
|
||
|
||
row_items = []
|
||
if poster:
|
||
row_items.append(
|
||
ft.Container(
|
||
content=ft.Image(
|
||
src=poster,
|
||
width=32,
|
||
height=48,
|
||
border_radius=6,
|
||
fit=ft.ImageFit.COVER,
|
||
),
|
||
border=ft.border.all(1, "#334155"),
|
||
border_radius=6,
|
||
shadow=ft.BoxShadow(spread_radius=1, blur_radius=3, color=ft.colors.with_opacity(0.3, ft.colors.BLACK))
|
||
)
|
||
)
|
||
row_items.append(
|
||
ft.Text(
|
||
verwaltung.job_text(j).split(",")[0].split(",")[0],
|
||
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():
|
||
status_text.value = "Worker-Prozess gestoppt"
|
||
status_text.color = c_danger
|
||
progress_ring.value = 0
|
||
progress_percentage.value = "0%"
|
||
job_title.value = "Nichts in Arbeit"
|
||
eta_text.value = "Bereit"
|
||
active_poster_container.visible = False
|
||
btn_toggle.visible = True
|
||
btn_toggle.disabled = False
|
||
btn_toggle.text = "Worker Starten"
|
||
btn_toggle.icon = ft.icons.PLAY_ARROW
|
||
btn_toggle.bgcolor = c_success
|
||
elif not online:
|
||
status_text.value = "Nicht mit Rippy verbunden (offline)"
|
||
status_text.color = c_warning
|
||
progress_ring.value = 0
|
||
progress_percentage.value = "0%"
|
||
active_poster_container.visible = False
|
||
btn_toggle.visible = True
|
||
btn_toggle.disabled = True
|
||
btn_toggle.text = "Warte auf Verbindung..."
|
||
btn_toggle.bgcolor = "#1e293b"
|
||
elif job:
|
||
status_text.value = "Aktiv: Komprimiert"
|
||
status_text.color = c_success
|
||
prog = float(job.get("progress") or 0)
|
||
progress_ring.value = prog / 100.0
|
||
progress_percentage.value = f"{int(prog)}%"
|
||
titel = job.get("title") or (job.get("id") or "")[:8]
|
||
job_title.value = titel
|
||
eta = job.get("eta_text") or "Restzeit wird berechnet..."
|
||
eta_text.value = f"Verbleibend: {eta}"
|
||
|
||
# Update Active Poster
|
||
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}"
|
||
)
|
||
|
||
if poster:
|
||
active_poster.src = poster
|
||
active_poster_container.visible = True
|
||
else:
|
||
active_poster_container.visible = False
|
||
|
||
btn_toggle.visible = False
|
||
else:
|
||
status_text.value = "Verbunden & Bereit"
|
||
status_text.color = c_accent
|
||
progress_ring.value = 0
|
||
progress_percentage.value = "0%"
|
||
job_title.value = "Warte auf Encoder-Aufträge..."
|
||
eta_text.value = "Nichts in Arbeit"
|
||
active_poster_container.visible = False
|
||
btn_toggle.visible = False
|
||
|
||
page.update()
|
||
except Exception:
|
||
pass
|
||
time.sleep(ABFRAGE_TAKT_SEKUNDEN)
|
||
|
||
# Start Celery automatically
|
||
worker_starten(add_log)
|
||
|
||
# Init initial logs
|
||
if os.path.exists(LOG_PFAD):
|
||
try:
|
||
with open(LOG_PFAD, "r", encoding="utf-8", errors="replace") as f:
|
||
lines = f.readlines()[-50:]
|
||
for line in lines:
|
||
add_log(line)
|
||
except OSError:
|
||
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()
|
||
|
||
|
||
|
||
if __name__ == "__main__":
|
||
ft.app(target=main)
|