import flet as ft import os import subprocess import threading import urllib.request import json import time import zipfile def main(page: ft.Page): page.title = "Rippy Encoding-Worker Installation" page.window_width = 800 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: 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: 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 as ex: 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: 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)