diff --git a/deploy/worker-windows/install-gui.ps1 b/deploy/worker-windows/install-gui.ps1 new file mode 100644 index 0000000..3369f1f --- /dev/null +++ b/deploy/worker-windows/install-gui.ps1 @@ -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() diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 1666b6e..a1e80e9 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -21,6 +21,6 @@ COPY docker/api/ . # native Worker aus (GET /worker-setup/windows bzw. /worker-setup/paket) — # die Zielmaschine braucht weder git noch Docker. 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"] diff --git a/docker/api/main.py b/docker/api/main.py index 1b8901d..f0fd9d5 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -1089,6 +1089,16 @@ async def worker_setup_windows(): 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") async def worker_setup_paket(): """Worker-Quellcode als Zip — der Windows-Installer lädt ihn von hier. diff --git a/docker/ui/src/components/WorkerVerwaltung.tsx b/docker/ui/src/components/WorkerVerwaltung.tsx index 4fa792b..87da6d5 100644 --- a/docker/ui/src/components/WorkerVerwaltung.tsx +++ b/docker/ui/src/components/WorkerVerwaltung.tsx @@ -1,5 +1,5 @@ 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 { useToast } from '../context/ToastContext' 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 (
+ Einfachster Weg: grafischen Installer herunterladen, doppelklicken, + Fenster ausfüllen, „Installieren". Braucht nur Python 3.10+ auf dem PC — kein Docker, kein git. +
+ ++ 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. +
+{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.' - : '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:'}