feat: complete rewrite of worker installer in Flet (standalone exe)
This commit is contained in:
Binary file not shown.
@@ -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"
|
||||
}
|
||||
@@ -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/<job> -
|
||||
# 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()
|
||||
@@ -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 <LAN-IP-der-Rippy-Maschine> [-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/<job>. 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'."
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user