# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime). # Wird über rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur # Python 3.10+ auf der Maschine - kein Docker, kein git. # # WICHTIG - KODIERUNG: Diese Datei MUSS als UTF-8 MIT BOM gespeichert werden. # PowerShell 5.1 liest .ps1 ohne BOM als ANSI; Umlaute werden dann zu Kauderwelsch # UND das Skript wirft einen Parser-Fehler, weil in "ö" ein Anführungszeichen # steckt. Mit BOM liest 5.1 korrekt UTF-8 - am 25.07.2026 beidseitig gemessen: # ohne BOM: "Größe" + Unerwartetes Token # mit BOM: "Größe" + laeuft # Nach jeder Änderung pruefen: powershell.exe -Command "Parser::ParseFile(...)" 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 # für ein Programm unüblich ist). GetFolderPath liefert den ECHTEN Pfad; # deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heißt # 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 wäre hier der nächste "Zugriff verweigert" programmiert. $StandardZiel = Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker" $InstallDir = $StandardZiel # --- Farben: dieselben Töne wie das Rippy-Web-UI --------------------------- # Übernommen aus docker/ui/src/lib/design.ts bzw. den Tailwind-Klassen des UI, # damit der Installer nicht wie ein Fremdkörper wirkt. $cBg = [System.Drawing.Color]::FromArgb(15, 23, 42) # slate-900, Fensterfläche $cPanel = [System.Drawing.Color]::FromArgb(2, 6, 23) # slate-950, Log + Eingaben $cFeld = [System.Drawing.Color]::FromArgb(30, 41, 59) # slate-800, Eingabefelder $cText = [System.Drawing.Color]::FromArgb(226, 232, 240) # slate-200 $cGedaempft= [System.Drawing.Color]::FromArgb(148, 163, 184) # slate-400, Beschriftungen $cIndigo = [System.Drawing.Color]::FromArgb(99, 102, 241) $cAmber = [System.Drawing.Color]::FromArgb(245, 158, 11) $cEmerald = [System.Drawing.Color]::FromArgb(16, 185, 129) $cPurple = [System.Drawing.Color]::FromArgb(126, 34, 206) # --- Fenster --------------------------------------------------------------- $form = New-Object System.Windows.Forms.Form $form.Text = "Rippy Encoding-Worker - Installation" $form.ClientSize = New-Object System.Drawing.Size(560, 672) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.BackColor = $cBg $form.ForeColor = $cText $form.Font = New-Object System.Drawing.Font("Segoe UI", 9) # Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weißes Loch) # - kein externes .ico nötig. 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 {} # --- Kopfbereich mit Farbverlauf -------------------------------------------- # Das Rippy-UI hat oben einen Verlauf amber -> indigo -> purple (siehe # FirstRunWizard.tsx). WinForms kann das nicht von sich aus, also wird der # Verlauf im Paint-Ereignis gezeichnet - inklusive Disc-Symbol. $kopf = New-Object System.Windows.Forms.Panel $kopf.Dock = "Top" $kopf.Height = 104 $form.Controls.Add($kopf) $kopf.Add_Paint({ param($absender, $e) $flaeche = New-Object System.Drawing.Rectangle 0, 0, $absender.Width, $absender.Height $pinsel = New-Object System.Drawing.Drawing2D.LinearGradientBrush( $flaeche, $cAmber, $cPurple, [System.Drawing.Drawing2D.LinearGradientMode]::Horizontal) # Dreifarbig wie im UI: der Indigo-Ton in der Mitte $mischung = New-Object System.Drawing.Drawing2D.ColorBlend 3 $mischung.Colors = @($cAmber, $cIndigo, $cPurple) $mischung.Positions = @(0.0, 0.45, 1.0) $pinsel.InterpolationColors = $mischung $e.Graphics.FillRectangle($pinsel, $flaeche) $pinsel.Dispose() # Disc-Symbol: Ring aussen, weisses Loch in der Mitte $e.Graphics.SmoothingMode = "AntiAlias" $weiss = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(235, 255, 255, 255)) $e.Graphics.FillEllipse($weiss, 24, 28, 48, 48) $e.Graphics.FillEllipse((New-Object System.Drawing.SolidBrush $cIndigo), 36, 40, 24, 24) $e.Graphics.FillEllipse($weiss, 45, 49, 6, 6) $weiss.Dispose() # Texte direkt zeichnen - so bleibt der Verlauf ohne Kästchen sichtbar $fTitel = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold) $fUnter = New-Object System.Drawing.Font("Segoe UI", 9) $e.Graphics.DrawString("Rippy Encoding-Worker", $fTitel, [System.Drawing.Brushes]::White, 88, 26) $e.Graphics.DrawString( "Diese Maschine übernimmt die Video-Kompression für Rippy." + [char]10 + "Gerippt wird weiter auf der Rippy-Hauptmaschine.", $fUnter, (New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(225, 255, 255, 255))), 90, 56) $fTitel.Dispose(); $fUnter.Dispose() }) # 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, 120) $lblHost.Size = New-Object System.Drawing.Size(500, 18) $lblHost.ForeColor = $cGedaempft $form.Controls.Add($lblHost) $txtHost = New-Object System.Windows.Forms.TextBox $txtHost.Location = New-Object System.Drawing.Point(24, 140) $txtHost.Size = New-Object System.Drawing.Size(300, 26) $txtHost.Text = $RippyHost $txtHost.Font = New-Object System.Drawing.Font("Consolas", 10) $txtHost.BackColor = $cFeld $txtHost.ForeColor = $cText $txtHost.BorderStyle = "FixedSingle" $form.Controls.Add($txtHost) # Worker-Name $lblName = New-Object System.Windows.Forms.Label $lblName.Text = "Name dieses Workers (frei wählbar):" $lblName.Location = New-Object System.Drawing.Point(24, 176) $lblName.Size = New-Object System.Drawing.Size(500, 18) $lblName.ForeColor = $cGedaempft $form.Controls.Add($lblName) $txtName = New-Object System.Windows.Forms.TextBox $txtName.Location = New-Object System.Drawing.Point(24, 196) $txtName.Size = New-Object System.Drawing.Size(240, 26) $txtName.Text = $WorkerName $txtName.BackColor = $cFeld $txtName.ForeColor = $cText $txtName.BorderStyle = "FixedSingle" $form.Controls.Add($txtName) # Gleichzeitige Auftraege (Commander 26.07.2026: "Mehrere Encodes gleichzeitig"). # Vorher lief der Worker fest mit --pool=solo und nahm genau EINEN Auftrag an. # Vorbelegung nach Kernzahl: ab 12 Kernen zwei, sonst einer. Grund: HandBrake # nutzt schon alle Kerne, aber x265 skaliert nicht linear - auf einer breiten # CPU bringt ein zweiter Encode mehr Durchsatz, auf einer schmalen bremst er nur. $lblSlots = New-Object System.Windows.Forms.Label $lblSlots.Text = "Gleichzeitig:" $lblSlots.Location = New-Object System.Drawing.Point(280, 176) $lblSlots.Size = New-Object System.Drawing.Size(120, 18) $lblSlots.ForeColor = $cGedaempft $form.Controls.Add($lblSlots) $numSlots = New-Object System.Windows.Forms.NumericUpDown $numSlots.Location = New-Object System.Drawing.Point(280, 196) $numSlots.Size = New-Object System.Drawing.Size(70, 26) $numSlots.Minimum = 1 $numSlots.Maximum = 8 $numSlots.Value = $(if ([Environment]::ProcessorCount -ge 12) { 2 } else { 1 }) $numSlots.BackColor = $cFeld $numSlots.ForeColor = $cText $numSlots.BorderStyle = "FixedSingle" $form.Controls.Add($numSlots) $lblKerne = New-Object System.Windows.Forms.Label $lblKerne.Text = "von $([Environment]::ProcessorCount) Kernen" $lblKerne.Location = New-Object System.Drawing.Point(358, 201) $lblKerne.Size = New-Object System.Drawing.Size(166, 18) $lblKerne.ForeColor = $cGedaempft $form.Controls.Add($lblKerne) # Zielverzeichnis - frei wählbar, Standard "Programme" $lblPfad = New-Object System.Windows.Forms.Label $lblPfad.Text = "Installieren nach:" $lblPfad.Location = New-Object System.Drawing.Point(24, 232) $lblPfad.Size = New-Object System.Drawing.Size(500, 18) $lblPfad.ForeColor = $cGedaempft $form.Controls.Add($lblPfad) $txtPfad = New-Object System.Windows.Forms.TextBox $txtPfad.Location = New-Object System.Drawing.Point(24, 252) $txtPfad.Size = New-Object System.Drawing.Size(374, 26) $txtPfad.Text = $StandardZiel $txtPfad.Font = New-Object System.Drawing.Font("Consolas", 9) $txtPfad.BackColor = $cFeld $txtPfad.ForeColor = $cText $txtPfad.BorderStyle = "FixedSingle" $form.Controls.Add($txtPfad) $btnPfad = New-Object System.Windows.Forms.Button $btnPfad.Text = "Ändern ..." $btnPfad.Location = New-Object System.Drawing.Point(408, 251) $btnPfad.Size = New-Object System.Drawing.Size(116, 28) $btnPfad.FlatStyle = "Flat" $btnPfad.BackColor = $cFeld $btnPfad.ForeColor = $cText $btnPfad.FlatAppearance.BorderColor = $cGedaempft $form.Controls.Add($btnPfad) $btnPfad.Add_Click({ $dlg = New-Object System.Windows.Forms.FolderBrowserDialog $dlg.Description = "Ordner wählen - 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 mitlöscht. $txtPfad.Text = (Join-Path $dlg.SelectedPath "Rippy Worker") } }) # Netzwerk-Freigabe fuer die Rohdaten (RIPPY_PATH_MAP) # # DAS entscheidet, ob dieser Worker ueberhaupt etwas komprimieren kann # (Befund 26.07.2026): Rippy schickt ihm Pfade wie /app/media/rippy/ - # Pfade INNERHALB des Rippy-Containers. Ohne Uebersetzung auf eine Freigabe # sieht dieser PC dort nichts, nimmt die Aufgabe an und lehnt sie 182 ms # spaeter ab. Der Nutzer soll das nicht verstehen muessen: Rippy hat die # Freigabe selbst eingehaengt und kennt sie, der Knopf holt sie. $lblMap = New-Object System.Windows.Forms.Label $lblMap.Text = "Netzwerk-Freigabe mit den Rohdaten (holt Rippy selbst):" $lblMap.Location = New-Object System.Drawing.Point(24, 288) $lblMap.Size = New-Object System.Drawing.Size(500, 18) $lblMap.ForeColor = $cGedaempft $form.Controls.Add($lblMap) $txtMap = New-Object System.Windows.Forms.TextBox $txtMap.Location = New-Object System.Drawing.Point(24, 308) $txtMap.Size = New-Object System.Drawing.Size(374, 26) $txtMap.Font = New-Object System.Drawing.Font("Consolas", 9) $txtMap.BackColor = $cFeld $txtMap.ForeColor = $cText $txtMap.BorderStyle = "FixedSingle" $form.Controls.Add($txtMap) $btnMap = New-Object System.Windows.Forms.Button $btnMap.Text = "Von Rippy holen" $btnMap.Location = New-Object System.Drawing.Point(408, 307) $btnMap.Size = New-Object System.Drawing.Size(116, 28) $btnMap.FlatStyle = "Flat" $btnMap.BackColor = $cFeld $btnMap.ForeColor = $cText $btnMap.FlatAppearance.BorderColor = $cGedaempft $form.Controls.Add($btnMap) $lblMapHilfe = New-Object System.Windows.Forms.Label $lblMapHilfe.Text = "Leer lassen und installieren - der Installer holt und prüft den Wert selbst." $lblMapHilfe.Location = New-Object System.Drawing.Point(24, 338) $lblMapHilfe.Size = New-Object System.Drawing.Size(500, 18) $lblMapHilfe.ForeColor = $cGedaempft $form.Controls.Add($lblMapHilfe) # Autostart $chkAuto = New-Object System.Windows.Forms.CheckBox $chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)" $chkAuto.Location = New-Object System.Drawing.Point(24, 364) $chkAuto.Size = New-Object System.Drawing.Size(400, 22) $chkAuto.Checked = $true $chkAuto.ForeColor = $cText $form.Controls.Add($chkAuto) # Verknuepfung auf dem Desktop (Commander-Wunsch 26.07.2026). Vorher lagen die # Startdateien nur im Programmordner - wer den Worker von Hand starten wollte, # musste erst "C:\Program Files\Rippy Worker" suchen. $chkDesktop = New-Object System.Windows.Forms.CheckBox $chkDesktop.Text = "Verknüpfung auf dem Desktop anlegen" $chkDesktop.Location = New-Object System.Drawing.Point(24, 388) $chkDesktop.Size = New-Object System.Drawing.Size(400, 22) $chkDesktop.Checked = $true $chkDesktop.ForeColor = $cText $form.Controls.Add($chkDesktop) # Log-Bereich $log = New-Object System.Windows.Forms.TextBox $log.Location = New-Object System.Drawing.Point(24, 420) $log.Size = New-Object System.Drawing.Size(500, 174) $log.Multiline = $true $log.ReadOnly = $true $log.ScrollBars = "Vertical" $log.BackColor = $cPanel $log.ForeColor = $cText $log.BorderStyle = "FixedSingle" $log.Font = New-Object System.Drawing.Font("Consolas", 8.5) $form.Controls.Add($log) # Knöpfe $btnInstall = New-Object System.Windows.Forms.Button $btnInstall.Text = "Installieren" $btnInstall.Location = New-Object System.Drawing.Point(24, 608) $btnInstall.Size = New-Object System.Drawing.Size(150, 40) $btnInstall.BackColor = $cAmber $btnInstall.ForeColor = $cBg $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, 608) $btnStart.Size = New-Object System.Drawing.Size(150, 40) $btnStart.BackColor = $cEmerald $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() } # --- RIPPY_PATH_MAP: holen und wirklich pruefen ----------------------------- # Rippy leitet den Wert aus seinen eigenen eingehaengten Freigaben ab # (GET /worker-setup/pfad-map) - hier wird nichts geraten. Der Rueckgabewert # ist der fertige Wert fuer die .bat-Dateien, oder "" wenn Rippy keine # Freigabe hat (dann kann dieser Worker nichts komprimieren, und das muss # im Log stehen statt still zu passieren). function Hole-PfadMap($rippyHost) { try { $antwort = Invoke-RestMethod "http://$rippyHost/api/worker-setup/pfad-map" -TimeoutSec 15 } catch { Log "HINWEIS: Rippy nach der Freigabe zu fragen schlug fehl ($($_.Exception.Message))." return "" } if ($antwort.hinweis) { Log $antwort.hinweis } if (-not $antwort.mapping) { return "" } return [string]$antwort.mapping } # Erreicht DIESER PC die Freigaben wirklich? Ohne diese Probe merkt der Nutzer # den Fehler erst, wenn ein Job Stunden spaeter scheitert. function Pruefe-PfadMap($mapping) { $alleDa = $true foreach ($paar in ($mapping -split ";")) { if ($paar -notmatch "=") { continue } $ziel = ($paar -split "=", 2)[1] if (Test-Path -LiteralPath $ziel -ErrorAction SilentlyContinue) { Log " OK: $ziel ist von hier erreichbar." } else { $alleDa = $false Log " NICHT erreichbar: $ziel" } } if (-not $alleDa) { Log " Meist fehlen nur die Zugangsdaten: die Freigabe einmal im Explorer" Log " oeffnen und 'Anmeldedaten speichern' anhaken, dann erreicht der" Log " Worker sie auch. Die Installation laeuft trotzdem durch." } return $alleDa } # --- 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. $zielEingabe = $txtPfad.Text.Trim() if (-not $zielEingabe) { $zielEingabe = $StandardZiel } if (-not [System.IO.Path]::IsPathRooted($zielEingabe)) { [System.Windows.Forms.MessageBox]::Show( "Bitte einen vollständigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.", "Pfad unvollständig") return } $script:InstallDir = $zielEingabe $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 überhaupt schreiben dürfen. 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' - dafür braucht es" Log "Administratorrechte. Zwei Wege:" Log " 1. Diesen Installer mit Rechtsklick > 'Als Administrator" Log " ausführen' starten (der empfohlene Weg), ODER" Log " 2. oben über 'Ändern ...' einen Ordner im eigenen" Log " Benutzerprofil wählen - 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 + Abhängigkeiten if (-not (Test-Path "venv")) { Log "Lege Python-Umgebung an ..."; Invoke-Expression "$python -m venv venv" } Log "Installiere Python-Abhängigkeiten (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 "Abhängigkeiten 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. Netzwerk-Freigabe klaeren, BEVOR die Start-Skripte geschrieben werden. # Ohne RIPPY_PATH_MAP kann dieser Worker grundsaetzlich nicht # komprimieren - das war bis zum 26.07.2026 der stille Blocker. $mapping = $txtMap.Text.Trim() if ($mapping) { Log "Freigabe (von Hand eingetragen): $mapping" } else { Log "Frage Rippy, ueber welche Freigabe die Rohdaten erreichbar sind ..." $mapping = Hole-PfadMap $rHost if ($mapping) { $txtMap.Text = $mapping Log "Freigabe von Rippy: $mapping" } } if ($mapping) { Log "Pruefe Erreichbarkeit ..." [void](Pruefe-PfadMap $mapping) } else { Log "ACHTUNG: Ohne Freigabe kann dieser Worker NICHTS komprimieren." Log " Der Worker wird trotzdem installiert und meldet sich in" Log " Rippy - Jobs schlagen aber mit einer Klartext-Meldung fehl." } # 6. Start-Skripte + Deinstaller erzeugen # RIPPY_PATH_MAP nur setzen, wenn es einen Wert gibt - eine leere # Zuweisung in einer .bat ist harmlos, aber ein gesetztes leeres # RIPPY_PATH_MAP liest pfad_lokal() als "kein Mapping", und die # Fehlermeldung im Worker unterscheidet genau diese beiden Faelle. $mapZeile = if ($mapping) { "set RIPPY_PATH_MAP=$mapping`r`n" } else { "" } # Gleichzeitige Auftraege. Auf Windows gibt es KEINEN prefork-Pool (kein # fork) - `solo` bedient genau einen Auftrag, `threads` mehrere. Das passt # hier, weil die eigentliche Arbeit ein Kind-Prozess ist (HandBrake) und # der Thread nur darauf wartet. $slots = [int]$numSlots.Value $poolArg = if ($slots -le 1) { "--pool=solo" } else { "--pool=threads --concurrency=$slots" } Log "Gleichzeitige Auftraege: $slots" $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 RIPPY_SLOTS=$slots`r`nset TZ=UTC`r`n${mapZeile}set PATH=%~dp0;%PATH%`r`nstart `"`" venv\Scripts\pythonw.exe gui.py" Set-Content -Path "start-gui.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 RIPPY_SLOTS=$slots`r`nset TZ=UTC`r`n${mapZeile}set PATH=%~dp0;%PATH%`r`nvenv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode $poolArg -n ${wName}@%%h" Set-Content -Path "start-worker.bat" -Value $workBat -Encoding ASCII # --- Deinstaller --------------------------------------------------- # # ⚠️ REPARIERT 26.07.2026. Der Commander meldete: „Der Uninstaller vom # Worker funktioniert nicht mehr." Reproduziert mit echtem PowerShell: # # Der Typ [System.Windows.Forms.MessageBox] wurde nicht gefunden. # # Ursache: `Add-Type -AssemblyName System.Windows.Forms` stand EINE ZEILE # ZU SPÄT — die MessageBox wurde davor benutzt. Der Deinstaller starb also # in seiner ersten Arbeitszeile, jedes Mal. # # Zwei weitere Mängel gleich mit erledigt: # - Kodierung war ASCII, obwohl Umlaute drin stehen (Kauderwelsch). # - `Remove-Item -Recurse -Force $PSScriptRoot` löscht den Ordner, in dem # das laufende Skript selbst liegt. Das klappt auf Windows nicht # zuverlässig (die venv-DLLs sind noch geladen). Jetzt räumt ein # losgelöstes cmd den Ordner ab, nachdem PowerShell beendet ist. $uninstall = @" # Rippy-Worker DEINSTALLIEREN — rückstandsfrei (MUSS-Kriterium). # Aufruf ohne Parameter fragt nach; -Force fragt nicht (so ruft das Tray auf). param([switch]`$Force) # ZUERST die Assembly laden, DANN die MessageBox benutzen. Genau daran ist der # alte Deinstaller gescheitert. Add-Type -AssemblyName System.Windows.Forms if (-not `$Force) { `$antwort = [System.Windows.Forms.MessageBox]::Show( "Den Rippy-Worker '$wName' von diesem PC entfernen?", "Rippy Worker deinstallieren", [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Warning) if (`$antwort -ne [System.Windows.Forms.DialogResult]::Yes) { exit } } `$ordner = `$PSScriptRoot # ⚠️ ADMINRECHTE. Der Standard-Zielordner liegt unter "C:\Program Files", und # der gehört nicht dem Benutzer — jedes Remove-Item darin scheitert mit "Der # Zugriff auf den Pfad wurde verweigert" (Commander-Befund 26.07.2026, an # HandBrakes doc\LICENSE und Konsorten aufgelaufen). Die Installer-.exe ist mit # -requireAdmin gebaut und fragt beim Start per UAC; der Deinstaller hatte # nichts Vergleichbares und war damit im Standardfall nutzlos. # # GEMESSEN statt angenommen: Es wird ein Schreibversuch gemacht. Wer den Worker # ins eigene Profil installiert hat, braucht keine Erhöhung und bekommt auch # keine UAC-Frage. `$schreibbar = `$false try { `$probe = Join-Path `$ordner ".deinstall-probe" Set-Content -Path `$probe -Value "x" -ErrorAction Stop Remove-Item -Force `$probe -ErrorAction SilentlyContinue `$schreibbar = `$true } catch { } `$istAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not `$schreibbar -and -not `$istAdmin) { # Sich selbst erhöht neu starten. -Force, weil oben schon zugestimmt wurde — # sonst käme die Frage zweimal. try { Start-Process powershell.exe -Verb RunAs -ArgumentList @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ('"' + `$PSCommandPath + '"'), "-Force") } catch { [System.Windows.Forms.MessageBox]::Show( "Zum Entfernen sind Administratorrechte nötig (der Ordner liegt " + "unter 'Programme'), und die Anfrage wurde abgelehnt.`n`n" + "Alternative: PowerShell mit Rechtsklick 'Als Administrator " + "ausführen' starten und dieses Skript erneut aufrufen.", "Rippy Worker", 0, [System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null } exit } # 1. Alles beenden, was aus diesem Ordner läuft (Tray, celery, HandBrake). # # Der Tray-Prozess (pythonw.exe tray.py) kann mehrere Instanzen haben. # Wir filtern via WMI anhand der CommandLine. Get-WmiObject Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" -or `$_.CommandLine -like "*gui.py*" -or `$_.CommandLine -like "*tray.py*" } | ForEach-Object { Write-Host "Beende Worker-Prozess: PID `$(`$_.ProcessId) (`$(`$_.Name))" Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep 3 # 2. Verknüpfungen entfernen — Autostart UND Desktop, jeweils an BEIDEN möglichen # Orten, weil die Installation je nach Zielverzeichnis persönlich oder # maschinenweit war. „Rückstandsfrei entfernbar" ist ein MUSS-Kriterium; eine # tote Verknüpfung auf dem Desktop wäre genau so ein Rückstand. foreach (`$ort in @("Startup", "CommonStartup")) { `$lnk = Join-Path ([Environment]::GetFolderPath(`$ort)) "RippyWorker.lnk" if (Test-Path `$lnk) { Remove-Item -Force `$lnk -ErrorAction SilentlyContinue } } foreach (`$ort in @("Desktop", "CommonDesktopDirectory")) { `$lnk = Join-Path ([Environment]::GetFolderPath(`$ort)) "Rippy Worker.lnk" if (Test-Path `$lnk) { Remove-Item -Force `$lnk -ErrorAction SilentlyContinue } } # ... und die geplante Aufgabe aus älteren Installationen, falls vorhanden. schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null # 3. In Rippy abmelden, damit dort keine Worker-Leiche stehen bleibt. try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch { } # 4. Ordner abräumen. NICHT von hier aus: Dieses Skript liegt darin, und die # venv-DLLs sind noch geladen. Ein losgelöstes cmd wartet, bis PowerShell # weg ist, und räumt dann auf. Set-Location `$env:TEMP Start-Process cmd.exe -ArgumentList '/c timeout /t 4 /nobreak >nul & rd /s /q "'"`$ordner"'"' -WindowStyle Hidden [System.Windows.Forms.MessageBox]::Show( "Rippy-Worker entfernt. Der Ordner verschwindet in wenigen Sekunden.", "Rippy Worker") | Out-Null "@ # UTF-8 MIT BOM, nicht ASCII: Die Datei enthält Umlaute, und PowerShell # 5.1 liest .ps1 ohne BOM als ANSI (dokumentiert im Kopf dieser Datei). [System.IO.File]::WriteAllText( (Join-Path $InstallDir "uninstall.ps1"), $uninstall, (New-Object System.Text.UTF8Encoding $true)) # 7. Autostart über den Task Scheduler (als Dienst-Ersatz). # # Commander-Wunsch 26.07.2026: SchTasks.exe statt Startmenü-Verknüpfung. # Da der Installer jetzt zwingend mit Administratorrechten läuft (für # das "Programme"-Verzeichnis), funktioniert schtasks problemlos. # /RL HIGHEST verhindert UAC-Popups beim automatischen Start. # /SC ONLOGON startet den Worker bei der Benutzeranmeldung — das ist # wichtig, damit er Zugriff auf die eingehängten Netzwerklaufwerke # des Benutzers hat (der SYSTEM-Account hätte das bei ONSTART nicht). if ($chkAuto.Checked) { try { # Der Pfad zur .bat muss in Anführungszeichen, falls Leerzeichen drin sind. # schtasks erwartet diese Escaped-Anführungszeichen: \"C:\Pfad\...\" $aktion = "\`"$InstallDir\start-gui.bat\`"" $process = Start-Process schtasks -ArgumentList "/create /tn `"RippyWorker`" /tr $aktion /sc ONLOGON /rl HIGHEST /f" -NoNewWindow -Wait -PassThru if ($process.ExitCode -eq 0) { Log "Autostart als Aufgabe (Task Scheduler) eingerichtet (startet bei Anmeldung)." } else { throw "ExitCode $($process.ExitCode)" } } catch { # Nur eine Warnung: der Worker selbst ist fertig und startbar. Log "HINWEIS: Autostart-Aufgabe konnte nicht eingerichtet werden ($_)." Log " Der Worker ist trotzdem fertig installiert und läuft." } } # 8. Verknüpfung auf dem Desktop (Commander-Wunsch 26.07.2026: „wenn man # den Worker installiert braucht man auch eine exe auf dem Desktop # zum Starten"). Vorher lagen die Startdateien nur im Programmordner — # wer den Worker von Hand starten wollte, musste erst # "C:\Program Files\Rippy Worker" suchen. # # Dieselbe Rechte-Überlegung wie beim Autostart: bei einer Installation # unter "Programme" auf den Desktop ALLER Benutzer, sonst auf den # eigenen. Und ein Fehlschlag hier ist nur eine Warnung — der Worker # läuft davon unberührt. if ($chkDesktop.Checked) { try { $programme = [Environment]::GetFolderPath("ProgramFiles") $programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86") $maschinenweit = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") ` -or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase")) $desktopDir = if ($maschinenweit) { [Environment]::GetFolderPath("CommonDesktopDirectory") } else { [Environment]::GetFolderPath("Desktop") } $wshell = New-Object -ComObject WScript.Shell $lnk = Join-Path $desktopDir "Rippy Worker.lnk" $sh = $wshell.CreateShortcut($lnk) $sh.TargetPath = Join-Path $InstallDir "start-gui.bat" $sh.WorkingDirectory = $InstallDir $sh.IconLocation = Join-Path $InstallDir "rippy.ico" $sh.Save() Log "Verknüpfung 'Rippy Worker' auf dem Desktop angelegt." } catch { Log "HINWEIS: Desktop-Verknüpfung ging nicht ($($_.Exception.Message))." Log " Der Worker ist trotzdem fertig — starten über" Log " $InstallDir\start-gui.bat" } } 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 } } $btnMap.Add_Click({ $rHost = $txtHost.Text.Trim() if (-not $rHost) { [System.Windows.Forms.MessageBox]::Show( "Bitte zuerst oben die LAN-IP der Rippy-Maschine eintragen.", "Fehlt") return } $m = Hole-PfadMap $rHost if ($m) { $txtMap.Text = $m Log "Freigabe von Rippy: $m" [void](Pruefe-PfadMap $m) } }) $btnInstall.Add_Click({ Do-Install }) $btnStart.Add_Click({ Start-Process -FilePath (Join-Path $InstallDir "start-gui.bat") -WindowStyle Hidden [System.Windows.Forms.MessageBox]::Show("Worker-Dashboard gestartet.", "Rippy") $form.Close() }) [void]$form.ShowDialog()