# Rippy: Nativer Windows-Transcode-Worker - Installation OHNE Docker. # # Was das Skript tut (alles nach -InstallDir, Standard "C:\Program Files\Rippy Worker"): # 1. Prüft Python (3.10+), legt ein venv an, installiert die Abhängigkeiten # 2. Lädt den Worker-Code direkt von deiner Rippy-Instanz (/api/worker-setup/paket) # 3. Lädt HandBrakeCLI (gepinnte Version, offizielles GitHub-Release), # falls nicht schon im PATH oder im Ordner vorhanden # 4. Fragt Rippy, über welche Netzwerk-Freigabe die Rohdaten erreichbar sind, # und prüft, ob DIESER PC sie wirklich sieht (RIPPY_PATH_MAP) # 5. Erzeugt start-worker.bat - Doppelklick startet den Worker # # Der Worker bedient NUR die Kompressions-Queue (transcode) - gerippt wird # weiterhin auf der Rippy-Hauptmaschine (dort hängt das Laufwerk). # # Aufruf (PowerShell): # .\install.ps1 -RippyHost [-WorkerName mein-pc] [-Autostart] # (die IP steht in Rippy unter Einstellungen -> Worker; NICHT die dieses PCs) # # -Autostart legt eine Verknüpfung im Autostart-Ordner an (Start bei Anmeldung). param( [Parameter(Mandatory = $true)] [string]$RippyHost, [string]$WorkerName = $env:COMPUTERNAME.ToLower(), [switch]$Autostart, # Verknüpfung auf dem Desktop. Standard AN (Commander-Wunsch 26.07.2026) — # mit -KeineDesktopVerknuepfung abschaltbar. [switch]$KeineDesktopVerknuepfung, # Übersetzung der Rippy-Container-Pfade auf Freigaben, die DIESER PC sieht. # Leer lassen: dann holt der Installer den Wert von Rippy selbst # (GET /worker-setup/pfad-map, abgeleitet aus dessen eigenen Mounts). # Von Hand nur, wenn dieser PC die Freigabe anders erreicht, z. B. # -PfadMap "/app/media/rippy=Z:\" [string]$PfadMap = "", # Wie viele Kompressions-Aufträge dieser PC gleichzeitig annimmt. 0 = nach # Kernzahl entscheiden (ab 12 Kernen zwei, sonst einer): 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. [int]$Slots = 0, # Zielverzeichnis. Standard ist "Programme", wie bei jedem anderen Programm # (Commander-Wunsch 25.07.2026). Dorthin schreiben braucht Adminrechte - # PowerShell also "Als Administrator" starten, oder hier einen Ordner im # eigenen Profil angeben. [string]$InstallDir = (Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker") ) $ErrorActionPreference = "Stop" # HandBrake-Version: die NEUESTE offizielle (GitHub-Release) - so passt der # Windows-Worker zu dem, was der Update-Check im UI meldet (früher war hier # eine feste 1.9.2 hardcodiert, was verwirrte). Fällt auf eine bekannte # Version zurück, falls die GitHub-API gerade nicht erreichbar ist. $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") } Write-Host "Neueste HandBrake-Version: $HandBrakeVersion" } catch { Write-Host "GitHub-API nicht erreichbar - nutze HandBrake $HandBrakeVersion" } $HandBrakeUrl = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip" Write-Host "== Rippy Windows-Worker Installation ==" -ForegroundColor Cyan # 1. Python finden (py-Launcher oder python im PATH) $python = $null foreach ($kandidat in @("py -3", "python")) { try { $version = Invoke-Expression "$kandidat --version" 2>&1 if ("$version" -match "Python 3\.(\d+)") { if ([int]$Matches[1] -ge 10) { $python = $kandidat; break } } } catch {} } if (-not $python) { Write-Host "FEHLER: Python 3.10+ nicht gefunden." -ForegroundColor Red Write-Host "Installieren mit: winget install Python.Python.3.12 (dann Skript erneut ausführen)" exit 1 } Write-Host "Python gefunden: $python" # 2. Zielordner + Worker-Code von der Rippy-Instanz laden $ziel = $InstallDir try { New-Item -ItemType Directory -Force $ziel -ErrorAction Stop | Out-Null $probe = Join-Path $ziel ".schreibtest" Set-Content -Path $probe -Value "x" -ErrorAction Stop Remove-Item -Force $probe -ErrorAction SilentlyContinue } catch { Write-Host "" Write-Host "FEHLER: In '$ziel' darf nicht geschrieben werden." -ForegroundColor Red Write-Host "Das ist normal bei Ordnern unter 'Programme'. Zwei Wege:" -ForegroundColor Red Write-Host " 1. PowerShell als Administrator starten und erneut aufrufen, ODER" -ForegroundColor Red Write-Host " 2. ein eigenes Ziel angeben, z. B.:" -ForegroundColor Red Write-Host " -InstallDir `"`$env:LOCALAPPDATA\Rippy Worker`"" -ForegroundColor Red exit 1 } Write-Host "Ziel: $ziel" Set-Location $ziel Write-Host "Lade Worker-Code von http://$RippyHost/api/worker-setup/paket ..." Invoke-WebRequest "http://$RippyHost/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")) { Write-Host "Lege Python-Umgebung an ..." Invoke-Expression "$python -m venv venv" } & .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip & .\venv\Scripts\pip.exe install --quiet -r requirements.txt # Nur für den Windows-Worker: Tray-Symbol (Status, Start/Stopp, Beenden) & .\venv\Scripts\pip.exe install --quiet pystray pillow Write-Host "Python-Abhängigkeiten installiert." # 4. HandBrakeCLI besorgen (falls nötig) $hb = Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue if (-not $hb -and -not (Test-Path ".\HandBrakeCLI.exe")) { Write-Host "Lade HandBrakeCLI $HandBrakeVersion (offizielles GitHub-Release) ..." Invoke-WebRequest $HandBrakeUrl -OutFile hb.zip -UseBasicParsing Expand-Archive hb.zip -DestinationPath . -Force Remove-Item hb.zip } if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not $hb) { Write-Host "FEHLER: HandBrakeCLI.exe fehlt." -ForegroundColor Red exit 1 } Write-Host "HandBrakeCLI bereit." # 5. Netzwerk-Freigabe klären (RIPPY_PATH_MAP) - der stille Blocker bis 26.07.2026 # # Rippy schickt dem Worker Pfade wie /app/media/rippy/. Das sind Pfade # INNERHALB des Rippy-Containers; dieser PC sieht sie nur, wenn sie auf eine # Netzwerk-Freigabe übersetzt werden. Ohne das nimmt der Worker die Aufgabe an # und lehnt sie Millisekunden später ab ("Keine Roh-MKVs gefunden") - gemessen # am Job 95afdc89. Geraten wird nichts: Rippy hat die Freigabe selbst # eingehängt und kennt ihre Quelle. if (-not $PfadMap) { Write-Host "Frage Rippy, über welche Freigabe die Rohdaten erreichbar sind ..." try { $antwort = Invoke-RestMethod "http://$RippyHost/api/worker-setup/pfad-map" -TimeoutSec 15 if ($antwort.hinweis) { Write-Host $antwort.hinweis -ForegroundColor Yellow } if ($antwort.mapping) { $PfadMap = [string]$antwort.mapping } } catch { Write-Host "HINWEIS: Abfrage fehlgeschlagen ($($_.Exception.Message))." -ForegroundColor Yellow } } if ($PfadMap) { Write-Host "Freigabe: $PfadMap" foreach ($paar in ($PfadMap -split ";")) { if ($paar -notmatch "=") { continue } $zielPfad = ($paar -split "=", 2)[1] if (Test-Path -LiteralPath $zielPfad -ErrorAction SilentlyContinue) { Write-Host " OK: $zielPfad ist von hier erreichbar." -ForegroundColor Green } else { Write-Host " NICHT erreichbar: $zielPfad" -ForegroundColor Yellow Write-Host " Meist fehlen nur die Zugangsdaten: die Freigabe einmal im" -ForegroundColor Yellow Write-Host " Explorer öffnen und 'Anmeldedaten speichern' anhaken." -ForegroundColor Yellow } } } else { Write-Host "ACHTUNG: Ohne Freigabe kann dieser Worker NICHTS komprimieren." -ForegroundColor Yellow Write-Host " Er wird trotzdem installiert; Jobs schlagen dann mit einer" -ForegroundColor Yellow Write-Host " Klartext-Meldung fehl statt still zu hängen." -ForegroundColor Yellow } # Leeres RIPPY_PATH_MAP nicht setzen: pfad_lokal() liest es als "kein Mapping", # und die Fehlermeldung im Worker unterscheidet genau diese beiden Fälle. $mapZeile = if ($PfadMap) { "set RIPPY_PATH_MAP=$PfadMap`r`n" } else { "" } # Gleichzeitige Aufträge. 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 wartet. if ($Slots -le 0) { $Slots = if ([Environment]::ProcessorCount -ge 12) { 2 } else { 1 } } $poolArg = if ($Slots -le 1) { "--pool=solo" } else { "--pool=threads --concurrency=$Slots" } Write-Host "Gleichzeitige Aufträge: $Slots (von $([Environment]::ProcessorCount) Kernen)" # 6. Start-Skript erzeugen $bat = @" @echo off rem Rippy Windows-Transcode-Worker - verbindet sich mit $RippyHost cd /d "%~dp0" set REDIS_URL=redis://${RippyHost}:6379/0 set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy set API_URL=http://${RippyHost}:8000 set WORKER_NAME=$WorkerName set RIPPY_SLOTS=$Slots set TZ=UTC ${mapZeile}set PATH=%~dp0;%PATH% venv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode $poolArg -n ${WorkerName}@%%h "@ Set-Content -Path "start-worker.bat" -Value $bat -Encoding ASCII # 7. GUI-Start (empfohlen): Modernes natives Fenster, kein Konsolenfenster $trayBat = @" @echo off rem Rippy-Worker GUI - verbindet sich mit $RippyHost cd /d "%~dp0" set REDIS_URL=redis://${RippyHost}:6379/0 set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy set API_URL=http://${RippyHost}:8000 set WORKER_NAME=$WorkerName set RIPPY_TRAY_HOST=$RippyHost set RIPPY_SLOTS=$Slots set TZ=UTC ${mapZeile}set PATH=%~dp0;%PATH% start "" venv\Scripts\pythonw.exe gui.py "@ Set-Content -Path "start-gui.bat" -Value $trayBat -Encoding ASCII Write-Host "start-worker.bat + start-gui.bat erzeugt." # 8. Deinstaller - MUSS-Kriterium: rückstandsfrei entfernbar $uninstall = @" # Rippy-Worker DEINSTALLIEREN: stoppt Prozesse, entfernt Autostart, # meldet den Worker in Rippy ab und löscht diesen Ordner komplett. param([switch]`$Force) if (-not `$Force) { `$antwort = Read-Host "Rippy-Worker '$WorkerName' wirklich deinstallieren? (j/n)" if (`$antwort -ne "j") { Write-Host "Abgebrochen."; exit } } # ⚠️ 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). Geprüft wird per # Schreibversuch, nicht per Annahme: Wer ins eigene Profil installiert hat, # bekommt keine UAC-Frage. `$schreibbar = `$false try { `$probe = Join-Path `$PSScriptRoot ".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) { Write-Host "Der Ordner liegt unter 'Programme' — starte mich mit Administratorrechten neu ..." -ForegroundColor Yellow try { Start-Process powershell.exe -Verb RunAs -ArgumentList @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ('"' + `$PSCommandPath + '"'), "-Force") } catch { Write-Host "FEHLER: Erhöhung abgelehnt. PowerShell mit Rechtsklick 'Als Administrator ausführen' starten und erneut aufrufen." -ForegroundColor Red } exit } Get-CimInstance 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 2 # Autostart-Verknüpfung entfernen - BEIDE möglichen Orte, weil die # Installation je nach Zielverzeichnis persönlich oder maschinenweit war. foreach (`$ordner in @("Startup", "CommonStartup")) { try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} } # ... und die Desktop-Verknüpfung. „Rückstandsfrei entfernbar" ist ein # MUSS-Kriterium; ein totes Symbol auf dem Desktop wäre genau so ein Rückstand. foreach (`$ordner in @("Desktop", "CommonDesktopDirectory")) { try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "Rippy Worker.lnk") -ErrorAction Stop } catch {} } # ... und die geplante Aufgabe aus älteren Installationen, falls vorhanden. schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null try { Invoke-RestMethod -Method Delete "http://$RippyHost/api/workers/$WorkerName" -TimeoutSec 5 | Out-Null Write-Host "Worker-Eintrag in Rippy entfernt." } catch { Write-Host "Hinweis: Eintrag in Rippy ggf. von Hand löschen (Einstellungen -> Worker)." } # Ordner abräumen — NICHT von hier aus: Dieses Skript liegt darin, und die # venv-DLLs sind noch geladen. `Remove-Item -Recurse -Force` auf den eigenen # Ordner klappt auf Windows nicht zuverlässig (Befund 26.07.2026). 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 "'"`$PSScriptRoot"'"' -WindowStyle Hidden Write-Host "Rippy-Worker deinstalliert (Ordner verschwindet in wenigen Sekunden)." -ForegroundColor Green "@ # UTF-8 MIT BOM statt ASCII: die Datei enthält Umlaute, und PowerShell 5.1 liest # .ps1 ohne BOM als ANSI. [System.IO.File]::WriteAllText( (Join-Path $ziel "uninstall.ps1"), $uninstall, (New-Object System.Text.UTF8Encoding $true)) Write-Host "uninstall.ps1 erzeugt." # 9. Optional: Autostart bei Anmeldung (mit Tray) # # Über den Autostart-ORDNER, nicht über schtasks: Eine Aufgabe im # Wurzelordner der Aufgabenplanung anzulegen verlangt Administratorrechte, und # der Installer läuft normal ohne. Auf dem Commander-PC scheiterte das am # 25.07.2026 mit "FEHLER: Zugriff verweigert." - und weil oben # $ErrorActionPreference = "Stop" steht, riss es die ganze Installation mit, # obwohl nur der Autostart fehlte. Der Autostart-Ordner braucht nie Adminrechte. if ($Autostart) { try { # Unter "Programme" = Installation für die Maschine, also Autostart für # alle Benutzer. Sonst persönlich. Siehe install-gui.ps1 für die # ausführliche Begründung (Rechtelage bei fremdem Admin-Konto). $programme = [Environment]::GetFolderPath("ProgramFiles") $maschinenweit = $ziel.StartsWith($programme, "OrdinalIgnoreCase") $autostartDir = if ($maschinenweit) { [Environment]::GetFolderPath("CommonStartup") } else { [Environment]::GetFolderPath("Startup") } $wsh = New-Object -ComObject WScript.Shell $lnk = $wsh.CreateShortcut((Join-Path $autostartDir "RippyWorker.lnk")) $lnk.TargetPath = (Join-Path $ziel "start-tray.bat") $lnk.WorkingDirectory = $ziel $lnk.Description = "Rippy Encoding-Worker" # WindowStyle 7 = minimiert: start-tray.bat startet pythonw (kein # Konsolenfenster), aber die .bat selbst blitzt sonst kurz auf. $lnk.WindowStyle = 7 $ico = Join-Path $ziel "rippy.ico" if (Test-Path $ico) { $lnk.IconLocation = $ico } $lnk.Save() Write-Host "Autostart eingerichtet (Verknüpfung im Autostart-Ordner)." } catch { Write-Host "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." -ForegroundColor Yellow Write-Host " Der Worker ist trotzdem fertig installiert." -ForegroundColor Yellow Write-Host " Von Hand: start-tray.bat in shell:startup verknüpfen." -ForegroundColor Yellow } } # 10. Verknüpfung 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. Ein Fehlschlag hier ist nur # eine Warnung; der Worker läuft davon unberührt. if (-not $KeineDesktopVerknuepfung) { try { if ($KeineDesktopVerknuepfung) { Write-Host "Desktop-Verknüpfung auf Wunsch übersprungen." } else { $desktop = Join-Path ([Environment]::GetFolderPath("Desktop")) "Rippy Worker.lnk" $wshell = New-Object -ComObject WScript.Shell $shortcut = $wshell.CreateShortcut($desktop) $shortcut.TargetPath = (Join-Path $ziel "start-gui.bat") $shortcut.WorkingDirectory = $ziel $shortcut.WindowStyle = 7 if (Test-Path (Join-Path $ziel "rippy.ico")) { $shortcut.IconLocation = (Join-Path $ziel "rippy.ico") } $shortcut.Save() Write-Host "Desktop-Verknüpfung 'Rippy Worker' erstellt." } } catch { Write-Host "HINWEIS: Desktop-Verknüpfung ging nicht ($($_.Exception.Message))." -ForegroundColor Yellow } } Write-Host "" Write-Host "FERTIG." -ForegroundColor Green Write-Host " Auf dem Desktop: Rippy Worker" Write-Host " Starten (mit Tray-Symbol): $ziel\start-tray.bat" Write-Host " Starten (Konsole/Debug): $ziel\start-worker.bat" Write-Host " Deinstallieren: $ziel\uninstall.ps1" Write-Host "Der Worker erscheint in Rippy unter Einstellungen -> Worker als '$WorkerName'."