style: echte Umlaute im ganzen Projekt + Installer im Rippy-Look
Ampel / ampel (push) Successful in 27s
Ampel / ampel (push) Successful in 27s
Commander-Rueckmeldung: Umlaute fehlen. Ausloeser war sichtbar der Installer -
"Diese Maschine uebernimmt die Video-Kompression fuer Rippy" stand woertlich im
Screenshot.
## Die .ps1-Falle war loesbar, nicht unumgehbar
Bisher galt: ausgelieferte .ps1 MUESSEN ASCII sein, weil PowerShell 5.1 sie
ohne BOM als ANSI liest. Das ist nur die halbe Wahrheit - gemessen mit echtem
powershell.exe 5.1:
ohne BOM: $s = "Größe: äöü" + Unerwartetes Token -> Skript kaputt
mit BOM: Groesse: aeoeue + laeuft, Length 16 korrekt
Alle drei Skripte sind jetzt UTF-8 MIT BOM und tragen echte Umlaute; unter 5.1
gegengeprueft (BOM vorhanden, Parser fehlerfrei, Text korrekt gelesen). Der
Kopfkommentar sagt das jetzt richtig statt "ASCII-only".
## Umstellung: Text ja, Bezeichner nein
Umlaute gehoeren in Kommentare und Anzeigetexte, nicht in Funktionsnamen oder
Datenschluessel. Deshalb je Sprache das passende Werkzeug:
- Python: ueber den TOKENIZER - angefasst wurden ausschliesslich COMMENT- und
STRING-Tokens. 156 Stellen. Code ist damit garantiert unberuehrt.
- TypeScript: nur // und /* */ Kommentare sowie JSX-Text (kann per Definition
kein Bezeichner sein). 24 Stellen. `const waehlen`, `let laeuft`, `plaetze`,
`GeraetInfo` sind nachweislich unversehrt.
- install.sh: Anzeigetext, aber die Shell-Funktionen (gruen/rot/gelb/titel) und
der Schalter --nur-pruefen bleiben ASCII - das sind Schnittstellen.
- Markdown: 0 Aenderungen, die Doku hatte schon Umlaute.
ZWEI FEHLER MEINES KONVERTERS, beide von Werkzeugen gefangen:
1. In f-Strings steht in {...} CODE, kein Text. Aus f"{groesse}" wurde
f"{größe}", waehrend die Variable groesse hiess - Ruff meldete F821
"Undefined name". Der Konverter lagert Einsetzungen jetzt aus.
2. Ein Dict-Schluessel wurde umbenannt: die Wortliste enthaelt das PRAEFIX
"uebersprung", der Tabu-Schutz prueft aber ganze Woerter. bericht[...] ist
wieder ASCII - Umlaute in Datenschluesseln brechen JSON-Runden und DB-Felder.
## Installer im Rippy-Look
Statt hellgrau jetzt dieselben Toene wie das Web-UI (aus lib/design.ts
uebernommen): slate-900 Flaeche, dunkle Eingabefelder, Amber-Hauptknopf wie
"Los geht's" im Wizard. Oben ein Kopfbereich mit dem Farbverlauf
amber -> indigo -> purple und dem Disc-Symbol - in WinForms per Paint-Ereignis
gezeichnet, weil es dort keine Verlaeufe von der Stange gibt.
Geprueft, nicht gehofft: Das Fenster wurde headless in ein PNG gerendert
(DrawToBitmap) und angesehen - Verlauf, Symbol, Umlaute und Farben sitzen.
RippyWorkerSetup.exe neu gebaut (57344 -> 60416 Bytes); Umlaute und das
requireAdministrator-Manifest sind in der .exe verifiziert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,89 +1,89 @@
|
|||||||
# AGENTS.md — Arbeits-Konventionen für Rippy
|
# AGENTS.md — Arbeits-Konventionen für Rippy
|
||||||
|
|
||||||
## Grundregeln
|
## Grundregeln
|
||||||
|
|
||||||
**1. Plan vor Code.** Vor jeder Etappe: Klare Akzeptanzkriterien schreiben, dann bauen.
|
**1. Plan vor Code.** Vor jeder Etappe: Klare Akzeptanzkriterien schreiben, dann bauen.
|
||||||
**2. Kleine Schritte.** Max. eine Feature pro Commit. Commit-Nachrichten beschreiben WAS und WARUM.
|
**2. Kleine Schritte.** Max. eine Feature pro Commit. Commit-Nachrichten beschreiben WAS und WARUM.
|
||||||
**3. Beweisen statt behaupten.** Testen, was gebaut wurde. Keine "es sollte funktionieren"-Commits.
|
**3. Beweisen statt behaupten.** Testen, was gebaut wurde. Keine "es sollte funktionieren"-Commits.
|
||||||
**4. Deutsch-Nicht-Entwickler.** Der Commander liest alles — Variablennamen auf Englisch, aber Kommentare und Docs auf Deutsch.
|
**4. Deutsch-Nicht-Entwickler.** Der Commander liest alles — Variablennamen auf Englisch, aber Kommentare und Docs auf Deutsch.
|
||||||
|
|
||||||
## ⛔ HARTE Regeln (mechanisch geprüft — seit 22.07.2026)
|
## ⛔ HARTE Regeln (mechanisch geprüft — seit 22.07.2026)
|
||||||
|
|
||||||
**A. Die CI-Ampel muss GRÜN sein, bevor irgendetwas „fertig" heißt.**
|
**A. Die CI-Ampel muss GRÜN sein, bevor irgendetwas „fertig" heißt.**
|
||||||
`.gitea/workflows/ci.yml` läuft bei jedem Push auf dem Gitea-Runner (NICHT löschen, NICHT
|
`.gitea/workflows/ci.yml` läuft bei jedem Push auf dem Gitea-Runner (NICHT löschen, NICHT
|
||||||
abschwächen). Keine Tests = rot = nicht fertig. Der Commander liest die Ampel, nicht den Code.
|
abschwächen). Keine Tests = rot = nicht fertig. Der Commander liest die Ampel, nicht den Code.
|
||||||
|
|
||||||
**B. Abweichung vom KONZEPT = STOPP + fragen.** Anderes Werkzeug, andere Bibliothek,
|
**B. Abweichung vom KONZEPT = STOPP + fragen.** Anderes Werkzeug, andere Bibliothek,
|
||||||
gestrichenes Muss-Feature → erst den Commander fragen, NIE still ersetzen.
|
gestrichenes Muss-Feature → erst den Commander fragen, NIE still ersetzen.
|
||||||
(Vorgefallen: Muss-Feature „MakeMKV lossless" wurde still durch lossy HandBrake ersetzt.)
|
(Vorgefallen: Muss-Feature „MakeMKV lossless" wurde still durch lossy HandBrake ersetzt.)
|
||||||
|
|
||||||
**C. Single Source of Truth ist `main` — es gibt nur diesen einen Branch**
|
**C. Single Source of Truth ist `main` — es gibt nur diesen einen Branch**
|
||||||
(seit 24.07.2026; der `stable`-Zwischenbranch ist abgeschafft). Die CI-Ampel
|
(seit 24.07.2026; der `stable`-Zwischenbranch ist abgeschafft). Die CI-Ampel
|
||||||
läuft bei jedem Push und PRÜFT nur (Ruff/pytest/Vite-Build) — sie befördert
|
läuft bei jedem Push und PRÜFT nur (Ruff/pytest/Vite-Build) — sie befördert
|
||||||
nichts mehr. Deployt wird direkt aus `main`: auf der VM `git pull` bzw.
|
nichts mehr. Deployt wird direkt aus `main`: auf der VM `git pull` bzw.
|
||||||
`./deploy.sh` (verifiziere vorher, dass die Ampel für den Commit GRÜN ist —
|
`./deploy.sh` (verifiziere vorher, dass die Ampel für den Commit GRÜN ist —
|
||||||
rot heißt: nicht deployen). Nie freihändig per SSH auf der VM bauen.
|
rot heißt: nicht deployen). Nie freihändig per SSH auf der VM bauen.
|
||||||
(Vorgefallen: Doppel-Anlage `Rippy` + `rippy` auf der VM durch Freihand-Deploys.)
|
(Vorgefallen: Doppel-Anlage `Rippy` + `rippy` auf der VM durch Freihand-Deploys.)
|
||||||
|
|
||||||
**D. Externe Schnittstellen NIE aus dem Kopf.** Vor Nutzung fremder CLI-Flags oder
|
**D. Externe Schnittstellen NIE aus dem Kopf.** Vor Nutzung fremder CLI-Flags oder
|
||||||
Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen.
|
Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen.
|
||||||
(Vorgefallen: erfundene Celery-Methode `self.send_task`, erfundene abcde-Flags.)
|
(Vorgefallen: erfundene Celery-Methode `self.send_task`, erfundene abcde-Flags.)
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. **Read:** KONZEPT.md + ROADMAP.md lesen. Verstehen, welche Etappe dran ist.
|
1. **Read:** KONZEPT.md + ROADMAP.md lesen. Verstehen, welche Etappe dran ist.
|
||||||
2. **Plan:** Was genau soll diese Sitzung bauen? Eine Zeile.
|
2. **Plan:** Was genau soll diese Sitzung bauen? Eine Zeile.
|
||||||
3. **Build:** Code schreiben, testen, committen.
|
3. **Build:** Code schreiben, testen, committen.
|
||||||
4. **Verify:** `docker compose up` — funktioniert das Ganze?
|
4. **Verify:** `docker compose up` — funktioniert das Ganze?
|
||||||
5. **Savepoint:** SAVEPOINT.md aktualisieren. Nächster Chat beginnt nicht von Null.
|
5. **Savepoint:** SAVEPOINT.md aktualisieren. Nächster Chat beginnt nicht von Null.
|
||||||
|
|
||||||
## Modelle je Aufgabe
|
## Modelle je Aufgabe
|
||||||
|
|
||||||
- **Planung:** Heavy-Modell (Reasoning) für Architektur-Entscheidungen.
|
- **Planung:** Heavy-Modell (Reasoning) für Architektur-Entscheidungen.
|
||||||
- **Bauen:** schnelles Coding-Modell.
|
- **Bauen:** schnelles Coding-Modell.
|
||||||
- **Review:** Heavy nochmal für Code-Review vor Merge.
|
- **Review:** Heavy nochmal für Code-Review vor Merge.
|
||||||
|
|
||||||
## Docker-Praxis
|
## Docker-Praxis
|
||||||
|
|
||||||
- Alles läuft in Containern: `docker compose up -d`
|
- Alles läuft in Containern: `docker compose up -d`
|
||||||
- Keine System-Pakete auf dem Host — alles im Container.
|
- Keine System-Pakete auf dem Host — alles im Container.
|
||||||
- Dockerfile immer multi-stage, kleinste Images.
|
- Dockerfile immer multi-stage, kleinste Images.
|
||||||
|
|
||||||
## Was NICHT gebaut wird
|
## Was NICHT gebaut wird
|
||||||
|
|
||||||
- Kein Code direkt im Host-OS.
|
- Kein Code direkt im Host-OS.
|
||||||
- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker.
|
- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker.
|
||||||
- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf.
|
- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf.
|
||||||
|
|
||||||
## Aktueller Stand (25.07.2026)
|
## Aktueller Stand (25.07.2026)
|
||||||
|
|
||||||
- ✅ **E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB)
|
- ✅ **E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB)
|
||||||
- ✅ **Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration,
|
- ✅ **Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration,
|
||||||
echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis,
|
echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis,
|
||||||
MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben
|
MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben
|
||||||
- ✅ **Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes
|
- ✅ **Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes
|
||||||
MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI
|
MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI
|
||||||
- ✅ **Etappe 18 (v3.11):** 4K-UHD gelöst — `makemkvcon` holt Schlüssel
|
- ✅ **Etappe 18 (v3.11):** 4K-UHD gelöst — `makemkvcon` holt Schlüssel
|
||||||
unter Linux nie, unter Windows schon; Schlüsselspeicher übernehmbar.
|
unter Linux nie, unter Windows schon; Schlüsselspeicher übernehmbar.
|
||||||
Akira-UHD geht auf der VM auf (`TCOUNT:5`, bewiesen)
|
Akira-UHD geht auf der VM auf (`TCOUNT:5`, bewiesen)
|
||||||
- ✅ **Etappe 19 (v3.14):** Durchsicht Frontend/Backend — vier Placebos weg
|
- ✅ **Etappe 19 (v3.14):** Durchsicht Frontend/Backend — vier Placebos weg
|
||||||
(Fortschritt log, Auswurf tat nichts, „Alle Tracks" konnte nichts, Encoder
|
(Fortschritt log, Auswurf tat nichts, „Alle Tracks" konnte nichts, Encoder
|
||||||
wurden behauptet statt gemessen), Zombie-Erkennung gebaut, Pfad-Prüfung
|
wurden behauptet statt gemessen), Zombie-Erkennung gebaut, Pfad-Prüfung
|
||||||
gehärtet, und der Platten-Schutz aus `c065967` als **unwirksam** entlarvt
|
gehärtet, und der Platten-Schutz aus `c065967` als **unwirksam** entlarvt
|
||||||
- ✅ **Etappe 20 (v3.15):** Aufräum-Runde — `/capabilities` 1,010 s → 0,003 s
|
- ✅ **Etappe 20 (v3.15):** Aufräum-Runde — `/capabilities` 1,010 s → 0,003 s
|
||||||
(Ping im Hintergrund), Kompression je Disc-Typ abwählbar (4K verlustfrei),
|
(Ping im Hintergrund), Kompression je Disc-Typ abwählbar (4K verlustfrei),
|
||||||
Wizard empfiehlt nach gemessener CPU, vier tote Routen entfernt
|
Wizard empfiehlt nach gemessener CPU, vier tote Routen entfernt
|
||||||
- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe
|
- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe
|
||||||
|
|
||||||
## Was diese Sitzungen wiederholt gekostet hat
|
## Was diese Sitzungen wiederholt gekostet hat
|
||||||
|
|
||||||
**Nicht aus einem Zustandswert auf einen Mechanismus schließen.** Vorgefallen:
|
**Nicht aus einem Zustandswert auf einen Mechanismus schließen.** Vorgefallen:
|
||||||
aus „kein Schlüssel da" → „Server abgeschaltet" (falsch), aus Status
|
aus „kein Schlüssel da" → „Server abgeschaltet" (falsch), aus Status
|
||||||
`transcoding` → „Celery hat neu zugestellt" (falsch), aus `progress=99` →
|
`transcoding` → „Celery hat neu zugestellt" (falsch), aus `progress=99` →
|
||||||
„Altwert aus dem Absturz" (falsch — ein Bug), aus gleichem `st_dev` →
|
„Altwert aus dem Absturz" (falsch — ein Bug), aus gleichem `st_dev` →
|
||||||
„`os.rename` funktioniert" (falsch — der Kernel vergleicht den Mount).
|
„`os.rename` funktioniert" (falsch — der Kernel vergleicht den Mount).
|
||||||
Jedes Mal hätte eine Messung von unter einer Minute gereicht.
|
Jedes Mal hätte eine Messung von unter einer Minute gereicht.
|
||||||
|
|
||||||
**Und die Umkehrung gilt genauso:** gleiches `st_dev` heißt NICHT gleicher
|
**Und die Umkehrung gilt genauso:** gleiches `st_dev` heißt NICHT gleicher
|
||||||
Mount. Wo eine Eigenschaft ausprobierbar ist, probiere sie aus, statt sie
|
Mount. Wo eine Eigenschaft ausprobierbar ist, probiere sie aus, statt sie
|
||||||
vorherzusagen.
|
vorherzusagen.
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
# Baut rippy.ico + RippyWorkerSetup.exe aus install-gui.ps1 (ps2exe).
|
# Baut rippy.ico + RippyWorkerSetup.exe aus install-gui.ps1 (ps2exe).
|
||||||
#
|
#
|
||||||
# WANN NEU BAUEN: nur wenn sich install-gui.ps1 (die Installer-Oberflaeche)
|
# WANN NEU BAUEN: nur wenn sich install-gui.ps1 (die Installer-Oberfläche)
|
||||||
# aendert. NICHT bei MakeMKV-/HandBrake-Updates - die .exe holt HandBrake zur
|
# ändert. NICHT bei MakeMKV-/HandBrake-Updates - die .exe holt HandBrake zur
|
||||||
# Laufzeit (GitHub latest) und den Worker-Code live von Rippy, friert also
|
# Laufzeit (GitHub latest) und den Worker-Code live von Rippy, friert also
|
||||||
# keine Versionen ein.
|
# keine Versionen ein.
|
||||||
# AUSFUEHREN AUF WINDOWS (eine Windows-.exe kann nicht von Linux gebaut werden):
|
# AUSFUEHREN AUF WINDOWS (eine Windows-.exe kann nicht von Linux gebaut werden):
|
||||||
@@ -35,7 +35,7 @@ $bw.Write($png); $bw.Flush()
|
|||||||
[System.IO.File]::WriteAllBytes("$PSScriptRoot\rippy.ico", $icoStream.ToArray())
|
[System.IO.File]::WriteAllBytes("$PSScriptRoot\rippy.ico", $icoStream.ToArray())
|
||||||
Write-Host "rippy.ico erzeugt ($($png.Length) Bytes PNG)"
|
Write-Host "rippy.ico erzeugt ($($png.Length) Bytes PNG)"
|
||||||
|
|
||||||
# 2. ps2exe sicherstellen - TLS 1.2 ist der Fix fuer den NuGet-Bootstrap-Fehler
|
# 2. ps2exe sicherstellen - TLS 1.2 ist der Fix für den NuGet-Bootstrap-Fehler
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
|
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
if (-not (Get-Module -ListAvailable ps2exe)) {
|
if (-not (Get-Module -ListAvailable ps2exe)) {
|
||||||
Write-Host "Bootstrappe NuGet + PSGallery ..."
|
Write-Host "Bootstrappe NuGet + PSGallery ..."
|
||||||
@@ -49,13 +49,13 @@ Import-Module ps2exe
|
|||||||
|
|
||||||
# 3. Kompilieren (WinForms -> noConsole + STA, Icon eingebettet)
|
# 3. Kompilieren (WinForms -> noConsole + STA, Icon eingebettet)
|
||||||
#
|
#
|
||||||
# -requireAdmin (seit 25.07.2026): Der Worker wird standardmaessig nach
|
# -requireAdmin (seit 25.07.2026): Der Worker wird standardmäßig nach
|
||||||
# "C:\Program Files\Rippy Worker" installiert, und dorthin darf nur ein
|
# "C:\Program Files\Rippy Worker" installiert, und dorthin darf nur ein
|
||||||
# Administrator schreiben. Mit dem Schalter tragt die .exe ein Manifest, das
|
# Administrator schreiben. Mit dem Schalter tragt die .exe ein Manifest, das
|
||||||
# Windows beim Doppelklick EINMAL per UAC fragen laesst - danach stimmen die
|
# Windows beim Doppelklick EINMAL per UAC fragen lässt - danach stimmen die
|
||||||
# Rechte fuer alles Weitere. Ohne ihn liefe der Installer unerhoeht und
|
# 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,
|
# scheiterte beim Anlegen des Ordners; wer das Ziel ins eigene Profil legt,
|
||||||
# braucht die Erhoehung nicht, wird aber trotzdem gefragt (normal fuer einen
|
# braucht die Erhöhung nicht, wird aber trotzdem gefragt (normal für einen
|
||||||
# Installer und besser als ein Fehlschlag mitten im Lauf).
|
# Installer und besser als ein Fehlschlag mitten im Lauf).
|
||||||
Invoke-ps2exe -inputFile $guiSrc -outputFile "$PSScriptRoot\RippyWorkerSetup.exe" `
|
Invoke-ps2exe -inputFile $guiSrc -outputFile "$PSScriptRoot\RippyWorkerSetup.exe" `
|
||||||
-iconFile "$PSScriptRoot\rippy.ico" -noConsole -STA -requireAdmin `
|
-iconFile "$PSScriptRoot\rippy.ico" -noConsole -STA -requireAdmin `
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime).
|
# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime).
|
||||||
# Wird ueber rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur
|
# Wird über rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur
|
||||||
# Python 3.10+ auf der Maschine - kein Docker, kein git.
|
# Python 3.10+ auf der Maschine - kein Docker, kein git.
|
||||||
#
|
#
|
||||||
# ASCII-only (PowerShell 5.1 liest .ps1 als ANSI - Sonderzeichen zerschiessen
|
# WICHTIG - KODIERUNG: Diese Datei MUSS als UTF-8 MIT BOM gespeichert werden.
|
||||||
# das Skript).
|
# 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(
|
param(
|
||||||
[string]$RippyHost = "",
|
[string]$RippyHost = "",
|
||||||
@@ -16,28 +21,42 @@ Add-Type -AssemblyName System.Drawing
|
|||||||
|
|
||||||
# Standard-Zielverzeichnis: wie jedes andere Programm unter "Programme"
|
# Standard-Zielverzeichnis: wie jedes andere Programm unter "Programme"
|
||||||
# (Commander-Wunsch 25.07.2026 - vorher lag der Worker im Benutzerprofil, was
|
# (Commander-Wunsch 25.07.2026 - vorher lag der Worker im Benutzerprofil, was
|
||||||
# fuer ein Programm unueblich ist). GetFolderPath liefert den ECHTEN Pfad;
|
# für ein Programm unüblich ist). GetFolderPath liefert den ECHTEN Pfad;
|
||||||
# deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heisst
|
# deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heißt
|
||||||
# trotzdem "C:\Program Files".
|
# trotzdem "C:\Program Files".
|
||||||
#
|
#
|
||||||
# Schreiben dorthin braucht Administratorrechte - deshalb ist die .exe mit
|
# Schreiben dorthin braucht Administratorrechte - deshalb ist die .exe mit
|
||||||
# -requireAdmin gebaut (build-exe.ps1) und fragt beim Start einmal per UAC.
|
# -requireAdmin gebaut (build-exe.ps1) und fragt beim Start einmal per UAC.
|
||||||
# Ohne das waere hier der naechste "Zugriff verweigert" programmiert.
|
# Ohne das wäre hier der nächste "Zugriff verweigert" programmiert.
|
||||||
$StandardZiel = Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker"
|
$StandardZiel = Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker"
|
||||||
$InstallDir = $StandardZiel
|
$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 ---------------------------------------------------------------
|
# --- Fenster ---------------------------------------------------------------
|
||||||
$form = New-Object System.Windows.Forms.Form
|
$form = New-Object System.Windows.Forms.Form
|
||||||
$form.Text = "Rippy Encoding-Worker - Installation"
|
$form.Text = "Rippy Encoding-Worker - Installation"
|
||||||
$form.Size = New-Object System.Drawing.Size(560, 575)
|
$form.ClientSize = New-Object System.Drawing.Size(560, 560)
|
||||||
$form.StartPosition = "CenterScreen"
|
$form.StartPosition = "CenterScreen"
|
||||||
$form.FormBorderStyle = "FixedDialog"
|
$form.FormBorderStyle = "FixedDialog"
|
||||||
$form.MaximizeBox = $false
|
$form.MaximizeBox = $false
|
||||||
$form.BackColor = [System.Drawing.Color]::FromArgb(248, 250, 252)
|
$form.BackColor = $cBg
|
||||||
|
$form.ForeColor = $cText
|
||||||
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
||||||
|
|
||||||
# Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weisses Loch)
|
# Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weißes Loch)
|
||||||
# - kein externes .ico noetig. Erscheint in Titelleiste und Taskleiste.
|
# - kein externes .ico nötig. Erscheint in Titelleiste und Taskleiste.
|
||||||
try {
|
try {
|
||||||
$ibmp = New-Object System.Drawing.Bitmap 32, 32
|
$ibmp = New-Object System.Drawing.Bitmap 32, 32
|
||||||
$ig = [System.Drawing.Graphics]::FromImage($ibmp)
|
$ig = [System.Drawing.Graphics]::FromImage($ibmp)
|
||||||
@@ -47,75 +66,116 @@ try {
|
|||||||
$form.Icon = [System.Drawing.Icon]::FromHandle($ibmp.GetHicon())
|
$form.Icon = [System.Drawing.Icon]::FromHandle($ibmp.GetHicon())
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
$titel = New-Object System.Windows.Forms.Label
|
# --- Kopfbereich mit Farbverlauf --------------------------------------------
|
||||||
$titel.Text = "Rippy Encoding-Worker einrichten"
|
# Das Rippy-UI hat oben einen Verlauf amber -> indigo -> purple (siehe
|
||||||
$titel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold)
|
# FirstRunWizard.tsx). WinForms kann das nicht von sich aus, also wird der
|
||||||
$titel.ForeColor = [System.Drawing.Color]::FromArgb(79, 70, 229)
|
# Verlauf im Paint-Ereignis gezeichnet - inklusive Disc-Symbol.
|
||||||
$titel.Location = New-Object System.Drawing.Point(24, 20)
|
$kopf = New-Object System.Windows.Forms.Panel
|
||||||
$titel.Size = New-Object System.Drawing.Size(500, 30)
|
$kopf.Dock = "Top"
|
||||||
$form.Controls.Add($titel)
|
$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()
|
||||||
|
|
||||||
$info = New-Object System.Windows.Forms.Label
|
# Disc-Symbol: Ring aussen, weisses Loch in der Mitte
|
||||||
$info.Text = "Diese Maschine uebernimmt die Video-Kompression fuer Rippy. Gerippt wird weiter auf der Rippy-Hauptmaschine."
|
$e.Graphics.SmoothingMode = "AntiAlias"
|
||||||
$info.Location = New-Object System.Drawing.Point(24, 52)
|
$weiss = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(235, 255, 255, 255))
|
||||||
$info.Size = New-Object System.Drawing.Size(500, 34)
|
$e.Graphics.FillEllipse($weiss, 24, 28, 48, 48)
|
||||||
$info.ForeColor = [System.Drawing.Color]::FromArgb(100, 116, 139)
|
$e.Graphics.FillEllipse((New-Object System.Drawing.SolidBrush $cIndigo), 36, 40, 24, 24)
|
||||||
$form.Controls.Add($info)
|
$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
|
# Rippy-Adresse
|
||||||
$lblHost = New-Object System.Windows.Forms.Label
|
$lblHost = New-Object System.Windows.Forms.Label
|
||||||
$lblHost.Text = "LAN-IP der Rippy-Maschine (Docker-Host) - NICHT dieser PC:"
|
$lblHost.Text = "LAN-IP der Rippy-Maschine (Docker-Host) - NICHT dieser PC:"
|
||||||
$lblHost.Location = New-Object System.Drawing.Point(24, 96)
|
$lblHost.Location = New-Object System.Drawing.Point(24, 120)
|
||||||
$lblHost.Size = New-Object System.Drawing.Size(500, 18)
|
$lblHost.Size = New-Object System.Drawing.Size(500, 18)
|
||||||
|
$lblHost.ForeColor = $cGedaempft
|
||||||
$form.Controls.Add($lblHost)
|
$form.Controls.Add($lblHost)
|
||||||
|
|
||||||
$txtHost = New-Object System.Windows.Forms.TextBox
|
$txtHost = New-Object System.Windows.Forms.TextBox
|
||||||
$txtHost.Location = New-Object System.Drawing.Point(24, 116)
|
$txtHost.Location = New-Object System.Drawing.Point(24, 140)
|
||||||
$txtHost.Size = New-Object System.Drawing.Size(300, 26)
|
$txtHost.Size = New-Object System.Drawing.Size(300, 26)
|
||||||
$txtHost.Text = $RippyHost
|
$txtHost.Text = $RippyHost
|
||||||
$txtHost.Font = New-Object System.Drawing.Font("Consolas", 10)
|
$txtHost.Font = New-Object System.Drawing.Font("Consolas", 10)
|
||||||
|
$txtHost.BackColor = $cFeld
|
||||||
|
$txtHost.ForeColor = $cText
|
||||||
|
$txtHost.BorderStyle = "FixedSingle"
|
||||||
$form.Controls.Add($txtHost)
|
$form.Controls.Add($txtHost)
|
||||||
|
|
||||||
# Worker-Name
|
# Worker-Name
|
||||||
$lblName = New-Object System.Windows.Forms.Label
|
$lblName = New-Object System.Windows.Forms.Label
|
||||||
$lblName.Text = "Name dieses Workers (frei waehlbar):"
|
$lblName.Text = "Name dieses Workers (frei wählbar):"
|
||||||
$lblName.Location = New-Object System.Drawing.Point(24, 152)
|
$lblName.Location = New-Object System.Drawing.Point(24, 176)
|
||||||
$lblName.Size = New-Object System.Drawing.Size(500, 18)
|
$lblName.Size = New-Object System.Drawing.Size(500, 18)
|
||||||
|
$lblName.ForeColor = $cGedaempft
|
||||||
$form.Controls.Add($lblName)
|
$form.Controls.Add($lblName)
|
||||||
|
|
||||||
$txtName = New-Object System.Windows.Forms.TextBox
|
$txtName = New-Object System.Windows.Forms.TextBox
|
||||||
$txtName.Location = New-Object System.Drawing.Point(24, 172)
|
$txtName.Location = New-Object System.Drawing.Point(24, 196)
|
||||||
$txtName.Size = New-Object System.Drawing.Size(300, 26)
|
$txtName.Size = New-Object System.Drawing.Size(300, 26)
|
||||||
$txtName.Text = $WorkerName
|
$txtName.Text = $WorkerName
|
||||||
|
$txtName.BackColor = $cFeld
|
||||||
|
$txtName.ForeColor = $cText
|
||||||
|
$txtName.BorderStyle = "FixedSingle"
|
||||||
$form.Controls.Add($txtName)
|
$form.Controls.Add($txtName)
|
||||||
|
|
||||||
# Zielverzeichnis - frei waehlbar, Standard "Programme"
|
# Zielverzeichnis - frei wählbar, Standard "Programme"
|
||||||
$lblPfad = New-Object System.Windows.Forms.Label
|
$lblPfad = New-Object System.Windows.Forms.Label
|
||||||
$lblPfad.Text = "Installieren nach:"
|
$lblPfad.Text = "Installieren nach:"
|
||||||
$lblPfad.Location = New-Object System.Drawing.Point(24, 206)
|
$lblPfad.Location = New-Object System.Drawing.Point(24, 232)
|
||||||
$lblPfad.Size = New-Object System.Drawing.Size(500, 18)
|
$lblPfad.Size = New-Object System.Drawing.Size(500, 18)
|
||||||
|
$lblPfad.ForeColor = $cGedaempft
|
||||||
$form.Controls.Add($lblPfad)
|
$form.Controls.Add($lblPfad)
|
||||||
|
|
||||||
$txtPfad = New-Object System.Windows.Forms.TextBox
|
$txtPfad = New-Object System.Windows.Forms.TextBox
|
||||||
$txtPfad.Location = New-Object System.Drawing.Point(24, 226)
|
$txtPfad.Location = New-Object System.Drawing.Point(24, 252)
|
||||||
$txtPfad.Size = New-Object System.Drawing.Size(390, 26)
|
$txtPfad.Size = New-Object System.Drawing.Size(390, 26)
|
||||||
$txtPfad.Text = $StandardZiel
|
$txtPfad.Text = $StandardZiel
|
||||||
$txtPfad.Font = New-Object System.Drawing.Font("Consolas", 9)
|
$txtPfad.Font = New-Object System.Drawing.Font("Consolas", 9)
|
||||||
|
$txtPfad.BackColor = $cFeld
|
||||||
|
$txtPfad.ForeColor = $cText
|
||||||
|
$txtPfad.BorderStyle = "FixedSingle"
|
||||||
$form.Controls.Add($txtPfad)
|
$form.Controls.Add($txtPfad)
|
||||||
|
|
||||||
$btnPfad = New-Object System.Windows.Forms.Button
|
$btnPfad = New-Object System.Windows.Forms.Button
|
||||||
$btnPfad.Text = "Aendern ..."
|
$btnPfad.Text = "Ändern ..."
|
||||||
$btnPfad.Location = New-Object System.Drawing.Point(424, 225)
|
$btnPfad.Location = New-Object System.Drawing.Point(424, 251)
|
||||||
$btnPfad.Size = New-Object System.Drawing.Size(100, 28)
|
$btnPfad.Size = New-Object System.Drawing.Size(100, 28)
|
||||||
$btnPfad.FlatStyle = "Flat"
|
$btnPfad.FlatStyle = "Flat"
|
||||||
|
$btnPfad.BackColor = $cFeld
|
||||||
|
$btnPfad.ForeColor = $cText
|
||||||
|
$btnPfad.FlatAppearance.BorderColor = $cGedaempft
|
||||||
$form.Controls.Add($btnPfad)
|
$form.Controls.Add($btnPfad)
|
||||||
$btnPfad.Add_Click({
|
$btnPfad.Add_Click({
|
||||||
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||||
$dlg.Description = "Ordner waehlen - darin wird 'Rippy Worker' angelegt"
|
$dlg.Description = "Ordner wählen - darin wird 'Rippy Worker' angelegt"
|
||||||
$dlg.ShowNewFolderButton = $true
|
$dlg.ShowNewFolderButton = $true
|
||||||
if ($dlg.ShowDialog() -eq "OK") {
|
if ($dlg.ShowDialog() -eq "OK") {
|
||||||
# Der Dialog liefert den ELTERN-Ordner; der Unterordner kommt von uns,
|
# Der Dialog liefert den ELTERN-Ordner; der Unterordner kommt von uns,
|
||||||
# damit die Deinstallation nie einen fremden Ordner mitloescht.
|
# damit die Deinstallation nie einen fremden Ordner mitlöscht.
|
||||||
$txtPfad.Text = (Join-Path $dlg.SelectedPath "Rippy Worker")
|
$txtPfad.Text = (Join-Path $dlg.SelectedPath "Rippy Worker")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -123,39 +183,41 @@ $btnPfad.Add_Click({
|
|||||||
# Autostart
|
# Autostart
|
||||||
$chkAuto = New-Object System.Windows.Forms.CheckBox
|
$chkAuto = New-Object System.Windows.Forms.CheckBox
|
||||||
$chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)"
|
$chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)"
|
||||||
$chkAuto.Location = New-Object System.Drawing.Point(24, 264)
|
$chkAuto.Location = New-Object System.Drawing.Point(24, 290)
|
||||||
$chkAuto.Size = New-Object System.Drawing.Size(400, 22)
|
$chkAuto.Size = New-Object System.Drawing.Size(400, 22)
|
||||||
$chkAuto.Checked = $true
|
$chkAuto.Checked = $true
|
||||||
|
$chkAuto.ForeColor = $cText
|
||||||
$form.Controls.Add($chkAuto)
|
$form.Controls.Add($chkAuto)
|
||||||
|
|
||||||
# Log-Bereich
|
# Log-Bereich
|
||||||
$log = New-Object System.Windows.Forms.TextBox
|
$log = New-Object System.Windows.Forms.TextBox
|
||||||
$log.Location = New-Object System.Drawing.Point(24, 296)
|
$log.Location = New-Object System.Drawing.Point(24, 322)
|
||||||
$log.Size = New-Object System.Drawing.Size(500, 160)
|
$log.Size = New-Object System.Drawing.Size(500, 150)
|
||||||
$log.Multiline = $true
|
$log.Multiline = $true
|
||||||
$log.ReadOnly = $true
|
$log.ReadOnly = $true
|
||||||
$log.ScrollBars = "Vertical"
|
$log.ScrollBars = "Vertical"
|
||||||
$log.BackColor = [System.Drawing.Color]::FromArgb(15, 23, 42)
|
$log.BackColor = $cPanel
|
||||||
$log.ForeColor = [System.Drawing.Color]::FromArgb(226, 232, 240)
|
$log.ForeColor = $cText
|
||||||
|
$log.BorderStyle = "FixedSingle"
|
||||||
$log.Font = New-Object System.Drawing.Font("Consolas", 8.5)
|
$log.Font = New-Object System.Drawing.Font("Consolas", 8.5)
|
||||||
$form.Controls.Add($log)
|
$form.Controls.Add($log)
|
||||||
|
|
||||||
# Knoepfe
|
# Knöpfe
|
||||||
$btnInstall = New-Object System.Windows.Forms.Button
|
$btnInstall = New-Object System.Windows.Forms.Button
|
||||||
$btnInstall.Text = "Installieren"
|
$btnInstall.Text = "Installieren"
|
||||||
$btnInstall.Location = New-Object System.Drawing.Point(24, 470)
|
$btnInstall.Location = New-Object System.Drawing.Point(24, 486)
|
||||||
$btnInstall.Size = New-Object System.Drawing.Size(150, 40)
|
$btnInstall.Size = New-Object System.Drawing.Size(150, 40)
|
||||||
$btnInstall.BackColor = [System.Drawing.Color]::FromArgb(79, 70, 229)
|
$btnInstall.BackColor = $cAmber
|
||||||
$btnInstall.ForeColor = [System.Drawing.Color]::White
|
$btnInstall.ForeColor = $cBg
|
||||||
$btnInstall.FlatStyle = "Flat"
|
$btnInstall.FlatStyle = "Flat"
|
||||||
$btnInstall.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
|
$btnInstall.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
|
||||||
$form.Controls.Add($btnInstall)
|
$form.Controls.Add($btnInstall)
|
||||||
|
|
||||||
$btnStart = New-Object System.Windows.Forms.Button
|
$btnStart = New-Object System.Windows.Forms.Button
|
||||||
$btnStart.Text = "Worker starten"
|
$btnStart.Text = "Worker starten"
|
||||||
$btnStart.Location = New-Object System.Drawing.Point(190, 470)
|
$btnStart.Location = New-Object System.Drawing.Point(190, 486)
|
||||||
$btnStart.Size = New-Object System.Drawing.Size(150, 40)
|
$btnStart.Size = New-Object System.Drawing.Size(150, 40)
|
||||||
$btnStart.BackColor = [System.Drawing.Color]::FromArgb(16, 185, 129)
|
$btnStart.BackColor = $cEmerald
|
||||||
$btnStart.ForeColor = [System.Drawing.Color]::White
|
$btnStart.ForeColor = [System.Drawing.Color]::White
|
||||||
$btnStart.FlatStyle = "Flat"
|
$btnStart.FlatStyle = "Flat"
|
||||||
$btnStart.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
|
$btnStart.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
|
||||||
@@ -176,15 +238,15 @@ function Do-Install {
|
|||||||
|
|
||||||
# Zielverzeichnis aus dem Feld. script:-Bereich, weil "Worker starten"
|
# Zielverzeichnis aus dem Feld. script:-Bereich, weil "Worker starten"
|
||||||
# und die Abschlussmeldung denselben Pfad brauchen.
|
# und die Abschlussmeldung denselben Pfad brauchen.
|
||||||
$gewaehlt = $txtPfad.Text.Trim()
|
$zielEingabe = $txtPfad.Text.Trim()
|
||||||
if (-not $gewaehlt) { $gewaehlt = $StandardZiel }
|
if (-not $zielEingabe) { $zielEingabe = $StandardZiel }
|
||||||
if (-not [System.IO.Path]::IsPathRooted($gewaehlt)) {
|
if (-not [System.IO.Path]::IsPathRooted($zielEingabe)) {
|
||||||
[System.Windows.Forms.MessageBox]::Show(
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
"Bitte einen vollstaendigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.",
|
"Bitte einen vollständigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.",
|
||||||
"Pfad unvollstaendig")
|
"Pfad unvollständig")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$script:InstallDir = $gewaehlt
|
$script:InstallDir = $zielEingabe
|
||||||
|
|
||||||
$btnInstall.Enabled = $false
|
$btnInstall.Enabled = $false
|
||||||
$log.Clear()
|
$log.Clear()
|
||||||
@@ -209,7 +271,7 @@ function Do-Install {
|
|||||||
Log "Python gefunden: $python"
|
Log "Python gefunden: $python"
|
||||||
|
|
||||||
# 2. Ordner + Worker-Code
|
# 2. Ordner + Worker-Code
|
||||||
# Erst PRUEFEN, ob wir dort ueberhaupt schreiben duerfen. Die .exe ist mit
|
# 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
|
# -requireAdmin gebaut und fragt per UAC, aber wer install-gui.ps1 direkt
|
||||||
# startet, hat die Rechte nicht - dann soll hier ein brauchbarer Satz
|
# startet, hat die Rechte nicht - dann soll hier ein brauchbarer Satz
|
||||||
# stehen und nicht "Zugriff verweigert" aus irgendeiner Tiefe.
|
# stehen und nicht "Zugriff verweigert" aus irgendeiner Tiefe.
|
||||||
@@ -221,12 +283,12 @@ function Do-Install {
|
|||||||
} catch {
|
} catch {
|
||||||
Log "FEHLER: In '$InstallDir' darf nicht geschrieben werden."
|
Log "FEHLER: In '$InstallDir' darf nicht geschrieben werden."
|
||||||
Log ""
|
Log ""
|
||||||
Log "Das ist normal bei Ordnern unter 'Programme' - dafuer braucht es"
|
Log "Das ist normal bei Ordnern unter 'Programme' - dafür braucht es"
|
||||||
Log "Administratorrechte. Zwei Wege:"
|
Log "Administratorrechte. Zwei Wege:"
|
||||||
Log " 1. Diesen Installer mit Rechtsklick > 'Als Administrator"
|
Log " 1. Diesen Installer mit Rechtsklick > 'Als Administrator"
|
||||||
Log " ausfuehren' starten (der empfohlene Weg), ODER"
|
Log " ausführen' starten (der empfohlene Weg), ODER"
|
||||||
Log " 2. oben ueber 'Aendern ...' einen Ordner im eigenen"
|
Log " 2. oben über 'Ändern ...' einen Ordner im eigenen"
|
||||||
Log " Benutzerprofil waehlen - dort geht es ohne Adminrechte."
|
Log " Benutzerprofil wählen - dort geht es ohne Adminrechte."
|
||||||
$btnInstall.Enabled = $true
|
$btnInstall.Enabled = $true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -242,13 +304,13 @@ function Do-Install {
|
|||||||
Expand-Archive worker.zip -DestinationPath . -Force
|
Expand-Archive worker.zip -DestinationPath . -Force
|
||||||
Remove-Item worker.zip
|
Remove-Item worker.zip
|
||||||
|
|
||||||
# 3. venv + Abhaengigkeiten
|
# 3. venv + Abhängigkeiten
|
||||||
if (-not (Test-Path "venv")) { Log "Lege Python-Umgebung an ..."; Invoke-Expression "$python -m venv venv" }
|
if (-not (Test-Path "venv")) { Log "Lege Python-Umgebung an ..."; Invoke-Expression "$python -m venv venv" }
|
||||||
Log "Installiere Python-Abhaengigkeiten (kann 1-2 min dauern) ..."
|
Log "Installiere Python-Abhängigkeiten (kann 1-2 min dauern) ..."
|
||||||
& .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip
|
& .\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 -r requirements.txt
|
||||||
& .\venv\Scripts\pip.exe install --quiet pystray pillow
|
& .\venv\Scripts\pip.exe install --quiet pystray pillow
|
||||||
Log "Abhaengigkeiten installiert."
|
Log "Abhängigkeiten installiert."
|
||||||
|
|
||||||
# 4. HandBrakeCLI
|
# 4. HandBrakeCLI
|
||||||
if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not (Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue)) {
|
if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not (Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue)) {
|
||||||
@@ -273,12 +335,12 @@ if (-not `$Force) { if ([System.Windows.Forms.MessageBox]::Show("Rippy-Worker '$
|
|||||||
Add-Type -AssemblyName System.Windows.Forms
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }
|
Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||||
Start-Sleep 2
|
Start-Sleep 2
|
||||||
# Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die
|
# Autostart-Verknüpfung entfernen - BEIDE möglichen Orte, weil die
|
||||||
# Installation je nach Zielverzeichnis persoenlich oder maschinenweit war.
|
# Installation je nach Zielverzeichnis persönlich oder maschinenweit war.
|
||||||
foreach (`$ordner in @("Startup", "CommonStartup")) {
|
foreach (`$ordner in @("Startup", "CommonStartup")) {
|
||||||
try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {}
|
try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {}
|
||||||
}
|
}
|
||||||
# ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden.
|
# ... und die geplante Aufgabe aus älteren Installationen, falls vorhanden.
|
||||||
schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null
|
schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null
|
||||||
try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch {}
|
try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch {}
|
||||||
Set-Location (Split-Path `$PSScriptRoot -Parent)
|
Set-Location (Split-Path `$PSScriptRoot -Parent)
|
||||||
@@ -287,55 +349,55 @@ Remove-Item -Recurse -Force `$PSScriptRoot
|
|||||||
"@
|
"@
|
||||||
Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII
|
Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII
|
||||||
|
|
||||||
# 6. Autostart ueber den Autostart-ORDNER, nicht ueber schtasks.
|
# 6. Autostart über den Autostart-ORDNER, nicht über schtasks.
|
||||||
#
|
#
|
||||||
# Befund 25.07.2026 (auf dem Commander-PC aufgetreten): schtasks /create
|
# Befund 25.07.2026 (auf dem Commander-PC aufgetreten): schtasks /create
|
||||||
# mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER der
|
# mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER der
|
||||||
# Aufgabenplanung an, und das verlangt Administratorrechte. Der
|
# Aufgabenplanung an, und das verlangt Administratorrechte. Der
|
||||||
# Installer laeuft normal ohne. schtasks schrieb deshalb
|
# Installer läuft normal ohne. schtasks schrieb deshalb
|
||||||
# "FEHLER: Zugriff verweigert." nach stderr, und weil oben
|
# "FEHLER: Zugriff verweigert." nach stderr, und weil oben
|
||||||
# $ErrorActionPreference = "Stop" steht, wurde daraus ein Abbruch der
|
# $ErrorActionPreference = "Stop" steht, wurde daraus ein Abbruch der
|
||||||
# GESAMTEN Installation - obwohl alles ausser dem Autostart fertig war.
|
# GESAMTEN Installation - obwohl alles außer dem Autostart fertig war.
|
||||||
# Der Nutzer sah nur "FEHLER: FEHLER: Zugriff verweigert" samt dem
|
# Der Nutzer sah nur "FEHLER: FEHLER: Zugriff verweigert" samt dem
|
||||||
# irrefuehrenden Tipp, die IP koenne falsch sein.
|
# irreführenden Tipp, die IP könne falsch sein.
|
||||||
#
|
#
|
||||||
# Der Autostart-Ordner braucht NIE Adminrechte, und der Nutzer kann die
|
# Der Autostart-Ordner braucht NIE Adminrechte, und der Nutzer kann die
|
||||||
# Verknuepfung dort selbst sehen und loeschen. Zwei Fliegen.
|
# Verknüpfung dort selbst sehen und löschen. Zwei Fliegen.
|
||||||
if ($chkAuto.Checked) {
|
if ($chkAuto.Checked) {
|
||||||
try {
|
try {
|
||||||
# Liegt das Programm unter "Programme", ist es eine Installation
|
# Liegt das Programm unter "Programme", ist es eine Installation
|
||||||
# fuer die ganze Maschine - dann gehoert der Autostart in den
|
# für die ganze Maschine - dann gehört der Autostart in den
|
||||||
# Autostart-Ordner ALLER Benutzer. Das passt auch zur Rechtelage:
|
# Autostart-Ordner ALLER Benutzer. Das passt auch zur Rechtelage:
|
||||||
# der Installer laeuft dafuer ohnehin erhoeht, und bei einer
|
# der Installer läuft dafür ohnehin erhöht, und bei einer
|
||||||
# Erhoehung ueber ein FREMDES Administratorkonto waere der
|
# Erhöhung über ein FREMDES Administratorkonto wäre der
|
||||||
# benutzereigene Ordner der des Admins - also der falsche.
|
# benutzereigene Ordner der des Admins - also der falsche.
|
||||||
# Bei einer Installation ins eigene Profil bleibt es persoenlich.
|
# Bei einer Installation ins eigene Profil bleibt es persönlich.
|
||||||
$programme = [Environment]::GetFolderPath("ProgramFiles")
|
$programme = [Environment]::GetFolderPath("ProgramFiles")
|
||||||
$programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86")
|
$programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86")
|
||||||
$fuerAlle = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") `
|
$maschinenweit = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") `
|
||||||
-or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase"))
|
-or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase"))
|
||||||
$autostartOrdner = if ($fuerAlle) {
|
$autostartDir = if ($maschinenweit) {
|
||||||
[Environment]::GetFolderPath("CommonStartup")
|
[Environment]::GetFolderPath("CommonStartup")
|
||||||
} else {
|
} else {
|
||||||
[Environment]::GetFolderPath("Startup")
|
[Environment]::GetFolderPath("Startup")
|
||||||
}
|
}
|
||||||
$verknuepfung = Join-Path $autostartOrdner "RippyWorker.lnk"
|
$lnkPfad = Join-Path $autostartDir "RippyWorker.lnk"
|
||||||
$wsh = New-Object -ComObject WScript.Shell
|
$wsh = New-Object -ComObject WScript.Shell
|
||||||
$lnk = $wsh.CreateShortcut($verknuepfung)
|
$lnk = $wsh.CreateShortcut($lnkPfad)
|
||||||
$lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat")
|
$lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat")
|
||||||
$lnk.WorkingDirectory = $InstallDir
|
$lnk.WorkingDirectory = $InstallDir
|
||||||
$lnk.Description = "Rippy Encoding-Worker"
|
$lnk.Description = "Rippy Encoding-Worker"
|
||||||
$lnk.Save()
|
$lnk.Save()
|
||||||
if ($fuerAlle) {
|
if ($maschinenweit) {
|
||||||
Log "Autostart eingerichtet (fuer alle Benutzer dieser Maschine)."
|
Log "Autostart eingerichtet (für alle Benutzer dieser Maschine)."
|
||||||
} else {
|
} else {
|
||||||
Log "Autostart eingerichtet (fuer dich, startet bei der Anmeldung)."
|
Log "Autostart eingerichtet (für dich, startet bei der Anmeldung)."
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
# Nur eine Warnung: der Worker selbst ist fertig und startbar.
|
# Nur eine Warnung: der Worker selbst ist fertig und startbar.
|
||||||
Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))."
|
Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))."
|
||||||
Log " Der Worker ist trotzdem fertig installiert und laeuft."
|
Log " Der Worker ist trotzdem fertig installiert und läuft."
|
||||||
Log " Von Hand: start-tray.bat in den Autostart-Ordner verknuepfen"
|
Log " Von Hand: start-tray.bat in den Autostart-Ordner verknüpfen"
|
||||||
Log " (Windows-Taste + R, dann shell:startup eingeben)."
|
Log " (Windows-Taste + R, dann shell:startup eingeben)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+211
-211
@@ -1,211 +1,211 @@
|
|||||||
# Rippy: Nativer Windows-Transcode-Worker - Installation OHNE Docker.
|
# Rippy: Nativer Windows-Transcode-Worker - Installation OHNE Docker.
|
||||||
#
|
#
|
||||||
# Was das Skript tut (alles nach -InstallDir, Standard "C:\Program Files\Rippy Worker"):
|
# Was das Skript tut (alles nach -InstallDir, Standard "C:\Program Files\Rippy Worker"):
|
||||||
# 1. Prueft Python (3.10+), legt ein venv an, installiert die Abhaengigkeiten
|
# 1. Prüft Python (3.10+), legt ein venv an, installiert die Abhängigkeiten
|
||||||
# 2. Laedt den Worker-Code direkt von deiner Rippy-Instanz (/api/worker-setup/paket)
|
# 2. Lädt den Worker-Code direkt von deiner Rippy-Instanz (/api/worker-setup/paket)
|
||||||
# 3. Laedt HandBrakeCLI (gepinnte Version, offizielles GitHub-Release),
|
# 3. Lädt HandBrakeCLI (gepinnte Version, offizielles GitHub-Release),
|
||||||
# falls nicht schon im PATH oder im Ordner vorhanden
|
# falls nicht schon im PATH oder im Ordner vorhanden
|
||||||
# 4. Erzeugt start-worker.bat - Doppelklick startet den Worker
|
# 4. Erzeugt start-worker.bat - Doppelklick startet den Worker
|
||||||
#
|
#
|
||||||
# Der Worker bedient NUR die Kompressions-Queue (transcode) - gerippt wird
|
# Der Worker bedient NUR die Kompressions-Queue (transcode) - gerippt wird
|
||||||
# weiterhin auf der Rippy-Hauptmaschine (dort haengt das Laufwerk).
|
# weiterhin auf der Rippy-Hauptmaschine (dort hängt das Laufwerk).
|
||||||
#
|
#
|
||||||
# Aufruf (PowerShell):
|
# Aufruf (PowerShell):
|
||||||
# .\install.ps1 -RippyHost 192.168.178.162 [-WorkerName mein-pc] [-Autostart]
|
# .\install.ps1 -RippyHost 192.168.178.162 [-WorkerName mein-pc] [-Autostart]
|
||||||
#
|
#
|
||||||
# -Autostart registriert eine Aufgabe "RippyWorker" (Start bei Anmeldung).
|
# -Autostart registriert eine Aufgabe "RippyWorker" (Start bei Anmeldung).
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)] [string]$RippyHost,
|
[Parameter(Mandatory = $true)] [string]$RippyHost,
|
||||||
[string]$WorkerName = $env:COMPUTERNAME.ToLower(),
|
[string]$WorkerName = $env:COMPUTERNAME.ToLower(),
|
||||||
[switch]$Autostart,
|
[switch]$Autostart,
|
||||||
# Zielverzeichnis. Standard ist "Programme", wie bei jedem anderen Programm
|
# Zielverzeichnis. Standard ist "Programme", wie bei jedem anderen Programm
|
||||||
# (Commander-Wunsch 25.07.2026). Dorthin schreiben braucht Adminrechte -
|
# (Commander-Wunsch 25.07.2026). Dorthin schreiben braucht Adminrechte -
|
||||||
# PowerShell also "Als Administrator" starten, oder hier einen Ordner im
|
# PowerShell also "Als Administrator" starten, oder hier einen Ordner im
|
||||||
# eigenen Profil angeben.
|
# eigenen Profil angeben.
|
||||||
[string]$InstallDir = (Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker")
|
[string]$InstallDir = (Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker")
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
# HandBrake-Version: die NEUESTE offizielle (GitHub-Release) - so passt der
|
# HandBrake-Version: die NEUESTE offizielle (GitHub-Release) - so passt der
|
||||||
# Windows-Worker zu dem, was der Update-Check im UI meldet (frueher war hier
|
# Windows-Worker zu dem, was der Update-Check im UI meldet (früher war hier
|
||||||
# eine feste 1.9.2 hardcodiert, was verwirrte). Faellt auf eine bekannte
|
# eine feste 1.9.2 hardcodiert, was verwirrte). Fällt auf eine bekannte
|
||||||
# Version zurueck, falls die GitHub-API gerade nicht erreichbar ist.
|
# Version zurück, falls die GitHub-API gerade nicht erreichbar ist.
|
||||||
$HandBrakeVersion = "1.11.2"
|
$HandBrakeVersion = "1.11.2"
|
||||||
try {
|
try {
|
||||||
$rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" `
|
$rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" `
|
||||||
-TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" }
|
-TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" }
|
||||||
if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") }
|
if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") }
|
||||||
Write-Host "Neueste HandBrake-Version: $HandBrakeVersion"
|
Write-Host "Neueste HandBrake-Version: $HandBrakeVersion"
|
||||||
} catch { Write-Host "GitHub-API nicht erreichbar - nutze HandBrake $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"
|
$HandBrakeUrl = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip"
|
||||||
|
|
||||||
Write-Host "== Rippy Windows-Worker Installation ==" -ForegroundColor Cyan
|
Write-Host "== Rippy Windows-Worker Installation ==" -ForegroundColor Cyan
|
||||||
|
|
||||||
# 1. Python finden (py-Launcher oder python im PATH)
|
# 1. Python finden (py-Launcher oder python im PATH)
|
||||||
$python = $null
|
$python = $null
|
||||||
foreach ($kandidat in @("py -3", "python")) {
|
foreach ($kandidat in @("py -3", "python")) {
|
||||||
try {
|
try {
|
||||||
$version = Invoke-Expression "$kandidat --version" 2>&1
|
$version = Invoke-Expression "$kandidat --version" 2>&1
|
||||||
if ("$version" -match "Python 3\.(\d+)") {
|
if ("$version" -match "Python 3\.(\d+)") {
|
||||||
if ([int]$Matches[1] -ge 10) { $python = $kandidat; break }
|
if ([int]$Matches[1] -ge 10) { $python = $kandidat; break }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
if (-not $python) {
|
if (-not $python) {
|
||||||
Write-Host "FEHLER: Python 3.10+ nicht gefunden." -ForegroundColor Red
|
Write-Host "FEHLER: Python 3.10+ nicht gefunden." -ForegroundColor Red
|
||||||
Write-Host "Installieren mit: winget install Python.Python.3.12 (dann Skript erneut ausfuehren)"
|
Write-Host "Installieren mit: winget install Python.Python.3.12 (dann Skript erneut ausführen)"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
Write-Host "Python gefunden: $python"
|
Write-Host "Python gefunden: $python"
|
||||||
|
|
||||||
# 2. Zielordner + Worker-Code von der Rippy-Instanz laden
|
# 2. Zielordner + Worker-Code von der Rippy-Instanz laden
|
||||||
$ziel = $InstallDir
|
$ziel = $InstallDir
|
||||||
try {
|
try {
|
||||||
New-Item -ItemType Directory -Force $ziel -ErrorAction Stop | Out-Null
|
New-Item -ItemType Directory -Force $ziel -ErrorAction Stop | Out-Null
|
||||||
$probe = Join-Path $ziel ".schreibtest"
|
$probe = Join-Path $ziel ".schreibtest"
|
||||||
Set-Content -Path $probe -Value "x" -ErrorAction Stop
|
Set-Content -Path $probe -Value "x" -ErrorAction Stop
|
||||||
Remove-Item -Force $probe -ErrorAction SilentlyContinue
|
Remove-Item -Force $probe -ErrorAction SilentlyContinue
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "FEHLER: In '$ziel' darf nicht geschrieben werden." -ForegroundColor Red
|
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 "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 " 1. PowerShell als Administrator starten und erneut aufrufen, ODER" -ForegroundColor Red
|
||||||
Write-Host " 2. ein eigenes Ziel angeben, z. B.:" -ForegroundColor Red
|
Write-Host " 2. ein eigenes Ziel angeben, z. B.:" -ForegroundColor Red
|
||||||
Write-Host " -InstallDir `"`$env:LOCALAPPDATA\Rippy Worker`"" -ForegroundColor Red
|
Write-Host " -InstallDir `"`$env:LOCALAPPDATA\Rippy Worker`"" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
Write-Host "Ziel: $ziel"
|
Write-Host "Ziel: $ziel"
|
||||||
Set-Location $ziel
|
Set-Location $ziel
|
||||||
|
|
||||||
Write-Host "Lade Worker-Code von http://$RippyHost/api/worker-setup/paket ..."
|
Write-Host "Lade Worker-Code von http://$RippyHost/api/worker-setup/paket ..."
|
||||||
Invoke-WebRequest "http://$RippyHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing
|
Invoke-WebRequest "http://$RippyHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing
|
||||||
Expand-Archive worker.zip -DestinationPath . -Force
|
Expand-Archive worker.zip -DestinationPath . -Force
|
||||||
Remove-Item worker.zip
|
Remove-Item worker.zip
|
||||||
|
|
||||||
# 3. venv + Abhaengigkeiten
|
# 3. venv + Abhängigkeiten
|
||||||
if (-not (Test-Path "venv")) {
|
if (-not (Test-Path "venv")) {
|
||||||
Write-Host "Lege Python-Umgebung an ..."
|
Write-Host "Lege Python-Umgebung an ..."
|
||||||
Invoke-Expression "$python -m venv venv"
|
Invoke-Expression "$python -m venv venv"
|
||||||
}
|
}
|
||||||
& .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip
|
& .\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 -r requirements.txt
|
||||||
# Nur fuer den Windows-Worker: Tray-Symbol (Status, Start/Stopp, Beenden)
|
# Nur für den Windows-Worker: Tray-Symbol (Status, Start/Stopp, Beenden)
|
||||||
& .\venv\Scripts\pip.exe install --quiet pystray pillow
|
& .\venv\Scripts\pip.exe install --quiet pystray pillow
|
||||||
Write-Host "Python-Abhaengigkeiten installiert."
|
Write-Host "Python-Abhängigkeiten installiert."
|
||||||
|
|
||||||
# 4. HandBrakeCLI besorgen (falls noetig)
|
# 4. HandBrakeCLI besorgen (falls nötig)
|
||||||
$hb = Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue
|
$hb = Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue
|
||||||
if (-not $hb -and -not (Test-Path ".\HandBrakeCLI.exe")) {
|
if (-not $hb -and -not (Test-Path ".\HandBrakeCLI.exe")) {
|
||||||
Write-Host "Lade HandBrakeCLI $HandBrakeVersion (offizielles GitHub-Release) ..."
|
Write-Host "Lade HandBrakeCLI $HandBrakeVersion (offizielles GitHub-Release) ..."
|
||||||
Invoke-WebRequest $HandBrakeUrl -OutFile hb.zip -UseBasicParsing
|
Invoke-WebRequest $HandBrakeUrl -OutFile hb.zip -UseBasicParsing
|
||||||
Expand-Archive hb.zip -DestinationPath . -Force
|
Expand-Archive hb.zip -DestinationPath . -Force
|
||||||
Remove-Item hb.zip
|
Remove-Item hb.zip
|
||||||
}
|
}
|
||||||
if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not $hb) {
|
if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not $hb) {
|
||||||
Write-Host "FEHLER: HandBrakeCLI.exe fehlt." -ForegroundColor Red
|
Write-Host "FEHLER: HandBrakeCLI.exe fehlt." -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
Write-Host "HandBrakeCLI bereit."
|
Write-Host "HandBrakeCLI bereit."
|
||||||
|
|
||||||
# 5. Start-Skript erzeugen
|
# 5. Start-Skript erzeugen
|
||||||
$bat = @"
|
$bat = @"
|
||||||
@echo off
|
@echo off
|
||||||
rem Rippy Windows-Transcode-Worker - verbindet sich mit $RippyHost
|
rem Rippy Windows-Transcode-Worker - verbindet sich mit $RippyHost
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
set REDIS_URL=redis://${RippyHost}:6379/0
|
set REDIS_URL=redis://${RippyHost}:6379/0
|
||||||
set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy
|
set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy
|
||||||
set API_URL=http://${RippyHost}:8000
|
set API_URL=http://${RippyHost}:8000
|
||||||
set WORKER_NAME=$WorkerName
|
set WORKER_NAME=$WorkerName
|
||||||
set PATH=%~dp0;%PATH%
|
set PATH=%~dp0;%PATH%
|
||||||
venv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode --pool=solo -n ${WorkerName}@%%h
|
venv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode --pool=solo -n ${WorkerName}@%%h
|
||||||
"@
|
"@
|
||||||
Set-Content -Path "start-worker.bat" -Value $bat -Encoding ASCII
|
Set-Content -Path "start-worker.bat" -Value $bat -Encoding ASCII
|
||||||
|
|
||||||
# 6. Tray-Start (empfohlen): Symbol neben der Uhr, kein Konsolenfenster
|
# 6. Tray-Start (empfohlen): Symbol neben der Uhr, kein Konsolenfenster
|
||||||
$trayBat = @"
|
$trayBat = @"
|
||||||
@echo off
|
@echo off
|
||||||
rem Rippy-Worker mit Tray-Symbol - verbindet sich mit $RippyHost
|
rem Rippy-Worker mit Tray-Symbol - verbindet sich mit $RippyHost
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
set REDIS_URL=redis://${RippyHost}:6379/0
|
set REDIS_URL=redis://${RippyHost}:6379/0
|
||||||
set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy
|
set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy
|
||||||
set API_URL=http://${RippyHost}:8000
|
set API_URL=http://${RippyHost}:8000
|
||||||
set WORKER_NAME=$WorkerName
|
set WORKER_NAME=$WorkerName
|
||||||
set RIPPY_TRAY_HOST=$RippyHost
|
set RIPPY_TRAY_HOST=$RippyHost
|
||||||
set PATH=%~dp0;%PATH%
|
set PATH=%~dp0;%PATH%
|
||||||
start "" venv\Scripts\pythonw.exe tray.py
|
start "" venv\Scripts\pythonw.exe tray.py
|
||||||
"@
|
"@
|
||||||
Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII
|
Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII
|
||||||
Write-Host "start-worker.bat + start-tray.bat erzeugt."
|
Write-Host "start-worker.bat + start-tray.bat erzeugt."
|
||||||
|
|
||||||
# 7. Deinstaller - MUSS-Kriterium: rueckstandsfrei entfernbar
|
# 7. Deinstaller - MUSS-Kriterium: rückstandsfrei entfernbar
|
||||||
$uninstall = @"
|
$uninstall = @"
|
||||||
# Rippy-Worker DEINSTALLIEREN: stoppt Prozesse, entfernt Autostart,
|
# Rippy-Worker DEINSTALLIEREN: stoppt Prozesse, entfernt Autostart,
|
||||||
# meldet den Worker in Rippy ab und loescht diesen Ordner komplett.
|
# meldet den Worker in Rippy ab und löscht diesen Ordner komplett.
|
||||||
param([switch]`$Force)
|
param([switch]`$Force)
|
||||||
if (-not `$Force) {
|
if (-not `$Force) {
|
||||||
`$antwort = Read-Host "Rippy-Worker '$WorkerName' wirklich deinstallieren? (j/n)"
|
`$antwort = Read-Host "Rippy-Worker '$WorkerName' wirklich deinstallieren? (j/n)"
|
||||||
if (`$antwort -ne "j") { Write-Host "Abgebrochen."; exit }
|
if (`$antwort -ne "j") { Write-Host "Abgebrochen."; exit }
|
||||||
}
|
}
|
||||||
Get-CimInstance Win32_Process | Where-Object {
|
Get-CimInstance Win32_Process | Where-Object {
|
||||||
`$_.ExecutablePath -like "`$PSScriptRoot*"
|
`$_.ExecutablePath -like "`$PSScriptRoot*"
|
||||||
} | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }
|
} | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||||
Start-Sleep 2
|
Start-Sleep 2
|
||||||
# Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die
|
# Autostart-Verknüpfung entfernen - BEIDE möglichen Orte, weil die
|
||||||
# Installation je nach Zielverzeichnis persoenlich oder maschinenweit war.
|
# Installation je nach Zielverzeichnis persönlich oder maschinenweit war.
|
||||||
foreach (`$ordner in @("Startup", "CommonStartup")) {
|
foreach (`$ordner in @("Startup", "CommonStartup")) {
|
||||||
try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {}
|
try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {}
|
||||||
}
|
}
|
||||||
# ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden.
|
# ... und die geplante Aufgabe aus älteren Installationen, falls vorhanden.
|
||||||
schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null
|
schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null
|
||||||
try {
|
try {
|
||||||
Invoke-RestMethod -Method Delete "http://$RippyHost/api/workers/$WorkerName" -TimeoutSec 5 | Out-Null
|
Invoke-RestMethod -Method Delete "http://$RippyHost/api/workers/$WorkerName" -TimeoutSec 5 | Out-Null
|
||||||
Write-Host "Worker-Eintrag in Rippy entfernt."
|
Write-Host "Worker-Eintrag in Rippy entfernt."
|
||||||
} catch { Write-Host "Hinweis: Eintrag in Rippy ggf. von Hand loeschen (Einstellungen -> Worker)." }
|
} catch { Write-Host "Hinweis: Eintrag in Rippy ggf. von Hand löschen (Einstellungen -> Worker)." }
|
||||||
Set-Location (Split-Path `$PSScriptRoot -Parent)
|
Set-Location (Split-Path `$PSScriptRoot -Parent)
|
||||||
Remove-Item -Recurse -Force `$PSScriptRoot
|
Remove-Item -Recurse -Force `$PSScriptRoot
|
||||||
Write-Host "Rippy-Worker deinstalliert." -ForegroundColor Green
|
Write-Host "Rippy-Worker deinstalliert." -ForegroundColor Green
|
||||||
"@
|
"@
|
||||||
Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII
|
Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII
|
||||||
Write-Host "uninstall.ps1 erzeugt."
|
Write-Host "uninstall.ps1 erzeugt."
|
||||||
|
|
||||||
# 8. Optional: Autostart bei Anmeldung (mit Tray)
|
# 8. Optional: Autostart bei Anmeldung (mit Tray)
|
||||||
#
|
#
|
||||||
# Ueber den Autostart-ORDNER, nicht ueber schtasks: Eine Aufgabe im
|
# Über den Autostart-ORDNER, nicht über schtasks: Eine Aufgabe im
|
||||||
# Wurzelordner der Aufgabenplanung anzulegen verlangt Administratorrechte, und
|
# Wurzelordner der Aufgabenplanung anzulegen verlangt Administratorrechte, und
|
||||||
# der Installer laeuft normal ohne. Auf dem Commander-PC scheiterte das am
|
# der Installer läuft normal ohne. Auf dem Commander-PC scheiterte das am
|
||||||
# 25.07.2026 mit "FEHLER: Zugriff verweigert." - und weil oben
|
# 25.07.2026 mit "FEHLER: Zugriff verweigert." - und weil oben
|
||||||
# $ErrorActionPreference = "Stop" steht, riss es die ganze Installation mit,
|
# $ErrorActionPreference = "Stop" steht, riss es die ganze Installation mit,
|
||||||
# obwohl nur der Autostart fehlte. Der Autostart-Ordner braucht nie Adminrechte.
|
# obwohl nur der Autostart fehlte. Der Autostart-Ordner braucht nie Adminrechte.
|
||||||
if ($Autostart) {
|
if ($Autostart) {
|
||||||
try {
|
try {
|
||||||
# Unter "Programme" = Installation fuer die Maschine, also Autostart fuer
|
# Unter "Programme" = Installation für die Maschine, also Autostart für
|
||||||
# alle Benutzer. Sonst persoenlich. Siehe install-gui.ps1 fuer die
|
# alle Benutzer. Sonst persönlich. Siehe install-gui.ps1 für die
|
||||||
# ausfuehrliche Begruendung (Rechtelage bei fremdem Admin-Konto).
|
# ausführliche Begründung (Rechtelage bei fremdem Admin-Konto).
|
||||||
$programme = [Environment]::GetFolderPath("ProgramFiles")
|
$programme = [Environment]::GetFolderPath("ProgramFiles")
|
||||||
$fuerAlle = $ziel.StartsWith($programme, "OrdinalIgnoreCase")
|
$maschinenweit = $ziel.StartsWith($programme, "OrdinalIgnoreCase")
|
||||||
$autostartOrdner = if ($fuerAlle) {
|
$autostartDir = if ($maschinenweit) {
|
||||||
[Environment]::GetFolderPath("CommonStartup")
|
[Environment]::GetFolderPath("CommonStartup")
|
||||||
} else {
|
} else {
|
||||||
[Environment]::GetFolderPath("Startup")
|
[Environment]::GetFolderPath("Startup")
|
||||||
}
|
}
|
||||||
$wsh = New-Object -ComObject WScript.Shell
|
$wsh = New-Object -ComObject WScript.Shell
|
||||||
$lnk = $wsh.CreateShortcut((Join-Path $autostartOrdner "RippyWorker.lnk"))
|
$lnk = $wsh.CreateShortcut((Join-Path $autostartDir "RippyWorker.lnk"))
|
||||||
$lnk.TargetPath = (Join-Path $ziel "start-tray.bat")
|
$lnk.TargetPath = (Join-Path $ziel "start-tray.bat")
|
||||||
$lnk.WorkingDirectory = $ziel
|
$lnk.WorkingDirectory = $ziel
|
||||||
$lnk.Description = "Rippy Encoding-Worker"
|
$lnk.Description = "Rippy Encoding-Worker"
|
||||||
$lnk.Save()
|
$lnk.Save()
|
||||||
Write-Host "Autostart eingerichtet (Verknuepfung im Autostart-Ordner)."
|
Write-Host "Autostart eingerichtet (Verknüpfung im Autostart-Ordner)."
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." -ForegroundColor Yellow
|
Write-Host "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." -ForegroundColor Yellow
|
||||||
Write-Host " Der Worker ist trotzdem fertig installiert." -ForegroundColor Yellow
|
Write-Host " Der Worker ist trotzdem fertig installiert." -ForegroundColor Yellow
|
||||||
Write-Host " Von Hand: start-tray.bat in shell:startup verknuepfen." -ForegroundColor Yellow
|
Write-Host " Von Hand: start-tray.bat in shell:startup verknüpfen." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "FERTIG." -ForegroundColor Green
|
Write-Host "FERTIG." -ForegroundColor Green
|
||||||
Write-Host " Starten (mit Tray-Symbol): $ziel\start-tray.bat"
|
Write-Host " Starten (mit Tray-Symbol): $ziel\start-tray.bat"
|
||||||
Write-Host " Starten (Konsole/Debug): $ziel\start-worker.bat"
|
Write-Host " Starten (Konsole/Debug): $ziel\start-worker.bat"
|
||||||
Write-Host " Deinstallieren: $ziel\uninstall.ps1"
|
Write-Host " Deinstallieren: $ziel\uninstall.ps1"
|
||||||
Write-Host "Der Worker erscheint in Rippy unter Einstellungen -> Worker als '$WorkerName'."
|
Write-Host "Der Worker erscheint in Rippy unter Einstellungen -> Worker als '$WorkerName'."
|
||||||
|
|||||||
+4
-4
@@ -58,9 +58,9 @@ async def startup_event():
|
|||||||
asyncio.create_task(asyncio.to_thread(remount))
|
asyncio.create_task(asyncio.to_thread(remount))
|
||||||
|
|
||||||
asyncio.create_task(disc_watcher())
|
asyncio.create_task(disc_watcher())
|
||||||
# MakeMKV-Beta-Key automatisch aktuell halten (wechselt ~monatlich, laeuft zum
|
# MakeMKV-Beta-Key automatisch aktuell halten (wechselt ~monatlich, läuft zum
|
||||||
# Monatsende ab) — sonst blockt Blu-ray-Ripping irgendwann still. Taeglicher
|
# Monatsende ab) — sonst blockt Blu-ray-Ripping irgendwann still. Taeglicher
|
||||||
# Forum-Abgleich; wirkt ohne Rebuild ab dem naechsten Rip.
|
# Forum-Abgleich; wirkt ohne Rebuild ab dem nächsten Rip.
|
||||||
asyncio.create_task(makemkv_key.refresh_loop())
|
asyncio.create_task(makemkv_key.refresh_loop())
|
||||||
# Worker-Erreichbarkeit im Hintergrund pingen (siehe _ping_knoten) — sonst
|
# Worker-Erreichbarkeit im Hintergrund pingen (siehe _ping_knoten) — sonst
|
||||||
# kostet JEDER Aufruf von /capabilities eine ganze Sekunde.
|
# kostet JEDER Aufruf von /capabilities eine ganze Sekunde.
|
||||||
@@ -467,7 +467,7 @@ def unter_wurzel(pfad: str, wurzel: str) -> bool:
|
|||||||
"""Liegt `pfad` wirklich unterhalb von `wurzel` (oder IST es die Wurzel)?
|
"""Liegt `pfad` wirklich unterhalb von `wurzel` (oder IST es die Wurzel)?
|
||||||
|
|
||||||
Pure Funktion, testbar. Ein nacktes `startswith()` genügt hier nicht:
|
Pure Funktion, testbar. Ein nacktes `startswith()` genügt hier nicht:
|
||||||
„/app/media-boese/x" beginnt mit „/app/media", liegt aber ausserhalb
|
„/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb
|
||||||
(Befund 25.07.2026 bei der Durchsicht). Deshalb Gleichheit ODER Wurzel
|
(Befund 25.07.2026 bei der Durchsicht). Deshalb Gleichheit ODER Wurzel
|
||||||
samt Trennzeichen. Erwartet werden normalisierte Container-Pfade mit „/".
|
samt Trennzeichen. Erwartet werden normalisierte Container-Pfade mit „/".
|
||||||
"""
|
"""
|
||||||
@@ -1383,7 +1383,7 @@ async def set_keystore(request: Request):
|
|||||||
rohdaten = await request.body()
|
rohdaten = await request.body()
|
||||||
|
|
||||||
def pruefe_und_schreibe():
|
def pruefe_und_schreibe():
|
||||||
# Prüfung liest ein mehrere MB grosses tar — gehört deshalb mit in
|
# Prüfung liest ein mehrere MB großes tar — gehört deshalb mit in
|
||||||
# den Thread und nicht in die Ereignisschleife.
|
# den Thread und nicht in die Ereignisschleife.
|
||||||
fehler = makemkv_daten.private_data_pruefen(rohdaten)
|
fehler = makemkv_daten.private_data_pruefen(rohdaten)
|
||||||
if fehler:
|
if fehler:
|
||||||
|
|||||||
+44
-44
@@ -1,8 +1,8 @@
|
|||||||
"""MakeMKV-Datenverzeichnis: Schluesselspeicher, KEYDB.cfg, AACS-Dumps.
|
"""MakeMKV-Datenverzeichnis: Schlüsselspeicher, KEYDB.cfg, AACS-Dumps.
|
||||||
|
|
||||||
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, auf BEIDEN Maschinen gemessen):
|
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, auf BEIDEN Maschinen gemessen):
|
||||||
4K-UHD-Discs scheiterten mit "The volume key is unknown for this disc". Die
|
4K-UHD-Discs scheiterten mit "The volume key is unknown for this disc". Die
|
||||||
Ursache ist weder die Disc noch das Laufwerk — LibreDrive v06.3 laeuft und
|
Ursache ist weder die Disc noch das Laufwerk — LibreDrive v06.3 läuft und
|
||||||
MakeMKV liest die Disc — sondern:
|
MakeMKV liest die Disc — sondern:
|
||||||
|
|
||||||
makemkvcon unter LINUX ruft die Disc-Schluessel nie ab.
|
makemkvcon unter LINUX ruft die Disc-Schluessel nie ab.
|
||||||
@@ -10,35 +10,35 @@ MakeMKV liest die Disc — sondern:
|
|||||||
|
|
||||||
Gemessen, nicht vermutet:
|
Gemessen, nicht vermutet:
|
||||||
* Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit
|
* Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit
|
||||||
leerem UND mit gefuelltem Schluesselspeicher, mit und ohne --noscan, mit
|
leerem UND mit gefülltem Schlüsselspeicher, mit und ohne --noscan, mit
|
||||||
dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Pruefung
|
dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Prüfung
|
||||||
(update.conf geloescht, Meldung 5074 belegt den Web-Kontakt). Immer:
|
(update.conf gelöscht, Meldung 5074 belegt den Web-Kontakt). Immer:
|
||||||
keine Verbindung, kein Schluessel.
|
keine Verbindung, kein Schluessel.
|
||||||
* Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading
|
* Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading
|
||||||
latest HK to ...", Verbindung nach 185.84.108.20:443
|
latest HK to ...", Verbindung nach 185.84.108.20:443
|
||||||
(web33.majordomo.ru), _private_data.tar waechst — und die Disc geht auf
|
(web33.majordomo.ru), _private_data.tar wächst — und die Disc geht auf
|
||||||
(TCOUNT:5, "Operation successfully completed").
|
(TCOUNT:5, "Operation successfully completed").
|
||||||
* Der Code dafuer steckt auch im Linux-Binary: die Meldungsvorlage
|
* Der Code dafür steckt auch im Linux-Binary: die Meldungsvorlage
|
||||||
"Downloading latest %1 to %2 ..." steht in makemkvcon. Sie loest nur nie
|
"Downloading latest %1 to %2 ..." steht in makemkvcon. Sie löst nur nie
|
||||||
aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet.
|
aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet.
|
||||||
|
|
||||||
FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst,
|
FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst,
|
||||||
MakeMKVs Schluessel-Kanal sei abgeschaltet. Das war FALSCH. Die Herleitung
|
MakeMKVs Schluessel-Kanal sei abgeschaltet. Das war FALSCH. Die Herleitung
|
||||||
stuetzte sich auf zwei Hostnamen aus alten Forumsbeitraegen
|
stützte sich auf zwei Hostnamen aus alten Forumsbeitraegen
|
||||||
(hkdata.fairuse.org, hkdata.crabdance.com), die tatsaechlich nicht mehr
|
(hkdata.fairuse.org, hkdata.crabdance.com), die tatsaechlich nicht mehr
|
||||||
aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der
|
aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der
|
||||||
Worker erreicht ihn sogar; er wird unter Linux nur nie gefragt.
|
Worker erreicht ihn sogar; er wird unter Linux nur nie gefragt.
|
||||||
|
|
||||||
WAS DARAUS FOLGT: Der Schluesselspeicher (_private_data.tar) muss von einer
|
WAS DARAUS FOLGT: Der Schlüsselspeicher (_private_data.tar) muss von einer
|
||||||
MakeMKV-Installation kommen, die ihn wirklich abruft — praktisch von Windows.
|
MakeMKV-Installation kommen, die ihn wirklich abruft — praktisch von Windows.
|
||||||
Rippy nimmt ihn unter Einstellungen -> System entgegen. Die KEYDB.cfg bleibt
|
Rippy nimmt ihn unter Einstellungen -> System entgegen. Die KEYDB.cfg bleibt
|
||||||
der Notnagel fuer Pressungen, die auch MakeMKV selbst nicht kennt.
|
der Notnagel für Pressungen, die auch MakeMKV selbst nicht kennt.
|
||||||
|
|
||||||
Rippy liefert KEINE Schluessel mit, laedt keine herunter und verteilt keine.
|
Rippy liefert KEINE Schluessel mit, lädt keine herunter und verteilt keine.
|
||||||
Es verwaltet nur, was der Nutzer selbst mitbringt.
|
Es verwaltet nur, was der Nutzer selbst mitbringt.
|
||||||
|
|
||||||
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
||||||
* Linux laedt keine Hashed Keys — dasselbe Symptom, mehrfach berichtet:
|
* Linux lädt keine Hashed Keys — dasselbe Symptom, mehrfach berichtet:
|
||||||
https://forum.makemkv.com/forum/viewtopic.php?t=25782
|
https://forum.makemkv.com/forum/viewtopic.php?t=25782
|
||||||
https://forum.makemkv.com/forum/viewtopic.php?t=34022
|
https://forum.makemkv.com/forum/viewtopic.php?t=34022
|
||||||
* Schluessel liegen als hkd_*.bin in _private_data.tar:
|
* Schluessel liegen als hkd_*.bin in _private_data.tar:
|
||||||
@@ -55,7 +55,7 @@ QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
|||||||
|
|
||||||
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
|
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
|
||||||
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
|
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
|
||||||
(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen.
|
(gleiche Lage wie bei db.py). Änderungen IMMER in BEIDEN Dateien nachziehen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@@ -72,14 +72,14 @@ DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")
|
|||||||
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
||||||
KEYDB_NAME = "KEYDB.cfg"
|
KEYDB_NAME = "KEYDB.cfg"
|
||||||
|
|
||||||
# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt
|
# Obergrenze für den Upload. Eine vollständige oeffentliche KEYDB.cfg liegt
|
||||||
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
|
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
|
||||||
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
|
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
|
||||||
MAX_KEYDB_BYTES = 64 * 1024 * 1024
|
MAX_KEYDB_BYTES = 64 * 1024 * 1024
|
||||||
|
|
||||||
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
|
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
|
||||||
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
|
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
|
||||||
# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag.
|
# ";", Leerzeilen, Fortsetzungsfelder) zählt nicht als Eintrag.
|
||||||
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
|
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
|
||||||
|
|
||||||
|
|
||||||
@@ -94,9 +94,9 @@ def keydb_pfad(daten_dir: str = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def zaehle_disc_eintraege(inhalt: str) -> int:
|
def zaehle_disc_eintraege(inhalt: str) -> int:
|
||||||
"""Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar).
|
"""Zeilen mit Disc-Kennung zählen (pure Funktion, testbar).
|
||||||
|
|
||||||
Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest
|
Bewusst eine Heuristik und keine vollständige Auswertung: MakeMKV liest
|
||||||
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
|
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
|
||||||
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
|
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
|
||||||
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
|
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
|
||||||
@@ -106,17 +106,17 @@ def zaehle_disc_eintraege(inhalt: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def keydb_pruefen(inhalt: str) -> str:
|
def keydb_pruefen(inhalt: str) -> str:
|
||||||
"""Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck.
|
"""Prüft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurück.
|
||||||
|
|
||||||
Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die
|
Verhindert den häufigsten Bedienfehler: statt der KEYDB.cfg landet die
|
||||||
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
|
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
|
||||||
MakeMKV wuerde dann still weiter "volume key is unknown" melden.
|
MakeMKV würde dann still weiter "volume key is unknown" melden.
|
||||||
"""
|
"""
|
||||||
if not inhalt.strip():
|
if not inhalt.strip():
|
||||||
return "Die Datei ist leer."
|
return "Die Datei ist leer."
|
||||||
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
|
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
|
||||||
return (
|
return (
|
||||||
"Die Datei ist groesser als "
|
"Die Datei ist größer als "
|
||||||
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
|
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
|
||||||
)
|
)
|
||||||
if inhalt.lstrip()[:1] == "<":
|
if inhalt.lstrip()[:1] == "<":
|
||||||
@@ -150,7 +150,7 @@ def keydb_status(daten_dir: str = None) -> dict:
|
|||||||
with open(pfad, encoding="utf-8", errors="replace") as datei:
|
with open(pfad, encoding="utf-8", errors="replace") as datei:
|
||||||
eintraege = zaehle_disc_eintraege(datei.read())
|
eintraege = zaehle_disc_eintraege(datei.read())
|
||||||
except OSError:
|
except OSError:
|
||||||
pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem
|
pass # Datei da, aber unlesbar: Größe/Datum stimmen trotzdem
|
||||||
return {
|
return {
|
||||||
"vorhanden": True,
|
"vorhanden": True,
|
||||||
"pfad": pfad,
|
"pfad": pfad,
|
||||||
@@ -163,7 +163,7 @@ def keydb_status(daten_dir: str = None) -> dict:
|
|||||||
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
|
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
|
||||||
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
|
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
|
||||||
|
|
||||||
Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf
|
Erst in eine Nebendatei, dann os.replace: während ein Rip läuft, darf
|
||||||
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
|
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
|
||||||
"""
|
"""
|
||||||
pfad = keydb_pfad(daten_dir)
|
pfad = keydb_pfad(daten_dir)
|
||||||
@@ -244,32 +244,32 @@ def dumps_auflisten(daten_dir: str = None) -> list:
|
|||||||
return liste
|
return liste
|
||||||
|
|
||||||
|
|
||||||
# --- Schluesselspeicher (_private_data.tar) -------------------------------
|
# --- Schlüsselspeicher (_private_data.tar) -------------------------------
|
||||||
#
|
#
|
||||||
# Das ist MakeMKVs eigener Speicher fuer die "Hashed Keys": ein tar-Archiv mit
|
# Das ist MakeMKVs eigener Speicher für die "Hashed Keys": ein tar-Archiv mit
|
||||||
# hkd_*.bin-Eintraegen. Unter Windows fuellt MakeMKV es selbst (Meldung 3338),
|
# hkd_*.bin-Eintraegen. Unter Windows füllt MakeMKV es selbst (Meldung 3338),
|
||||||
# unter Linux nie — siehe Modul-Kopf. Rippy nimmt die Datei deshalb entgegen
|
# unter Linux nie — siehe Modul-Kopf. Rippy nimmt die Datei deshalb entgegen
|
||||||
# und legt sie ins Datenverzeichnis; MakeMKV liest sie beim naechsten Start.
|
# und legt sie ins Datenverzeichnis; MakeMKV liest sie beim nächsten Start.
|
||||||
|
|
||||||
PRIVATE_DATA_NAME = "_private_data.tar"
|
PRIVATE_DATA_NAME = "_private_data.tar"
|
||||||
|
|
||||||
# Der Speicher lag am 25.07.2026 bei rund 6 MB und waechst mit jeder neuen
|
# Der Speicher lag am 25.07.2026 bei rund 6 MB und wächst mit jeder neuen
|
||||||
# Pressung. 64 MB sind reichlich Luft — und derselbe Wert wie
|
# Pressung. 64 MB sind reichlich Luft — und derselbe Wert wie
|
||||||
# client_max_body_size in docker/ui/nginx.conf: waere die Grenze hier hoeher,
|
# client_max_body_size in docker/ui/nginx.conf: wäre die Grenze hier höher,
|
||||||
# wuerde nginx den Upload abweisen, bevor die API ihn ueberhaupt sieht.
|
# würde nginx den Upload abweisen, bevor die API ihn überhaupt sieht.
|
||||||
MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024
|
MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
def private_data_pfad(daten_dir: str = None) -> str:
|
def private_data_pfad(daten_dir: str = None) -> str:
|
||||||
"""Voller Pfad zum Schluesselspeicher im Datenverzeichnis."""
|
"""Voller Pfad zum Schlüsselspeicher im Datenverzeichnis."""
|
||||||
return os.path.join(daten_dir or DATEN_DIR, PRIVATE_DATA_NAME)
|
return os.path.join(daten_dir or DATEN_DIR, PRIVATE_DATA_NAME)
|
||||||
|
|
||||||
|
|
||||||
def zaehle_schluessel(rohdaten: bytes) -> int:
|
def zaehle_schluessel(rohdaten: bytes) -> int:
|
||||||
"""Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar).
|
"""Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar).
|
||||||
|
|
||||||
Das ist die ehrliche Kennzahl fuer "wie viele Disc-Schluessel kennt diese
|
Das ist die ehrliche Kennzahl für "wie viele Disc-Schluessel kennt diese
|
||||||
Installation". Ein frischer, leerer Speicher enthaelt nur eine
|
Installation". Ein frischer, leerer Speicher enthält nur eine
|
||||||
Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede
|
Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede
|
||||||
unbekannte UHD-Disc scheitert.
|
unbekannte UHD-Disc scheitert.
|
||||||
"""
|
"""
|
||||||
@@ -281,20 +281,20 @@ def zaehle_schluessel(rohdaten: bytes) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def private_data_pruefen(rohdaten: bytes) -> str:
|
def private_data_pruefen(rohdaten: bytes) -> str:
|
||||||
"""Prueft hochgeladene Rohdaten; deutscher Fehlertext oder "".
|
"""Prüft hochgeladene Rohdaten; deutscher Fehlertext oder "".
|
||||||
|
|
||||||
Faengt die beiden Bedienfehler ab, die sonst still danebengehen: eine
|
Faengt die beiden Bedienfehler ab, die sonst still danebengehen: eine
|
||||||
voellig andere Datei hochladen, oder den Speicher einer Installation, die
|
voellig andere Datei hochladen, oder den Speicher einer Installation, die
|
||||||
selbst noch keine Schluessel geholt hat (dann aendert sich nichts, und
|
selbst noch keine Schluessel geholt hat (dann ändert sich nichts, und
|
||||||
niemand versteht warum).
|
niemand versteht warum).
|
||||||
"""
|
"""
|
||||||
if not rohdaten:
|
if not rohdaten:
|
||||||
return "Die Datei ist leer."
|
return "Die Datei ist leer."
|
||||||
if len(rohdaten) > MAX_PRIVATE_DATA_BYTES:
|
if len(rohdaten) > MAX_PRIVATE_DATA_BYTES:
|
||||||
return (
|
return (
|
||||||
"Die Datei ist groesser als "
|
"Die Datei ist größer als "
|
||||||
f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein "
|
f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein "
|
||||||
"MakeMKV-Schluesselspeicher."
|
"MakeMKV-Schlüsselspeicher."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv:
|
with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv:
|
||||||
@@ -308,13 +308,13 @@ def private_data_pruefen(rohdaten: bytes) -> str:
|
|||||||
return (
|
return (
|
||||||
"In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). "
|
"In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). "
|
||||||
"Sie stammt vermutlich von einer MakeMKV-Installation, die selbst "
|
"Sie stammt vermutlich von einer MakeMKV-Installation, die selbst "
|
||||||
"noch keine geholt hat — oeffne dort erst einmal eine Disc."
|
"noch keine geholt hat — öffne dort erst einmal eine Disc."
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def schluesselspeicher_status(daten_dir: str = None) -> dict:
|
def schluesselspeicher_status(daten_dir: str = None) -> dict:
|
||||||
"""Zustand des Schluesselspeichers. Fehlt er, ist das kein Fehler."""
|
"""Zustand des Schlüsselspeichers. Fehlt er, ist das kein Fehler."""
|
||||||
pfad = private_data_pfad(daten_dir)
|
pfad = private_data_pfad(daten_dir)
|
||||||
try:
|
try:
|
||||||
angaben = os.stat(pfad)
|
angaben = os.stat(pfad)
|
||||||
@@ -338,9 +338,9 @@ def schluesselspeicher_status(daten_dir: str = None) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def private_data_schreiben(rohdaten: bytes, daten_dir: str = None) -> dict:
|
def private_data_schreiben(rohdaten: bytes, daten_dir: str = None) -> dict:
|
||||||
"""Legt den Schluesselspeicher atomar ab und meldet den neuen Zustand.
|
"""Legt den Schlüsselspeicher atomar ab und meldet den neuen Zustand.
|
||||||
|
|
||||||
Atomar aus demselben Grund wie bei der KEYDB.cfg: waehrend ein Rip laeuft,
|
Atomar aus demselben Grund wie bei der KEYDB.cfg: während ein Rip läuft,
|
||||||
darf makemkvcon nie ein halb geschriebenes Archiv sehen.
|
darf makemkvcon nie ein halb geschriebenes Archiv sehen.
|
||||||
"""
|
"""
|
||||||
pfad = private_data_pfad(daten_dir)
|
pfad = private_data_pfad(daten_dir)
|
||||||
@@ -363,9 +363,9 @@ def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str:
|
|||||||
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
|
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
|
||||||
|
|
||||||
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
|
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
|
||||||
ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit
|
überschrieben. Mit dem jetzt persistenten Datenverzeichnis wäre damit
|
||||||
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
|
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
|
||||||
app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen,
|
app_UpdateEnable. Leerer Key lässt die vorhandene Zeile ebenfalls fallen,
|
||||||
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
|
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
|
||||||
"""
|
"""
|
||||||
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
|
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
"""Automatische Erneuerung des MakeMKV-Beta-Keys.
|
"""Automatische Erneuerung des MakeMKV-Beta-Keys.
|
||||||
|
|
||||||
Der kostenlose MakeMKV-Beta-Key (Forum-Thread "MakeMKV is free while in beta",
|
Der kostenlose MakeMKV-Beta-Key (Forum-Thread "MakeMKV is free while in beta",
|
||||||
t=1053) wechselt etwa monatlich und laeuft zum Monatsende ab. Ohne gueltigen Key
|
t=1053) wechselt etwa monatlich und läuft zum Monatsende ab. Ohne gültigen Key
|
||||||
faellt Blu-ray/UHD-Ripping in den 30-Tage-Testmodus; DVD-Ripping bleibt frei.
|
fällt Blu-ray/UHD-Ripping in den 30-Tage-Testmodus; DVD-Ripping bleibt frei.
|
||||||
Bisher musste der Key von Hand nachgetragen werden (UI: Einstellungen -> System).
|
Bisher musste der Key von Hand nachgetragen werden (UI: Einstellungen -> System).
|
||||||
|
|
||||||
Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings
|
Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings
|
||||||
(makemkvAppKey). Der Worker liest den Key VOR jedem Rip aus den Settings
|
(makemkvAppKey). Der Worker liest den Key VOR jedem Rip aus den Settings
|
||||||
(docker/worker/tasks.py) und schreibt settings.conf -> die Erneuerung wirkt ohne
|
(docker/worker/tasks.py) und schreibt settings.conf -> die Erneuerung wirkt ohne
|
||||||
Rebuild/Neustart, ab dem naechsten Rip.
|
Rebuild/Neustart, ab dem nächsten Rip.
|
||||||
|
|
||||||
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
|
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
|
||||||
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
|
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
|
||||||
unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine.
|
unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine.
|
||||||
|
|
||||||
Richtigstellung 25.07.2026: Hier stand frueher, MakeMKV ziehe den AACS-Schluessel
|
Richtigstellung 25.07.2026: Hier stand früher, MakeMKV ziehe den AACS-Schluessel
|
||||||
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt fuer Blu-ray, aber
|
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt für Blu-ray, aber
|
||||||
NICHT fuer 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
|
NICHT für 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
|
||||||
und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und
|
und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und
|
||||||
Messungen stehen im Modul-Kopf von makemkv_daten.py.
|
Messungen stehen im Modul-Kopf von makemkv_daten.py.
|
||||||
|
|
||||||
@@ -41,14 +41,14 @@ def extract_key(html: str) -> str | None:
|
|||||||
|
|
||||||
def fetch_current_key(timeout: int = 15) -> str | None:
|
def fetch_current_key(timeout: int = 15) -> str | None:
|
||||||
"""Holt den aktuellen Beta-Key vom MakeMKV-Forum. None bei Fehler/kein Treffer."""
|
"""Holt den aktuellen Beta-Key vom MakeMKV-Forum. None bei Fehler/kein Treffer."""
|
||||||
import requests # lazy: haelt extract_key + den Test dependency-frei
|
import requests # lazy: hält extract_key + den Test dependency-frei
|
||||||
antwort = requests.get(FORUM_URL, headers=_HEADERS, timeout=timeout)
|
antwort = requests.get(FORUM_URL, headers=_HEADERS, timeout=timeout)
|
||||||
antwort.raise_for_status()
|
antwort.raise_for_status()
|
||||||
return extract_key(antwort.text)
|
return extract_key(antwort.text)
|
||||||
|
|
||||||
|
|
||||||
def _apply_key(key: str) -> bool:
|
def _apply_key(key: str) -> bool:
|
||||||
"""Merge den Key in die Settings. save_settings ueberschreibt das GANZE JSON,
|
"""Merge den Key in die Settings. save_settings überschreibt das GANZE JSON,
|
||||||
darum erst lesen, dann setzen. Rueckgabe True, wenn sich der Key geaendert hat."""
|
darum erst lesen, dann setzen. Rueckgabe True, wenn sich der Key geaendert hat."""
|
||||||
import db
|
import db
|
||||||
einstellungen = db.get_settings()
|
einstellungen = db.get_settings()
|
||||||
@@ -76,7 +76,7 @@ async def refresh_once() -> bool:
|
|||||||
|
|
||||||
async def refresh_loop(intervall_stunden: int = 24, start_verzoegerung_s: int = 60) -> None:
|
async def refresh_loop(intervall_stunden: int = 24, start_verzoegerung_s: int = 60) -> None:
|
||||||
"""Periodische Erneuerung (Default: taeglich, damit ein Monatswechsel nie verpasst
|
"""Periodische Erneuerung (Default: taeglich, damit ein Monatswechsel nie verpasst
|
||||||
wird). Fehler werden geloggt, aber nie geworfen -> die API laeuft weiter."""
|
wird). Fehler werden geloggt, aber nie geworfen -> die API läuft weiter."""
|
||||||
import db
|
import db
|
||||||
await asyncio.sleep(start_verzoegerung_s)
|
await asyncio.sleep(start_verzoegerung_s)
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ def schreibtest(pfad: str) -> bool:
|
|||||||
def _stale_mounts_loesen(ziel: str) -> int:
|
def _stale_mounts_loesen(ziel: str) -> int:
|
||||||
"""Loest ALLE (evtl. gestapelten/toten) Mounts an `ziel` per lazy umount.
|
"""Loest ALLE (evtl. gestapelten/toten) Mounts an `ziel` per lazy umount.
|
||||||
|
|
||||||
Warum: mounten() faellt bei einem TOTEN Mount (os.path.ismount wirft OSError)
|
Warum: mounten() fällt bei einem TOTEN Mount (os.path.ismount wirft OSError)
|
||||||
auf den echten `mount` durch — der stapelt dann auf die Leiche. Vorfall 24.07.:
|
auf den echten `mount` durch — der stapelt dann auf die Leiche. Vorfall 24.07.:
|
||||||
ueber viele Neustarts 12 Schichten, die tote oberste blockierte jeden Zugriff
|
über viele Neustarts 12 Schichten, die tote oberste blockierte jeden Zugriff
|
||||||
(ls-Timeout, obwohl SMB-445 offen). Erst alle Schichten loesen macht das
|
(ls-Timeout, obwohl SMB-445 offen). Erst alle Schichten loesen macht das
|
||||||
Re-Mounten idempotent. `umount -l` (lazy) haengt nicht an einem toten CIFS.
|
Re-Mounten idempotent. `umount -l` (lazy) hängt nicht an einem toten CIFS.
|
||||||
Rueckgabe: Zahl der geloesten Schichten.
|
Rueckgabe: Zahl der geloesten Schichten.
|
||||||
"""
|
"""
|
||||||
geloest = 0
|
geloest = 0
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ def test_main_importierbar_und_routen_verdrahtet():
|
|||||||
# Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht
|
# Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht
|
||||||
# mehr online nach). Ohne diese Routen ist die Seite im UI tot.
|
# mehr online nach). Ohne diese Routen ist die Seite im UI tot.
|
||||||
"/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}",
|
"/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}",
|
||||||
# Der Hauptweg fuer 4K-UHD (25.07.2026): makemkvcon holt Disc-Schluessel
|
# Der Hauptweg für 4K-UHD (25.07.2026): makemkvcon holt Disc-Schluessel
|
||||||
# unter Linux nie selbst, sie kommen von Hand ueber diesen Endpunkt.
|
# unter Linux nie selbst, sie kommen von Hand über diesen Endpunkt.
|
||||||
"/system/keystore",
|
"/system/keystore",
|
||||||
):
|
):
|
||||||
assert pfad in routen, f"Route {pfad} fehlt"
|
assert pfad in routen, f"Route {pfad} fehlt"
|
||||||
@@ -69,10 +69,10 @@ def test_remount_blockiert_den_api_start_nicht():
|
|||||||
|
|
||||||
|
|
||||||
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
||||||
"""Befund 25.07.2026: In main.py prueften neun Stellen mit nacktem
|
"""Befund 25.07.2026: In main.py prüften neun Stellen mit nacktem
|
||||||
startswith(MEDIA_ROOT) — darunter /browse und /browse/mkdir, wo der Pfad
|
startswith(MEDIA_ROOT) — darunter /browse und /browse/mkdir, wo der Pfad
|
||||||
vom Nutzer kommt. „/app/media-boese/x" beginnt mit „/app/media", liegt
|
vom Nutzer kommt. „/app/media-boese/x" beginnt mit „/app/media", liegt
|
||||||
aber ausserhalb. Zwilling von tasks.unter_wurzel im Worker."""
|
aber außerhalb. Zwilling von tasks.unter_wurzel im Worker."""
|
||||||
from main import unter_wurzel
|
from main import unter_wurzel
|
||||||
|
|
||||||
assert unter_wurzel("/app/media", "/app/media") is True
|
assert unter_wurzel("/app/media", "/app/media") is True
|
||||||
@@ -87,9 +87,9 @@ def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
|||||||
|
|
||||||
def test_ping_vorrat_verhindert_die_wartesekunde(monkeypatch):
|
def test_ping_vorrat_verhindert_die_wartesekunde(monkeypatch):
|
||||||
"""Gemessen 25.07.2026: /capabilities brauchte 1,010 s - jedes Mal. Der
|
"""Gemessen 25.07.2026: /capabilities brauchte 1,010 s - jedes Mal. Der
|
||||||
Celery-Ping sammelt Antworten bis zum Timeout und kann nicht frueher
|
Celery-Ping sammelt Antworten bis zum Timeout und kann nicht früher
|
||||||
aufhoeren. Fuenf UI-Stellen holen /capabilities, also zahlte jede Seite
|
aufhoeren. Fuenf UI-Stellen holen /capabilities, also zahlte jede Seite
|
||||||
eine Sekunde, waehrend alle anderen Endpunkte unter 25 ms lagen.
|
eine Sekunde, während alle anderen Endpunkte unter 25 ms lagen.
|
||||||
|
|
||||||
Der Vorrat muss deshalb abgelesen und NICHT neu gepingt werden, solange er
|
Der Vorrat muss deshalb abgelesen und NICHT neu gepingt werden, solange er
|
||||||
frisch ist - und bei altem Vorrat lieber einmal langsam als falsch.
|
frisch ist - und bei altem Vorrat lieber einmal langsam als falsch.
|
||||||
@@ -119,7 +119,7 @@ def test_ping_vorrat_verhindert_die_wartesekunde(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_ping_takt_ist_kuerzer_als_die_haltbarkeit():
|
def test_ping_takt_ist_kuerzer_als_die_haltbarkeit():
|
||||||
"""Sonst laeuft der Vorrat zwischen zwei Hintergrund-Laeufen ab und der
|
"""Sonst läuft der Vorrat zwischen zwei Hintergrund-Laeufen ab und der
|
||||||
Endpunkt pingt doch wieder synchron."""
|
Endpunkt pingt doch wieder synchron."""
|
||||||
import main
|
import main
|
||||||
|
|
||||||
@@ -127,11 +127,11 @@ def test_ping_takt_ist_kuerzer_als_die_haltbarkeit():
|
|||||||
|
|
||||||
|
|
||||||
def test_tote_routen_sind_und_bleiben_weg():
|
def test_tote_routen_sind_und_bleiben_weg():
|
||||||
"""Entfernt am 25.07.2026, jede ein Ueberrest eines ersetzten Entwurfs und
|
"""Entfernt am 25.07.2026, jede ein Überrest eines ersetzten Entwurfs und
|
||||||
ohne einen einzigen Aufrufer (mechanisch gegengeprueft: alle api.*-Aufrufe
|
ohne einen einzigen Aufrufer (mechanisch gegengeprueft: alle api.*-Aufrufe
|
||||||
des UI gegen alle Routen).
|
des UI gegen alle Routen).
|
||||||
|
|
||||||
Der Test haelt sie draussen. /stream/jobs ist der Grund fuer diese
|
Der Test hält sie draussen. /stream/jobs ist der Grund für diese
|
||||||
Absicherung: er war schon einmal ein Placebo, wurde dann "repariert" statt
|
Absicherung: er war schon einmal ein Placebo, wurde dann "repariert" statt
|
||||||
entfernt - und war danach eine Endlosschleife je Verbindung ohne jeden
|
entfernt - und war danach eine Endlosschleife je Verbindung ohne jeden
|
||||||
Verbraucher. Wer echtes Push will, braucht BEIDE Seiten (Server UND ein
|
Verbraucher. Wer echtes Push will, braucht BEIDE Seiten (Server UND ein
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Tests fuer die puren Helfer aus makemkv_daten.
|
"""Tests für die puren Helfer aus makemkv_daten.
|
||||||
|
|
||||||
Bewusst OHNE Dateisystem, DB und fcntl — deshalb laufen sie auch auf Windows
|
Bewusst OHNE Dateisystem, DB und fcntl — deshalb laufen sie auch auf Windows
|
||||||
und nicht nur in der Ampel. Geprueft wird genau das, was ohne Container und
|
und nicht nur in der Ampel. Geprueft wird genau das, was ohne Container und
|
||||||
@@ -24,8 +24,8 @@ _GUELTIG = """; KEYDB.cfg — Beispiel
|
|||||||
|
|
||||||
|
|
||||||
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
||||||
# Kommentar- und Leerzeilen duerfen NICHT mitgezaehlt werden, sonst meldet
|
# Kommentar- und Leerzeilen dürfen NICHT mitgezaehlt werden, sonst meldet
|
||||||
# das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthaelt.
|
# das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthält.
|
||||||
assert zaehle_disc_eintraege(_GUELTIG) == 2
|
assert zaehle_disc_eintraege(_GUELTIG) == 2
|
||||||
|
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ def test_keydb_pruefen_meldet_leere_datei():
|
|||||||
|
|
||||||
|
|
||||||
def test_keydb_pruefen_erkennt_html():
|
def test_keydb_pruefen_erkennt_html():
|
||||||
# Haeufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite
|
# Häufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite
|
||||||
# eines Downloads im Feld. MakeMKV wuerde dann still weiter meckern.
|
# eines Downloads im Feld. MakeMKV würde dann still weiter meckern.
|
||||||
fehler = keydb_pruefen("<!DOCTYPE html>\n<html><body>404 Not Found</body></html>\n")
|
fehler = keydb_pruefen("<!DOCTYPE html>\n<html><body>404 Not Found</body></html>\n")
|
||||||
assert "HTML" in fehler
|
assert "HTML" in fehler
|
||||||
|
|
||||||
@@ -63,19 +63,19 @@ def test_keydb_pruefen_meldet_datei_ohne_disc_zeile():
|
|||||||
|
|
||||||
|
|
||||||
def test_keydb_pruefen_laesst_gueltige_datei_durch():
|
def test_keydb_pruefen_laesst_gueltige_datei_durch():
|
||||||
# "" heisst laut Vertrag: alles in Ordnung, darf geschrieben werden.
|
# "" heißt laut Vertrag: alles in Ordnung, darf geschrieben werden.
|
||||||
assert keydb_pruefen(_GUELTIG) == ""
|
assert keydb_pruefen(_GUELTIG) == ""
|
||||||
|
|
||||||
|
|
||||||
def test_ist_aacs_dump_erkennt_echten_namen():
|
def test_ist_aacs_dump_erkennt_echten_namen():
|
||||||
# So heisst der Dump, den MakeMKV am 25.07.2026 fuer Akira UHD abgelegt hat
|
# So heißt der Dump, den MakeMKV am 25.07.2026 für Akira UHD abgelegt hat
|
||||||
# (Meldung 3332) — dieser Name MUSS zum Download durchkommen.
|
# (Meldung 3332) — dieser Name MUSS zum Download durchkommen.
|
||||||
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
||||||
|
|
||||||
|
|
||||||
def test_ist_aacs_dump_blockt_pfad_tricks():
|
def test_ist_aacs_dump_blockt_pfad_tricks():
|
||||||
# Der Download-Endpunkt haengt den Namen an das Datenverzeichnis — ein
|
# Der Download-Endpunkt hängt den Namen an das Datenverzeichnis — ein
|
||||||
# durchgelassenes ".." oder ein Pfadtrenner waere ein Ausbruch.
|
# durchgelassenes ".." oder ein Pfadtrenner wäre ein Ausbruch.
|
||||||
assert ist_aacs_dump("../x.tgz") is False
|
assert ist_aacs_dump("../x.tgz") is False
|
||||||
assert ist_aacs_dump("../../etc/passwd.tgz") is False
|
assert ist_aacs_dump("../../etc/passwd.tgz") is False
|
||||||
assert ist_aacs_dump("unter/ordner.tgz") is False
|
assert ist_aacs_dump("unter/ordner.tgz") is False
|
||||||
@@ -93,8 +93,8 @@ def test_ist_aacs_dump_lehnt_andere_endungen_ab():
|
|||||||
|
|
||||||
|
|
||||||
def test_settings_conf_ersetzt_alten_key_und_behaelt_den_rest():
|
def test_settings_conf_ersetzt_alten_key_und_behaelt_den_rest():
|
||||||
# Regression: bis 25.07.2026 wurde die Datei komplett ueberschrieben. Mit
|
# Regression: bis 25.07.2026 wurde die Datei komplett überschrieben. Mit
|
||||||
# dem jetzt persistenten Datenverzeichnis waere app_UpdateEnable vor jedem
|
# dem jetzt persistenten Datenverzeichnis wäre app_UpdateEnable vor jedem
|
||||||
# Rip weg gewesen.
|
# Rip weg gewesen.
|
||||||
alt = 'app_UpdateEnable = "1"\napp_Key = "T-alt"\napp_DestinationDir = "/tmp"\n'
|
alt = 'app_UpdateEnable = "1"\napp_Key = "T-alt"\napp_DestinationDir = "/tmp"\n'
|
||||||
neu = settings_conf_zusammenfuehren(alt, "T-neu")
|
neu = settings_conf_zusammenfuehren(alt, "T-neu")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Tests fuer den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key)."""
|
"""Tests für den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key)."""
|
||||||
from makemkv_key import extract_key
|
from makemkv_key import extract_key
|
||||||
|
|
||||||
# Beispiel-Key im echten Format (T- + 64 Zeichen [A-Za-z0-9_]).
|
# Beispiel-Key im echten Format (T- + 64 Zeichen [A-Za-z0-9_]).
|
||||||
@@ -16,12 +16,12 @@ def test_extract_key_ohne_key_gibt_none():
|
|||||||
|
|
||||||
|
|
||||||
def test_extract_key_zu_kurz_kein_treffer():
|
def test_extract_key_zu_kurz_kein_treffer():
|
||||||
# Format ist streng T- + genau 64 Zeichen — zu kurz faellt raus.
|
# Format ist streng T- + genau 64 Zeichen — zu kurz fällt raus.
|
||||||
assert extract_key("T-zukurz123") is None
|
assert extract_key("T-zukurz123") is None
|
||||||
|
|
||||||
|
|
||||||
def test_extract_key_liefert_ganzen_key():
|
def test_extract_key_liefert_ganzen_key():
|
||||||
# Regression: die Regex darf den Key NICHT abschneiden (Beta-Keys sind laenger
|
# Regression: die Regex darf den Key NICHT abschneiden (Beta-Keys sind länger
|
||||||
# als die im Forum genannten 64 Zeichen — hier der volle Key zwischen Text).
|
# als die im Forum genannten 64 Zeichen — hier der volle Key zwischen Text).
|
||||||
treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text")
|
treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text")
|
||||||
assert treffer == _KEY
|
assert treffer == _KEY
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ const ENCODER_LABELS: Record<string, string> = {
|
|||||||
// braucht roh rund 40 GB, eine 4K-UHD bis 100 GB.
|
// braucht roh rund 40 GB, eine 4K-UHD bis 100 GB.
|
||||||
const PLATZ_WARNUNG_GB = 60
|
const PLATZ_WARNUNG_GB = 60
|
||||||
|
|
||||||
/** Was beim Abschliessen gespeichert wird — abhaengig davon, was die Maschine kann. */
|
/** Was beim Abschliessen gespeichert wird — abhängig davon, was die Maschine kann. */
|
||||||
function presetsFuer(schwach: boolean) {
|
function presetsFuer(schwach: boolean) {
|
||||||
if (schwach) {
|
if (schwach) {
|
||||||
// Gemessen auf einer CPU ohne AVX2: 4K-HEVC = 28-55 h pro Film. Also 4K
|
// Gemessen auf einer CPU ohne AVX2: 4K-HEVC = 28-55 h pro Film. Also 4K
|
||||||
// verlustfrei behalten und fuer den Rest H.264 nehmen (deutlich schneller
|
// verlustfrei behalten und für den Rest H.264 nehmen (deutlich schneller
|
||||||
// als H.265, dafuer etwas groesser).
|
// als H.265, dafür etwas größer).
|
||||||
return {
|
return {
|
||||||
transcodePreset: 'HQ 1080p30 Surround',
|
transcodePreset: 'HQ 1080p30 Surround',
|
||||||
transcodePresetBluray: 'HQ 1080p30 Surround',
|
transcodePresetBluray: 'HQ 1080p30 Surround',
|
||||||
@@ -72,7 +72,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
const [keyWarnung, setKeyWarnung] = useState<string | null>(null)
|
const [keyWarnung, setKeyWarnung] = useState<string | null>(null)
|
||||||
|
|
||||||
// Wiederholt laden, bis Worker UND Laufwerk da sind: beim allerersten Start
|
// Wiederholt laden, bis Worker UND Laufwerk da sind: beim allerersten Start
|
||||||
// laeuft der Worker-Container oft noch hoch. Ohne das stand hier dauerhaft
|
// läuft der Worker-Container oft noch hoch. Ohne das stand hier dauerhaft
|
||||||
// „Noch kein Worker gemeldet" und der Nutzer wusste nicht, ob er warten soll.
|
// „Noch kein Worker gemeldet" und der Nutzer wusste nicht, ob er warten soll.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let laeuft = true
|
let laeuft = true
|
||||||
@@ -112,8 +112,8 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
transcodeEnabled,
|
transcodeEnabled,
|
||||||
...presets,
|
...presets,
|
||||||
})
|
})
|
||||||
// Erst SPEICHERN, dann pruefen: /metadata/status liest die Keys aus der
|
// Erst SPEICHERN, dann prüfen: /metadata/status liest die Keys aus der
|
||||||
// Datenbank, nicht aus dem Formular. Ein falsch kopierter Key faellt so
|
// Datenbank, nicht aus dem Formular. Ein falsch kopierter Key fällt so
|
||||||
// sofort auf, statt erst beim ersten Rip als „Unknown Disc".
|
// sofort auf, statt erst beim ersten Rip als „Unknown Disc".
|
||||||
if (tmdbKey.trim() || omdbKey.trim()) {
|
if (tmdbKey.trim() || omdbKey.trim()) {
|
||||||
try {
|
try {
|
||||||
@@ -131,7 +131,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
return // nicht wegklicken, damit die Warnung gelesen wird
|
return // nicht wegklicken, damit die Warnung gelesen wird
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Pruefung selbst kaputt → kein Grund, die Einrichtung zu blockieren
|
// Prüfung selbst kaputt → kein Grund, die Einrichtung zu blockieren
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await api.post('/setup/complete')
|
await api.post('/setup/complete')
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function WorkerVerwaltung() {
|
|||||||
: [
|
: [
|
||||||
// Standard-Ziel ist "C:\Program Files\Rippy Worker" - dorthin darf nur
|
// Standard-Ziel ist "C:\Program Files\Rippy Worker" - dorthin darf nur
|
||||||
// ein Administrator schreiben, deshalb der Hinweis direkt im Befehl.
|
// ein Administrator schreiben, deshalb der Hinweis direkt im Befehl.
|
||||||
// Die .exe fragt selbst per UAC; hier muss die Konsole erhoeht sein.
|
// Die .exe fragt selbst per UAC; hier muss die Konsole erhöht sein.
|
||||||
`# PowerShell als Administrator starten (Standard-Ziel: Programme)`,
|
`# PowerShell als Administrator starten (Standard-Ziel: Programme)`,
|
||||||
`iwr http://${hostFuerBefehl}/api/worker-setup/windows -OutFile install-rippy-worker.ps1`,
|
`iwr http://${hostFuerBefehl}/api/worker-setup/windows -OutFile install-rippy-worker.ps1`,
|
||||||
`powershell -ExecutionPolicy Bypass -File .\\install-rippy-worker.ps1 \``,
|
`powershell -ExecutionPolicy Bypass -File .\\install-rippy-worker.ps1 \``,
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ interface WorkerInfo {
|
|||||||
encoders: string[]
|
encoders: string[]
|
||||||
// keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
|
// keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
|
||||||
// in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
|
// in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
|
||||||
// rippt, zaehlt, deshalb steht die Angabe pro Worker und nicht global.
|
// rippt, zählt, deshalb steht die Angabe pro Worker und nicht global.
|
||||||
// cpu_kerne/cpu_simd: Rechenleistung fuers Software-Encodieren. Ohne diese
|
// cpu_kerne/cpu_simd: Rechenleistung fürs Software-Encodieren. Ohne diese
|
||||||
// Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
|
// Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
|
||||||
// Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
|
// Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
|
||||||
// handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
|
// handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
|
||||||
@@ -101,7 +101,7 @@ interface StorageZiel {
|
|||||||
free_gb: number | null
|
free_gb: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Antwort von GET/POST /system/keystore — MakeMKVs eigener Schluesselvorrat.
|
// Antwort von GET/POST /system/keystore — MakeMKVs eigener Schlüsselvorrat.
|
||||||
interface KeystoreStatus {
|
interface KeystoreStatus {
|
||||||
vorhanden: boolean
|
vorhanden: boolean
|
||||||
pfad: string
|
pfad: string
|
||||||
@@ -125,7 +125,7 @@ interface SystemInfo {
|
|||||||
webhook_gesetzt: boolean
|
webhook_gesetzt: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB gross.
|
// Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB groß.
|
||||||
function bytesLesbar(bytes: number): string {
|
function bytesLesbar(bytes: number): string {
|
||||||
if (!bytes) return '0 B'
|
if (!bytes) return '0 B'
|
||||||
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
@@ -134,7 +134,7 @@ function bytesLesbar(bytes: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Die API liefert ISO-8601 in UTC (oder einen leeren String). Ohne Zonen-Kennung
|
// Die API liefert ISO-8601 in UTC (oder einen leeren String). Ohne Zonen-Kennung
|
||||||
// wuerde der Browser den Zeitstempel als Ortszeit lesen und die Uhrzeit um den
|
// würde der Browser den Zeitstempel als Ortszeit lesen und die Uhrzeit um den
|
||||||
// Zonen-Versatz verschieben — deshalb notfalls ein Z anhaengen.
|
// Zonen-Versatz verschieben — deshalb notfalls ein Z anhaengen.
|
||||||
function zeitLesbar(iso: string): string {
|
function zeitLesbar(iso: string): string {
|
||||||
if (!iso) return 'unbekannt'
|
if (!iso) return 'unbekannt'
|
||||||
@@ -158,7 +158,7 @@ export default function SettingsPage() {
|
|||||||
const [updatesBusy, setUpdatesBusy] = useState(false)
|
const [updatesBusy, setUpdatesBusy] = useState(false)
|
||||||
// KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine
|
// KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine
|
||||||
// Einstellungen — deshalb bewusst NICHT in SettingsState: der globale
|
// Einstellungen — deshalb bewusst NICHT in SettingsState: der globale
|
||||||
// Speichern-Knopf postet dieses Objekt komplett und wuerde sie mitschleifen.
|
// Speichern-Knopf postet dieses Objekt komplett und würde sie mitschleifen.
|
||||||
const [keydb, setKeydb] = useState<KeydbStatus | null>(null)
|
const [keydb, setKeydb] = useState<KeydbStatus | null>(null)
|
||||||
const [keydbBusy, setKeydbBusy] = useState(false)
|
const [keydbBusy, setKeydbBusy] = useState(false)
|
||||||
const [dumps, setDumps] = useState<AacsDump[]>([])
|
const [dumps, setDumps] = useState<AacsDump[]>([])
|
||||||
@@ -200,7 +200,7 @@ export default function SettingsPage() {
|
|||||||
// letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen
|
// letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen
|
||||||
// widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung.
|
// widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung.
|
||||||
// Ganz sofort ist sie trotzdem nicht: der Worker meldet erst beim
|
// Ganz sofort ist sie trotzdem nicht: der Worker meldet erst beim
|
||||||
// naechsten Herzschlag neu. Genau so steht es auch im Erklaertext.
|
// nächsten Herzschlag neu. Genau so steht es auch im Erklaertext.
|
||||||
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
|
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ export default function SettingsPage() {
|
|||||||
setKeydbBusy(true)
|
setKeydbBusy(true)
|
||||||
try {
|
try {
|
||||||
// Bewusst als JSON-Text und nicht als Multipart-Upload: der API fehlt
|
// Bewusst als JSON-Text und nicht als Multipart-Upload: der API fehlt
|
||||||
// python-multipart, ein File()-Endpunkt wuerde sie beim Import killen.
|
// python-multipart, ein File()-Endpunkt würde sie beim Import killen.
|
||||||
const inhalt = await datei.text()
|
const inhalt = await datei.text()
|
||||||
await api.post('/system/keydb', { inhalt })
|
await api.post('/system/keydb', { inhalt })
|
||||||
keydbLaden()
|
keydbLaden()
|
||||||
@@ -436,9 +436,9 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen.
|
{/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen.
|
||||||
Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe
|
Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe
|
||||||
build_abcde_cmd) und es gibt auch keine Oberflaeche, um eine
|
build_abcde_cmd) und es gibt auch keine Oberfläche, um eine
|
||||||
Teilmenge zu waehlen — eine Audio-CD wurde also immer
|
Teilmenge zu wählen — eine Audio-CD wurde also immer
|
||||||
vollstaendig gerippt, egal wie der Schalter stand. Statt einer
|
vollständig gerippt, egal wie der Schalter stand. Statt einer
|
||||||
Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}
|
Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
<p className="text-xs text-slate-500 dark:text-slate-400 px-1">
|
||||||
💿 Eine Audio-CD wird immer vollständig gerippt — alle Tracks als
|
💿 Eine Audio-CD wird immer vollständig gerippt — alle Tracks als
|
||||||
@@ -892,9 +892,9 @@ export default function SettingsPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/*
|
{/*
|
||||||
Nur das Verzeichnis des rippenden Workers zaehlt — deshalb je Worker.
|
Nur das Verzeichnis des rippenden Workers zählt — deshalb je Worker.
|
||||||
Die Zahl der Disc-Schluessel ist die entscheidende Angabe fuer 4K-UHD:
|
Die Zahl der Disc-Schluessel ist die entscheidende Angabe für 4K-UHD:
|
||||||
0 heisst, dass JEDE unbekannte UHD-Disc scheitert.
|
0 heißt, dass JEDE unbekannte UHD-Disc scheitert.
|
||||||
*/}
|
*/}
|
||||||
{w.info?.schluessel && w.info.schluessel !== '0' && w.info.schluessel !== 'unbekannt' && (
|
{w.info?.schluessel && w.info.schluessel !== '0' && w.info.schluessel !== 'unbekannt' && (
|
||||||
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
|
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
|
||||||
@@ -919,7 +919,7 @@ export default function SettingsPage() {
|
|||||||
Disc-Schluessel-Datenbank mit. Nachgemessen: falsch — MakeMKV liefert
|
Disc-Schluessel-Datenbank mit. Nachgemessen: falsch — MakeMKV liefert
|
||||||
gar keine Schluessel mit, es holt sie zur Laufzeit. Und die
|
gar keine Schluessel mit, es holt sie zur Laufzeit. Und die
|
||||||
Linux-Version holt sie nie (kein einziger Verbindungsversuch, auf
|
Linux-Version holt sie nie (kein einziger Verbindungsversuch, auf
|
||||||
beiden Maschinen verglichen). Deshalb der Schluesselspeicher unten.
|
beiden Maschinen verglichen). Deshalb der Schlüsselspeicher unten.
|
||||||
*/}
|
*/}
|
||||||
<p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400">
|
<p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400">
|
||||||
<strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten
|
<strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten
|
||||||
@@ -943,7 +943,7 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Schluesselspeicher — der Hauptweg fuer 4K-UHD (Befund 25.07.2026,
|
Schlüsselspeicher — der Hauptweg für 4K-UHD (Befund 25.07.2026,
|
||||||
auf beiden Maschinen gemessen): makemkvcon holt Disc-Schluessel
|
auf beiden Maschinen gemessen): makemkvcon holt Disc-Schluessel
|
||||||
unter Linux NIE selbst, die Windows-Version tut es (Meldung 3338,
|
unter Linux NIE selbst, die Windows-Version tut es (Meldung 3338,
|
||||||
Verbindung nach 185.84.108.20:443). Deshalb wird der Vorrat hier
|
Verbindung nach 185.84.108.20:443). Deshalb wird der Vorrat hier
|
||||||
@@ -1019,8 +1019,8 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/*
|
{/*
|
||||||
KEYDB.cfg — der NOTNAGEL, nicht der Hauptweg. Sie hilft bei Pressungen,
|
KEYDB.cfg — der NOTNAGEL, nicht der Hauptweg. Sie hilft bei Pressungen,
|
||||||
die auch MakeMKV selbst nicht kennt. Der Regelfall laeuft ueber den
|
die auch MakeMKV selbst nicht kennt. Der Regelfall läuft über den
|
||||||
Schluesselspeicher im Block darueber. Dateiname GROSS geschrieben, unter
|
Schlüsselspeicher im Block darüber. Dateiname GROSS geschrieben, unter
|
||||||
Linux case-sensitiv. Rippy stellt nur den Platz bereit.
|
Linux case-sensitiv. Rippy stellt nur den Platz bereit.
|
||||||
*/}
|
*/}
|
||||||
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
@@ -1107,7 +1107,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/*
|
{/*
|
||||||
AACS-Dumps: MakeMKV legt sie bei einer unbekannten UHD-Disc ab (Meldung 3332).
|
AACS-Dumps: MakeMKV legt sie bei einer unbekannten UHD-Disc ab (Meldung 3332).
|
||||||
Seit das Datenverzeichnis persistent gemountet ist, ueberleben sie den
|
Seit das Datenverzeichnis persistent gemountet ist, überleben sie den
|
||||||
Container-Neustart — vorher waren sie nach jedem Rip weg.
|
Container-Neustart — vorher waren sie nach jedem Rip weg.
|
||||||
*/}
|
*/}
|
||||||
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
|
|||||||
+1
-1
@@ -203,7 +203,7 @@ def zaehle_online_worker(sekunden: int = 120) -> int:
|
|||||||
|
|
||||||
Die Zombie-Erkennung vergleicht das mit der Zahl der Celery-Antworten:
|
Die Zombie-Erkennung vergleicht das mit der Zahl der Celery-Antworten:
|
||||||
melden sich weniger Worker als bekannt sind, ist die Auskunft
|
melden sich weniger Worker als bekannt sind, ist die Auskunft
|
||||||
unvollstaendig — dann wird NICHTS als Leiche gewertet.
|
unvollständig — dann wird NICHTS als Leiche gewertet.
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""MakeMKV-Datenverzeichnis: Schluesselspeicher, KEYDB.cfg, AACS-Dumps.
|
"""MakeMKV-Datenverzeichnis: Schlüsselspeicher, KEYDB.cfg, AACS-Dumps.
|
||||||
|
|
||||||
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, auf BEIDEN Maschinen gemessen):
|
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, auf BEIDEN Maschinen gemessen):
|
||||||
4K-UHD-Discs scheiterten mit "The volume key is unknown for this disc". Die
|
4K-UHD-Discs scheiterten mit "The volume key is unknown for this disc". Die
|
||||||
Ursache ist weder die Disc noch das Laufwerk — LibreDrive v06.3 laeuft und
|
Ursache ist weder die Disc noch das Laufwerk — LibreDrive v06.3 läuft und
|
||||||
MakeMKV liest die Disc — sondern:
|
MakeMKV liest die Disc — sondern:
|
||||||
|
|
||||||
makemkvcon unter LINUX ruft die Disc-Schluessel nie ab.
|
makemkvcon unter LINUX ruft die Disc-Schluessel nie ab.
|
||||||
@@ -10,35 +10,35 @@ MakeMKV liest die Disc — sondern:
|
|||||||
|
|
||||||
Gemessen, nicht vermutet:
|
Gemessen, nicht vermutet:
|
||||||
* Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit
|
* Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit
|
||||||
leerem UND mit gefuelltem Schluesselspeicher, mit und ohne --noscan, mit
|
leerem UND mit gefülltem Schlüsselspeicher, mit und ohne --noscan, mit
|
||||||
dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Pruefung
|
dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Prüfung
|
||||||
(update.conf geloescht, Meldung 5074 belegt den Web-Kontakt). Immer:
|
(update.conf gelöscht, Meldung 5074 belegt den Web-Kontakt). Immer:
|
||||||
keine Verbindung, kein Schluessel.
|
keine Verbindung, kein Schluessel.
|
||||||
* Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading
|
* Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading
|
||||||
latest HK to ...", Verbindung nach 185.84.108.20:443
|
latest HK to ...", Verbindung nach 185.84.108.20:443
|
||||||
(web33.majordomo.ru), _private_data.tar waechst — und die Disc geht auf
|
(web33.majordomo.ru), _private_data.tar wächst — und die Disc geht auf
|
||||||
(TCOUNT:5, "Operation successfully completed").
|
(TCOUNT:5, "Operation successfully completed").
|
||||||
* Der Code dafuer steckt auch im Linux-Binary: die Meldungsvorlage
|
* Der Code dafür steckt auch im Linux-Binary: die Meldungsvorlage
|
||||||
"Downloading latest %1 to %2 ..." steht in makemkvcon. Sie loest nur nie
|
"Downloading latest %1 to %2 ..." steht in makemkvcon. Sie löst nur nie
|
||||||
aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet.
|
aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet.
|
||||||
|
|
||||||
FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst,
|
FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst,
|
||||||
MakeMKVs Schluessel-Kanal sei abgeschaltet. Das war FALSCH. Die Herleitung
|
MakeMKVs Schluessel-Kanal sei abgeschaltet. Das war FALSCH. Die Herleitung
|
||||||
stuetzte sich auf zwei Hostnamen aus alten Forumsbeitraegen
|
stützte sich auf zwei Hostnamen aus alten Forumsbeitraegen
|
||||||
(hkdata.fairuse.org, hkdata.crabdance.com), die tatsaechlich nicht mehr
|
(hkdata.fairuse.org, hkdata.crabdance.com), die tatsaechlich nicht mehr
|
||||||
aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der
|
aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der
|
||||||
Worker erreicht ihn sogar; er wird unter Linux nur nie gefragt.
|
Worker erreicht ihn sogar; er wird unter Linux nur nie gefragt.
|
||||||
|
|
||||||
WAS DARAUS FOLGT: Der Schluesselspeicher (_private_data.tar) muss von einer
|
WAS DARAUS FOLGT: Der Schlüsselspeicher (_private_data.tar) muss von einer
|
||||||
MakeMKV-Installation kommen, die ihn wirklich abruft — praktisch von Windows.
|
MakeMKV-Installation kommen, die ihn wirklich abruft — praktisch von Windows.
|
||||||
Rippy nimmt ihn unter Einstellungen -> System entgegen. Die KEYDB.cfg bleibt
|
Rippy nimmt ihn unter Einstellungen -> System entgegen. Die KEYDB.cfg bleibt
|
||||||
der Notnagel fuer Pressungen, die auch MakeMKV selbst nicht kennt.
|
der Notnagel für Pressungen, die auch MakeMKV selbst nicht kennt.
|
||||||
|
|
||||||
Rippy liefert KEINE Schluessel mit, laedt keine herunter und verteilt keine.
|
Rippy liefert KEINE Schluessel mit, lädt keine herunter und verteilt keine.
|
||||||
Es verwaltet nur, was der Nutzer selbst mitbringt.
|
Es verwaltet nur, was der Nutzer selbst mitbringt.
|
||||||
|
|
||||||
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
||||||
* Linux laedt keine Hashed Keys — dasselbe Symptom, mehrfach berichtet:
|
* Linux lädt keine Hashed Keys — dasselbe Symptom, mehrfach berichtet:
|
||||||
https://forum.makemkv.com/forum/viewtopic.php?t=25782
|
https://forum.makemkv.com/forum/viewtopic.php?t=25782
|
||||||
https://forum.makemkv.com/forum/viewtopic.php?t=34022
|
https://forum.makemkv.com/forum/viewtopic.php?t=34022
|
||||||
* Schluessel liegen als hkd_*.bin in _private_data.tar:
|
* Schluessel liegen als hkd_*.bin in _private_data.tar:
|
||||||
@@ -55,7 +55,7 @@ QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
|||||||
|
|
||||||
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
|
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
|
||||||
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
|
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
|
||||||
(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen.
|
(gleiche Lage wie bei db.py). Änderungen IMMER in BEIDEN Dateien nachziehen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@@ -72,14 +72,14 @@ DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")
|
|||||||
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
||||||
KEYDB_NAME = "KEYDB.cfg"
|
KEYDB_NAME = "KEYDB.cfg"
|
||||||
|
|
||||||
# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt
|
# Obergrenze für den Upload. Eine vollständige oeffentliche KEYDB.cfg liegt
|
||||||
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
|
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
|
||||||
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
|
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
|
||||||
MAX_KEYDB_BYTES = 64 * 1024 * 1024
|
MAX_KEYDB_BYTES = 64 * 1024 * 1024
|
||||||
|
|
||||||
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
|
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
|
||||||
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
|
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
|
||||||
# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag.
|
# ";", Leerzeilen, Fortsetzungsfelder) zählt nicht als Eintrag.
|
||||||
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
|
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
|
||||||
|
|
||||||
|
|
||||||
@@ -94,9 +94,9 @@ def keydb_pfad(daten_dir: str = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def zaehle_disc_eintraege(inhalt: str) -> int:
|
def zaehle_disc_eintraege(inhalt: str) -> int:
|
||||||
"""Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar).
|
"""Zeilen mit Disc-Kennung zählen (pure Funktion, testbar).
|
||||||
|
|
||||||
Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest
|
Bewusst eine Heuristik und keine vollständige Auswertung: MakeMKV liest
|
||||||
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
|
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
|
||||||
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
|
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
|
||||||
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
|
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
|
||||||
@@ -106,17 +106,17 @@ def zaehle_disc_eintraege(inhalt: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def keydb_pruefen(inhalt: str) -> str:
|
def keydb_pruefen(inhalt: str) -> str:
|
||||||
"""Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck.
|
"""Prüft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurück.
|
||||||
|
|
||||||
Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die
|
Verhindert den häufigsten Bedienfehler: statt der KEYDB.cfg landet die
|
||||||
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
|
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
|
||||||
MakeMKV wuerde dann still weiter "volume key is unknown" melden.
|
MakeMKV würde dann still weiter "volume key is unknown" melden.
|
||||||
"""
|
"""
|
||||||
if not inhalt.strip():
|
if not inhalt.strip():
|
||||||
return "Die Datei ist leer."
|
return "Die Datei ist leer."
|
||||||
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
|
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
|
||||||
return (
|
return (
|
||||||
"Die Datei ist groesser als "
|
"Die Datei ist größer als "
|
||||||
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
|
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
|
||||||
)
|
)
|
||||||
if inhalt.lstrip()[:1] == "<":
|
if inhalt.lstrip()[:1] == "<":
|
||||||
@@ -150,7 +150,7 @@ def keydb_status(daten_dir: str = None) -> dict:
|
|||||||
with open(pfad, encoding="utf-8", errors="replace") as datei:
|
with open(pfad, encoding="utf-8", errors="replace") as datei:
|
||||||
eintraege = zaehle_disc_eintraege(datei.read())
|
eintraege = zaehle_disc_eintraege(datei.read())
|
||||||
except OSError:
|
except OSError:
|
||||||
pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem
|
pass # Datei da, aber unlesbar: Größe/Datum stimmen trotzdem
|
||||||
return {
|
return {
|
||||||
"vorhanden": True,
|
"vorhanden": True,
|
||||||
"pfad": pfad,
|
"pfad": pfad,
|
||||||
@@ -163,7 +163,7 @@ def keydb_status(daten_dir: str = None) -> dict:
|
|||||||
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
|
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
|
||||||
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
|
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
|
||||||
|
|
||||||
Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf
|
Erst in eine Nebendatei, dann os.replace: während ein Rip läuft, darf
|
||||||
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
|
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
|
||||||
"""
|
"""
|
||||||
pfad = keydb_pfad(daten_dir)
|
pfad = keydb_pfad(daten_dir)
|
||||||
@@ -244,32 +244,32 @@ def dumps_auflisten(daten_dir: str = None) -> list:
|
|||||||
return liste
|
return liste
|
||||||
|
|
||||||
|
|
||||||
# --- Schluesselspeicher (_private_data.tar) -------------------------------
|
# --- Schlüsselspeicher (_private_data.tar) -------------------------------
|
||||||
#
|
#
|
||||||
# Das ist MakeMKVs eigener Speicher fuer die "Hashed Keys": ein tar-Archiv mit
|
# Das ist MakeMKVs eigener Speicher für die "Hashed Keys": ein tar-Archiv mit
|
||||||
# hkd_*.bin-Eintraegen. Unter Windows fuellt MakeMKV es selbst (Meldung 3338),
|
# hkd_*.bin-Eintraegen. Unter Windows füllt MakeMKV es selbst (Meldung 3338),
|
||||||
# unter Linux nie — siehe Modul-Kopf. Rippy nimmt die Datei deshalb entgegen
|
# unter Linux nie — siehe Modul-Kopf. Rippy nimmt die Datei deshalb entgegen
|
||||||
# und legt sie ins Datenverzeichnis; MakeMKV liest sie beim naechsten Start.
|
# und legt sie ins Datenverzeichnis; MakeMKV liest sie beim nächsten Start.
|
||||||
|
|
||||||
PRIVATE_DATA_NAME = "_private_data.tar"
|
PRIVATE_DATA_NAME = "_private_data.tar"
|
||||||
|
|
||||||
# Der Speicher lag am 25.07.2026 bei rund 6 MB und waechst mit jeder neuen
|
# Der Speicher lag am 25.07.2026 bei rund 6 MB und wächst mit jeder neuen
|
||||||
# Pressung. 64 MB sind reichlich Luft — und derselbe Wert wie
|
# Pressung. 64 MB sind reichlich Luft — und derselbe Wert wie
|
||||||
# client_max_body_size in docker/ui/nginx.conf: waere die Grenze hier hoeher,
|
# client_max_body_size in docker/ui/nginx.conf: wäre die Grenze hier höher,
|
||||||
# wuerde nginx den Upload abweisen, bevor die API ihn ueberhaupt sieht.
|
# würde nginx den Upload abweisen, bevor die API ihn überhaupt sieht.
|
||||||
MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024
|
MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
def private_data_pfad(daten_dir: str = None) -> str:
|
def private_data_pfad(daten_dir: str = None) -> str:
|
||||||
"""Voller Pfad zum Schluesselspeicher im Datenverzeichnis."""
|
"""Voller Pfad zum Schlüsselspeicher im Datenverzeichnis."""
|
||||||
return os.path.join(daten_dir or DATEN_DIR, PRIVATE_DATA_NAME)
|
return os.path.join(daten_dir or DATEN_DIR, PRIVATE_DATA_NAME)
|
||||||
|
|
||||||
|
|
||||||
def zaehle_schluessel(rohdaten: bytes) -> int:
|
def zaehle_schluessel(rohdaten: bytes) -> int:
|
||||||
"""Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar).
|
"""Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar).
|
||||||
|
|
||||||
Das ist die ehrliche Kennzahl fuer "wie viele Disc-Schluessel kennt diese
|
Das ist die ehrliche Kennzahl für "wie viele Disc-Schluessel kennt diese
|
||||||
Installation". Ein frischer, leerer Speicher enthaelt nur eine
|
Installation". Ein frischer, leerer Speicher enthält nur eine
|
||||||
Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede
|
Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede
|
||||||
unbekannte UHD-Disc scheitert.
|
unbekannte UHD-Disc scheitert.
|
||||||
"""
|
"""
|
||||||
@@ -281,20 +281,20 @@ def zaehle_schluessel(rohdaten: bytes) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def private_data_pruefen(rohdaten: bytes) -> str:
|
def private_data_pruefen(rohdaten: bytes) -> str:
|
||||||
"""Prueft hochgeladene Rohdaten; deutscher Fehlertext oder "".
|
"""Prüft hochgeladene Rohdaten; deutscher Fehlertext oder "".
|
||||||
|
|
||||||
Faengt die beiden Bedienfehler ab, die sonst still danebengehen: eine
|
Faengt die beiden Bedienfehler ab, die sonst still danebengehen: eine
|
||||||
voellig andere Datei hochladen, oder den Speicher einer Installation, die
|
voellig andere Datei hochladen, oder den Speicher einer Installation, die
|
||||||
selbst noch keine Schluessel geholt hat (dann aendert sich nichts, und
|
selbst noch keine Schluessel geholt hat (dann ändert sich nichts, und
|
||||||
niemand versteht warum).
|
niemand versteht warum).
|
||||||
"""
|
"""
|
||||||
if not rohdaten:
|
if not rohdaten:
|
||||||
return "Die Datei ist leer."
|
return "Die Datei ist leer."
|
||||||
if len(rohdaten) > MAX_PRIVATE_DATA_BYTES:
|
if len(rohdaten) > MAX_PRIVATE_DATA_BYTES:
|
||||||
return (
|
return (
|
||||||
"Die Datei ist groesser als "
|
"Die Datei ist größer als "
|
||||||
f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein "
|
f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein "
|
||||||
"MakeMKV-Schluesselspeicher."
|
"MakeMKV-Schlüsselspeicher."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv:
|
with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv:
|
||||||
@@ -308,13 +308,13 @@ def private_data_pruefen(rohdaten: bytes) -> str:
|
|||||||
return (
|
return (
|
||||||
"In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). "
|
"In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). "
|
||||||
"Sie stammt vermutlich von einer MakeMKV-Installation, die selbst "
|
"Sie stammt vermutlich von einer MakeMKV-Installation, die selbst "
|
||||||
"noch keine geholt hat — oeffne dort erst einmal eine Disc."
|
"noch keine geholt hat — öffne dort erst einmal eine Disc."
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def schluesselspeicher_status(daten_dir: str = None) -> dict:
|
def schluesselspeicher_status(daten_dir: str = None) -> dict:
|
||||||
"""Zustand des Schluesselspeichers. Fehlt er, ist das kein Fehler."""
|
"""Zustand des Schlüsselspeichers. Fehlt er, ist das kein Fehler."""
|
||||||
pfad = private_data_pfad(daten_dir)
|
pfad = private_data_pfad(daten_dir)
|
||||||
try:
|
try:
|
||||||
angaben = os.stat(pfad)
|
angaben = os.stat(pfad)
|
||||||
@@ -338,9 +338,9 @@ def schluesselspeicher_status(daten_dir: str = None) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def private_data_schreiben(rohdaten: bytes, daten_dir: str = None) -> dict:
|
def private_data_schreiben(rohdaten: bytes, daten_dir: str = None) -> dict:
|
||||||
"""Legt den Schluesselspeicher atomar ab und meldet den neuen Zustand.
|
"""Legt den Schlüsselspeicher atomar ab und meldet den neuen Zustand.
|
||||||
|
|
||||||
Atomar aus demselben Grund wie bei der KEYDB.cfg: waehrend ein Rip laeuft,
|
Atomar aus demselben Grund wie bei der KEYDB.cfg: während ein Rip läuft,
|
||||||
darf makemkvcon nie ein halb geschriebenes Archiv sehen.
|
darf makemkvcon nie ein halb geschriebenes Archiv sehen.
|
||||||
"""
|
"""
|
||||||
pfad = private_data_pfad(daten_dir)
|
pfad = private_data_pfad(daten_dir)
|
||||||
@@ -363,9 +363,9 @@ def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str:
|
|||||||
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
|
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
|
||||||
|
|
||||||
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
|
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
|
||||||
ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit
|
überschrieben. Mit dem jetzt persistenten Datenverzeichnis wäre damit
|
||||||
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
|
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
|
||||||
app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen,
|
app_UpdateEnable. Leerer Key lässt die vorhandene Zeile ebenfalls fallen,
|
||||||
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
|
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
|
||||||
"""
|
"""
|
||||||
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
|
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
|
||||||
|
|||||||
@@ -223,9 +223,9 @@ def parse_msg(zeile: str):
|
|||||||
fertig zusammengesetzte Klartext.
|
fertig zusammengesetzte Klartext.
|
||||||
|
|
||||||
Bis zum 25.07.2026 stand hier line.split(",", 4)[3]: das schnitt JEDE
|
Bis zum 25.07.2026 stand hier line.split(",", 4)[3]: das schnitt JEDE
|
||||||
Meldung ab, die selbst ein Komma enthaelt — und MakeMKV schreibt solche
|
Meldung ab, die selbst ein Komma enthält — und MakeMKV schreibt solche
|
||||||
laufend ("Title #1 has length of 12 seconds, which is less than ...").
|
laufend ("Title #1 has length of 12 seconds, which is less than ...").
|
||||||
Deshalb eine Regex, die die Anfuehrungszeichen respektiert.
|
Deshalb eine Regex, die die Anführungszeichen respektiert.
|
||||||
"""
|
"""
|
||||||
treffer = _MSG_RE.match(zeile.strip())
|
treffer = _MSG_RE.match(zeile.strip())
|
||||||
if not treffer:
|
if not treffer:
|
||||||
@@ -409,7 +409,7 @@ def _handbrake_schleife(process, output_path: str, abbruch_cb=None, progress_cb=
|
|||||||
try:
|
try:
|
||||||
for line in process.stdout:
|
for line in process.stdout:
|
||||||
# Zuerst der Abbruch — unabhängig davon, ob die Zeile überhaupt
|
# Zuerst der Abbruch — unabhängig davon, ob die Zeile überhaupt
|
||||||
# einen Fortschritt enthält. Sonst haengt „Abbrechen" daran, dass
|
# einen Fortschritt enthält. Sonst hängt „Abbrechen" daran, dass
|
||||||
# sich die Prozentzahl bewegt (Befund 25.07.2026).
|
# sich die Prozentzahl bewegt (Befund 25.07.2026).
|
||||||
if abbruch_cb:
|
if abbruch_cb:
|
||||||
abbruch_cb()
|
abbruch_cb()
|
||||||
@@ -475,7 +475,7 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
|
|||||||
log_cb(code, text) bekommt JEDE MakeMKV-Meldung. Bewusst ein eigener
|
log_cb(code, text) bekommt JEDE MakeMKV-Meldung. Bewusst ein eigener
|
||||||
Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft
|
Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft
|
||||||
Aufrufe, bei denen sich die Prozentzahl nicht geaendert hat — Meldungen
|
Aufrufe, bei denen sich die Prozentzahl nicht geaendert hat — Meldungen
|
||||||
waeren dort also grossteils verschwunden. Ohne diesen Kanal war am
|
wären dort also großteils verschwunden. Ohne diesen Kanal war am
|
||||||
25.07.2026 nicht von aussen erkennbar, dass MakeMKV bei der UHD-Disc
|
25.07.2026 nicht von aussen erkennbar, dass MakeMKV bei der UHD-Disc
|
||||||
nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten).
|
nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten).
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ def unter_wurzel(pfad: str, wurzel: str) -> bool:
|
|||||||
"""Liegt `pfad` wirklich unterhalb von `wurzel` (oder IST es die Wurzel)?
|
"""Liegt `pfad` wirklich unterhalb von `wurzel` (oder IST es die Wurzel)?
|
||||||
|
|
||||||
Pure Funktion, testbar. Ein nacktes `startswith()` genügt nicht:
|
Pure Funktion, testbar. Ein nacktes `startswith()` genügt nicht:
|
||||||
„/app/media-boese/x" beginnt mit „/app/media", liegt aber ausserhalb
|
„/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb
|
||||||
(Befund 25.07.2026 bei der Durchsicht). Immer „/" als Trenner — das sind
|
(Befund 25.07.2026 bei der Durchsicht). Immer „/" als Trenner — das sind
|
||||||
Container-Pfade, auch wenn ein nativer Windows-Worker das Modul lädt.
|
Container-Pfade, auch wenn ein nativer Windows-Worker das Modul lädt.
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Tests fuer makemkv_daten.py: die reinen Helfer rund um das MakeMKV-Datenverzeichnis.
|
"""Tests für makemkv_daten.py: die reinen Helfer rund um das MakeMKV-Datenverzeichnis.
|
||||||
|
|
||||||
WARUM ES DIESE TESTS GIBT (Befund 25.07.2026, live im Worker nachgemessen):
|
WARUM ES DIESE TESTS GIBT (Befund 25.07.2026, live im Worker nachgemessen):
|
||||||
Eine 4K-UHD-Disc (Akira UHD, MKB v76) scheiterte mit "The volume key is unknown
|
Eine 4K-UHD-Disc (Akira UHD, MKB v76) scheiterte mit "The volume key is unknown
|
||||||
for this disc", obwohl Laufwerk und MakeMKV in Ordnung waren. Der einzige heute
|
for this disc", obwohl Laufwerk und MakeMKV in Ordnung waren. Der einzige heute
|
||||||
noch funktionierende Weg ist eine selbst mitgebrachte KEYDB.cfg im
|
noch funktionierende Weg ist eine selbst mitgebrachte KEYDB.cfg im
|
||||||
Datenverzeichnis. Damit haengt einiges an diesen kleinen Funktionen: erkennen wir
|
Datenverzeichnis. Damit hängt einiges an diesen kleinen Funktionen: erkennen wir
|
||||||
die Datei falsch, meldet das UI "alles gut", waehrend MakeMKV weiter scheitert.
|
die Datei falsch, meldet das UI "alles gut", während MakeMKV weiter scheitert.
|
||||||
|
|
||||||
Getestet wird nur, was ohne Postgres, Redis und ohne Laufwerk laeuft — also die
|
Getestet wird nur, was ohne Postgres, Redis und ohne Laufwerk läuft — also die
|
||||||
puren Funktionen mit echten Beispieldaten. Zeilenformat der KEYDB.cfg laut
|
puren Funktionen mit echten Beispieldaten. Zeilenformat der KEYDB.cfg laut
|
||||||
libaacs (AGENTS Regel D, externe Schnittstellen nie aus dem Kopf):
|
libaacs (AGENTS Regel D, externe Schnittstellen nie aus dem Kopf):
|
||||||
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
|
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
|
||||||
@@ -17,7 +17,7 @@ ist eine Zwillingsdatei, es gibt sie unter docker/api/ UND docker/worker/, und
|
|||||||
beide Seiten haben Tests. Da im Projekt keine __init__.py liegen, importiert
|
beide Seiten haben Tests. Da im Projekt keine __init__.py liegen, importiert
|
||||||
pytest Testdateien unter ihrem blossen Dateinamen — zwei Dateien namens
|
pytest Testdateien unter ihrem blossen Dateinamen — zwei Dateien namens
|
||||||
test_makemkv_daten.py brechen deshalb die Sammelphase ab ("import file
|
test_makemkv_daten.py brechen deshalb die Sammelphase ab ("import file
|
||||||
mismatch") und faerben die ganze Ampel rot. Nicht zurueckbenennen.
|
mismatch") und faerben die ganze Ampel rot. Nicht zurückbenennen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -47,10 +47,10 @@ zaehle_schluessel = _modul.zaehle_schluessel
|
|||||||
|
|
||||||
|
|
||||||
def _tar_mit(namen):
|
def _tar_mit(namen):
|
||||||
"""Baut ein tar-Archiv im Speicher — so sieht MakeMKVs Schluesselspeicher aus.
|
"""Baut ein tar-Archiv im Speicher — so sieht MakeMKVs Schlüsselspeicher aus.
|
||||||
|
|
||||||
Echte Eintragsnamen aus dem Speicher der Windows-Installation vom
|
Echte Eintragsnamen aus dem Speicher der Windows-Installation vom
|
||||||
25.07.2026: hkd_<8 Hex>.bin fuer die Schluessel, dazu Index-Dateien.
|
25.07.2026: hkd_<8 Hex>.bin für die Schluessel, dazu Index-Dateien.
|
||||||
"""
|
"""
|
||||||
import io
|
import io
|
||||||
import tarfile
|
import tarfile
|
||||||
@@ -67,7 +67,7 @@ def _tar_mit(namen):
|
|||||||
def test_zaehle_schluessel_zaehlt_nur_hkd_eintraege():
|
def test_zaehle_schluessel_zaehlt_nur_hkd_eintraege():
|
||||||
# Nur hkd_*.bin sind Disc-Schluessel. Index- und sdf-Dateien gehoeren zum
|
# Nur hkd_*.bin sind Disc-Schluessel. Index- und sdf-Dateien gehoeren zum
|
||||||
# Speicher dazu, sind aber keine Schluessel — sonst meldete das UI
|
# Speicher dazu, sind aber keine Schluessel — sonst meldete das UI
|
||||||
# "1 Schluessel vorhanden" fuer einen komplett leeren Vorrat.
|
# "1 Schluessel vorhanden" für einen komplett leeren Vorrat.
|
||||||
voll = _tar_mit([
|
voll = _tar_mit([
|
||||||
"hkd_00000059.bin",
|
"hkd_00000059.bin",
|
||||||
"hkd_0000005a.bin",
|
"hkd_0000005a.bin",
|
||||||
@@ -101,7 +101,7 @@ def test_private_data_pruefen_lehnt_leere_und_falsche_dateien_ab():
|
|||||||
|
|
||||||
def test_private_data_pruefen_lehnt_speicher_ohne_schluessel_ab():
|
def test_private_data_pruefen_lehnt_speicher_ohne_schluessel_ab():
|
||||||
# Der teuerste Bedienfehler: den Speicher einer Installation hochladen,
|
# Der teuerste Bedienfehler: den Speicher einer Installation hochladen,
|
||||||
# die selbst noch nie Schluessel geholt hat. Ohne diese Pruefung aendert
|
# die selbst noch nie Schluessel geholt hat. Ohne diese Prüfung ändert
|
||||||
# sich nichts und niemand versteht, warum.
|
# sich nichts und niemand versteht, warum.
|
||||||
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
|
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
|
||||||
assert "kein einziger Schluessel" in private_data_pruefen(leer)
|
assert "kein einziger Schluessel" in private_data_pruefen(leer)
|
||||||
@@ -123,7 +123,7 @@ def test_zwillinge_sind_byteweise_identisch():
|
|||||||
api = hashlib.sha256(datei.read()).hexdigest()
|
api = hashlib.sha256(datei.read()).hexdigest()
|
||||||
assert worker == api, (
|
assert worker == api, (
|
||||||
"docker/worker/makemkv_daten.py und docker/api/makemkv_daten.py sind "
|
"docker/worker/makemkv_daten.py und docker/api/makemkv_daten.py sind "
|
||||||
"auseinandergelaufen - Aenderungen immer in BEIDE Dateien uebernehmen."
|
"auseinandergelaufen - Änderungen immer in BEIDE Dateien übernehmen."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Eine kleine, aber echte KEYDB.cfg im libaacs-Format: Kommentarkopf, eine
|
# Eine kleine, aber echte KEYDB.cfg im libaacs-Format: Kommentarkopf, eine
|
||||||
@@ -142,7 +142,7 @@ A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D4 = BLADE RUNNER 2049
|
|||||||
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
||||||
"""Nur Zeilen mit 40 Hex-Zeichen und Gleichheitszeichen sind Eintraege.
|
"""Nur Zeilen mit 40 Hex-Zeichen und Gleichheitszeichen sind Eintraege.
|
||||||
|
|
||||||
Kommentare, Leerzeilen und Fortsetzungsfelder duerfen nicht mitzaehlen —
|
Kommentare, Leerzeilen und Fortsetzungsfelder dürfen nicht mitzaehlen —
|
||||||
sonst meldet das UI bei einer reinen Kommentardatei stolz "42 Eintraege".
|
sonst meldet das UI bei einer reinen Kommentardatei stolz "42 Eintraege".
|
||||||
"""
|
"""
|
||||||
assert zaehle_disc_eintraege(BEISPIEL_KEYDB) == 2
|
assert zaehle_disc_eintraege(BEISPIEL_KEYDB) == 2
|
||||||
@@ -158,14 +158,14 @@ def test_zaehle_disc_eintraege_ignoriert_zu_kurze_kennung():
|
|||||||
"""39 Hex-Zeichen sind keine Disc-Kennung.
|
"""39 Hex-Zeichen sind keine Disc-Kennung.
|
||||||
|
|
||||||
Genau so sieht eine beim Kopieren verstuemmelte Datei aus — die darf nicht
|
Genau so sieht eine beim Kopieren verstuemmelte Datei aus — die darf nicht
|
||||||
als gueltig durchgehen, sonst sucht der Commander den Fehler beim Laufwerk.
|
als gültig durchgehen, sonst sucht der Commander den Fehler beim Laufwerk.
|
||||||
"""
|
"""
|
||||||
zu_kurz = "A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D = KAPUTT\n"
|
zu_kurz = "A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D = KAPUTT\n"
|
||||||
assert zaehle_disc_eintraege(zu_kurz) == 0
|
assert zaehle_disc_eintraege(zu_kurz) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_keydb_pruefen_meldet_leere_datei():
|
def test_keydb_pruefen_meldet_leere_datei():
|
||||||
# Haeufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
|
# Häufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
|
||||||
assert keydb_pruefen("") != ""
|
assert keydb_pruefen("") != ""
|
||||||
assert keydb_pruefen(" \n\n ") != ""
|
assert keydb_pruefen(" \n\n ") != ""
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ def test_keydb_pruefen_meldet_leere_datei():
|
|||||||
def test_keydb_pruefen_erkennt_html_fehlerseite():
|
def test_keydb_pruefen_erkennt_html_fehlerseite():
|
||||||
"""Der zweithaeufigste Fehlgriff: der Download lieferte eine HTML-Seite.
|
"""Der zweithaeufigste Fehlgriff: der Download lieferte eine HTML-Seite.
|
||||||
|
|
||||||
MakeMKV wuerde die Datei still ignorieren und weiter "volume key is unknown"
|
MakeMKV würde die Datei still ignorieren und weiter "volume key is unknown"
|
||||||
melden — deshalb muss der Fehler schon beim Hochladen sichtbar werden.
|
melden — deshalb muss der Fehler schon beim Hochladen sichtbar werden.
|
||||||
"""
|
"""
|
||||||
html = "<!DOCTYPE html>\n<html><body><h1>404 Not Found</h1></body></html>\n"
|
html = "<!DOCTYPE html>\n<html><body><h1>404 Not Found</h1></body></html>\n"
|
||||||
@@ -184,12 +184,12 @@ def test_keydb_pruefen_erkennt_html_fehlerseite():
|
|||||||
|
|
||||||
def test_keydb_pruefen_meldet_text_ohne_disc_zeile():
|
def test_keydb_pruefen_meldet_text_ohne_disc_zeile():
|
||||||
# Irgendein Text (hier: eine README) ist keine KEYDB.cfg.
|
# Irgendein Text (hier: eine README) ist keine KEYDB.cfg.
|
||||||
meldung = keydb_pruefen("Diese Datei enthaelt keine Schluessel, nur Prosa.\n")
|
meldung = keydb_pruefen("Diese Datei enthält keine Schluessel, nur Prosa.\n")
|
||||||
assert meldung != ""
|
assert meldung != ""
|
||||||
|
|
||||||
|
|
||||||
def test_keydb_pruefen_akzeptiert_gueltigen_inhalt():
|
def test_keydb_pruefen_akzeptiert_gueltigen_inhalt():
|
||||||
# Leerer Rueckgabewert heisst laut Vertrag: alles in Ordnung.
|
# Leerer Rueckgabewert heißt laut Vertrag: alles in Ordnung.
|
||||||
assert keydb_pruefen(BEISPIEL_KEYDB) == ""
|
assert keydb_pruefen(BEISPIEL_KEYDB) == ""
|
||||||
|
|
||||||
|
|
||||||
@@ -200,9 +200,9 @@ def test_ist_aacs_dump_akzeptiert_echten_namen():
|
|||||||
|
|
||||||
|
|
||||||
def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
|
def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
|
||||||
"""Der Download-Endpunkt haengt den Namen an das Datenverzeichnis an —
|
"""Der Download-Endpunkt hängt den Namen an das Datenverzeichnis an —
|
||||||
ohne diese Pruefung koennte man sich damit aus dem Verzeichnis heraus
|
ohne diese Prüfung könnte man sich damit aus dem Verzeichnis heraus
|
||||||
lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts fuer die
|
lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts für die
|
||||||
Liste."""
|
Liste."""
|
||||||
assert ist_aacs_dump("../ausbruch.tgz") is False
|
assert ist_aacs_dump("../ausbruch.tgz") is False
|
||||||
assert ist_aacs_dump(".versteckt.tgz") is False
|
assert ist_aacs_dump(".versteckt.tgz") is False
|
||||||
@@ -212,8 +212,8 @@ def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
|
|||||||
|
|
||||||
def test_settings_conf_ersetzt_key_und_behaelt_den_rest():
|
def test_settings_conf_ersetzt_key_und_behaelt_den_rest():
|
||||||
"""DIE Regression, um die es geht: bis zum 25.07.2026 haben entrypoint.sh
|
"""DIE Regression, um die es geht: bis zum 25.07.2026 haben entrypoint.sh
|
||||||
und tasks.py die settings.conf komplett ueberschrieben. Mit dem jetzt
|
und tasks.py die settings.conf komplett überschrieben. Mit dem jetzt
|
||||||
persistenten Datenverzeichnis waere damit bei jedem Containerstart und vor
|
persistenten Datenverzeichnis wäre damit bei jedem Containerstart und vor
|
||||||
jedem Rip alles andere weg — allen voran app_UpdateEnable."""
|
jedem Rip alles andere weg — allen voran app_UpdateEnable."""
|
||||||
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\napp_DefaultSelectionString = "+sel:all"\n'
|
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\napp_DefaultSelectionString = "+sel:all"\n'
|
||||||
neu = settings_conf_zusammenfuehren(alt, "T-neuerSchluessel")
|
neu = settings_conf_zusammenfuehren(alt, "T-neuerSchluessel")
|
||||||
|
|||||||
@@ -114,15 +114,15 @@ def test_pfad_lokal_uebersetzt_fuer_windows_worker():
|
|||||||
assert pfad_lokal("/app/temp/raw/abc", "") == "/app/temp/raw/abc"
|
assert pfad_lokal("/app/temp/raw/abc", "") == "/app/temp/raw/abc"
|
||||||
|
|
||||||
|
|
||||||
# Die beiden frueheren Tests zu _original_aufheben sind nach
|
# Die beiden früheren Tests zu _original_aufheben sind nach
|
||||||
# test_original_aufheben.py gewandert und dort erweitert. Sie pruefen jetzt das
|
# test_original_aufheben.py gewandert und dort erweitert. Sie prüfen jetzt das
|
||||||
# richtige Kriterium: Vorher taeuschten sie per gefaelschtem os.stat
|
# richtige Kriterium: Vorher taeuschten sie per gefaelschtem os.stat
|
||||||
# „verschiedene Dateisysteme" vor — genau die Annahme, an der der Schutz in der
|
# „verschiedene Dateisysteme" vor — genau die Annahme, an der der Schutz in der
|
||||||
# Praxis scheiterte (st_dev war identisch, os.rename trotzdem EXDEV).
|
# Praxis scheiterte (st_dev war identisch, os.rename trotzdem EXDEV).
|
||||||
|
|
||||||
|
|
||||||
def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung():
|
def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung():
|
||||||
"""Pro Rip waehlbar (Commander 25.07.2026), Einstellung bleibt Standard.
|
"""Pro Rip wählbar (Commander 25.07.2026), Einstellung bleibt Standard.
|
||||||
|
|
||||||
Reihenfolge: Wahl dieses Rips -> Setting -> Container-Default. Der
|
Reihenfolge: Wahl dieses Rips -> Setting -> Container-Default. Der
|
||||||
Setting-Wert ist genau der, der bei Vollautomatik-Rips greift, weil dort
|
Setting-Wert ist genau der, der bei Vollautomatik-Rips greift, weil dort
|
||||||
@@ -134,16 +134,16 @@ def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung():
|
|||||||
assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy") == "/app/media/rippy"
|
assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy") == "/app/media/rippy"
|
||||||
assert tasks._arbeitsverzeichnis(einst) == "/app/media/movies"
|
assert tasks._arbeitsverzeichnis(einst) == "/app/media/movies"
|
||||||
assert tasks._arbeitsverzeichnis({}) == tasks.RAW_DIR
|
assert tasks._arbeitsverzeichnis({}) == tasks.RAW_DIR
|
||||||
# Ausbruchsversuche und Pfade ausserhalb /app/media fallen durch
|
# Ausbruchsversuche und Pfade außerhalb /app/media fallen durch
|
||||||
assert tasks._arbeitsverzeichnis({}, "/etc") == tasks.RAW_DIR
|
assert tasks._arbeitsverzeichnis({}, "/etc") == tasks.RAW_DIR
|
||||||
assert tasks._arbeitsverzeichnis({}, "/app/media/../etc") == tasks.RAW_DIR
|
assert tasks._arbeitsverzeichnis({}, "/app/media/../etc") == tasks.RAW_DIR
|
||||||
# Leere Wahl faellt sauber auf die Einstellung zurueck
|
# Leere Wahl fällt sauber auf die Einstellung zurück
|
||||||
assert tasks._arbeitsverzeichnis(einst, " ") == "/app/media/movies"
|
assert tasks._arbeitsverzeichnis(einst, " ") == "/app/media/movies"
|
||||||
|
|
||||||
|
|
||||||
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
||||||
"""Befund 25.07.2026: Elf Stellen prueften mit nacktem startswith().
|
"""Befund 25.07.2026: Elf Stellen prüften mit nacktem startswith().
|
||||||
„/app/media-boese/x" beginnt mit „/app/media", liegt aber ausserhalb."""
|
„/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb."""
|
||||||
import tasks
|
import tasks
|
||||||
|
|
||||||
assert tasks.unter_wurzel("/app/media", "/app/media") is True
|
assert tasks.unter_wurzel("/app/media", "/app/media") is True
|
||||||
@@ -155,7 +155,7 @@ def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
|||||||
assert tasks.unter_wurzel("/etc/passwd", "/app/media") is False
|
assert tasks.unter_wurzel("/etc/passwd", "/app/media") is False
|
||||||
assert tasks.unter_wurzel("", "/app/media") is False
|
assert tasks.unter_wurzel("", "/app/media") is False
|
||||||
assert tasks.unter_wurzel("/app/media", "") is False
|
assert tasks.unter_wurzel("/app/media", "") is False
|
||||||
# Wurzel mit Schrägstrich am Ende darf nichts aendern
|
# Wurzel mit Schrägstrich am Ende darf nichts ändern
|
||||||
assert tasks.unter_wurzel("/app/media/movies", "/app/media/") is True
|
assert tasks.unter_wurzel("/app/media/movies", "/app/media/") is True
|
||||||
|
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ def test_zielbasis_lehnt_praefix_ausbruch_ab():
|
|||||||
import tasks
|
import tasks
|
||||||
|
|
||||||
assert tasks._zielbasis("/app/media/movies", "bluray") == "/app/media/movies"
|
assert tasks._zielbasis("/app/media/movies", "bluray") == "/app/media/movies"
|
||||||
# Ausbruch per Praefix-Namen faellt auf den Standard zurueck
|
# Ausbruch per Praefix-Namen fällt auf den Standard zurück
|
||||||
assert tasks._zielbasis("/app/media-boese", "bluray") != "/app/media-boese"
|
assert tasks._zielbasis("/app/media-boese", "bluray") != "/app/media-boese"
|
||||||
assert tasks._zielbasis("/etc", "bluray") != "/etc"
|
assert tasks._zielbasis("/etc", "bluray") != "/etc"
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""Tests fuer _original_aufheben — der Pfad, der am 25.07.2026 die Platte fuellte.
|
"""Tests für _original_aufheben — der Pfad, der am 25.07.2026 die Platte füllte.
|
||||||
|
|
||||||
Der Kern: Es wird NICHT vorhergesagt, ob umgehaengt werden kann, sondern
|
Der Kern: Es wird NICHT vorhergesagt, ob umgehaengt werden kann, sondern
|
||||||
os.rename versucht. Die alte Fassung verglich st_dev und lag damit falsch —
|
os.rename versucht. Die alte Fassung verglich st_dev und lag damit falsch —
|
||||||
auf der Rippy-VM sind st_dev von /app/temp und /app/media identisch (2050),
|
auf der Rippy-VM sind st_dev von /app/temp und /app/media identisch (2050),
|
||||||
os.rename scheitert zwischen ihnen trotzdem mit EXDEV, weil der Kernel den
|
os.rename scheitert zwischen ihnen trotzdem mit EXDEV, weil der Kernel den
|
||||||
Mount vergleicht und nicht das Geraet. Die Platzpruefung wurde deshalb
|
Mount vergleicht und nicht das Gerät. Die Platzpruefung wurde deshalb
|
||||||
uebersprungen und shutil.move kopierte 75 GB bei 37 GB frei.
|
übersprungen und shutil.move kopierte 75 GB bei 37 GB frei.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
@@ -46,7 +46,7 @@ def _lege_rohdaten_an(tmp_path, groesse=2048):
|
|||||||
|
|
||||||
|
|
||||||
def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
|
def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
|
||||||
"""Der gute Fall: rename klappt, nichts wird kopiert, kein Platz noetig."""
|
"""Der gute Fall: rename klappt, nichts wird kopiert, kein Platz nötig."""
|
||||||
raw, final = _lege_rohdaten_an(tmp_path)
|
raw, final = _lege_rohdaten_an(tmp_path)
|
||||||
|
|
||||||
tasks._original_aufheben("job-1", raw, final)
|
tasks._original_aufheben("job-1", raw, final)
|
||||||
@@ -58,9 +58,9 @@ def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
|
|||||||
|
|
||||||
|
|
||||||
def test_bei_exdev_und_zu_wenig_platz_wird_nur_gewarnt(tmp_path, fake_db, monkeypatch):
|
def test_bei_exdev_und_zu_wenig_platz_wird_nur_gewarnt(tmp_path, fake_db, monkeypatch):
|
||||||
"""Der Fall, der die Platte fuellte: rename geht nicht, Platz reicht nicht.
|
"""Der Fall, der die Platte füllte: rename geht nicht, Platz reicht nicht.
|
||||||
|
|
||||||
Vorher lief hier eine Vollkopie an, weil die st_dev-Pruefung „gleiches
|
Vorher lief hier eine Vollkopie an, weil die st_dev-Prüfung „gleiches
|
||||||
Dateisystem" meldete und die Platzpruefung deshalb ausblieb.
|
Dateisystem" meldete und die Platzpruefung deshalb ausblieb.
|
||||||
"""
|
"""
|
||||||
raw, final = _lege_rohdaten_an(tmp_path)
|
raw, final = _lege_rohdaten_an(tmp_path)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ def test_parse_msg_trennt_code_und_klartext():
|
|||||||
|
|
||||||
def test_parse_msg_liest_die_uhd_fehlermeldung():
|
def test_parse_msg_liest_die_uhd_fehlermeldung():
|
||||||
"""3303 ist der Befund, um den es beim ganzen KEYDB-Thema geht: das
|
"""3303 ist der Befund, um den es beim ganzen KEYDB-Thema geht: das
|
||||||
Laufwerk laeuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel
|
Laufwerk läuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel
|
||||||
DIESER Pressung nicht. Ohne diese Zeile im Log raet der Commander."""
|
DIESER Pressung nicht. Ohne diese Zeile im Log raet der Commander."""
|
||||||
assert parse_msg(
|
assert parse_msg(
|
||||||
'MSG:3303,16777216,0,"The volume key is unknown for this disc - video can\'t be decrypted","The volume key is unknown for this disc - video can\'t be decrypted"'
|
'MSG:3303,16777216,0,"The volume key is unknown for this disc - video can\'t be decrypted","The volume key is unknown for this disc - video can\'t be decrypted"'
|
||||||
@@ -125,9 +125,9 @@ def test_parse_msg_liest_die_uhd_fehlermeldung():
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
|
def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
|
||||||
"""DER Grund fuer die Regex (Stand 25.07.2026): vorher stand hier
|
"""DER Grund für die Regex (Stand 25.07.2026): vorher stand hier
|
||||||
line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma
|
line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma
|
||||||
enthaelt — und MakeMKV schreibt solche laufend. Im Log stand dann nur noch
|
enthält — und MakeMKV schreibt solche laufend. Im Log stand dann nur noch
|
||||||
ein Satzfragment, das mehr verwirrt als hilft."""
|
ein Satzfragment, das mehr verwirrt als hilft."""
|
||||||
zeile = (
|
zeile = (
|
||||||
'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than '
|
'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than '
|
||||||
@@ -142,7 +142,7 @@ def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_msg_ignoriert_fremde_zeilen():
|
def test_parse_msg_ignoriert_fremde_zeilen():
|
||||||
# Alles ausser MSG muss None liefern, sonst landet Fortschritts-Rauschen
|
# Alles außer MSG muss None liefern, sonst landet Fortschritts-Rauschen
|
||||||
# (PRGV kommt mehrmals pro Sekunde) als Log-Eintrag in der Datenbank.
|
# (PRGV kommt mehrmals pro Sekunde) als Log-Eintrag in der Datenbank.
|
||||||
assert parse_msg("PRGV:100,32768,65536") is None
|
assert parse_msg("PRGV:100,32768,65536") is None
|
||||||
assert parse_msg('DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT","AKIRA","/dev/sr0"') is None
|
assert parse_msg('DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT","AKIRA","/dev/sr0"') is None
|
||||||
@@ -188,8 +188,8 @@ def test_abcde_config_enthaelt_zielverzeichnis():
|
|||||||
|
|
||||||
|
|
||||||
def test_preset_fuer_nimmt_das_preset_des_disc_typs():
|
def test_preset_fuer_nimmt_das_preset_des_disc_typs():
|
||||||
# Befund 25.07.2026: vorher galt EIN Preset fuer alles — eine 4K-UHD wurde
|
# Befund 25.07.2026: vorher galt EIN Preset für alles — eine 4K-UHD wurde
|
||||||
# damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip waere
|
# damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip wäre
|
||||||
# die 4K-Aufloesung still verlorengegangen.
|
# die 4K-Aufloesung still verlorengegangen.
|
||||||
from ripping import preset_fuer
|
from ripping import preset_fuer
|
||||||
|
|
||||||
@@ -206,13 +206,13 @@ def test_preset_fuer_nimmt_das_preset_des_disc_typs():
|
|||||||
|
|
||||||
def test_preset_fuer_faellt_auf_das_allgemeine_preset_zurueck():
|
def test_preset_fuer_faellt_auf_das_allgemeine_preset_zurueck():
|
||||||
# Bestandsinstallationen kennen die drei neuen Felder nicht. Solange der
|
# Bestandsinstallationen kennen die drei neuen Felder nicht. Solange der
|
||||||
# Nutzer sie nicht speichert, MUSS sich sein Verhalten nicht aendern.
|
# Nutzer sie nicht speichert, MUSS sich sein Verhalten nicht ändern.
|
||||||
from ripping import preset_fuer
|
from ripping import preset_fuer
|
||||||
|
|
||||||
alt = {"transcodePreset": "HQ 1080p30 Surround"}
|
alt = {"transcodePreset": "HQ 1080p30 Surround"}
|
||||||
assert preset_fuer("uhd", alt) == "HQ 1080p30 Surround"
|
assert preset_fuer("uhd", alt) == "HQ 1080p30 Surround"
|
||||||
assert preset_fuer("dvd", alt) == "HQ 1080p30 Surround"
|
assert preset_fuer("dvd", alt) == "HQ 1080p30 Surround"
|
||||||
# Leere Zeichenkette zaehlt als "nicht gesetzt" (leeres Select-Feld im UI)
|
# Leere Zeichenkette zählt als "nicht gesetzt" (leeres Select-Feld im UI)
|
||||||
assert preset_fuer("uhd", {"transcodePresetUhd": " ", "transcodePreset": "X"}) == "X"
|
assert preset_fuer("uhd", {"transcodePresetUhd": " ", "transcodePreset": "X"}) == "X"
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ def test_handbrake_prueft_abbruch_bei_jeder_zeile_nicht_nur_bei_fortschritt():
|
|||||||
|
|
||||||
assert ergebnis["status"] == "cancelled"
|
assert ergebnis["status"] == "cancelled"
|
||||||
assert prozess.getoetet is True
|
assert prozess.getoetet is True
|
||||||
# Zwei Scan-Zeilen genuegten — es musste NICHT auf eine Encode-Zeile gewartet
|
# Zwei Scan-Zeilen genügten — es musste NICHT auf eine Encode-Zeile gewartet
|
||||||
# werden. Genau das war der Fehler.
|
# werden. Genau das war der Fehler.
|
||||||
assert len(gesehen) == 2
|
assert len(gesehen) == 2
|
||||||
|
|
||||||
@@ -279,16 +279,16 @@ def test_komprimieren_fuer_globaler_schalter_schlaegt_alles():
|
|||||||
|
|
||||||
assert komprimieren_fuer("uhd", {"transcodeEnabled": False}) is False
|
assert komprimieren_fuer("uhd", {"transcodeEnabled": False}) is False
|
||||||
assert komprimieren_fuer("dvd", {"transcodeEnabled": False}) is False
|
assert komprimieren_fuer("dvd", {"transcodeEnabled": False}) is False
|
||||||
# Standard ist AN - Bestandsinstallationen aendern ihr Verhalten nicht
|
# Standard ist AN - Bestandsinstallationen ändern ihr Verhalten nicht
|
||||||
assert komprimieren_fuer("bluray", {}) is True
|
assert komprimieren_fuer("bluray", {}) is True
|
||||||
assert komprimieren_fuer("bluray", None) is True
|
assert komprimieren_fuer("bluray", None) is True
|
||||||
|
|
||||||
|
|
||||||
def test_komprimieren_fuer_einzelner_disc_typ_abwaehlbar():
|
def test_komprimieren_fuer_einzelner_disc_typ_abwaehlbar():
|
||||||
"""Der Grund dafuer (Befund 25.07.2026): 4K-HEVC brauchte auf der Rippy-VM
|
"""Der Grund dafür (Befund 25.07.2026): 4K-HEVC brauchte auf der Rippy-VM
|
||||||
gemessene 28-55 Stunden, weil die CPU kein AVX2 hat. Vorher gab es nur
|
gemessene 28-55 Stunden, weil die CPU kein AVX2 hat. Vorher gab es nur
|
||||||
einen globalen Schalter - wer 4K verlustfrei behalten und DVDs trotzdem
|
einen globalen Schalter - wer 4K verlustfrei behalten und DVDs trotzdem
|
||||||
schrumpfen wollte, hatte keine Moeglichkeit."""
|
schrumpfen wollte, hatte keine Möglichkeit."""
|
||||||
from ripping import PRESET_KEINE, komprimieren_fuer
|
from ripping import PRESET_KEINE, komprimieren_fuer
|
||||||
|
|
||||||
nur_uhd_aus = {"transcodePresetUhd": PRESET_KEINE,
|
nur_uhd_aus = {"transcodePresetUhd": PRESET_KEINE,
|
||||||
@@ -299,20 +299,20 @@ def test_komprimieren_fuer_einzelner_disc_typ_abwaehlbar():
|
|||||||
|
|
||||||
|
|
||||||
def test_preset_fuer_gibt_den_reservewert_nie_als_preset_zurueck():
|
def test_preset_fuer_gibt_den_reservewert_nie_als_preset_zurueck():
|
||||||
"""Sonst bekaeme HandBrake `--preset keine` und wuerde scheitern. Wer ueber
|
"""Sonst bekaeme HandBrake `--preset keine` und würde scheitern. Wer über
|
||||||
"Neu komprimieren" ausdruecklich doch komprimieren will, soll ein
|
"Neu komprimieren" ausdruecklich doch komprimieren will, soll ein
|
||||||
brauchbares Preset bekommen statt eines Fehlers."""
|
brauchbares Preset bekommen statt eines Fehlers."""
|
||||||
from ripping import DEFAULT_HB_PRESET, PRESET_KEINE, preset_fuer
|
from ripping import DEFAULT_HB_PRESET, PRESET_KEINE, preset_fuer
|
||||||
|
|
||||||
assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE,
|
assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE,
|
||||||
"transcodePreset": "HQ 1080p30 Surround"}) == "HQ 1080p30 Surround"
|
"transcodePreset": "HQ 1080p30 Surround"}) == "HQ 1080p30 Surround"
|
||||||
# Auch ohne allgemeines Preset kommt ein gueltiger Name heraus
|
# Auch ohne allgemeines Preset kommt ein gültiger Name heraus
|
||||||
assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE}) == DEFAULT_HB_PRESET
|
assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE}) == DEFAULT_HB_PRESET
|
||||||
|
|
||||||
|
|
||||||
def test_reservewert_kollidiert_mit_keinem_echten_preset():
|
def test_reservewert_kollidiert_mit_keinem_echten_preset():
|
||||||
"""Gegengeprueft gegen alle 90 Presets aus `HandBrakeCLI --preset-list`
|
"""Gegengeprueft gegen alle 90 Presets aus `HandBrakeCLI --preset-list`
|
||||||
im Worker-Image (25.07.2026) - keines heisst so."""
|
im Worker-Image (25.07.2026) - keines heißt so."""
|
||||||
from ripping import PRESET_KEINE
|
from ripping import PRESET_KEINE
|
||||||
|
|
||||||
assert PRESET_KEINE == "keine"
|
assert PRESET_KEINE == "keine"
|
||||||
|
|||||||
+39
-39
@@ -3,15 +3,15 @@
|
|||||||
# Rippy einrichten — ein Befehl, keine Handarbeit.
|
# Rippy einrichten — ein Befehl, keine Handarbeit.
|
||||||
#
|
#
|
||||||
# Das Skript nimmt genau die Schritte ab, an denen man vorher scheitern konnte:
|
# Das Skript nimmt genau die Schritte ab, an denen man vorher scheitern konnte:
|
||||||
# * Laufwerk finden (BEIDE Geraeteknoten — der sg-Knoten ist je Host anders,
|
# * Laufwerk finden (BEIDE Geräteknoten — der sg-Knoten ist je Host anders,
|
||||||
# und ein fest eingetragener Standard ist damit auf fremden Hosts falsch)
|
# und ein fest eingetragener Standard ist damit auf fremden Hosts falsch)
|
||||||
# * Ablage- und MakeMKV-Verzeichnis anlegen
|
# * Ablage- und MakeMKV-Verzeichnis anlegen
|
||||||
# * shared-Mount-Propagation herstellen, falls Docker sie verlangt
|
# * shared-Mount-Propagation herstellen, falls Docker sie verlangt
|
||||||
# * .env anlegen bzw. fehlende Werte ergaenzen, ohne Bestehendes zu ueberschreiben
|
# * .env anlegen bzw. fehlende Werte ergänzen, ohne Bestehendes zu überschreiben
|
||||||
# * bauen und starten
|
# * bauen und starten
|
||||||
#
|
#
|
||||||
# Es ist absichtlich WIEDERHOLBAR: mehrfach ausfuehren aendert nichts kaputt.
|
# Es ist absichtlich WIEDERHOLBAR: mehrfach ausführen ändert nichts kaputt.
|
||||||
# Es aendert NIE einen Wert, der schon in der .env steht.
|
# Es ändert NIE einen Wert, der schon in der .env steht.
|
||||||
#
|
#
|
||||||
# Aufruf: sudo ./install.sh
|
# Aufruf: sudo ./install.sh
|
||||||
# sudo ./install.sh --nur-pruefen (nichts anfassen, nur berichten)
|
# sudo ./install.sh --nur-pruefen (nichts anfassen, nur berichten)
|
||||||
@@ -33,7 +33,7 @@ titel() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
|||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
echo " Rippy — Einrichtung"
|
echo " Rippy — Einrichtung"
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
[ "$NUR_PRUEFEN" = 1 ] && gelb "Nur-Pruefen-Modus: es wird nichts geaendert."
|
[ "$NUR_PRUEFEN" = 1 ] && gelb "Nur-Prüfen-Modus: es wird nichts geändert."
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 1. Voraussetzungen
|
# 1. Voraussetzungen
|
||||||
@@ -43,7 +43,7 @@ titel "1/6 Voraussetzungen"
|
|||||||
if [ "$(uname -s)" != "Linux" ]; then
|
if [ "$(uname -s)" != "Linux" ]; then
|
||||||
rot " X Das hier ist kein Linux. Rippy braucht einen Linux-Host mit Docker."
|
rot " X Das hier ist kein Linux. Rippy braucht einen Linux-Host mit Docker."
|
||||||
rot " (Docker Desktop unter Windows/macOS kann keine optischen Laufwerke"
|
rot " (Docker Desktop unter Windows/macOS kann keine optischen Laufwerke"
|
||||||
rot " durchreichen — deshalb geht es dort grundsaetzlich nicht.)"
|
rot " durchreichen — deshalb geht es dort grundsätzlich nicht.)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
gruen " OK Linux erkannt ($(uname -r))"
|
gruen " OK Linux erkannt ($(uname -r))"
|
||||||
@@ -71,14 +71,14 @@ fi
|
|||||||
if [ "$(id -u)" = "0" ]; then
|
if [ "$(id -u)" = "0" ]; then
|
||||||
gruen " OK Als root gestartet"
|
gruen " OK Als root gestartet"
|
||||||
elif [ "$NUR_PRUEFEN" = 1 ]; then
|
elif [ "$NUR_PRUEFEN" = 1 ]; then
|
||||||
# Der Pruef-Modus aendert nichts und darf deshalb NICHT an root scheitern —
|
# Der Prüf-Modus ändert nichts und darf deshalb NICHT an root scheitern —
|
||||||
# sonst kann man vor der Installation nicht nachsehen, ob alles passt.
|
# sonst kann man vor der Installation nicht nachsehen, ob alles passt.
|
||||||
gelb " ! Ohne root — fuer die Pruefung in Ordnung. Die echte Installation"
|
gelb " ! Ohne root — für die Prüfung in Ordnung. Die echte Installation"
|
||||||
gelb " braucht sudo (Verzeichnisse unter /srv, Mount-Propagation)."
|
gelb " braucht sudo (Verzeichnisse unter /srv, Mount-Propagation)."
|
||||||
else
|
else
|
||||||
rot " X Bitte mit sudo starten: sudo ./install.sh"
|
rot " X Bitte mit sudo starten: sudo ./install.sh"
|
||||||
rot " (Verzeichnisse unter /srv und die Mount-Propagation brauchen root.)"
|
rot " (Verzeichnisse unter /srv und die Mount-Propagation brauchen root.)"
|
||||||
rot " Nur nachsehen, ohne etwas zu aendern: ./install.sh --nur-pruefen"
|
rot " Nur nachsehen, ohne etwas zu ändern: ./install.sh --nur-pruefen"
|
||||||
FEHLER=1
|
FEHLER=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -89,11 +89,11 @@ fi
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
titel "2/6 Optisches Laufwerk"
|
titel "2/6 Optisches Laufwerk"
|
||||||
|
|
||||||
# MakeMKV spricht Laufwerke ueber die SCSI-Generic-Schicht an und braucht
|
# MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an und braucht
|
||||||
# deshalb ZWEI Knoten: /dev/srN und den passenden /dev/sgM. Welche sg-Nummer
|
# deshalb ZWEI Knoten: /dev/srN und den passenden /dev/sgM. Welche sg-Nummer
|
||||||
# dazugehoert, ist je Host anders — deshalb wird sie hier ueber die
|
# dazugehört, ist je Host anders — deshalb wird sie hier über die
|
||||||
# SCSI-Adresse ermittelt statt geraten. Beide Knoten zeigen in /sys auf
|
# SCSI-Adresse ermittelt statt geraten. Beide Knoten zeigen in /sys auf
|
||||||
# dasselbe Geraeteverzeichnis (z. B. 3:0:0:0), das ist der Abgleich.
|
# dasselbe Geräteverzeichnis (z. B. 3:0:0:0), das ist der Abgleich.
|
||||||
SR=""
|
SR=""
|
||||||
SG=""
|
SG=""
|
||||||
LAUFWERK=""
|
LAUFWERK=""
|
||||||
@@ -118,14 +118,14 @@ if [ -n "$SR" ]; then
|
|||||||
gruen " $SR + $SG (SCSI-Adresse abgeglichen, nicht geraten)"
|
gruen " $SR + $SG (SCSI-Adresse abgeglichen, nicht geraten)"
|
||||||
else
|
else
|
||||||
gelb " ! Kein optisches Laufwerk gefunden."
|
gelb " ! Kein optisches Laufwerk gefunden."
|
||||||
gelb " Rippy laesst sich trotzdem installieren und kann dann als reine"
|
gelb " Rippy lässt sich trotzdem installieren und kann dann als reine"
|
||||||
gelb " KOMPRIMIER-Maschine fuer andere Rippy-Instanzen dienen."
|
gelb " KOMPRIMIER-Maschine für andere Rippy-Instanzen dienen."
|
||||||
gelb " Zum Rippen fehlt das Laufwerk. Haeufigste Ursachen:"
|
gelb " Zum Rippen fehlt das Laufwerk. Häufigste Ursachen:"
|
||||||
gelb " - In einer VM: Laufwerk per USB-Passthrough durchreichen"
|
gelb " - In einer VM: Laufwerk per USB-Passthrough durchreichen"
|
||||||
gelb " (Proxmox: qm set <vmid> -usb0 host=<hersteller>:<produkt>,usb3=1)"
|
gelb " (Proxmox: qm set <vmid> -usb0 host=<hersteller>:<produkt>,usb3=1)"
|
||||||
gelb " NICHT als emuliertes CD-ROM (media=cdrom) — das kann keine"
|
gelb " NICHT als emuliertes CD-ROM (media=cdrom) — das kann keine"
|
||||||
gelb " SCSI-Kommandos und MakeMKV sieht es nie."
|
gelb " SCSI-Kommandos und MakeMKV sieht es nie."
|
||||||
gelb " - Auf echter Hardware: 'ls /dev/sr*' pruefen, Kabel/Strom."
|
gelb " - Auf echter Hardware: 'ls /dev/sr*' prüfen, Kabel/Strom."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -136,20 +136,20 @@ for d in "$MEDIA_DIR" "$MAKEMKV_DIR"; do
|
|||||||
if [ -d "$d" ]; then
|
if [ -d "$d" ]; then
|
||||||
gruen " OK $d liegt schon da"
|
gruen " OK $d liegt schon da"
|
||||||
elif [ "$NUR_PRUEFEN" = 1 ]; then
|
elif [ "$NUR_PRUEFEN" = 1 ]; then
|
||||||
gelb " ! $d fehlt (wuerde angelegt)"
|
gelb " ! $d fehlt (würde angelegt)"
|
||||||
else
|
else
|
||||||
mkdir -p "$d" && gruen " OK $d angelegt"
|
mkdir -p "$d" && gruen " OK $d angelegt"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo " Ablage: $MEDIA_DIR (hier landen die fertigen Filme)"
|
echo " Ablage: $MEDIA_DIR (hier landen die fertigen Filme)"
|
||||||
echo " MakeMKV-Daten: $MAKEMKV_DIR (Schluesselspeicher, rebuild-fest)"
|
echo " MakeMKV-Daten: $MAKEMKV_DIR (Schlüsselspeicher, rebuild-fest)"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4. Mount-Propagation
|
# 4. Mount-Propagation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
titel "4/6 Mount-Propagation (fuer NAS-Freigaben aus dem UI)"
|
titel "4/6 Mount-Propagation (für NAS-Freigaben aus dem UI)"
|
||||||
|
|
||||||
# Rippy haengt NAS-Freigaben aus dem UI ein. Damit die auch in den anderen
|
# Rippy hängt NAS-Freigaben aus dem UI ein. Damit die auch in den anderen
|
||||||
# Containern sichtbar werden, muss der Ablage-Pfad ein 'shared' Mount sein.
|
# Containern sichtbar werden, muss der Ablage-Pfad ein 'shared' Mount sein.
|
||||||
# Auf den meisten systemd-Hosts ist / schon rshared und es ist nichts zu tun.
|
# Auf den meisten systemd-Hosts ist / schon rshared und es ist nichts zu tun.
|
||||||
propagation_ok() {
|
propagation_ok() {
|
||||||
@@ -159,19 +159,19 @@ propagation_ok() {
|
|||||||
if propagation_ok; then
|
if propagation_ok; then
|
||||||
gruen " OK $MEDIA_DIR ist bereits 'shared' — nichts zu tun"
|
gruen " OK $MEDIA_DIR ist bereits 'shared' — nichts zu tun"
|
||||||
elif [ "$NUR_PRUEFEN" = 1 ]; then
|
elif [ "$NUR_PRUEFEN" = 1 ]; then
|
||||||
gelb " ! Propagation fehlt (wuerde eingerichtet)"
|
gelb " ! Propagation fehlt (würde eingerichtet)"
|
||||||
else
|
else
|
||||||
mount --bind "$MEDIA_DIR" "$MEDIA_DIR" 2>/dev/null
|
mount --bind "$MEDIA_DIR" "$MEDIA_DIR" 2>/dev/null
|
||||||
mount --make-rshared "$MEDIA_DIR" 2>/dev/null
|
mount --make-rshared "$MEDIA_DIR" 2>/dev/null
|
||||||
if propagation_ok; then
|
if propagation_ok; then
|
||||||
gruen " OK Propagation eingerichtet"
|
gruen " OK Propagation eingerichtet"
|
||||||
# Neustart-fest machen, sonst ist es nach dem naechsten Reboot wieder weg
|
# Neustart-fest machen, sonst ist es nach dem nächsten Reboot wieder weg
|
||||||
# und das Einhaengen aus dem UI scheitert stillschweigend.
|
# und das Einhängen aus dem UI scheitert stillschweigend.
|
||||||
UNIT="/etc/systemd/system/$(systemd-escape -p --suffix=mount "$MEDIA_DIR" 2>/dev/null)"
|
UNIT="/etc/systemd/system/$(systemd-escape -p --suffix=mount "$MEDIA_DIR" 2>/dev/null)"
|
||||||
if [ -n "${UNIT:-}" ] && [ ! -e "$UNIT" ] && command -v systemctl >/dev/null 2>&1; then
|
if [ -n "${UNIT:-}" ] && [ ! -e "$UNIT" ] && command -v systemctl >/dev/null 2>&1; then
|
||||||
cat > "$UNIT" <<UNITEOF
|
cat > "$UNIT" <<UNITEOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Rippy-Ablage als shared mount (fuer NAS-Freigaben aus dem UI)
|
Description=Rippy-Ablage als shared mount (für NAS-Freigaben aus dem UI)
|
||||||
After=local-fs.target
|
After=local-fs.target
|
||||||
|
|
||||||
[Mount]
|
[Mount]
|
||||||
@@ -187,12 +187,12 @@ UNITEOF
|
|||||||
systemctl enable "$(basename "$UNIT")" >/dev/null 2>&1 \
|
systemctl enable "$(basename "$UNIT")" >/dev/null 2>&1 \
|
||||||
&& gruen " OK Neustart-fest gemacht ($(basename "$UNIT"))" \
|
&& gruen " OK Neustart-fest gemacht ($(basename "$UNIT"))" \
|
||||||
|| gelb " ! Konnte nicht neustart-fest gemacht werden — nach einem Reboot"
|
|| gelb " ! Konnte nicht neustart-fest gemacht werden — nach einem Reboot"
|
||||||
gelb " ggf. dieses Skript erneut ausfuehren."
|
gelb " ggf. dieses Skript erneut ausführen."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
gelb " ! Propagation liess sich nicht einrichten."
|
gelb " ! Propagation liess sich nicht einrichten."
|
||||||
gelb " Rippy laeuft trotzdem — nur das Einhaengen von NAS-Freigaben AUS DEM UI"
|
gelb " Rippy läuft trotzdem — nur das Einhängen von NAS-Freigaben AUS DEM UI"
|
||||||
gelb " funktioniert dann nicht. Freigaben stattdessen auf dem Host einhaengen,"
|
gelb " funktioniert dann nicht. Freigaben stattdessen auf dem Host einhängen,"
|
||||||
gelb " z. B. unter $MEDIA_DIR/nas. Betrifft nur klassisches Docker mit"
|
gelb " z. B. unter $MEDIA_DIR/nas. Betrifft nur klassisches Docker mit"
|
||||||
gelb " SYS_ADMIN — nicht rootless/Podman."
|
gelb " SYS_ADMIN — nicht rootless/Podman."
|
||||||
fi
|
fi
|
||||||
@@ -207,7 +207,7 @@ cd "$(dirname "$0")" || exit 1
|
|||||||
|
|
||||||
if [ ! -f .env ]; then
|
if [ ! -f .env ]; then
|
||||||
if [ "$NUR_PRUEFEN" = 1 ]; then
|
if [ "$NUR_PRUEFEN" = 1 ]; then
|
||||||
gelb " ! .env fehlt (wuerde aus .env.example erzeugt)"
|
gelb " ! .env fehlt (würde aus .env.example erzeugt)"
|
||||||
else
|
else
|
||||||
cp .env.example .env && gruen " OK .env aus .env.example erzeugt"
|
cp .env.example .env && gruen " OK .env aus .env.example erzeugt"
|
||||||
fi
|
fi
|
||||||
@@ -218,10 +218,10 @@ fi
|
|||||||
# Setzt einen Wert NUR, wenn er noch nicht (unkommentiert) gesetzt ist.
|
# Setzt einen Wert NUR, wenn er noch nicht (unkommentiert) gesetzt ist.
|
||||||
setze_wenn_leer() {
|
setze_wenn_leer() {
|
||||||
schluessel="$1"; wert="$2"
|
schluessel="$1"; wert="$2"
|
||||||
[ "$NUR_PRUEFEN" = 1 ] && { echo " wuerde setzen: $schluessel=$wert"; return; }
|
[ "$NUR_PRUEFEN" = 1 ] && { echo " würde setzen: $schluessel=$wert"; return; }
|
||||||
[ -f .env ] || return
|
[ -f .env ] || return
|
||||||
if grep -qE "^${schluessel}=.+" .env; then
|
if grep -qE "^${schluessel}=.+" .env; then
|
||||||
echo " $schluessel steht schon in der .env — unveraendert gelassen"
|
echo " $schluessel steht schon in der .env — unverändert gelassen"
|
||||||
else
|
else
|
||||||
sed -i "/^#*${schluessel}=/d" .env
|
sed -i "/^#*${schluessel}=/d" .env
|
||||||
printf '%s=%s\n' "$schluessel" "$wert" >> .env
|
printf '%s=%s\n' "$schluessel" "$wert" >> .env
|
||||||
@@ -236,9 +236,9 @@ fi
|
|||||||
setze_wenn_leer MAKEMKV_DATA_HOST "$MAKEMKV_DIR"
|
setze_wenn_leer MAKEMKV_DATA_HOST "$MAKEMKV_DIR"
|
||||||
|
|
||||||
echo " API-Keys (TMDB/OMDb) brauchst du hier NICHT einzutragen —"
|
echo " API-Keys (TMDB/OMDb) brauchst du hier NICHT einzutragen —"
|
||||||
echo " der Einrichtungs-Assistent im Browser fragt sie ab und prueft sie."
|
echo " der Einrichtungs-Assistent im Browser fragt sie ab und prüft sie."
|
||||||
|
|
||||||
# Gegenprobe: Zeigen die WIRKSAMEN Werte auf Geraete, die es hier gibt? Faengt
|
# Gegenprobe: Zeigen die WIRKSAMEN Werte auf Geräte, die es hier gibt? Fängt
|
||||||
# den Fall "'.env' von einem anderen Rechner kopiert" und Tippfehler ab — sonst
|
# den Fall "'.env' von einem anderen Rechner kopiert" und Tippfehler ab — sonst
|
||||||
# startet Docker mit einem Pfad, den es nicht gibt, und die Meldung ist kryptisch.
|
# startet Docker mit einem Pfad, den es nicht gibt, und die Meldung ist kryptisch.
|
||||||
if [ -f .env ]; then
|
if [ -f .env ]; then
|
||||||
@@ -247,10 +247,10 @@ if [ -f .env ]; then
|
|||||||
wirksam=$(grep -E "^${schluessel}=" .env 2>/dev/null | tail -1 | cut -d= -f2-)
|
wirksam=$(grep -E "^${schluessel}=" .env 2>/dev/null | tail -1 | cut -d= -f2-)
|
||||||
[ -n "${wirksam:-}" ] || wirksam="$standard"
|
[ -n "${wirksam:-}" ] || wirksam="$standard"
|
||||||
if [ ! -e "$wirksam" ]; then
|
if [ ! -e "$wirksam" ]; then
|
||||||
gelb " ! $schluessel zeigt auf $wirksam — dieses Geraet gibt es hier NICHT."
|
gelb " ! $schluessel zeigt auf $wirksam — dieses Gerät gibt es hier NICHT."
|
||||||
if [ -n "$SR" ]; then
|
if [ -n "$SR" ]; then
|
||||||
gelb " Gefunden wurde stattdessen: $SR + $SG"
|
gelb " Gefunden wurde stattdessen: $SR + $SG"
|
||||||
gelb " Trage das in der .env ein (oder loesche die Zeile $schluessel=)."
|
gelb " Trage das in der .env ein (oder lösche die Zeile $schluessel=)."
|
||||||
else
|
else
|
||||||
gelb " Es wurde auch kein anderes Laufwerk gefunden — siehe Schritt 2."
|
gelb " Es wurde auch kein anderes Laufwerk gefunden — siehe Schritt 2."
|
||||||
fi
|
fi
|
||||||
@@ -264,9 +264,9 @@ fi
|
|||||||
titel "6/6 Bauen und starten"
|
titel "6/6 Bauen und starten"
|
||||||
|
|
||||||
if [ "$NUR_PRUEFEN" = 1 ]; then
|
if [ "$NUR_PRUEFEN" = 1 ]; then
|
||||||
gelb " ! Uebersprungen (Nur-Pruefen-Modus)."
|
gelb " ! Übersprungen (Nur-Prüfen-Modus)."
|
||||||
echo ""
|
echo ""
|
||||||
gruen "Pruefung fertig. Ohne --nur-pruefen wuerde jetzt gebaut und gestartet."
|
gruen "Prüfung fertig. Ohne --nur-pruefen würde jetzt gebaut und gestartet."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ echo " Der erste Bau dauert je nach Internet 3-10 Minuten. Nur beim ersten
|
|||||||
echo ""
|
echo ""
|
||||||
if ! $COMPOSE up -d --build; then
|
if ! $COMPOSE up -d --build; then
|
||||||
rot ""
|
rot ""
|
||||||
rot "Der Start ist gescheitert. Die haeufigsten Ursachen:"
|
rot "Der Start ist gescheitert. Die häufigsten Ursachen:"
|
||||||
rot " - Kein Platz auf der Platte: df -h /"
|
rot " - Kein Platz auf der Platte: df -h /"
|
||||||
rot " - Port 80 schon belegt: ss -tlnp | grep ':80 '"
|
rot " - Port 80 schon belegt: ss -tlnp | grep ':80 '"
|
||||||
rot " - Download von makemkv.com gedrosselt (Cloudflare): Tarballs von"
|
rot " - Download von makemkv.com gedrosselt (Cloudflare): Tarballs von"
|
||||||
@@ -288,10 +288,10 @@ IP=$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oE 'src [0-9.]+' | awk '{print
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
gruen " Fertig. Rippy laeuft."
|
gruen " Fertig. Rippy läuft."
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Jetzt im Browser oeffnen: http://$IP"
|
echo " Jetzt im Browser öffnen: http://$IP"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Dort startet der Einrichtungs-Assistent. Er zeigt dir, ob Laufwerk,"
|
echo " Dort startet der Einrichtungs-Assistent. Er zeigt dir, ob Laufwerk,"
|
||||||
echo " Worker und Platz in Ordnung sind, und stellt die Kompression passend"
|
echo " Worker und Platz in Ordnung sind, und stellt die Kompression passend"
|
||||||
|
|||||||
Reference in New Issue
Block a user