# 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 # Standard-Zielverzeichnis: wie jedes andere Programm unter "Programme" # (Commander-Wunsch 25.07.2026 - vorher lag der Worker im Benutzerprofil, was # fuer ein Programm unueblich ist). GetFolderPath liefert den ECHTEN Pfad; # deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heisst # trotzdem "C:\Program Files". # # Schreiben dorthin braucht Administratorrechte - deshalb ist die .exe mit # -requireAdmin gebaut (build-exe.ps1) und fragt beim Start einmal per UAC. # Ohne das waere hier der naechste "Zugriff verweigert" programmiert. $StandardZiel = Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker" $InstallDir = $StandardZiel # --- Fenster --------------------------------------------------------------- $form = New-Object System.Windows.Forms.Form $form.Text = "Rippy Encoding-Worker - Installation" $form.Size = New-Object System.Drawing.Size(560, 575) $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) # Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weisses Loch) # - kein externes .ico noetig. Erscheint in Titelleiste und Taskleiste. try { $ibmp = New-Object System.Drawing.Bitmap 32, 32 $ig = [System.Drawing.Graphics]::FromImage($ibmp) $ig.SmoothingMode = "AntiAlias" $ig.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(79, 70, 229))), 1, 1, 30, 30) $ig.FillEllipse([System.Drawing.Brushes]::White, 12, 12, 8, 8) $form.Icon = [System.Drawing.Icon]::FromHandle($ibmp.GetHicon()) } catch {} $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) # Zielverzeichnis - frei waehlbar, Standard "Programme" $lblPfad = New-Object System.Windows.Forms.Label $lblPfad.Text = "Installieren nach:" $lblPfad.Location = New-Object System.Drawing.Point(24, 206) $lblPfad.Size = New-Object System.Drawing.Size(500, 18) $form.Controls.Add($lblPfad) $txtPfad = New-Object System.Windows.Forms.TextBox $txtPfad.Location = New-Object System.Drawing.Point(24, 226) $txtPfad.Size = New-Object System.Drawing.Size(390, 26) $txtPfad.Text = $StandardZiel $txtPfad.Font = New-Object System.Drawing.Font("Consolas", 9) $form.Controls.Add($txtPfad) $btnPfad = New-Object System.Windows.Forms.Button $btnPfad.Text = "Aendern ..." $btnPfad.Location = New-Object System.Drawing.Point(424, 225) $btnPfad.Size = New-Object System.Drawing.Size(100, 28) $btnPfad.FlatStyle = "Flat" $form.Controls.Add($btnPfad) $btnPfad.Add_Click({ $dlg = New-Object System.Windows.Forms.FolderBrowserDialog $dlg.Description = "Ordner waehlen - darin wird 'Rippy Worker' angelegt" $dlg.ShowNewFolderButton = $true if ($dlg.ShowDialog() -eq "OK") { # Der Dialog liefert den ELTERN-Ordner; der Unterordner kommt von uns, # damit die Deinstallation nie einen fremden Ordner mitloescht. $txtPfad.Text = (Join-Path $dlg.SelectedPath "Rippy Worker") } }) # Autostart $chkAuto = New-Object System.Windows.Forms.CheckBox $chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)" $chkAuto.Location = New-Object System.Drawing.Point(24, 264) $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, 296) $log.Size = New-Object System.Drawing.Size(500, 160) $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, 470) $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, 470) $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() } # Zielverzeichnis aus dem Feld. script:-Bereich, weil "Worker starten" # und die Abschlussmeldung denselben Pfad brauchen. $gewaehlt = $txtPfad.Text.Trim() if (-not $gewaehlt) { $gewaehlt = $StandardZiel } if (-not [System.IO.Path]::IsPathRooted($gewaehlt)) { [System.Windows.Forms.MessageBox]::Show( "Bitte einen vollstaendigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.", "Pfad unvollstaendig") return } $script:InstallDir = $gewaehlt $btnInstall.Enabled = $false $log.Clear() Log "Ziel: $InstallDir" 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 # Erst PRUEFEN, ob wir dort ueberhaupt schreiben duerfen. Die .exe ist mit # -requireAdmin gebaut und fragt per UAC, aber wer install-gui.ps1 direkt # startet, hat die Rechte nicht - dann soll hier ein brauchbarer Satz # stehen und nicht "Zugriff verweigert" aus irgendeiner Tiefe. try { New-Item -ItemType Directory -Force $InstallDir -ErrorAction Stop | Out-Null $probe = Join-Path $InstallDir ".schreibtest" Set-Content -Path $probe -Value "x" -ErrorAction Stop Remove-Item -Force $probe -ErrorAction SilentlyContinue } catch { Log "FEHLER: In '$InstallDir' darf nicht geschrieben werden." Log "" Log "Das ist normal bei Ordnern unter 'Programme' - dafuer braucht es" Log "Administratorrechte. Zwei Wege:" Log " 1. Diesen Installer mit Rechtsklick > 'Als Administrator" Log " ausfuehren' starten (der empfohlene Weg), ODER" Log " 2. oben ueber 'Aendern ...' einen Ordner im eigenen" Log " Benutzerprofil waehlen - dort geht es ohne Adminrechte." $btnInstall.Enabled = $true return } 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 # Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die # Installation je nach Zielverzeichnis persoenlich oder maschinenweit war. foreach (`$ordner in @("Startup", "CommonStartup")) { try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} } # ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden. 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 ueber den Autostart-ORDNER, nicht ueber schtasks. # # Befund 25.07.2026 (auf dem Commander-PC aufgetreten): schtasks /create # mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER der # Aufgabenplanung an, und das verlangt Administratorrechte. Der # Installer laeuft normal ohne. schtasks schrieb deshalb # "FEHLER: Zugriff verweigert." nach stderr, und weil oben # $ErrorActionPreference = "Stop" steht, wurde daraus ein Abbruch der # GESAMTEN Installation - obwohl alles ausser dem Autostart fertig war. # Der Nutzer sah nur "FEHLER: FEHLER: Zugriff verweigert" samt dem # irrefuehrenden Tipp, die IP koenne falsch sein. # # Der Autostart-Ordner braucht NIE Adminrechte, und der Nutzer kann die # Verknuepfung dort selbst sehen und loeschen. Zwei Fliegen. if ($chkAuto.Checked) { try { # Liegt das Programm unter "Programme", ist es eine Installation # fuer die ganze Maschine - dann gehoert der Autostart in den # Autostart-Ordner ALLER Benutzer. Das passt auch zur Rechtelage: # der Installer laeuft dafuer ohnehin erhoeht, und bei einer # Erhoehung ueber ein FREMDES Administratorkonto waere der # benutzereigene Ordner der des Admins - also der falsche. # Bei einer Installation ins eigene Profil bleibt es persoenlich. $programme = [Environment]::GetFolderPath("ProgramFiles") $programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86") $fuerAlle = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") ` -or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase")) $autostartOrdner = if ($fuerAlle) { [Environment]::GetFolderPath("CommonStartup") } else { [Environment]::GetFolderPath("Startup") } $verknuepfung = Join-Path $autostartOrdner "RippyWorker.lnk" $wsh = New-Object -ComObject WScript.Shell $lnk = $wsh.CreateShortcut($verknuepfung) $lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat") $lnk.WorkingDirectory = $InstallDir $lnk.Description = "Rippy Encoding-Worker" $lnk.Save() if ($fuerAlle) { Log "Autostart eingerichtet (fuer alle Benutzer dieser Maschine)." } else { Log "Autostart eingerichtet (fuer dich, startet bei der Anmeldung)." } } catch { # Nur eine Warnung: der Worker selbst ist fertig und startbar. Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." Log " Der Worker ist trotzdem fertig installiert und laeuft." Log " Von Hand: start-tray.bat in den Autostart-Ordner verknuepfen" Log " (Windows-Taste + R, dann shell:startup eingeben)." } } 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()