feat(worker): grafischer Windows-Installer (GUI statt CLI)
Ampel / ampel (push) Successful in 28s

Commander-Wunsch: die CLI-Installation ist grausam. Jetzt:
- install-gui.ps1 (WinForms, ASCII-only): Fenster mit Feldern (Rippy-IP,
  Worker-Name, Autostart-Checkbox), Install-Knopf mit Live-Log, danach
  'Worker starten'-Knopf. Gleiche Schritte wie der CLI-Installer
  (Worker-Code + HandBrake latest + venv + Tray/Start/Uninstall).
  Braucht nur Python 3.10+, keine externe GUI-Runtime.
- API: GET /worker-setup/windows-gui serviert die GUI; Dockerfile kopiert
  sie ins worker_dist/.
- UI (Worker-Tab, Windows): 'Grafischen Installer herunterladen (.bat)' als
  Haupt-Weg — die .bat wird CLIENT-seitig mit der eingetragenen LAN-IP
  erzeugt (Blob-Download), Doppelklick laedt+startet die GUI von Rippy.
  Der CLI-Befehl bleibt als Profi-Alternative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-24 18:47:57 +02:00
parent 57171b4c88
commit 4fb29d16cf
4 changed files with 280 additions and 3 deletions
+226
View File
@@ -0,0 +1,226 @@
# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime).
# Wird ueber rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur
# Python 3.10+ auf der Maschine - kein Docker, kein git.
#
# ASCII-only (PowerShell 5.1 liest .ps1 als ANSI - Sonderzeichen zerschiessen
# das Skript).
param(
[string]$RippyHost = "",
[string]$WorkerName = $env:COMPUTERNAME.ToLower()
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$InstallDir = Join-Path $env:USERPROFILE "rippy-worker"
# --- Fenster ---------------------------------------------------------------
$form = New-Object System.Windows.Forms.Form
$form.Text = "Rippy Encoding-Worker - Installation"
$form.Size = New-Object System.Drawing.Size(560, 520)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.BackColor = [System.Drawing.Color]::FromArgb(248, 250, 252)
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$titel = New-Object System.Windows.Forms.Label
$titel.Text = "Rippy Encoding-Worker einrichten"
$titel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold)
$titel.ForeColor = [System.Drawing.Color]::FromArgb(79, 70, 229)
$titel.Location = New-Object System.Drawing.Point(24, 20)
$titel.Size = New-Object System.Drawing.Size(500, 30)
$form.Controls.Add($titel)
$info = New-Object System.Windows.Forms.Label
$info.Text = "Diese Maschine uebernimmt die Video-Kompression fuer Rippy. Gerippt wird weiter auf der Rippy-Hauptmaschine."
$info.Location = New-Object System.Drawing.Point(24, 52)
$info.Size = New-Object System.Drawing.Size(500, 34)
$info.ForeColor = [System.Drawing.Color]::FromArgb(100, 116, 139)
$form.Controls.Add($info)
# Rippy-Adresse
$lblHost = New-Object System.Windows.Forms.Label
$lblHost.Text = "LAN-IP der Rippy-Maschine (Docker-Host) - NICHT dieser PC:"
$lblHost.Location = New-Object System.Drawing.Point(24, 96)
$lblHost.Size = New-Object System.Drawing.Size(500, 18)
$form.Controls.Add($lblHost)
$txtHost = New-Object System.Windows.Forms.TextBox
$txtHost.Location = New-Object System.Drawing.Point(24, 116)
$txtHost.Size = New-Object System.Drawing.Size(300, 26)
$txtHost.Text = $RippyHost
$txtHost.Font = New-Object System.Drawing.Font("Consolas", 10)
$form.Controls.Add($txtHost)
# Worker-Name
$lblName = New-Object System.Windows.Forms.Label
$lblName.Text = "Name dieses Workers (frei waehlbar):"
$lblName.Location = New-Object System.Drawing.Point(24, 152)
$lblName.Size = New-Object System.Drawing.Size(500, 18)
$form.Controls.Add($lblName)
$txtName = New-Object System.Windows.Forms.TextBox
$txtName.Location = New-Object System.Drawing.Point(24, 172)
$txtName.Size = New-Object System.Drawing.Size(300, 26)
$txtName.Text = $WorkerName
$form.Controls.Add($txtName)
# Autostart
$chkAuto = New-Object System.Windows.Forms.CheckBox
$chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)"
$chkAuto.Location = New-Object System.Drawing.Point(24, 206)
$chkAuto.Size = New-Object System.Drawing.Size(400, 22)
$chkAuto.Checked = $true
$form.Controls.Add($chkAuto)
# Log-Bereich
$log = New-Object System.Windows.Forms.TextBox
$log.Location = New-Object System.Drawing.Point(24, 240)
$log.Size = New-Object System.Drawing.Size(500, 170)
$log.Multiline = $true
$log.ReadOnly = $true
$log.ScrollBars = "Vertical"
$log.BackColor = [System.Drawing.Color]::FromArgb(15, 23, 42)
$log.ForeColor = [System.Drawing.Color]::FromArgb(226, 232, 240)
$log.Font = New-Object System.Drawing.Font("Consolas", 8.5)
$form.Controls.Add($log)
# Knoepfe
$btnInstall = New-Object System.Windows.Forms.Button
$btnInstall.Text = "Installieren"
$btnInstall.Location = New-Object System.Drawing.Point(24, 424)
$btnInstall.Size = New-Object System.Drawing.Size(150, 40)
$btnInstall.BackColor = [System.Drawing.Color]::FromArgb(79, 70, 229)
$btnInstall.ForeColor = [System.Drawing.Color]::White
$btnInstall.FlatStyle = "Flat"
$btnInstall.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
$form.Controls.Add($btnInstall)
$btnStart = New-Object System.Windows.Forms.Button
$btnStart.Text = "Worker starten"
$btnStart.Location = New-Object System.Drawing.Point(190, 424)
$btnStart.Size = New-Object System.Drawing.Size(150, 40)
$btnStart.BackColor = [System.Drawing.Color]::FromArgb(16, 185, 129)
$btnStart.ForeColor = [System.Drawing.Color]::White
$btnStart.FlatStyle = "Flat"
$btnStart.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
$btnStart.Visible = $false
$form.Controls.Add($btnStart)
function Log($text) {
$log.AppendText($text + "`r`n")
[System.Windows.Forms.Application]::DoEvents()
}
# --- Installations-Logik ---------------------------------------------------
function Do-Install {
$rHost = $txtHost.Text.Trim()
$wName = $txtName.Text.Trim()
if (-not $rHost) { [System.Windows.Forms.MessageBox]::Show("Bitte die LAN-IP der Rippy-Maschine eintragen.", "Fehlt"); return }
if (-not $wName) { $wName = $env:COMPUTERNAME.ToLower() }
$btnInstall.Enabled = $false
$log.Clear()
try {
# 1. Python finden
$python = $null
foreach ($k in @("py -3", "python")) {
try {
$v = Invoke-Expression "$k --version" 2>&1
if ("$v" -match "Python 3\.(\d+)" -and [int]$Matches[1] -ge 10) { $python = $k; break }
} catch {}
}
if (-not $python) {
Log "FEHLER: Python 3.10+ nicht gefunden."
Log "Installieren mit: winget install Python.Python.3.12"
Log "Danach diesen Installer erneut starten."
$btnInstall.Enabled = $true
return
}
Log "Python gefunden: $python"
# 2. Ordner + Worker-Code
New-Item -ItemType Directory -Force $InstallDir | Out-Null
Set-Location $InstallDir
$HandBrakeVersion = "1.11.2"
try {
$rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" -TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" }
if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") }
} catch {}
Log "Lade Worker-Code von $rHost ..."
Invoke-WebRequest "http://$rHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing
Expand-Archive worker.zip -DestinationPath . -Force
Remove-Item worker.zip
# 3. venv + Abhaengigkeiten
if (-not (Test-Path "venv")) { Log "Lege Python-Umgebung an ..."; Invoke-Expression "$python -m venv venv" }
Log "Installiere Python-Abhaengigkeiten (kann 1-2 min dauern) ..."
& .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip
& .\venv\Scripts\pip.exe install --quiet -r requirements.txt
& .\venv\Scripts\pip.exe install --quiet pystray pillow
Log "Abhaengigkeiten installiert."
# 4. HandBrakeCLI
if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not (Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue)) {
Log "Lade HandBrakeCLI $HandBrakeVersion (offizielles Release) ..."
$url = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip"
Invoke-WebRequest $url -OutFile hb.zip -UseBasicParsing
Expand-Archive hb.zip -DestinationPath . -Force
Remove-Item hb.zip
}
Log "HandBrake bereit."
# 5. Start-Skripte + Deinstaller erzeugen
$trayBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_TRAY_HOST=$rHost`r`nset PATH=%~dp0;%PATH%`r`nstart `"`" venv\Scripts\pythonw.exe tray.py"
Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII
$workBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset PATH=%~dp0;%PATH%`r`nvenv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode --pool=solo -n ${wName}@%%h"
Set-Content -Path "start-worker.bat" -Value $workBat -Encoding ASCII
$uninstall = @"
param([switch]`$Force)
if (-not `$Force) { if ([System.Windows.Forms.MessageBox]::Show("Rippy-Worker '$wName' deinstallieren?","Rippy",4) -ne "Yes") { exit } }
Add-Type -AssemblyName System.Windows.Forms
Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }
Start-Sleep 2
schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null
try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch {}
Set-Location (Split-Path `$PSScriptRoot -Parent)
Remove-Item -Recurse -Force `$PSScriptRoot
[System.Windows.Forms.MessageBox]::Show("Rippy-Worker deinstalliert.","Rippy")
"@
Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII
# 6. Autostart
if ($chkAuto.Checked) {
schtasks /create /f /tn "RippyWorker" /tr "`"$InstallDir\start-tray.bat`"" /sc onlogon | Out-Null
Log "Autostart eingerichtet (Start bei Anmeldung)."
}
Log ""
Log "FERTIG! Ordner: $InstallDir"
Log "Klick 'Worker starten' - er erscheint dann in Rippy unter Einstellungen -> Worker."
$btnStart.Visible = $true
$btnInstall.Text = "Neu installieren"
$btnInstall.Enabled = $true
} catch {
Log ""
Log "FEHLER: $_"
Log "Tipp: Ist die IP korrekt und die Rippy-Maschine im Heimnetz erreichbar?"
$btnInstall.Enabled = $true
}
}
$btnInstall.Add_Click({ Do-Install })
$btnStart.Add_Click({
Start-Process -FilePath (Join-Path $InstallDir "start-tray.bat") -WindowStyle Hidden
[System.Windows.Forms.MessageBox]::Show("Worker gestartet - Symbol erscheint unten rechts neben der Uhr.", "Rippy")
$form.Close()
})
[void]$form.ShowDialog()
+1 -1
View File
@@ -21,6 +21,6 @@ COPY docker/api/ .
# native Worker aus (GET /worker-setup/windows bzw. /worker-setup/paket) — # native Worker aus (GET /worker-setup/windows bzw. /worker-setup/paket) —
# die Zielmaschine braucht weder git noch Docker. # die Zielmaschine braucht weder git noch Docker.
COPY docker/worker/*.py docker/worker/requirements.txt worker_dist/ COPY docker/worker/*.py docker/worker/requirements.txt worker_dist/
COPY deploy/worker-windows/install.ps1 worker_dist/ COPY deploy/worker-windows/install.ps1 deploy/worker-windows/install-gui.ps1 worker_dist/
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app"]
+10
View File
@@ -1089,6 +1089,16 @@ async def worker_setup_windows():
return FileResponse(pfad, media_type="text/plain", filename="install-rippy-worker.ps1") return FileResponse(pfad, media_type="text/plain", filename="install-rippy-worker.ps1")
@app.get("/worker-setup/windows-gui")
async def worker_setup_windows_gui():
"""Grafischer Windows-Installer (WinForms) — vom .bat-Launcher geladen und
gestartet, damit der Nutzer ein Fenster statt CLI bekommt."""
pfad = "worker_dist/install-gui.ps1"
if not os.path.isfile(pfad):
raise HTTPException(status_code=404, detail="GUI-Installer nicht im Image — API neu bauen")
return FileResponse(pfad, media_type="text/plain", filename="rippy-worker-gui.ps1")
@app.get("/worker-setup/paket") @app.get("/worker-setup/paket")
async def worker_setup_paket(): async def worker_setup_paket():
"""Worker-Quellcode als Zip — der Windows-Installer lädt ihn von hier. """Worker-Quellcode als Zip — der Windows-Installer lädt ihn von hier.
+43 -2
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Server, RefreshCw, Copy, CheckCircle, Trash2 } from 'lucide-react' import { Server, RefreshCw, Copy, CheckCircle, Trash2, Download } from 'lucide-react'
import { api } from '../lib/api' import { api } from '../lib/api'
import { useToast } from '../context/ToastContext' import { useToast } from '../context/ToastContext'
import { Card, CardHeader, CardTitle, CardContent } from './ui/Card' import { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
@@ -91,6 +91,30 @@ export default function WorkerVerwaltung() {
}) })
} }
// Grafischer Installer: erzeugt eine .bat CLIENT-seitig mit der eingetragenen
// LAN-IP (der Browser kennt sie, die API nicht sicher). Doppelklick lädt den
// WinForms-Installer von Rippy und startet ihn — kein CLI mehr.
const guiInstallerHerunterladen = () => {
const host = rippyHost.trim()
if (!host) { toast('error', 'Erst die LAN-IP der Rippy-Maschine eintragen'); return }
const psInner = `try { Invoke-WebRequest 'http://${host}/api/worker-setup/windows-gui' -OutFile ($env:TEMP + '\\rippy-gui.ps1') -UseBasicParsing; & ($env:TEMP + '\\rippy-gui.ps1') -RippyHost '${host}' } catch { Write-Host $_.Exception.Message -ForegroundColor Red; Read-Host 'Fehler - Enter zum Schliessen' }`
const bat = [
'@echo off',
'title Rippy Worker Setup',
'echo.',
`echo Starte grafischen Rippy-Worker-Installer von ${host} ...`,
'echo.',
`powershell -NoProfile -ExecutionPolicy Bypass -STA -Command "${psInner}"`,
].join('\r\n')
const blob = new Blob([bat], { type: 'application/octet-stream' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = 'rippy-worker-setup.bat'
document.body.appendChild(a); a.click(); a.remove()
URL.revokeObjectURL(a.href)
toast('success', 'rippy-worker-setup.bat heruntergeladen — doppelklicken zum Installieren')
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Verbundene Worker */} {/* Verbundene Worker */}
@@ -200,10 +224,27 @@ export default function WorkerVerwaltung() {
))} ))}
</div> </div>
{/* Windows: grafischer Installer als Haupt-Weg (Doppelklick statt CLI) */}
{variante === 'windows' && (
<div className="rounded-xl p-4 border border-amber-500/30 bg-amber-500/5 space-y-2">
<p className="text-sm text-slate-700 dark:text-slate-300">
<strong>Einfachster Weg:</strong> grafischen Installer herunterladen, doppelklicken,
Fenster ausfüllen, Installieren". Braucht nur Python 3.10+ auf dem PC — kein Docker, kein git.
</p>
<Button variant="amber" onClick={guiInstallerHerunterladen} disabled={!rippyHost.trim()}>
<Download size={16} /> Grafischen Installer herunterladen (.bat)
</Button>
<p className="text-xs text-slate-500 dark:text-slate-400">
Windows warnt beim Doppelklick evtl. („Windows hat Ihren PC geschützt") Weitere Informationen"
→ „Trotzdem ausführen". Die .bat lädt den Installer nur von deiner Rippy-Maschine.
</p>
</div>
)}
<p className="text-xs text-slate-500 dark:text-slate-400"> <p className="text-xs text-slate-500 dark:text-slate-400">
{variante === 'linux' {variante === 'linux'
? 'Auf der Linux-Maschine (mit Docker) ausführen — WORKER_NAME frei wählen, unter dem Namen taucht sie oben auf und übernimmt Kompressions-Jobs. GPU wird erkannt und angezeigt.' ? 'Auf der Linux-Maschine (mit Docker) ausführen — WORKER_NAME frei wählen, unter dem Namen taucht sie oben auf und übernimmt Kompressions-Jobs. GPU wird erkannt und angezeigt.'
: 'In einer PowerShell auf dem Windows-PC ausführen (braucht nur Python 3.10+ — kein Docker, kein git). Der Installer holt Worker-Code und HandBrake automatisch.'} : 'Alternativ (für Profis) per PowerShell-Befehl:'}
</p> </p>
<div className="relative rounded-xl p-4 font-mono text-xs whitespace-pre overflow-x-auto bg-slate-900 text-slate-200 border border-slate-800"> <div className="relative rounded-xl p-4 font-mono text-xs whitespace-pre overflow-x-auto bg-slate-900 text-slate-200 border border-slate-800">