413 lines
15 KiB
Python
413 lines
15 KiB
Python
import flet as ft
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
import urllib.request
|
|
import json
|
|
import zipfile
|
|
|
|
|
|
def main(page: ft.Page):
|
|
page.title = "Rippy Encoding-Worker Installation"
|
|
page.window_width = 800
|
|
page.window_height = 650
|
|
page.bgcolor = "#0f172a"
|
|
page.theme_mode = ft.ThemeMode.DARK
|
|
page.scroll = ft.ScrollMode.AUTO
|
|
|
|
# Colors
|
|
c_panel = "#1e293b"
|
|
c_text = "#e2e8f0"
|
|
c_muted = "#94a3b8"
|
|
c_accent = "#6366f1"
|
|
c_success = "#10b981"
|
|
|
|
# State
|
|
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_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,
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
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)
|
|
)
|
|
try:
|
|
page.update()
|
|
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.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:
|
|
data = json.loads(r.read().decode())
|
|
if data.get("hinweis"):
|
|
log(data["hinweis"])
|
|
if data.get("mapping"):
|
|
txt_map.value = data["mapping"]
|
|
log(f"Erfolgreich geholt: {data['mapping']}")
|
|
page.update()
|
|
except Exception as ex:
|
|
log(f"Fehler: {ex}")
|
|
|
|
def do_install(e):
|
|
btn_install.disabled = True
|
|
page.update()
|
|
threading.Thread(target=run_installation, daemon=True).start()
|
|
|
|
def run_installation():
|
|
r_host = txt_host.value.strip()
|
|
w_name = txt_name.value.strip()
|
|
install_dir = txt_pfad.value.strip()
|
|
|
|
if not r_host:
|
|
log("FEHLER: Rippy LAN-IP fehlt!")
|
|
btn_install.disabled = False
|
|
page.update()
|
|
return
|
|
|
|
log(f"Zielverzeichnis: {install_dir}")
|
|
|
|
# Check Python
|
|
python_exe = None
|
|
for cmd in ["py -3", "python"]:
|
|
try:
|
|
res = subprocess.run(
|
|
f"{cmd} --version", shell=True, capture_output=True, text=True
|
|
)
|
|
if "Python 3." in res.stdout:
|
|
python_exe = cmd
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if not python_exe:
|
|
log("FEHLER: Python 3.10+ nicht gefunden.")
|
|
log("Bitte mit 'winget install Python.Python.3.12' installieren.")
|
|
btn_install.disabled = False
|
|
page.update()
|
|
return
|
|
|
|
log(f"Python gefunden: {python_exe}")
|
|
|
|
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")
|
|
os.remove(test_file)
|
|
except Exception:
|
|
log(f"FEHLER: Keine Schreibrechte in {install_dir}")
|
|
log("Bitte als Administrator starten oder Pfad aendern.")
|
|
btn_install.disabled = False
|
|
page.update()
|
|
return
|
|
|
|
# Download worker package
|
|
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:
|
|
zip_ref.extractall(install_dir)
|
|
os.remove(zip_path)
|
|
except Exception as ex:
|
|
log(f"FEHLER beim Download: {ex}")
|
|
btn_install.disabled = False
|
|
page.update()
|
|
return
|
|
|
|
# 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
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
# 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
|
|
try:
|
|
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 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:
|
|
zip_ref.extractall(install_dir)
|
|
os.remove(hb_zip)
|
|
except Exception as ex:
|
|
log(f"Warnung bei HandBrake-Download: {ex}")
|
|
|
|
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 ""
|
|
)
|
|
|
|
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)
|
|
|
|
# Uninstaller
|
|
uninstall_ps1 = f"""param([switch]$Force)
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
if (-not $Force) {{
|
|
$antwort = [System.Windows.Forms.MessageBox]::Show("Rippy-Worker '{w_name}' von diesem PC entfernen?", "Rippy Worker deinstallieren", 4, 32)
|
|
if ($antwort -ne "Yes") {{ exit }}
|
|
}}
|
|
Get-WmiObject Win32_Process | Where-Object {{
|
|
$_.ExecutablePath -like "$PSScriptRoot*" -or $_.CommandLine -like "*gui.py*" -or $_.CommandLine -like "*tray.py*"
|
|
}} | ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }}
|
|
Start-Sleep 3
|
|
$desktopDir = if ($PSScriptRoot -match "(?i)Program Files") {{ [Environment]::GetFolderPath("CommonDesktopDirectory") }} else {{ [Environment]::GetFolderPath("Desktop") }}
|
|
Remove-Item -Force "$desktopDir\\Rippy Worker.lnk" -ErrorAction SilentlyContinue
|
|
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)
|
|
|
|
# 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,
|
|
)
|
|
|
|
# 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"
|
|
)
|
|
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
|
|
)
|
|
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()"
|
|
)
|
|
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}")
|
|
|
|
log("FERTIG!")
|
|
btn_start.visible = True
|
|
btn_install.text = "Neu installieren"
|
|
btn_install.disabled = False
|
|
page.update()
|
|
|
|
def start_worker(e):
|
|
install_dir = txt_pfad.value.strip()
|
|
bat_path = os.path.join(install_dir, "start-gui.bat")
|
|
# os.startfile startet die .bat ueber die Shell-Verknuepfung —
|
|
# CREATE_NO_WINDOW wuerde sie stumm schlucken.
|
|
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,
|
|
)
|
|
|
|
def get_dir(e: ft.FilePickerResultEvent):
|
|
if e.path:
|
|
txt_pfad.value = e.path
|
|
page.update()
|
|
|
|
dir_picker = ft.FilePicker(on_result=get_dir)
|
|
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.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,
|
|
),
|
|
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]),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ft.app(target=main)
|