diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe index 415103e..96c5a0a 100644 Binary files a/deploy/worker-windows/RippyWorkerSetup.exe and b/deploy/worker-windows/RippyWorkerSetup.exe differ diff --git a/deploy/worker-windows/build-exe.ps1 b/deploy/worker-windows/build-exe.ps1 deleted file mode 100644 index e9edbb6..0000000 --- a/deploy/worker-windows/build-exe.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -# Baut rippy.ico + RippyWorkerSetup.exe aus install-gui.ps1 (ps2exe). -# -# WANN NEU BAUEN: nur wenn sich install-gui.ps1 (die Installer-Oberfläche) -# ändert. NICHT bei MakeMKV-/HandBrake-Updates - die .exe holt HandBrake zur -# Laufzeit (GitHub latest) und den Worker-Code live von Rippy, friert also -# keine Versionen ein. -# AUSFUEHREN AUF WINDOWS (eine Windows-.exe kann nicht von Linux gebaut werden): -# powershell -ExecutionPolicy Bypass -File build-exe.ps1 -# Danach RippyWorkerSetup.exe committen (die API serviert sie). -$ErrorActionPreference = "Stop" -Add-Type -AssemblyName System.Drawing -Set-Location $PSScriptRoot - -$guiSrc = Join-Path $PSScriptRoot "install-gui.ps1" - -# 1. Rippy-Disc-Icon 256x256 zeichnen -> PNG -> ICO-Container -$bmp = New-Object System.Drawing.Bitmap 256, 256 -$g = [System.Drawing.Graphics]::FromImage($bmp) -$g.SmoothingMode = "AntiAlias" -$g.Clear([System.Drawing.Color]::Transparent) -$g.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(99, 102, 241))), 8, 8, 240, 240) -$g.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(147, 51, 234))), 40, 40, 176, 176) -$g.FillEllipse([System.Drawing.Brushes]::White, 104, 104, 48, 48) -$ms = New-Object System.IO.MemoryStream -$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) -$png = $ms.ToArray() - -$icoStream = New-Object System.IO.MemoryStream -$bw = New-Object System.IO.BinaryWriter $icoStream -$bw.Write([UInt16]0); $bw.Write([UInt16]1); $bw.Write([UInt16]1) # ICONDIR -$bw.Write([Byte]0); $bw.Write([Byte]0); $bw.Write([Byte]0); $bw.Write([Byte]0) # 256x256, colors, res -$bw.Write([UInt16]1); $bw.Write([UInt16]32) # planes, bpp -$bw.Write([UInt32]$png.Length); $bw.Write([UInt32]22) # size, offset -$bw.Write($png); $bw.Flush() -[System.IO.File]::WriteAllBytes("$PSScriptRoot\rippy.ico", $icoStream.ToArray()) -Write-Host "rippy.ico erzeugt ($($png.Length) Bytes PNG)" - -# 2. ps2exe sicherstellen - TLS 1.2 ist der Fix für den NuGet-Bootstrap-Fehler -[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072 -if (-not (Get-Module -ListAvailable ps2exe)) { - Write-Host "Bootstrappe NuGet + PSGallery ..." - try { Get-PackageProvider -Name NuGet -ErrorAction Stop | Out-Null } - catch { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null } - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue - Write-Host "Installiere ps2exe ..." - Install-Module ps2exe -Scope CurrentUser -Force -AllowClobber -} -Import-Module ps2exe - -# 3. Kompilieren (WinForms -> noConsole + STA, Icon eingebettet) -# -# -requireAdmin (seit 25.07.2026): Der Worker wird standardmäßig nach -# "C:\Program Files\Rippy Worker" installiert, und dorthin darf nur ein -# Administrator schreiben. Mit dem Schalter tragt die .exe ein Manifest, das -# Windows beim Doppelklick EINMAL per UAC fragen lässt - danach stimmen die -# Rechte für alles Weitere. Ohne ihn liefe der Installer unerhöht und -# scheiterte beim Anlegen des Ordners; wer das Ziel ins eigene Profil legt, -# braucht die Erhöhung nicht, wird aber trotzdem gefragt (normal für einen -# Installer und besser als ein Fehlschlag mitten im Lauf). -Invoke-ps2exe -inputFile $guiSrc -outputFile "$PSScriptRoot\RippyWorkerSetup.exe" ` - -iconFile "$PSScriptRoot\rippy.ico" -noConsole -STA -requireAdmin ` - -title "Rippy Worker Setup" -product "Rippy" -company "Rippy" -version "1.0.1.0" - -if (Test-Path "$PSScriptRoot\RippyWorkerSetup.exe") { - Write-Host "OK: RippyWorkerSetup.exe = $((Get-Item "$PSScriptRoot\RippyWorkerSetup.exe").Length) Bytes" -} else { - Write-Host "FEHLER: keine .exe erzeugt" -} diff --git a/deploy/worker-windows/install-gui.ps1 b/deploy/worker-windows/install-gui.ps1 deleted file mode 100644 index a8b88dc..0000000 --- a/deploy/worker-windows/install-gui.ps1 +++ /dev/null @@ -1,714 +0,0 @@ -# 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() diff --git a/deploy/worker-windows/install.ps1 b/deploy/worker-windows/install.ps1 deleted file mode 100644 index ecf84e6..0000000 --- a/deploy/worker-windows/install.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -# 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'." diff --git a/deploy/worker-windows/installer.py b/deploy/worker-windows/installer.py new file mode 100644 index 0000000..4a9ddec --- /dev/null +++ b/deploy/worker-windows/installer.py @@ -0,0 +1,251 @@ +import flet as ft +import os +import subprocess +import threading +import urllib.request +import json +import time +import zipfile + +def main(page: ft.Page): + page.title = "Rippy Encoding-Worker Installation" + page.window_width = 800 + page.window_height = 650 + page.bgcolor = "#0f172a" + page.theme_mode = ft.ThemeMode.DARK + page.scroll = ft.ScrollMode.AUTO + + # Colors + c_panel = "#1e293b" + c_text = "#e2e8f0" + c_muted = "#94a3b8" + c_accent = "#6366f1" + c_success = "#10b981" + + # State + std_ziel = r"C:\Program Files\Rippy Worker" + + # UI Fields + txt_host = ft.TextField(label="LAN-IP der Rippy-Maschine (Docker-Host)", width=400, bgcolor=c_panel, color=c_text) + txt_name = ft.TextField(label="Name dieses Workers (frei wählbar)", width=250, value=os.environ.get("COMPUTERNAME", "worker").lower(), bgcolor=c_panel, color=c_text) + txt_slots = ft.Dropdown( + label="Kerne (Gleichzeitig)", + width=150, + options=[ft.dropdown.Option(str(i)) for i in range(1, 9)], + value="2" if os.cpu_count() and os.cpu_count() >= 12 else "1", + bgcolor=c_panel, + color=c_text + ) + + txt_pfad = ft.TextField(label="Installieren nach", value=std_ziel, expand=True, bgcolor=c_panel, color=c_text) + txt_map = ft.TextField(label="Netzwerk-Freigabe mit den Rohdaten", expand=True, bgcolor=c_panel, color=c_text) + + chk_auto = ft.Checkbox(label="Beim Anmelden automatisch starten (Hintergrund)", value=True, fill_color=c_accent) + chk_desktop = ft.Checkbox(label="Verknüpfung auf dem Desktop anlegen", value=True, fill_color=c_accent) + + log_view = ft.ListView(height=150, auto_scroll=True, spacing=2) + + def log(msg: str): + log_view.controls.append(ft.Text(msg, color=c_muted, font_family="Consolas", size=12)) + try: + page.update() + except: + pass + + def check_map(e): + r_host = txt_host.value.strip() + if not r_host: + page.snack_bar = ft.SnackBar(ft.Text("Bitte zuerst die LAN-IP eintragen!"), bgcolor="red") + page.snack_bar.open = True + page.update() + return + + try: + log(f"Frage Rippy nach Freigabe ({r_host})...") + with urllib.request.urlopen(f"http://{r_host}/api/worker-setup/pfad-map", timeout=5) as r: + data = json.loads(r.read().decode()) + if data.get("hinweis"): + log(data["hinweis"]) + if data.get("mapping"): + txt_map.value = data["mapping"] + log(f"Erfolgreich geholt: {data['mapping']}") + page.update() + except Exception as ex: + log(f"Fehler: {ex}") + + def do_install(e): + btn_install.disabled = True + page.update() + threading.Thread(target=run_installation, daemon=True).start() + + def run_installation(): + r_host = txt_host.value.strip() + w_name = txt_name.value.strip() + install_dir = txt_pfad.value.strip() + + if not r_host: + log("FEHLER: Rippy LAN-IP fehlt!") + btn_install.disabled = False + page.update() + return + + log(f"Zielverzeichnis: {install_dir}") + + # Check Python + python_exe = None + for cmd in ["py -3", "python"]: + try: + res = subprocess.run(f"{cmd} --version", shell=True, capture_output=True, text=True) + if "Python 3." in res.stdout: + python_exe = cmd + break + except: pass + + if not python_exe: + log("FEHLER: Python 3.10+ nicht gefunden.") + log("Bitte mit 'winget install Python.Python.3.12' installieren.") + btn_install.disabled = False + page.update() + return + + log(f"Python gefunden: {python_exe}") + + try: + os.makedirs(install_dir, exist_ok=True) + test_file = os.path.join(install_dir, ".test") + with open(test_file, "w") as f: f.write("x") + os.remove(test_file) + except Exception as ex: + log(f"FEHLER: Keine Schreibrechte in {install_dir}") + log("Bitte als Administrator starten oder Pfad aendern.") + btn_install.disabled = False + page.update() + return + + # Download worker package + log(f"Lade Worker-Code von {r_host}...") + try: + zip_path = os.path.join(install_dir, "worker.zip") + urllib.request.urlretrieve(f"http://{r_host}/api/worker-setup/paket", zip_path) + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(install_dir) + os.remove(zip_path) + except Exception as ex: + log(f"FEHLER beim Download: {ex}") + btn_install.disabled = False + page.update() + return + + # Setup venv + log("Lege Python-Umgebung an (venv)...") + subprocess.run(f"{python_exe} -m venv venv", cwd=install_dir, shell=True) + + log("Installiere Abhaengigkeiten (pip)...") + subprocess.run(r"venv\Scripts\python.exe -m pip install --quiet --upgrade pip", cwd=install_dir, shell=True) + subprocess.run(r"venv\Scripts\pip.exe install --quiet -r requirements.txt", cwd=install_dir, shell=True) + subprocess.run(r"venv\Scripts\pip.exe install --quiet flet requests pillow", cwd=install_dir, shell=True) + + # Download Handbrake + hb_path = os.path.join(install_dir, "HandBrakeCLI.exe") + if not os.path.exists(hb_path): + log("Lade HandBrakeCLI herunter...") + try: + hb_ver = "1.7.3" # Fallback if github fails + try: + with urllib.request.urlopen("https://api.github.com/repos/HandBrake/HandBrake/releases/latest", timeout=5) as r: + hb_data = json.loads(r.read().decode()) + if hb_data.get("tag_name"): hb_ver = hb_data["tag_name"].lstrip("v") + except: pass + hb_zip = os.path.join(install_dir, "hb.zip") + urllib.request.urlretrieve(f"https://github.com/HandBrake/HandBrake/releases/download/{hb_ver}/HandBrakeCLI-{hb_ver}-win-x86_64.zip", hb_zip) + with zipfile.ZipFile(hb_zip, 'r') as zip_ref: + zip_ref.extractall(install_dir) + os.remove(hb_zip) + except Exception as ex: + log(f"Warnung bei HandBrake-Download: {ex}") + + log("Erstelle Start-Skripte...") + slots = txt_slots.value + pool_arg = "--pool=solo" if int(slots) <= 1 else f"--pool=threads --concurrency={slots}" + map_zeile = f"set RIPPY_PATH_MAP={txt_map.value.strip()}\r\n" if txt_map.value.strip() else "" + + gui_bat = f"@echo off\r\ncd /d \"%~dp0\"\r\nset REDIS_URL=redis://{r_host}:6379/0\r\nset DATABASE_URL=postgresql://rippy:rippy@{r_host}:5432/rippy\r\nset API_URL=http://{r_host}:8000\r\nset WORKER_NAME={w_name}\r\nset RIPPY_TRAY_HOST={r_host}\r\nset RIPPY_SLOTS={slots}\r\nset TZ=UTC\r\n{map_zeile}set PATH=%~dp0;%PATH%\r\nstart \"\" venv\\Scripts\\pythonw.exe gui.py" + with open(os.path.join(install_dir, "start-gui.bat"), "w") as f: f.write(gui_bat) + + work_bat = f"@echo off\r\ncd /d \"%~dp0\"\r\nset REDIS_URL=redis://{r_host}:6379/0\r\nset DATABASE_URL=postgresql://rippy:rippy@{r_host}:5432/rippy\r\nset API_URL=http://{r_host}:8000\r\nset WORKER_NAME={w_name}\r\nset RIPPY_SLOTS={slots}\r\nset TZ=UTC\r\n{map_zeile}set PATH=%~dp0;%PATH%\r\nvenv\\Scripts\\celery.exe -A celery_app worker --loglevel=info -Q transcode {pool_arg} -n {w_name}@%%h" + with open(os.path.join(install_dir, "start-worker.bat"), "w") as f: f.write(work_bat) + + # Uninstaller + uninstall_ps1 = f"""param([switch]$Force) +Add-Type -AssemblyName System.Windows.Forms +if (-not $Force) {{ + $antwort = [System.Windows.Forms.MessageBox]::Show("Rippy-Worker '{w_name}' von diesem PC entfernen?", "Rippy Worker deinstallieren", 4, 32) + if ($antwort -ne "Yes") {{ exit }} +}} +Get-WmiObject 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 3 +$desktopDir = if ($PSScriptRoot -match "(?i)Program Files") {{ [Environment]::GetFolderPath("CommonDesktopDirectory") }} else {{ [Environment]::GetFolderPath("Desktop") }} +Remove-Item -Force "$desktopDir\\Rippy Worker.lnk" -ErrorAction SilentlyContinue +schtasks /delete /tn "RippyWorker" /f *>$null +Start-Process cmd -ArgumentList "/c ping 127.0.0.1 -n 3 >nul & rmdir /s /q `"$PSScriptRoot`"" -WindowStyle Hidden +if (-not $Force) {{ [System.Windows.Forms.MessageBox]::Show("Rippy-Worker entfernt.", "Rippy Worker") }} +""" + with open(os.path.join(install_dir, "uninstall.ps1"), "w", encoding="utf-8") as f: f.write(uninstall_ps1) + + # Autostart + if chk_auto.value: + log("Richte Autostart (Task Scheduler) ein...") + bat_path = os.path.join(install_dir, "start-gui.bat") + subprocess.run(f'schtasks /create /tn "RippyWorker" /tr "\\"{bat_path}\\"" /sc ONLOGON /rl HIGHEST /f', shell=True, capture_output=True) + + # Desktop Shortcut + if chk_desktop.value: + try: + import win32com.client + shell = win32com.client.Dispatch("WScript.Shell") + public_desktop = os.environ.get("PUBLIC", "") + "\\Desktop" + desktop = public_desktop if "Program Files" in install_dir and os.path.exists(public_desktop) else shell.SpecialFolders("Desktop") + shortcut = shell.CreateShortCut(os.path.join(desktop, "Rippy Worker.lnk")) + shortcut.Targetpath = os.path.join(install_dir, "start-gui.bat") + shortcut.WorkingDirectory = install_dir + shortcut.IconLocation = os.path.join(install_dir, "rippy.ico") + shortcut.save() + log("Desktop-Verknüpfung erstellt.") + except Exception as e: + log(f"Warnung: Shortcut konnte nicht erstellt werden: {e}") + + log("FERTIG!") + btn_start.visible = True + btn_install.text = "Neu installieren" + btn_install.disabled = False + page.update() + + def start_worker(e): + install_dir = txt_pfad.value.strip() + subprocess.Popen(os.path.join(install_dir, "start-gui.bat"), creationflags=subprocess.CREATE_NO_WINDOW) + page.window_close() + + btn_install = ft.ElevatedButton("Installieren", on_click=do_install, bgcolor=c_accent, color=ft.colors.WHITE, width=200, height=40) + btn_start = ft.ElevatedButton("Worker starten", on_click=start_worker, bgcolor=c_success, color=ft.colors.WHITE, width=200, height=40, visible=False) + + page.add( + ft.Row([ + ft.Icon(ft.icons.DISC_FULL, size=40, color=c_accent), + ft.Text("Rippy Encoding-Worker", size=28, weight=ft.FontWeight.BOLD, color=c_text) + ]), + ft.Text("Diese Maschine übernimmt die Video-Kompression für Rippy. Gerippt wird weiter auf der Hauptmaschine.", color=c_muted), + ft.Divider(color="#334155"), + ft.Row([txt_host], alignment=ft.MainAxisAlignment.START), + ft.Row([txt_name, txt_slots], alignment=ft.MainAxisAlignment.START), + ft.Row([txt_pfad, ft.ElevatedButton("Rippy holen", on_click=check_map, bgcolor=c_panel, color=c_text)], alignment=ft.MainAxisAlignment.START), + ft.Row([txt_map, ft.ElevatedButton("Freigabe testen", on_click=check_map, bgcolor=c_panel, color=c_text)], alignment=ft.MainAxisAlignment.START), + chk_auto, + chk_desktop, + ft.Container(content=log_view, bgcolor="#020617", padding=10, border_radius=5, border=ft.border.all(1, "#334155")), + ft.Row([btn_install, btn_start]) + ) + +if __name__ == "__main__": + ft.app(target=main) diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 609b9c4..19136cb 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -21,7 +21,7 @@ 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 deploy/worker-windows/install-gui.ps1 worker_dist/ +COPY deploy/worker-windows/installer.py worker_dist/ # Rippy-Icon: Der Installer legt damit eine Verknüpfung auf den Desktop # (Commander-Wunsch 26.07.2026). Ohne die Datei auf der Zielmaschine trüge die # Verknüpfung das Standard-Batch-Symbol — und niemand fände sie zwischen den