Compare commits
8 Commits
32979d7bde
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a14449ef85 | |||
| 4343b0901d | |||
| c3e77399ad | |||
| 75426224d1 | |||
| e5ff5373bb | |||
| 9b30c0a490 | |||
| 061f79ecda | |||
| a32a1bb075 |
@@ -4,9 +4,9 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
def main(page: ft.Page):
|
def main(page: ft.Page):
|
||||||
page.title = "Rippy Encoding-Worker Installation"
|
page.title = "Rippy Encoding-Worker Installation"
|
||||||
page.window_width = 800
|
page.window_width = 800
|
||||||
@@ -26,43 +26,77 @@ def main(page: ft.Page):
|
|||||||
std_ziel = r"C:\Program Files\Rippy Worker"
|
std_ziel = r"C:\Program Files\Rippy Worker"
|
||||||
|
|
||||||
# UI Fields
|
# UI Fields
|
||||||
txt_host = ft.TextField(label="LAN-IP der Rippy-Maschine (Docker-Host)", width=400, bgcolor=c_panel, color=c_text)
|
txt_host = ft.TextField(
|
||||||
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)
|
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(
|
txt_slots = ft.Dropdown(
|
||||||
label="Kerne (Gleichzeitig)",
|
label="Kerne (Gleichzeitig)",
|
||||||
width=150,
|
width=150,
|
||||||
options=[ft.dropdown.Option(str(i)) for i in range(1, 9)],
|
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",
|
value="2" if os.cpu_count() and os.cpu_count() >= 12 else "1",
|
||||||
bgcolor=c_panel,
|
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_pfad = ft.TextField(
|
||||||
txt_map = ft.TextField(label="Netzwerk-Freigabe mit den Rohdaten", expand=True, bgcolor=c_panel, color=c_text)
|
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_auto = ft.Checkbox(
|
||||||
chk_desktop = ft.Checkbox(label="Verknüpfung auf dem Desktop anlegen", value=True, fill_color=c_accent)
|
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)
|
log_view = ft.ListView(height=150, auto_scroll=True, spacing=2)
|
||||||
|
|
||||||
def log(msg: str):
|
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:
|
try:
|
||||||
page.update()
|
page.update()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_map(e):
|
def check_map(e):
|
||||||
r_host = txt_host.value.strip()
|
r_host = txt_host.value.strip()
|
||||||
if not r_host:
|
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.snack_bar.open = True
|
||||||
page.update()
|
page.update()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log(f"Frage Rippy nach Freigabe ({r_host})...")
|
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())
|
data = json.loads(r.read().decode())
|
||||||
if data.get("hinweis"):
|
if data.get("hinweis"):
|
||||||
log(data["hinweis"])
|
log(data["hinweis"])
|
||||||
@@ -95,11 +129,14 @@ def main(page: ft.Page):
|
|||||||
python_exe = None
|
python_exe = None
|
||||||
for cmd in ["py -3", "python"]:
|
for cmd in ["py -3", "python"]:
|
||||||
try:
|
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:
|
if "Python 3." in res.stdout:
|
||||||
python_exe = cmd
|
python_exe = cmd
|
||||||
break
|
break
|
||||||
except: pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if not python_exe:
|
if not python_exe:
|
||||||
log("FEHLER: Python 3.10+ nicht gefunden.")
|
log("FEHLER: Python 3.10+ nicht gefunden.")
|
||||||
@@ -113,9 +150,10 @@ def main(page: ft.Page):
|
|||||||
try:
|
try:
|
||||||
os.makedirs(install_dir, exist_ok=True)
|
os.makedirs(install_dir, exist_ok=True)
|
||||||
test_file = os.path.join(install_dir, ".test")
|
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)
|
os.remove(test_file)
|
||||||
except Exception as ex:
|
except Exception:
|
||||||
log(f"FEHLER: Keine Schreibrechte in {install_dir}")
|
log(f"FEHLER: Keine Schreibrechte in {install_dir}")
|
||||||
log("Bitte als Administrator starten oder Pfad aendern.")
|
log("Bitte als Administrator starten oder Pfad aendern.")
|
||||||
btn_install.disabled = False
|
btn_install.disabled = False
|
||||||
@@ -126,8 +164,10 @@ def main(page: ft.Page):
|
|||||||
log(f"Lade Worker-Code von {r_host}...")
|
log(f"Lade Worker-Code von {r_host}...")
|
||||||
try:
|
try:
|
||||||
zip_path = os.path.join(install_dir, "worker.zip")
|
zip_path = os.path.join(install_dir, "worker.zip")
|
||||||
urllib.request.urlretrieve(f"http://{r_host}/api/worker-setup/paket", zip_path)
|
urllib.request.urlretrieve(
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
f"http://{r_host}/api/worker-setup/paket", zip_path
|
||||||
|
)
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||||
zip_ref.extractall(install_dir)
|
zip_ref.extractall(install_dir)
|
||||||
os.remove(zip_path)
|
os.remove(zip_path)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -139,12 +179,26 @@ def main(page: ft.Page):
|
|||||||
# Setup venv — --clear loescht einen alten venv komplett, damit keine
|
# Setup venv — --clear loescht einen alten venv komplett, damit keine
|
||||||
# veralteten Pakete (z.B. flet 0.86 statt 0.23) ueberleben.
|
# veralteten Pakete (z.B. flet 0.86 statt 0.23) ueberleben.
|
||||||
log("Lege Python-Umgebung an (venv)...")
|
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)...")
|
log("Installiere Abhaengigkeiten (pip)...")
|
||||||
subprocess.run(r"venv\Scripts\python.exe -m pip install --quiet --upgrade pip", cwd=install_dir, shell=True)
|
subprocess.run(
|
||||||
subprocess.run(r"venv\Scripts\pip.exe install --quiet -r requirements.txt", cwd=install_dir, shell=True)
|
r"venv\Scripts\python.exe -m pip install --quiet --upgrade pip",
|
||||||
subprocess.run(r"venv\Scripts\pip.exe install --quiet requests pillow", cwd=install_dir, shell=True)
|
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
|
# Download Handbrake
|
||||||
hb_path = os.path.join(install_dir, "HandBrakeCLI.exe")
|
hb_path = os.path.join(install_dir, "HandBrakeCLI.exe")
|
||||||
@@ -153,13 +207,21 @@ def main(page: ft.Page):
|
|||||||
try:
|
try:
|
||||||
hb_ver = "1.7.3" # Fallback if github fails
|
hb_ver = "1.7.3" # Fallback if github fails
|
||||||
try:
|
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())
|
hb_data = json.loads(r.read().decode())
|
||||||
if hb_data.get("tag_name"): hb_ver = hb_data["tag_name"].lstrip("v")
|
if hb_data.get("tag_name"):
|
||||||
except: pass
|
hb_ver = hb_data["tag_name"].lstrip("v")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
hb_zip = os.path.join(install_dir, "hb.zip")
|
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)
|
urllib.request.urlretrieve(
|
||||||
with zipfile.ZipFile(hb_zip, 'r') as zip_ref:
|
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)
|
zip_ref.extractall(install_dir)
|
||||||
os.remove(hb_zip)
|
os.remove(hb_zip)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -167,14 +229,24 @@ def main(page: ft.Page):
|
|||||||
|
|
||||||
log("Erstelle Start-Skripte...")
|
log("Erstelle Start-Skripte...")
|
||||||
slots = txt_slots.value
|
slots = txt_slots.value
|
||||||
pool_arg = "--pool=solo" if int(slots) <= 1 else f"--pool=threads --concurrency={slots}"
|
pool_arg = (
|
||||||
map_zeile = f"set RIPPY_PATH_MAP={txt_map.value.strip()}\r\n" if txt_map.value.strip() else ""
|
"--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"
|
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)
|
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"
|
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)
|
with open(os.path.join(install_dir, "start-worker.bat"), "w") as f:
|
||||||
|
f.write(work_bat)
|
||||||
|
|
||||||
# Uninstaller
|
# Uninstaller
|
||||||
uninstall_ps1 = f"""param([switch]$Force)
|
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
|
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") }}
|
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
|
# Autostart
|
||||||
if chk_auto.value:
|
if chk_auto.value:
|
||||||
log("Richte Autostart (Task Scheduler) ein...")
|
log("Richte Autostart (Task Scheduler) ein...")
|
||||||
bat_path = os.path.join(install_dir, "start-gui.bat")
|
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
|
# Desktop Shortcut — ueber PowerShell/COM, weil win32com im
|
||||||
# PyInstaller-Bundle nicht enthalten ist.
|
# PyInstaller-Bundle nicht enthalten ist.
|
||||||
if chk_desktop.value:
|
if chk_desktop.value:
|
||||||
try:
|
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")
|
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")
|
lnk_path = os.path.join(desktop, "Rippy Worker.lnk")
|
||||||
bat_target = os.path.join(install_dir, "start-gui.bat")
|
bat_target = os.path.join(install_dir, "start-gui.bat")
|
||||||
ico_path = os.path.join(install_dir, "rippy.ico")
|
ico_path = os.path.join(install_dir, "rippy.ico")
|
||||||
ps_cmd = (
|
ps_cmd = (
|
||||||
f'$ws = New-Object -ComObject WScript.Shell; '
|
f"$ws = New-Object -ComObject WScript.Shell; "
|
||||||
f'$sc = $ws.CreateShortcut(\"{lnk_path}\"); '
|
f'$sc = $ws.CreateShortcut("{lnk_path}"); '
|
||||||
f'$sc.TargetPath = \"{bat_target}\"; '
|
f'$sc.TargetPath = "{bat_target}"; '
|
||||||
f'$sc.WorkingDirectory = \"{install_dir}\"; '
|
f'$sc.WorkingDirectory = "{install_dir}"; '
|
||||||
f'$sc.IconLocation = \"{ico_path}\"; '
|
f'$sc.IconLocation = "{ico_path}"; '
|
||||||
f'$sc.WindowStyle = 7; '
|
f"$sc.WindowStyle = 7; "
|
||||||
f'$sc.Save()'
|
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.")
|
log("Desktop-Verknüpfung erstellt.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Warnung: Shortcut konnte nicht erstellt werden: {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)
|
os.startfile(bat_path)
|
||||||
page.window_close()
|
page.window_close()
|
||||||
|
|
||||||
btn_install = ft.ElevatedButton("Installieren", on_click=do_install, bgcolor=c_accent, color=ft.colors.WHITE, width=200, height=40)
|
btn_install = ft.ElevatedButton(
|
||||||
btn_start = ft.ElevatedButton("Worker starten", on_click=start_worker, bgcolor=c_success, color=ft.colors.WHITE, width=200, height=40, visible=False)
|
"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):
|
def get_dir(e: ft.FilePickerResultEvent):
|
||||||
if e.path:
|
if e.path:
|
||||||
@@ -251,21 +354,59 @@ if (-not $Force) {{ [System.Windows.Forms.MessageBox]::Show("Rippy-Worker entfer
|
|||||||
page.overlay.append(dir_picker)
|
page.overlay.append(dir_picker)
|
||||||
|
|
||||||
page.add(
|
page.add(
|
||||||
ft.Row([
|
ft.Row(
|
||||||
|
[
|
||||||
ft.Icon(ft.icons.DISC_FULL, size=40, color=c_accent),
|
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(
|
||||||
]),
|
"Rippy Encoding-Worker",
|
||||||
ft.Text("Diese Maschine übernimmt die Video-Kompression für Rippy. Gerippt wird weiter auf der Hauptmaschine.", color=c_muted),
|
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.Divider(color="#334155"),
|
||||||
ft.Row([txt_host], alignment=ft.MainAxisAlignment.START),
|
ft.Row([txt_host], alignment=ft.MainAxisAlignment.START),
|
||||||
ft.Row([txt_name, txt_slots], 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(
|
||||||
ft.Row([txt_map, ft.ElevatedButton("Freigabe holen", on_click=check_map, bgcolor=c_panel, color=c_text)], alignment=ft.MainAxisAlignment.START),
|
[
|
||||||
|
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_auto,
|
||||||
chk_desktop,
|
chk_desktop,
|
||||||
ft.Container(content=log_view, bgcolor="#020617", padding=10, border_radius=5, border=ft.border.all(1, "#334155")),
|
ft.Container(
|
||||||
ft.Row([btn_install, btn_start])
|
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__":
|
if __name__ == "__main__":
|
||||||
ft.app(target=main)
|
ft.app(target=main)
|
||||||
|
|||||||
+1
-1
@@ -1329,7 +1329,7 @@ async def mediaserver_refresh(request: MediaServerRefreshRequest):
|
|||||||
res = _requests.post(url + "/jsonrpc", json=payload, auth=auth, timeout=5)
|
res = _requests.post(url + "/jsonrpc", json=payload, auth=auth, timeout=5)
|
||||||
if res.status_code < 300:
|
if res.status_code < 300:
|
||||||
return res
|
return res
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Emby / Jellyfin
|
# Emby / Jellyfin
|
||||||
|
|||||||
+261
-100
@@ -1,6 +1,6 @@
|
|||||||
|
import verwaltung
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -13,6 +13,7 @@ import flet as ft
|
|||||||
try:
|
try:
|
||||||
import pystray
|
import pystray
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
TRAY_VERFUEGBAR = True
|
TRAY_VERFUEGBAR = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
TRAY_VERFUEGBAR = False
|
TRAY_VERFUEGBAR = False
|
||||||
@@ -24,6 +25,7 @@ WORKER_NAME = os.getenv("WORKER_NAME", "windows-worker")
|
|||||||
SLOTS = max(1, int(os.getenv("RIPPY_SLOTS", "1") or "1"))
|
SLOTS = max(1, int(os.getenv("RIPPY_SLOTS", "1") or "1"))
|
||||||
ABFRAGE_TAKT_SEKUNDEN = 4
|
ABFRAGE_TAKT_SEKUNDEN = 4
|
||||||
|
|
||||||
|
|
||||||
# Log file path
|
# Log file path
|
||||||
def _log_pfad() -> str:
|
def _log_pfad() -> str:
|
||||||
basis_daten = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") or ""
|
basis_daten = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") or ""
|
||||||
@@ -33,6 +35,7 @@ def _log_pfad() -> str:
|
|||||||
return os.path.join(ordner, "worker.log")
|
return os.path.join(ordner, "worker.log")
|
||||||
return os.path.join(BASIS, "worker.log")
|
return os.path.join(BASIS, "worker.log")
|
||||||
|
|
||||||
|
|
||||||
LOG_PFAD = _log_pfad()
|
LOG_PFAD = _log_pfad()
|
||||||
|
|
||||||
# State variables
|
# State variables
|
||||||
@@ -42,11 +45,13 @@ aktuell = {"job": None, "online": False}
|
|||||||
# Windows Standby blocker
|
# Windows Standby blocker
|
||||||
_wach = {"an": False}
|
_wach = {"an": False}
|
||||||
|
|
||||||
|
|
||||||
def wach_halten(an: bool) -> None:
|
def wach_halten(an: bool) -> None:
|
||||||
if _wach["an"] == an:
|
if _wach["an"] == an:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
ES_CONTINUOUS = 0x80000000
|
ES_CONTINUOUS = 0x80000000
|
||||||
ES_SYSTEM_REQUIRED = 0x00000001
|
ES_SYSTEM_REQUIRED = 0x00000001
|
||||||
flags = (ES_CONTINUOUS | ES_SYSTEM_REQUIRED) if an else ES_CONTINUOUS
|
flags = (ES_CONTINUOUS | ES_SYSTEM_REQUIRED) if an else ES_CONTINUOUS
|
||||||
@@ -55,6 +60,7 @@ def wach_halten(an: bool) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Celery Subprocess Management
|
# Celery Subprocess Management
|
||||||
def _leser(prozess_ref, datei, log_callback) -> None:
|
def _leser(prozess_ref, datei, log_callback) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -74,9 +80,11 @@ def _leser(prozess_ref, datei, log_callback) -> None:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def worker_laeuft() -> bool:
|
def worker_laeuft() -> bool:
|
||||||
return prozess is not None and prozess.poll() is None
|
return prozess is not None and prozess.poll() is None
|
||||||
|
|
||||||
|
|
||||||
def worker_starten(log_callback=None):
|
def worker_starten(log_callback=None):
|
||||||
global prozess
|
global prozess
|
||||||
if worker_laeuft():
|
if worker_laeuft():
|
||||||
@@ -86,23 +94,39 @@ def worker_starten(log_callback=None):
|
|||||||
celery = "celery" # fallback to global if testing outside venv
|
celery = "celery" # fallback to global if testing outside venv
|
||||||
|
|
||||||
befehl = [
|
befehl = [
|
||||||
celery, "-A", "celery_app", "worker", "--loglevel=info",
|
celery,
|
||||||
"-Q", "transcode", "-n", f"{WORKER_NAME}@%h",
|
"-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")
|
log = open(LOG_PFAD, "a", encoding="utf-8", errors="replace")
|
||||||
prozess = subprocess.Popen(
|
prozess = subprocess.Popen(
|
||||||
befehl, cwd=BASIS,
|
befehl,
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
cwd=BASIS,
|
||||||
text=True, bufsize=1, errors="replace",
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
errors="replace",
|
||||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
)
|
)
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=_leser, args=(prozess, log, log_callback),
|
target=_leser,
|
||||||
daemon=True, name="log-leser",
|
args=(prozess, log, log_callback),
|
||||||
|
daemon=True,
|
||||||
|
name="log-leser",
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
|
||||||
def worker_stoppen():
|
def worker_stoppen():
|
||||||
global prozess
|
global prozess
|
||||||
if worker_laeuft():
|
if worker_laeuft():
|
||||||
@@ -115,8 +139,9 @@ def worker_stoppen():
|
|||||||
wach_halten(False)
|
wach_halten(False)
|
||||||
aktuell["job"] = None
|
aktuell["job"] = None
|
||||||
|
|
||||||
|
|
||||||
# API Polling
|
# API Polling
|
||||||
def hole(pfad: str, host: str = None, timeout: int = 5):
|
def old_hole(pfad: str, host: str = None, timeout: int = 5):
|
||||||
ziel = host if host is not None else RIPPY_HOST
|
ziel = host if host is not None else RIPPY_HOST
|
||||||
if not ziel:
|
if not ziel:
|
||||||
return None
|
return None
|
||||||
@@ -126,13 +151,14 @@ def hole(pfad: str, host: str = None, timeout: int = 5):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def hole_job_und_zustand():
|
def hole_job_und_zustand():
|
||||||
caps = hole("/capabilities")
|
caps = verwaltung.hole("/capabilities")
|
||||||
jobs = hole("/jobs") or []
|
jobs = verwaltung.hole("/jobs") or []
|
||||||
|
|
||||||
online = False
|
online = False
|
||||||
w_info = {}
|
w_info = {}
|
||||||
for w in ((caps or {}).get("workers") or []):
|
for w in (caps or {}).get("workers") or []:
|
||||||
if w.get("name") == WORKER_NAME:
|
if w.get("name") == WORKER_NAME:
|
||||||
online = bool(w.get("online"))
|
online = bool(w.get("online"))
|
||||||
w_info = w
|
w_info = w
|
||||||
@@ -149,6 +175,7 @@ def hole_job_und_zustand():
|
|||||||
wach_halten(bool(laufend))
|
wach_halten(bool(laufend))
|
||||||
return online, laufend, caps, jobs, w_info
|
return online, laufend, caps, jobs, w_info
|
||||||
|
|
||||||
|
|
||||||
def deinstallieren() -> None:
|
def deinstallieren() -> None:
|
||||||
skript = os.path.join(BASIS, "uninstall.ps1")
|
skript = os.path.join(BASIS, "uninstall.ps1")
|
||||||
if not os.path.isfile(skript):
|
if not os.path.isfile(skript):
|
||||||
@@ -158,6 +185,7 @@ def deinstallieren() -> None:
|
|||||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _bruecke_bauen():
|
def _bruecke_bauen():
|
||||||
try:
|
try:
|
||||||
import db
|
import db
|
||||||
@@ -172,9 +200,11 @@ def _bruecke_bauen():
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _schluessel_wache(bruecke=None, log_callback=None):
|
def _schluessel_wache(bruecke=None, log_callback=None):
|
||||||
try:
|
try:
|
||||||
import schluessel
|
import schluessel
|
||||||
|
|
||||||
if not schluessel.makemkvcon_pfad():
|
if not schluessel.makemkvcon_pfad():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -191,11 +221,14 @@ def _schluessel_wache(bruecke=None, log_callback=None):
|
|||||||
stand, _was = schluessel.runde(RIPPY_HOST, stand, melden=melden)
|
stand, _was = schluessel.runde(RIPPY_HOST, stand, melden=melden)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if log_callback:
|
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)
|
time.sleep(schluessel.TAKT_SEKUNDEN)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# GUI Application
|
# GUI Application
|
||||||
def main(page: ft.Page):
|
def main(page: ft.Page):
|
||||||
page.title = f"Rippy Worker — {WORKER_NAME}"
|
page.title = f"Rippy Worker — {WORKER_NAME}"
|
||||||
@@ -246,8 +279,9 @@ def main(page: ft.Page):
|
|||||||
pystray.Menu.SEPARATOR,
|
pystray.Menu.SEPARATOR,
|
||||||
pystray.MenuItem("Beenden", tray_beenden),
|
pystray.MenuItem("Beenden", tray_beenden),
|
||||||
)
|
)
|
||||||
icon = pystray.Icon("rippy_worker", _tray_icon_bild(),
|
icon = pystray.Icon(
|
||||||
f"Rippy Worker — {WORKER_NAME}", menu)
|
"rippy_worker", _tray_icon_bild(), f"Rippy Worker — {WORKER_NAME}", menu
|
||||||
|
)
|
||||||
tray_icon_ref["icon"] = icon
|
tray_icon_ref["icon"] = icon
|
||||||
threading.Thread(target=icon.run, daemon=True, name="tray-icon").start()
|
threading.Thread(target=icon.run, daemon=True, name="tray-icon").start()
|
||||||
|
|
||||||
@@ -267,7 +301,7 @@ def main(page: ft.Page):
|
|||||||
tray_starten()
|
tray_starten()
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
c_bg = "#0f172a"
|
|
||||||
c_panel = "#1e293b"
|
c_panel = "#1e293b"
|
||||||
c_text = "#e2e8f0"
|
c_text = "#e2e8f0"
|
||||||
c_muted = "#94a3b8"
|
c_muted = "#94a3b8"
|
||||||
@@ -277,7 +311,7 @@ def main(page: ft.Page):
|
|||||||
c_danger = "#f43f5e" # Rose
|
c_danger = "#f43f5e" # Rose
|
||||||
|
|
||||||
# Formatter for job status
|
# Formatter for job status
|
||||||
def job_text(job: dict) -> str:
|
def old_job_text(job: dict) -> str:
|
||||||
titel = (job.get("title") or (job.get("id") or "")[:8]) or "?"
|
titel = (job.get("title") or (job.get("id") or "")[:8]) or "?"
|
||||||
status = {
|
status = {
|
||||||
"transcoding": "komprimiert",
|
"transcoding": "komprimiert",
|
||||||
@@ -291,12 +325,22 @@ def main(page: ft.Page):
|
|||||||
return f"{titel} — {status}"
|
return f"{titel} — {status}"
|
||||||
|
|
||||||
# UI Components
|
# UI Components
|
||||||
status_text = ft.Text("Verbinde...", color=c_muted, size=18, weight=ft.FontWeight.BOLD)
|
status_text = ft.Text(
|
||||||
title_text = ft.Text(f"Rippy Worker: {WORKER_NAME}", size=24, weight=ft.FontWeight.BOLD, color=c_text)
|
"Verbinde...", color=c_muted, size=18, weight=ft.FontWeight.BOLD
|
||||||
machine_info = ft.Text("System-Infos werden geladen...", color=c_muted, size=13, font_family="Consolas")
|
)
|
||||||
|
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_ring = ft.ProgressRing(
|
||||||
progress_percentage = ft.Text("0%", size=28, weight=ft.FontWeight.BOLD, color=c_text)
|
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)
|
job_title = ft.Text("Nichts in Arbeit", size=20, color=c_muted)
|
||||||
eta_text = ft.Text("Bereit", size=14, color=c_muted)
|
eta_text = ft.Text("Bereit", size=14, color=c_muted)
|
||||||
|
|
||||||
@@ -308,7 +352,7 @@ def main(page: ft.Page):
|
|||||||
alignment=ft.alignment.center,
|
alignment=ft.alignment.center,
|
||||||
width=120,
|
width=120,
|
||||||
height=120,
|
height=120,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
width=120,
|
width=120,
|
||||||
height=120,
|
height=120,
|
||||||
@@ -316,32 +360,53 @@ def main(page: ft.Page):
|
|||||||
|
|
||||||
# Live Log Regex and Layout
|
# Live Log Regex and Layout
|
||||||
# [2026-07-26 20:34:00,093: INFO/MainProcess] message...
|
# [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)
|
log_view = ft.ListView(expand=1, spacing=0, auto_scroll=True)
|
||||||
queue_view = ft.ListView(height=120, spacing=4, auto_scroll=False)
|
queue_view = ft.ListView(height=120, spacing=4, auto_scroll=False)
|
||||||
|
|
||||||
def add_log(zeile):
|
def add_log(zeile):
|
||||||
z = zeile.strip()
|
z = zeile.strip()
|
||||||
if not z: return
|
if not z:
|
||||||
|
return
|
||||||
|
|
||||||
match = LOG_REGEX.match(z)
|
match = LOG_REGEX.match(z)
|
||||||
if match:
|
if match:
|
||||||
zeit, level, msg = match.groups()
|
zeit, level, msg = match.groups()
|
||||||
color = c_text
|
color = c_text
|
||||||
if level == "INFO": color = "#38bdf8" # sky-400
|
if level == "INFO":
|
||||||
elif level == "WARNING": color = "#fbbf24" # amber-400
|
color = "#38bdf8" # sky-400
|
||||||
elif level == "ERROR": color = "#f87171" # red-400
|
elif level == "WARNING":
|
||||||
elif level == "SUCCESS": color = "#34d399" # emerald-400
|
color = "#fbbf24" # amber-400
|
||||||
|
elif level == "ERROR":
|
||||||
|
color = "#f87171" # red-400
|
||||||
|
elif level == "SUCCESS":
|
||||||
|
color = "#34d399" # emerald-400
|
||||||
|
|
||||||
row = ft.Row([
|
row = ft.Row(
|
||||||
|
[
|
||||||
ft.Text(zeit, color=c_muted, size=12, font_family="Consolas"),
|
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(
|
||||||
ft.Text(msg, color=c_text, size=12, font_family="Consolas", expand=True)
|
f"[{level}]",
|
||||||
], spacing=10, vertical_alignment=ft.CrossAxisAlignment.START)
|
color=color,
|
||||||
|
size=12,
|
||||||
|
font_family="Consolas",
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
),
|
||||||
|
ft.Text(
|
||||||
|
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)
|
log_view.controls.append(row)
|
||||||
else:
|
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", selectable=True)
|
||||||
|
)
|
||||||
|
|
||||||
if len(log_view.controls) > 200:
|
if len(log_view.controls) > 200:
|
||||||
log_view.controls.pop(0)
|
log_view.controls.pop(0)
|
||||||
@@ -353,10 +418,7 @@ def main(page: ft.Page):
|
|||||||
def toggle_worker(e):
|
def toggle_worker(e):
|
||||||
if not worker_laeuft():
|
if not worker_laeuft():
|
||||||
worker_starten(add_log)
|
worker_starten(add_log)
|
||||||
btn_toggle.text = "Worker läuft"
|
btn_toggle.visible = False
|
||||||
btn_toggle.icon = ft.icons.CHECK
|
|
||||||
btn_toggle.bgcolor = "#1e293b"
|
|
||||||
btn_toggle.disabled = True
|
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
btn_toggle = ft.ElevatedButton(
|
btn_toggle = ft.ElevatedButton(
|
||||||
@@ -365,7 +427,7 @@ def main(page: ft.Page):
|
|||||||
bgcolor="#1e293b" if worker_laeuft() else c_success,
|
bgcolor="#1e293b" if worker_laeuft() else c_success,
|
||||||
color="white",
|
color="white",
|
||||||
disabled=worker_laeuft(),
|
disabled=worker_laeuft(),
|
||||||
on_click=toggle_worker
|
on_click=toggle_worker,
|
||||||
)
|
)
|
||||||
|
|
||||||
def uninstall_click(e):
|
def uninstall_click(e):
|
||||||
@@ -379,10 +441,16 @@ def main(page: ft.Page):
|
|||||||
|
|
||||||
dlg = ft.AlertDialog(
|
dlg = ft.AlertDialog(
|
||||||
title=ft.Text("Deinstallieren?"),
|
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=[
|
actions=[
|
||||||
ft.TextButton("Abbrechen", on_click=close_dlg),
|
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,
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
)
|
)
|
||||||
@@ -394,63 +462,105 @@ def main(page: ft.Page):
|
|||||||
text="Deinstallieren",
|
text="Deinstallieren",
|
||||||
icon=ft.icons.DELETE,
|
icon=ft.icons.DELETE,
|
||||||
icon_color=c_danger,
|
icon_color=c_danger,
|
||||||
on_click=uninstall_click
|
on_click=uninstall_click,
|
||||||
)
|
)
|
||||||
|
|
||||||
btn_dashboard = ft.TextButton(
|
btn_dashboard = ft.TextButton(
|
||||||
text="Rippy Dashboard",
|
text="Rippy Dashboard",
|
||||||
icon=ft.icons.OPEN_IN_BROWSER,
|
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(
|
btn_logs = ft.TextButton(
|
||||||
text="Log in Rippy",
|
text="Log in Rippy",
|
||||||
icon=ft.icons.FORMAT_ALIGN_LEFT,
|
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(
|
btn_local_log = ft.TextButton(
|
||||||
text="Lokales Log",
|
text="Lokales Log",
|
||||||
icon=ft.icons.INSERT_DRIVE_FILE,
|
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
|
# Layout
|
||||||
header = ft.Row(
|
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=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(
|
dashboard_card = ft.Container(
|
||||||
content=ft.Column([
|
content=ft.Column(
|
||||||
|
[
|
||||||
status_text,
|
status_text,
|
||||||
machine_info,
|
machine_info,
|
||||||
ft.Container(height=5),
|
ft.Container(height=5),
|
||||||
ft.Row([
|
ft.Row(
|
||||||
active_poster,
|
[
|
||||||
|
active_poster_container,
|
||||||
progress_stack,
|
progress_stack,
|
||||||
ft.Column([job_title, eta_text], alignment=ft.MainAxisAlignment.CENTER, spacing=10)
|
ft.Column(
|
||||||
], alignment=ft.MainAxisAlignment.START, spacing=30),
|
[job_title, eta_text],
|
||||||
|
alignment=ft.MainAxisAlignment.CENTER,
|
||||||
|
spacing=10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
alignment=ft.MainAxisAlignment.START,
|
||||||
|
spacing=30,
|
||||||
|
),
|
||||||
ft.Container(height=10),
|
ft.Container(height=10),
|
||||||
btn_toggle
|
btn_toggle,
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
bgcolor=c_panel,
|
bgcolor=c_panel,
|
||||||
border_radius=15,
|
border_radius=15,
|
||||||
padding=30,
|
padding=30,
|
||||||
border=ft.border.all(1, "#334155")
|
border=ft.border.all(1, "#334155"),
|
||||||
)
|
)
|
||||||
|
|
||||||
queue_card = ft.Container(
|
queue_card = ft.Container(
|
||||||
content=ft.Column([
|
content=ft.Column(
|
||||||
ft.Text("Aufgaben (Warteschlange)", size=16, weight=ft.FontWeight.BOLD, color=c_text),
|
[
|
||||||
queue_view
|
ft.Text(
|
||||||
]),
|
"Aufgaben (Warteschlange)",
|
||||||
|
size=16,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=c_text,
|
||||||
|
),
|
||||||
|
queue_view,
|
||||||
|
]
|
||||||
|
),
|
||||||
bgcolor="#020617",
|
bgcolor="#020617",
|
||||||
border_radius=10,
|
border_radius=10,
|
||||||
padding=15,
|
padding=15,
|
||||||
border=ft.border.all(1, "#1e293b")
|
border=ft.border.all(1, "#1e293b"),
|
||||||
)
|
)
|
||||||
|
|
||||||
terminal_card = ft.Container(
|
terminal_card = ft.Container(
|
||||||
@@ -459,7 +569,7 @@ def main(page: ft.Page):
|
|||||||
border_radius=10,
|
border_radius=10,
|
||||||
padding=15,
|
padding=15,
|
||||||
expand=True,
|
expand=True,
|
||||||
border=ft.border.all(1, "#1e293b")
|
border=ft.border.all(1, "#1e293b"),
|
||||||
)
|
)
|
||||||
|
|
||||||
page.add(
|
page.add(
|
||||||
@@ -470,7 +580,7 @@ def main(page: ft.Page):
|
|||||||
queue_card,
|
queue_card,
|
||||||
ft.Container(height=10),
|
ft.Container(height=10),
|
||||||
ft.Text("Live Logs", size=16, weight=ft.FontWeight.BOLD, color=c_text),
|
ft.Text("Live Logs", size=16, weight=ft.FontWeight.BOLD, color=c_text),
|
||||||
terminal_card
|
terminal_card,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Background polling loop
|
# Background polling loop
|
||||||
@@ -481,30 +591,80 @@ def main(page: ft.Page):
|
|||||||
|
|
||||||
# Update System Info
|
# Update System Info
|
||||||
info = w_info.get("info") or {}
|
info = w_info.get("info") or {}
|
||||||
teile = [t for t in (
|
teile = [
|
||||||
|
t
|
||||||
|
for t in (
|
||||||
info.get("cpu_modell"),
|
info.get("cpu_modell"),
|
||||||
f"{info.get('cpu_kerne')} Kerne" if info.get("cpu_kerne") else "",
|
f"{info.get('cpu_kerne')} Kerne"
|
||||||
info.get("cpu_simd") if info.get("cpu_simd") not in ("", "unbekannt") else "",
|
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 []),
|
", ".join(w_info.get("encoders") or []),
|
||||||
) if t]
|
)
|
||||||
machine_info.value = " · ".join(teile) if teile else f"{SLOTS} Aufträge gleichzeitig möglich"
|
if t
|
||||||
|
]
|
||||||
|
machine_info.value = (
|
||||||
|
" · ".join(teile)
|
||||||
|
if teile
|
||||||
|
else f"{SLOTS} Aufträge gleichzeitig möglich"
|
||||||
|
)
|
||||||
|
|
||||||
# Update Queue
|
# Update Queue
|
||||||
queue_view.controls.clear()
|
queue_view.controls.clear()
|
||||||
if not jobs:
|
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:
|
else:
|
||||||
for j in jobs[:8]:
|
for j in jobs[:8]:
|
||||||
poster = None
|
poster = None
|
||||||
if j.get("meta") and j["meta"].get("poster_path"):
|
if j.get("meta") and j["meta"].get("poster_path"):
|
||||||
p = j["meta"]["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 = []
|
row_items = []
|
||||||
if poster:
|
if poster:
|
||||||
row_items.append(ft.Image(src=poster, width=40, height=60, border_radius=5, fit=ft.ImageFit.COVER))
|
row_items.append(
|
||||||
row_items.append(ft.Text(job_text(j), color=c_muted, font_family="Consolas", size=13, expand=True))
|
ft.Container(
|
||||||
queue_view.controls.append(ft.Row(row_items, alignment=ft.MainAxisAlignment.START, spacing=15))
|
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
|
# Update status
|
||||||
if not worker_laeuft():
|
if not worker_laeuft():
|
||||||
@@ -514,7 +674,8 @@ def main(page: ft.Page):
|
|||||||
progress_percentage.value = "0%"
|
progress_percentage.value = "0%"
|
||||||
job_title.value = "Nichts in Arbeit"
|
job_title.value = "Nichts in Arbeit"
|
||||||
eta_text.value = "Bereit"
|
eta_text.value = "Bereit"
|
||||||
active_poster.visible = False
|
active_poster_container.visible = False
|
||||||
|
btn_toggle.visible = True
|
||||||
btn_toggle.disabled = False
|
btn_toggle.disabled = False
|
||||||
btn_toggle.text = "Worker Starten"
|
btn_toggle.text = "Worker Starten"
|
||||||
btn_toggle.icon = ft.icons.PLAY_ARROW
|
btn_toggle.icon = ft.icons.PLAY_ARROW
|
||||||
@@ -524,7 +685,11 @@ def main(page: ft.Page):
|
|||||||
status_text.color = c_warning
|
status_text.color = c_warning
|
||||||
progress_ring.value = 0
|
progress_ring.value = 0
|
||||||
progress_percentage.value = "0%"
|
progress_percentage.value = "0%"
|
||||||
active_poster.visible = False
|
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:
|
elif job:
|
||||||
status_text.value = "Aktiv: Komprimiert"
|
status_text.value = "Aktiv: Komprimiert"
|
||||||
status_text.color = c_success
|
status_text.color = c_success
|
||||||
@@ -540,13 +705,19 @@ def main(page: ft.Page):
|
|||||||
poster = None
|
poster = None
|
||||||
if job.get("meta") and job["meta"].get("poster_path"):
|
if job.get("meta") and job["meta"].get("poster_path"):
|
||||||
p = job["meta"]["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:
|
if poster:
|
||||||
active_poster.src = poster
|
active_poster.src = poster
|
||||||
active_poster.visible = True
|
active_poster_container.visible = True
|
||||||
else:
|
else:
|
||||||
active_poster.visible = False
|
active_poster_container.visible = False
|
||||||
|
|
||||||
|
btn_toggle.visible = False
|
||||||
else:
|
else:
|
||||||
status_text.value = "Verbunden & Bereit"
|
status_text.value = "Verbunden & Bereit"
|
||||||
status_text.color = c_accent
|
status_text.color = c_accent
|
||||||
@@ -554,15 +725,11 @@ def main(page: ft.Page):
|
|||||||
progress_percentage.value = "0%"
|
progress_percentage.value = "0%"
|
||||||
job_title.value = "Warte auf Encoder-Aufträge..."
|
job_title.value = "Warte auf Encoder-Aufträge..."
|
||||||
eta_text.value = "Nichts in Arbeit"
|
eta_text.value = "Nichts in Arbeit"
|
||||||
active_poster.visible = False
|
active_poster_container.visible = False
|
||||||
|
btn_toggle.visible = False
|
||||||
btn_toggle.disabled = True
|
|
||||||
btn_toggle.text = "Worker läuft"
|
|
||||||
btn_toggle.icon = ft.icons.CHECK
|
|
||||||
btn_toggle.bgcolor = "#1e293b"
|
|
||||||
|
|
||||||
page.update()
|
page.update()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
time.sleep(ABFRAGE_TAKT_SEKUNDEN)
|
time.sleep(ABFRAGE_TAKT_SEKUNDEN)
|
||||||
|
|
||||||
@@ -580,20 +747,14 @@ def main(page: ft.Page):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
threading.Thread(target=poll_loop, daemon=True).start()
|
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__":
|
if __name__ == "__main__":
|
||||||
ft.app(target=main)
|
ft.app(target=main)
|
||||||
|
|||||||
@@ -75,9 +75,11 @@ def test_leerer_pfad_stuerzt_nicht_ab():
|
|||||||
# --- Die Meldung: eine falsche Ursache ist teurer als gar keine -------------
|
# --- Die Meldung: eine falsche Ursache ist teurer als gar keine -------------
|
||||||
|
|
||||||
def _pruefen(monkeypatch, vorhandene, mapping, im_container=False):
|
def _pruefen(monkeypatch, vorhandene, mapping, im_container=False):
|
||||||
|
import ntpath
|
||||||
kennt = _fake_isdir(vorhandene)
|
kennt = _fake_isdir(vorhandene)
|
||||||
monkeypatch.setattr(tasks.os.path, "isdir",
|
monkeypatch.setattr(tasks.os.path, "isdir",
|
||||||
lambda p: kennt(p) or (im_container and p == "/app"))
|
lambda p: kennt(p) or (im_container and p == "/app"))
|
||||||
|
monkeypatch.setattr(tasks.os.path, "dirname", ntpath.dirname)
|
||||||
monkeypatch.setenv("RIPPY_PATH_MAP", mapping)
|
monkeypatch.setenv("RIPPY_PATH_MAP", mapping)
|
||||||
return tasks._erreichbarkeit_pruefen(
|
return tasks._erreichbarkeit_pruefen(
|
||||||
"/app/media/rippy/job1", "\\\\NAS\\rippy\\job1",
|
"/app/media/rippy/job1", "\\\\NAS\\rippy\\job1",
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ Die Oberfläche selbst (tkinter) ist ausgenommen; geprüft wird, was das Fenster
|
|||||||
ANZEIGT: Zustand des Workers, Aufgabentexte, Log-Ausschnitt.
|
ANZEIGT: Zustand des Workers, Aufgabentexte, Log-Ausschnitt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import verwaltung
|
import verwaltung
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Kleines Verwaltungsfenster für den Windows-Worker (Doppelklick aufs Tray).
|
||||||
|
|
||||||
|
Commander-Wunsch 26.07.2026: *„Es wäre cool wenn man einfach mit nem Doppelklick
|
||||||
|
ne kleine Verwaltung hat wo man direkt Logs, Aufgaben, usw. sieht."*
|
||||||
|
|
||||||
|
## Warum ein eigener PROZESS und kein Fenster im Tray
|
||||||
|
|
||||||
|
pystray belegt mit `icon.run()` den Haupt-Thread, und tkinter will seine
|
||||||
|
Ereignisschleife ebenfalls dort haben. Beides in einem Prozess zu verschränken
|
||||||
|
ist eine bekannte Quelle für Fenster, die sich nicht mehr schließen lassen. Als
|
||||||
|
eigener Prozess (`pythonw.exe verwaltung.py`) gibt es das Problem gar nicht: Das
|
||||||
|
Fenster erbt die Umgebung vom Tray (Rippy-Adresse, Worker-Name) und kann
|
||||||
|
abstürzen, ohne den Worker mitzunehmen.
|
||||||
|
|
||||||
|
## Was es anzeigt — und woher
|
||||||
|
|
||||||
|
Alles Fachliche kommt von RIPPY, nicht aus eigener Rechnung: Ob dieser Worker als
|
||||||
|
erreichbar gilt, steht in `/capabilities`; was gerade läuft und wie lange es noch
|
||||||
|
dauert, in `/jobs`. So zeigt das Fenster dieselben Zahlen wie das Dashboard statt
|
||||||
|
einer zweiten, abweichenden Wahrheit.
|
||||||
|
|
||||||
|
Nur das Log kommt lokal: Es ist genau dann die einzige Auskunft, wenn Rippy nicht
|
||||||
|
erreichbar ist.
|
||||||
|
|
||||||
|
tkinter statt WinForms, weil es bei jeder Windows-Python-Installation dabei ist —
|
||||||
|
der Installer soll keine weitere Abhängigkeit brauchen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "")
|
||||||
|
WORKER_NAME = os.getenv("WORKER_NAME", "windows-worker")
|
||||||
|
SLOTS = os.getenv("RIPPY_SLOTS", "1")
|
||||||
|
BASIS = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Farben aus dem Rippy-UI (docker/ui/src/lib/design.ts), damit das Fenster nicht
|
||||||
|
# wie ein Fremdkörper wirkt.
|
||||||
|
BG = "#0f172a"
|
||||||
|
PANEL = "#020617"
|
||||||
|
TEXT = "#e2e8f0"
|
||||||
|
GEDAEMPFT = "#94a3b8"
|
||||||
|
AMBER = "#f59e0b"
|
||||||
|
EMERALD = "#10b981"
|
||||||
|
ROSE = "#f43f5e"
|
||||||
|
|
||||||
|
LOG_ZEILEN = 200
|
||||||
|
TAKT_MS = 4000
|
||||||
|
|
||||||
|
|
||||||
|
def letzte_zeilen(pfad: str, anzahl: int = LOG_ZEILEN, oeffnen=None) -> list:
|
||||||
|
"""Die letzten `anzahl` Zeilen einer Datei (pure genug für einen Test).
|
||||||
|
|
||||||
|
Liest bewusst die GANZE Datei und schneidet ab: Das Worker-Log wird nicht
|
||||||
|
groß (die Brücke drosselt auf 30 Zeilen/Minute), und ein Rückwärts-Suchen
|
||||||
|
über Blockgrenzen wäre mehr Code als Nutzen. Fehler beim Lesen geben eine
|
||||||
|
leere Liste — ein Fenster, das wegen des Logs nicht aufgeht, wäre schlimmer.
|
||||||
|
"""
|
||||||
|
macher = oeffnen or (lambda p: open(p, encoding="utf-8", errors="replace"))
|
||||||
|
try:
|
||||||
|
with macher(pfad) as f:
|
||||||
|
return f.read().splitlines()[-anzahl:]
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def hole(pfad: str, host: str = None, timeout: int = 8):
|
||||||
|
"""GET auf Rippys API — None, wenn es nicht klappt."""
|
||||||
|
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 worker_zustand(capabilities, name: str) -> dict:
|
||||||
|
"""Was Rippy über DIESEN Worker weiß (pure Funktion).
|
||||||
|
|
||||||
|
Rückgabe: {"bekannt": bool, "online": bool, "encoders": [...], "info": {...}}
|
||||||
|
"""
|
||||||
|
for w in ((capabilities or {}).get("workers") or []):
|
||||||
|
if w.get("name") == name:
|
||||||
|
return {
|
||||||
|
"bekannt": True,
|
||||||
|
"online": bool(w.get("online")),
|
||||||
|
"encoders": w.get("encoders") or [],
|
||||||
|
"info": w.get("info") or {},
|
||||||
|
}
|
||||||
|
return {"bekannt": False, "online": False, "encoders": [], "info": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def job_text(job: dict) -> str:
|
||||||
|
"""Eine Zeile für die Aufgabenliste (pure Funktion)."""
|
||||||
|
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 "?")
|
||||||
|
zeile = f"{titel} — {status}"
|
||||||
|
if job.get("status") in ("transcoding", "processing", "running"):
|
||||||
|
zeile += f", {job.get('progress') or 0} %"
|
||||||
|
if job.get("eta_text"):
|
||||||
|
zeile += f" ({job['eta_text']})"
|
||||||
|
return zeile
|
||||||
|
|
||||||
|
|
||||||
|
def status_text(zustand: dict, aktueller_job: dict) -> tuple:
|
||||||
|
"""(Text, Farbe) für die Statuszeile (pure Funktion)."""
|
||||||
|
if not zustand.get("bekannt"):
|
||||||
|
return ("Rippy kennt diesen Worker nicht — läuft er? Ist die Adresse richtig?", ROSE)
|
||||||
|
if not zustand.get("online"):
|
||||||
|
return ("Bei Rippy als NICHT erreichbar gemeldet", ROSE)
|
||||||
|
if aktueller_job:
|
||||||
|
return (job_text(aktueller_job), AMBER)
|
||||||
|
return ("Erreichbar und bereit — nichts in Arbeit", EMERALD)
|
||||||
|
|
||||||
|
|
||||||
|
def deinstallieren() -> None:
|
||||||
|
"""Startet den Deinstaller im selben Ordner (fragt dort selbst nach)."""
|
||||||
|
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 _log_pfad() -> str:
|
||||||
|
"""Dieselbe Wahl wie im Tray (dort ausführlich begründet)."""
|
||||||
|
basis_daten = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") or ""
|
||||||
|
if basis_daten:
|
||||||
|
kandidat = os.path.join(basis_daten, "Rippy Worker", "worker.log")
|
||||||
|
if os.path.exists(kandidat):
|
||||||
|
return kandidat
|
||||||
|
return os.path.join(BASIS, "worker.log")
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user