From 35cfcbcb073bc3b930d83c902d2426324a4e5e0f Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 25 Jul 2026 22:37:23 +0200 Subject: [PATCH] style: echte Umlaute im ganzen Projekt + Installer im Rippy-Look MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 178 ++++---- deploy/worker-windows/RippyWorkerSetup.exe | Bin 57344 -> 60416 bytes deploy/worker-windows/build-exe.ps1 | 16 +- deploy/worker-windows/install-gui.ps1 | 226 ++++++---- deploy/worker-windows/install.ps1 | 422 +++++++++--------- docker/api/main.py | 8 +- docker/api/makemkv_daten.py | 88 ++-- docker/api/makemkv_key.py | 18 +- docker/api/mounts.py | 6 +- docker/api/test_api_smoke.py | 18 +- docker/api/test_makemkv_daten.py | 22 +- docker/api/test_makemkv_key.py | 6 +- docker/ui/src/components/FirstRunWizard.tsx | 14 +- docker/ui/src/components/WorkerVerwaltung.tsx | 2 +- docker/ui/src/pages/Settings.tsx | 38 +- docker/worker/db.py | 2 +- docker/worker/makemkv_daten.py | 88 ++-- docker/worker/ripping.py | 8 +- docker/worker/tasks.py | 2 +- docker/worker/test_makemkv_daten_worker.py | 42 +- docker/worker/test_medien.py | 18 +- docker/worker/test_original_aufheben.py | 12 +- docker/worker/test_ripping_helpers.py | 30 +- install.sh | 78 ++-- 24 files changed, 702 insertions(+), 640 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 67ddf42..7cf3e0a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,89 +1,89 @@ -# AGENTS.md — Arbeits-Konventionen für Rippy - -## Grundregeln - -**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. -**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. - -## ⛔ HARTE Regeln (mechanisch geprüft — seit 22.07.2026) - -**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 -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, -gestrichenes Muss-Feature → erst den Commander fragen, NIE still ersetzen. -(Vorgefallen: Muss-Feature „MakeMKV lossless" wurde still durch lossy HandBrake ersetzt.) - -**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 -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. -`./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. -(Vorgefallen: Doppel-Anlage `Rippy` + `rippy` auf der VM durch Freihand-Deploys.) - -**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. -(Vorgefallen: erfundene Celery-Methode `self.send_task`, erfundene abcde-Flags.) - -## Workflow - -1. **Read:** KONZEPT.md + ROADMAP.md lesen. Verstehen, welche Etappe dran ist. -2. **Plan:** Was genau soll diese Sitzung bauen? Eine Zeile. -3. **Build:** Code schreiben, testen, committen. -4. **Verify:** `docker compose up` — funktioniert das Ganze? -5. **Savepoint:** SAVEPOINT.md aktualisieren. Nächster Chat beginnt nicht von Null. - -## Modelle je Aufgabe - -- **Planung:** Heavy-Modell (Reasoning) für Architektur-Entscheidungen. -- **Bauen:** schnelles Coding-Modell. -- **Review:** Heavy nochmal für Code-Review vor Merge. - -## Docker-Praxis - -- Alles läuft in Containern: `docker compose up -d` -- Keine System-Pakete auf dem Host — alles im Container. -- Dockerfile immer multi-stage, kleinste Images. - -## Was NICHT gebaut wird - -- Kein Code direkt im Host-OS. -- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker. -- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf. - -## Aktueller Stand (25.07.2026) - -- ✅ **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, - echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis, - MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben -- ✅ **Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes - MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI -- ✅ **Etappe 18 (v3.11):** 4K-UHD gelöst — `makemkvcon` holt Schlüssel - unter Linux nie, unter Windows schon; Schlüsselspeicher übernehmbar. - Akira-UHD geht auf der VM auf (`TCOUNT:5`, bewiesen) -- ✅ **Etappe 19 (v3.14):** Durchsicht Frontend/Backend — vier Placebos weg - (Fortschritt log, Auswurf tat nichts, „Alle Tracks" konnte nichts, Encoder - wurden behauptet statt gemessen), Zombie-Erkennung gebaut, Pfad-Prüfung - 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 - (Ping im Hintergrund), Kompression je Disc-Typ abwählbar (4K verlustfrei), - Wizard empfiehlt nach gemessener CPU, vier tote Routen entfernt -- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe - -## Was diese Sitzungen wiederholt gekostet hat - -**Nicht aus einem Zustandswert auf einen Mechanismus schließen.** Vorgefallen: -aus „kein Schlüssel da" → „Server abgeschaltet" (falsch), aus Status -`transcoding` → „Celery hat neu zugestellt" (falsch), aus `progress=99` → -„Altwert aus dem Absturz" (falsch — ein Bug), aus gleichem `st_dev` → -„`os.rename` funktioniert" (falsch — der Kernel vergleicht den Mount). -Jedes Mal hätte eine Messung von unter einer Minute gereicht. - -**Und die Umkehrung gilt genauso:** gleiches `st_dev` heißt NICHT gleicher -Mount. Wo eine Eigenschaft ausprobierbar ist, probiere sie aus, statt sie -vorherzusagen. +# AGENTS.md — Arbeits-Konventionen für Rippy + +## Grundregeln + +**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. +**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. + +## ⛔ HARTE Regeln (mechanisch geprüft — seit 22.07.2026) + +**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 +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, +gestrichenes Muss-Feature → erst den Commander fragen, NIE still ersetzen. +(Vorgefallen: Muss-Feature „MakeMKV lossless" wurde still durch lossy HandBrake ersetzt.) + +**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 +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. +`./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. +(Vorgefallen: Doppel-Anlage `Rippy` + `rippy` auf der VM durch Freihand-Deploys.) + +**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. +(Vorgefallen: erfundene Celery-Methode `self.send_task`, erfundene abcde-Flags.) + +## Workflow + +1. **Read:** KONZEPT.md + ROADMAP.md lesen. Verstehen, welche Etappe dran ist. +2. **Plan:** Was genau soll diese Sitzung bauen? Eine Zeile. +3. **Build:** Code schreiben, testen, committen. +4. **Verify:** `docker compose up` — funktioniert das Ganze? +5. **Savepoint:** SAVEPOINT.md aktualisieren. Nächster Chat beginnt nicht von Null. + +## Modelle je Aufgabe + +- **Planung:** Heavy-Modell (Reasoning) für Architektur-Entscheidungen. +- **Bauen:** schnelles Coding-Modell. +- **Review:** Heavy nochmal für Code-Review vor Merge. + +## Docker-Praxis + +- Alles läuft in Containern: `docker compose up -d` +- Keine System-Pakete auf dem Host — alles im Container. +- Dockerfile immer multi-stage, kleinste Images. + +## Was NICHT gebaut wird + +- Kein Code direkt im Host-OS. +- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker. +- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf. + +## Aktueller Stand (25.07.2026) + +- ✅ **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, + echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis, + MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben +- ✅ **Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes + MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI +- ✅ **Etappe 18 (v3.11):** 4K-UHD gelöst — `makemkvcon` holt Schlüssel + unter Linux nie, unter Windows schon; Schlüsselspeicher übernehmbar. + Akira-UHD geht auf der VM auf (`TCOUNT:5`, bewiesen) +- ✅ **Etappe 19 (v3.14):** Durchsicht Frontend/Backend — vier Placebos weg + (Fortschritt log, Auswurf tat nichts, „Alle Tracks" konnte nichts, Encoder + wurden behauptet statt gemessen), Zombie-Erkennung gebaut, Pfad-Prüfung + 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 + (Ping im Hintergrund), Kompression je Disc-Typ abwählbar (4K verlustfrei), + Wizard empfiehlt nach gemessener CPU, vier tote Routen entfernt +- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe + +## Was diese Sitzungen wiederholt gekostet hat + +**Nicht aus einem Zustandswert auf einen Mechanismus schließen.** Vorgefallen: +aus „kein Schlüssel da" → „Server abgeschaltet" (falsch), aus Status +`transcoding` → „Celery hat neu zugestellt" (falsch), aus `progress=99` → +„Altwert aus dem Absturz" (falsch — ein Bug), aus gleichem `st_dev` → +„`os.rename` funktioniert" (falsch — der Kernel vergleicht den Mount). +Jedes Mal hätte eine Messung von unter einer Minute gereicht. + +**Und die Umkehrung gilt genauso:** gleiches `st_dev` heißt NICHT gleicher +Mount. Wo eine Eigenschaft ausprobierbar ist, probiere sie aus, statt sie +vorherzusagen. diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe index ab72a3ae6e14e4b2002332fa59c630b8f3f5dcc6..47a92484aab79d8073dc692b09671de1d05a88fc 100644 GIT binary patch delta 4595 zcmaJ_Yi!%r6_(^QiR0RJ+a!&hCb^ciMrtfZZ@=Qi4_UF2+Vx_&ZrT+KC0`+SG205=<=P&C z=im%TezZF6xp`zq0My?4)pZ^d&U$L={F}n^<8$e;iA?$=36j%~C)49+W~WXLkR+o9 zB}vhwj7-kX%n(sF$ZRGR>?4!u3>kfVk`$<+QI;!G-6R*NE>T5j-PKL_{-d2sYEhvU z5weX;&7=p(Y*`j7CUr=OBq|EISRtpyibVAbR5o%&WS^Z%!kj^7N;=a(Fw^rUp-iC) znHF_}>cJE(%2e+GK};r0A>P{audaEKhKwlr+Ims1EGY%!qGg+cG7XdFN~S<2nQ0j% zgL5W{3t>`Hb)A+>$Fm?2%OnyPLcKyH6iJXQWs*Ue$qL|XnHmOF22dU$b^iv4_oR;c zYtR4Y7t~9RkXZ%1SrT=Vn$#c}wL~ow2q}Yd)NZq^)vf|L0R~y5mAr{!r^H;5%u@&l zsa;XvR#6H>(B$TqYF-694bevZr>HFNR;=S6kI7DKgNCnw8pWt_T4mUfy2Z&9%rk zdPF^o21r;EGCYz@Z4J_Ufl(_{XPBlf2IputIGZN?{^qYZ;lb?F`yD2W(BG%>9h zrYJ)V5CzH)YlK=JrgU1CO0~6zbPeX9av7mXHk9 z#iKwH3-^$Czr|eF+J{;lraaJ79XT^}0cylCbRgB%v0-H-6heZNR1#@f%bWaz-tI&= z*4IPAi75Q*iv>1lV<>x+!g|tK-h>*m%}Og0E2y^cj{bgN3V~mdSU7-^N6L^cM|DRe z7KiCLO1Edn%TyP^D|D$N46z9JAw{I$;+?MOnoMoUt>FkvM`Iv4lnC&DMAo;D!E#ef z{>tDZJL3s34gT@>qF*_Pi2>DlRWA!ERi|UBtmU$=*{=8vMzIdMNs8%)d8VSwv5TTEL9s3b1Tx1IFt}`x5`q|_m0gI>stPgKGhi1~ zd=EmVie)Ha)uOPvVpaw$V~QCZ2QLZ=GXP#JxKgMIFwB&aTrt?QfWOSh1!oq^SyjeL zE`jH~t8WiK7i|k#73vHMNmyMFI&VhRy!EjOkPWM22ofFRE2s9JKz2+R-h~lK`m$w& z|GCy(Sag~Q=-~H>Sy*>qZx8WNAjTPA9xy6b10~Qc@RTjTa(G;W2=9Q9r4R04<6+s#lDn-==R+OF^bxp7>zbS`T@8 zW)W6wS#X5a?{^8ER3-Y%z`%s6vx}+%uo0NBHFT3C1V0b81L%$j0!f)o1Ik$dgY^*1 zKssSl!4cRwOsEITtTH5}2+QgYXRPual_7W}dIzOo%^8r2VSj@!_KMu}svhpT0%2M; zVDnKGn=>R}$OUU{3L%?#iTLcu3TXuZOj7~VfMG+QuP)26hzbyGt;lkQ02_t8kxjoN z28^+jy^(uIJXE5l8q^PJH{u6z7D|FW!9* z?Bs@l;pC4df=@-FfGv@D!@od1GMjvs`Qx(8G%(6%2|zG|AD|ZEVyy3OChkc@mwY)p z?g4-)fT|b-)#8iYO~0iQjDHN^6L=GK7E8adMT`*9VV7i9lVL6KW7x?A2_QpOrYvjk zXmwz)+KSV)Rl|gB2(bjVx2y=*VXub_Z}08kFZOnB0~?;^=Z7EU3lsOX*pstZ09Fu} zhqv?Z^|l;BbhhRi!iMW*#?&EMettQ+caIk{o?#}HVUtB-y!Z==uHiliVr0X^twn6I zc;4cjo&8TLNr!F!z6Ey?cOm1 zX^^QG!I_^sw&3 zE}Nb_{s`K%eEcV#XoQDOST;@PMd@>GT0XJpiAVS+C%%I=S>&H((-yKeZL%isvgxJa zX;1A6|71AT8n>F0tf?R+zdm@FS4RBv&bzRs&idrt{RZS_V-i4gMAKkv5|tbkMpZc5 z=$mP7ERqgh9k~QCszyee-PorLjqP)L`a^s!IoFvhmPRV3dPlI(K`Y}ol1saykqFk8 ztQOGz9dT^k!g0PZo;~dJp!Vz*GMotvP^(i&j--{CpC3Ec!Y`*FeQW%s)VDo-2|j*u z?hDT519af4(?;JRdbDtD;+Pv5j;CjueL1J~t6op%n_4b1Dmz^(4^AH5-|!wF&k|F84_|7fxc%d2H-4sX&_|9uVgxX*CG*k65ZYIoDV+B$T6 zMX>@WLhNe+IPq{$Td&^!;)fnQ$iAKV5#BD(rkX8Uau|x_t=ZFGO z`tsSGkgC<{^`};L9H^~pc|2x8|B;9DiiORii%fCaqHPZake%UtnNS6#NG!U-9VPH^Znps!Tm2qqC3sQy*_ zDf0Y~9YUVpvafg6R^c!REhU^|b?cM~XiC6!m{#Wf&9yb`JP=*4&ii&+CpV}~9-iOL z|1$qP3Xw`Lx0W3gR$yN;d6#@+Uy@;G&GN+W`}E|04k9uYMTW=5}@LMGE`s>vX$z*I(csa>JJv{A}V})1B^W ackTVo=9@0py)gFHVDN?*0=3`WAl1!|Z5)zfgudice zPUe3Golwsb;|Iz9P$Q_|@}h|mWEl9vkPR@#5E5YoqXtcu_>T!u@!r>M_;HilbI(1$ z^E>CBckb=PO~b^j;pEHht_t!zS{e(8=kmBbgl6eRWp?w+J5*vi7wANd*)@+)4pZq^ zmI!CQu@#CG-Nm*S5c+vTrfRK$L-|mA-wnN7ZSLiZ)Pm`ph2z{76UCs7bbbY&ahpwu zLx}RC+i;t!u9;?G>1d;qN2^c|AXL_Y7zil{t+vy(r4{bZJ+6iLg{?Wb6(R81difNA z*KHH|P2oT=xLuaSG`529M1@BXlelS5IEc67uq+J_yt&Mmg@B;FC$ncU?6LL?M_`D46nM zNg(~Pq)K4^pcxGA*Px_+25y9EV6lER@AJU#^)JG=JBvW}m6q0T!%mN#{W$9iaxGC= z8HSk_D>Ocn2XhVWEI|2=YF;Lqf5$B@$D+nnXnb-#Omr6(bO;e5QYX4cM3}7KTuPHv z=%G|1Rj=%4+44Cz$WK33lm**bWQ7D|QC8r_&Qj>wxyiVKckHs~I_)eO#tSoHD|EH3 zGPvw@a4ERHkj0`#YK}`$8D^TdRZg~?f2hDJ(b%YlP+-wc-9w%&S^Xs4! zZE4=;gxp{s1vN0h!{Axo?uB#hSwPk9H1G7nJGxL_#{kplM`Fgoh}HppG-&1$7f+e5jsd z+9#>aOuLbqtPn^ls?vue%KBW6Nrd)3rst#P(GjajCb5(>$nL`F5etiYZR8Ylm5zLB zWG+pjNWeaN$=sz3NR+{k<56t2teE7nayXW*gvRu%))Ar%(bp#tWsH8@F2|+qAwi8{ zi;z-j+Y5@Sv%s(E@%(>+A#wD#N73XlA3MVbV+mG`iWhqHY9z*=W$N7cNrQIirH=wd zCr&x1=6*cWIXTt*lgIhSq_*+O0b|M3(%a15=d)~$GgW4O`VPE57lTHw2ny%#Ie$Q? x+{_T!QU3s;waW^X_Pg{Cf<5<3BlG}iTdv)%FkMF|pYF{*r4`>_ 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 # Administrator schreiben. Mit dem Schalter tragt die .exe ein Manifest, das -# Windows beim Doppelklick EINMAL per UAC fragen laesst - danach stimmen die -# Rechte fuer alles Weitere. Ohne ihn liefe der Installer unerhoeht und +# Windows beim Doppelklick EINMAL per UAC fragen lässt - danach stimmen die +# Rechte für alles Weitere. Ohne ihn liefe der Installer unerhöht und # scheiterte beim Anlegen des Ordners; wer das Ziel ins eigene Profil legt, -# braucht die 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). Invoke-ps2exe -inputFile $guiSrc -outputFile "$PSScriptRoot\RippyWorkerSetup.exe" ` -iconFile "$PSScriptRoot\rippy.ico" -noConsole -STA -requireAdmin ` diff --git a/deploy/worker-windows/install-gui.ps1 b/deploy/worker-windows/install-gui.ps1 index 30d799d..abf407b 100644 --- a/deploy/worker-windows/install-gui.ps1 +++ b/deploy/worker-windows/install-gui.ps1 @@ -1,9 +1,14 @@ -# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime). -# Wird ueber rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur +# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime). +# Wird über rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur # Python 3.10+ auf der Maschine - kein Docker, kein git. # -# ASCII-only (PowerShell 5.1 liest .ps1 als ANSI - Sonderzeichen zerschiessen -# das Skript). +# WICHTIG - KODIERUNG: Diese Datei MUSS als UTF-8 MIT BOM gespeichert werden. +# PowerShell 5.1 liest .ps1 ohne BOM als ANSI; Umlaute werden dann zu Kauderwelsch +# UND das Skript wirft einen Parser-Fehler, weil in "ö" ein Anführungszeichen +# steckt. Mit BOM liest 5.1 korrekt UTF-8 - am 25.07.2026 beidseitig gemessen: +# ohne BOM: "Größe" + Unerwartetes Token +# mit BOM: "Größe" + laeuft +# Nach jeder Änderung pruefen: powershell.exe -Command "Parser::ParseFile(...)" param( [string]$RippyHost = "", @@ -16,28 +21,42 @@ Add-Type -AssemblyName System.Drawing # Standard-Zielverzeichnis: wie jedes andere Programm unter "Programme" # (Commander-Wunsch 25.07.2026 - vorher lag der Worker im Benutzerprofil, was -# fuer ein Programm unueblich ist). GetFolderPath liefert den ECHTEN Pfad; -# deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heisst +# für ein Programm unüblich ist). GetFolderPath liefert den ECHTEN Pfad; +# deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heißt # trotzdem "C:\Program Files". # # Schreiben dorthin braucht Administratorrechte - deshalb ist die .exe mit # -requireAdmin gebaut (build-exe.ps1) und fragt beim Start einmal per UAC. -# Ohne das 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" $InstallDir = $StandardZiel +# --- Farben: dieselben Töne wie das Rippy-Web-UI --------------------------- +# Übernommen aus docker/ui/src/lib/design.ts bzw. den Tailwind-Klassen des UI, +# damit der Installer nicht wie ein Fremdkörper wirkt. +$cBg = [System.Drawing.Color]::FromArgb(15, 23, 42) # slate-900, Fensterfläche +$cPanel = [System.Drawing.Color]::FromArgb(2, 6, 23) # slate-950, Log + Eingaben +$cFeld = [System.Drawing.Color]::FromArgb(30, 41, 59) # slate-800, Eingabefelder +$cText = [System.Drawing.Color]::FromArgb(226, 232, 240) # slate-200 +$cGedaempft= [System.Drawing.Color]::FromArgb(148, 163, 184) # slate-400, Beschriftungen +$cIndigo = [System.Drawing.Color]::FromArgb(99, 102, 241) +$cAmber = [System.Drawing.Color]::FromArgb(245, 158, 11) +$cEmerald = [System.Drawing.Color]::FromArgb(16, 185, 129) +$cPurple = [System.Drawing.Color]::FromArgb(126, 34, 206) + # --- Fenster --------------------------------------------------------------- $form = New-Object System.Windows.Forms.Form $form.Text = "Rippy Encoding-Worker - Installation" -$form.Size = New-Object System.Drawing.Size(560, 575) +$form.ClientSize = New-Object System.Drawing.Size(560, 560) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $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) -# Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weisses Loch) -# - kein externes .ico noetig. Erscheint in Titelleiste und Taskleiste. +# Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weißes Loch) +# - kein externes .ico nötig. Erscheint in Titelleiste und Taskleiste. try { $ibmp = New-Object System.Drawing.Bitmap 32, 32 $ig = [System.Drawing.Graphics]::FromImage($ibmp) @@ -47,75 +66,116 @@ try { $form.Icon = [System.Drawing.Icon]::FromHandle($ibmp.GetHicon()) } catch {} -$titel = New-Object System.Windows.Forms.Label -$titel.Text = "Rippy Encoding-Worker einrichten" -$titel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold) -$titel.ForeColor = [System.Drawing.Color]::FromArgb(79, 70, 229) -$titel.Location = New-Object System.Drawing.Point(24, 20) -$titel.Size = New-Object System.Drawing.Size(500, 30) -$form.Controls.Add($titel) +# --- Kopfbereich mit Farbverlauf -------------------------------------------- +# Das Rippy-UI hat oben einen Verlauf amber -> indigo -> purple (siehe +# FirstRunWizard.tsx). WinForms kann das nicht von sich aus, also wird der +# Verlauf im Paint-Ereignis gezeichnet - inklusive Disc-Symbol. +$kopf = New-Object System.Windows.Forms.Panel +$kopf.Dock = "Top" +$kopf.Height = 104 +$form.Controls.Add($kopf) +$kopf.Add_Paint({ + param($absender, $e) + $flaeche = New-Object System.Drawing.Rectangle 0, 0, $absender.Width, $absender.Height + $pinsel = New-Object System.Drawing.Drawing2D.LinearGradientBrush( + $flaeche, $cAmber, $cPurple, [System.Drawing.Drawing2D.LinearGradientMode]::Horizontal) + # Dreifarbig wie im UI: der Indigo-Ton in der Mitte + $mischung = New-Object System.Drawing.Drawing2D.ColorBlend 3 + $mischung.Colors = @($cAmber, $cIndigo, $cPurple) + $mischung.Positions = @(0.0, 0.45, 1.0) + $pinsel.InterpolationColors = $mischung + $e.Graphics.FillRectangle($pinsel, $flaeche) + $pinsel.Dispose() -$info = New-Object System.Windows.Forms.Label -$info.Text = "Diese Maschine uebernimmt die Video-Kompression fuer Rippy. Gerippt wird weiter auf der Rippy-Hauptmaschine." -$info.Location = New-Object System.Drawing.Point(24, 52) -$info.Size = New-Object System.Drawing.Size(500, 34) -$info.ForeColor = [System.Drawing.Color]::FromArgb(100, 116, 139) -$form.Controls.Add($info) + # Disc-Symbol: Ring aussen, weisses Loch in der Mitte + $e.Graphics.SmoothingMode = "AntiAlias" + $weiss = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(235, 255, 255, 255)) + $e.Graphics.FillEllipse($weiss, 24, 28, 48, 48) + $e.Graphics.FillEllipse((New-Object System.Drawing.SolidBrush $cIndigo), 36, 40, 24, 24) + $e.Graphics.FillEllipse($weiss, 45, 49, 6, 6) + $weiss.Dispose() + + # Texte direkt zeichnen - so bleibt der Verlauf ohne Kästchen sichtbar + $fTitel = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold) + $fUnter = New-Object System.Drawing.Font("Segoe UI", 9) + $e.Graphics.DrawString("Rippy Encoding-Worker", $fTitel, + [System.Drawing.Brushes]::White, 88, 26) + $e.Graphics.DrawString( + "Diese Maschine übernimmt die Video-Kompression für Rippy." + [char]10 + + "Gerippt wird weiter auf der Rippy-Hauptmaschine.", $fUnter, + (New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(225, 255, 255, 255))), 90, 56) + $fTitel.Dispose(); $fUnter.Dispose() +}) # Rippy-Adresse $lblHost = New-Object System.Windows.Forms.Label $lblHost.Text = "LAN-IP der Rippy-Maschine (Docker-Host) - NICHT dieser PC:" -$lblHost.Location = New-Object System.Drawing.Point(24, 96) +$lblHost.Location = New-Object System.Drawing.Point(24, 120) $lblHost.Size = New-Object System.Drawing.Size(500, 18) +$lblHost.ForeColor = $cGedaempft $form.Controls.Add($lblHost) $txtHost = New-Object System.Windows.Forms.TextBox -$txtHost.Location = New-Object System.Drawing.Point(24, 116) +$txtHost.Location = New-Object System.Drawing.Point(24, 140) $txtHost.Size = New-Object System.Drawing.Size(300, 26) $txtHost.Text = $RippyHost $txtHost.Font = New-Object System.Drawing.Font("Consolas", 10) +$txtHost.BackColor = $cFeld +$txtHost.ForeColor = $cText +$txtHost.BorderStyle = "FixedSingle" $form.Controls.Add($txtHost) # Worker-Name $lblName = New-Object System.Windows.Forms.Label -$lblName.Text = "Name dieses Workers (frei waehlbar):" -$lblName.Location = New-Object System.Drawing.Point(24, 152) +$lblName.Text = "Name dieses Workers (frei wählbar):" +$lblName.Location = New-Object System.Drawing.Point(24, 176) $lblName.Size = New-Object System.Drawing.Size(500, 18) +$lblName.ForeColor = $cGedaempft $form.Controls.Add($lblName) $txtName = New-Object System.Windows.Forms.TextBox -$txtName.Location = New-Object System.Drawing.Point(24, 172) +$txtName.Location = New-Object System.Drawing.Point(24, 196) $txtName.Size = New-Object System.Drawing.Size(300, 26) $txtName.Text = $WorkerName +$txtName.BackColor = $cFeld +$txtName.ForeColor = $cText +$txtName.BorderStyle = "FixedSingle" $form.Controls.Add($txtName) -# Zielverzeichnis - frei waehlbar, Standard "Programme" +# Zielverzeichnis - frei wählbar, Standard "Programme" $lblPfad = New-Object System.Windows.Forms.Label $lblPfad.Text = "Installieren nach:" -$lblPfad.Location = New-Object System.Drawing.Point(24, 206) +$lblPfad.Location = New-Object System.Drawing.Point(24, 232) $lblPfad.Size = New-Object System.Drawing.Size(500, 18) +$lblPfad.ForeColor = $cGedaempft $form.Controls.Add($lblPfad) $txtPfad = New-Object System.Windows.Forms.TextBox -$txtPfad.Location = New-Object System.Drawing.Point(24, 226) +$txtPfad.Location = New-Object System.Drawing.Point(24, 252) $txtPfad.Size = New-Object System.Drawing.Size(390, 26) $txtPfad.Text = $StandardZiel $txtPfad.Font = New-Object System.Drawing.Font("Consolas", 9) +$txtPfad.BackColor = $cFeld +$txtPfad.ForeColor = $cText +$txtPfad.BorderStyle = "FixedSingle" $form.Controls.Add($txtPfad) $btnPfad = New-Object System.Windows.Forms.Button -$btnPfad.Text = "Aendern ..." -$btnPfad.Location = New-Object System.Drawing.Point(424, 225) +$btnPfad.Text = "Ändern ..." +$btnPfad.Location = New-Object System.Drawing.Point(424, 251) $btnPfad.Size = New-Object System.Drawing.Size(100, 28) $btnPfad.FlatStyle = "Flat" +$btnPfad.BackColor = $cFeld +$btnPfad.ForeColor = $cText +$btnPfad.FlatAppearance.BorderColor = $cGedaempft $form.Controls.Add($btnPfad) $btnPfad.Add_Click({ $dlg = New-Object System.Windows.Forms.FolderBrowserDialog - $dlg.Description = "Ordner waehlen - darin wird 'Rippy Worker' angelegt" + $dlg.Description = "Ordner wählen - darin wird 'Rippy Worker' angelegt" $dlg.ShowNewFolderButton = $true if ($dlg.ShowDialog() -eq "OK") { # Der Dialog liefert den ELTERN-Ordner; der Unterordner kommt von uns, - # damit die Deinstallation nie einen fremden Ordner mitloescht. + # damit die Deinstallation nie einen fremden Ordner mitlöscht. $txtPfad.Text = (Join-Path $dlg.SelectedPath "Rippy Worker") } }) @@ -123,39 +183,41 @@ $btnPfad.Add_Click({ # Autostart $chkAuto = New-Object System.Windows.Forms.CheckBox $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.Checked = $true +$chkAuto.ForeColor = $cText $form.Controls.Add($chkAuto) # Log-Bereich $log = New-Object System.Windows.Forms.TextBox -$log.Location = New-Object System.Drawing.Point(24, 296) -$log.Size = New-Object System.Drawing.Size(500, 160) +$log.Location = New-Object System.Drawing.Point(24, 322) +$log.Size = New-Object System.Drawing.Size(500, 150) $log.Multiline = $true $log.ReadOnly = $true $log.ScrollBars = "Vertical" -$log.BackColor = [System.Drawing.Color]::FromArgb(15, 23, 42) -$log.ForeColor = [System.Drawing.Color]::FromArgb(226, 232, 240) +$log.BackColor = $cPanel +$log.ForeColor = $cText +$log.BorderStyle = "FixedSingle" $log.Font = New-Object System.Drawing.Font("Consolas", 8.5) $form.Controls.Add($log) -# Knoepfe +# Knöpfe $btnInstall = New-Object System.Windows.Forms.Button $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.BackColor = [System.Drawing.Color]::FromArgb(79, 70, 229) -$btnInstall.ForeColor = [System.Drawing.Color]::White +$btnInstall.BackColor = $cAmber +$btnInstall.ForeColor = $cBg $btnInstall.FlatStyle = "Flat" $btnInstall.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) $form.Controls.Add($btnInstall) $btnStart = New-Object System.Windows.Forms.Button $btnStart.Text = "Worker starten" -$btnStart.Location = New-Object System.Drawing.Point(190, 470) +$btnStart.Location = New-Object System.Drawing.Point(190, 486) $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.FlatStyle = "Flat" $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" # und die Abschlussmeldung denselben Pfad brauchen. - $gewaehlt = $txtPfad.Text.Trim() - if (-not $gewaehlt) { $gewaehlt = $StandardZiel } - if (-not [System.IO.Path]::IsPathRooted($gewaehlt)) { + $zielEingabe = $txtPfad.Text.Trim() + if (-not $zielEingabe) { $zielEingabe = $StandardZiel } + if (-not [System.IO.Path]::IsPathRooted($zielEingabe)) { [System.Windows.Forms.MessageBox]::Show( - "Bitte einen vollstaendigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.", - "Pfad unvollstaendig") + "Bitte einen vollständigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.", + "Pfad unvollständig") return } - $script:InstallDir = $gewaehlt + $script:InstallDir = $zielEingabe $btnInstall.Enabled = $false $log.Clear() @@ -209,7 +271,7 @@ function Do-Install { Log "Python gefunden: $python" # 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 # startet, hat die Rechte nicht - dann soll hier ein brauchbarer Satz # stehen und nicht "Zugriff verweigert" aus irgendeiner Tiefe. @@ -221,12 +283,12 @@ function Do-Install { } catch { Log "FEHLER: In '$InstallDir' darf nicht geschrieben werden." 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 " 1. Diesen Installer mit Rechtsklick > 'Als Administrator" - Log " ausfuehren' starten (der empfohlene Weg), ODER" - Log " 2. oben ueber 'Aendern ...' einen Ordner im eigenen" - Log " Benutzerprofil waehlen - dort geht es ohne Adminrechte." + Log " ausführen' starten (der empfohlene Weg), ODER" + Log " 2. oben über 'Ändern ...' einen Ordner im eigenen" + Log " Benutzerprofil wählen - dort geht es ohne Adminrechte." $btnInstall.Enabled = $true return } @@ -242,13 +304,13 @@ function Do-Install { Expand-Archive worker.zip -DestinationPath . -Force 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" } - 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\pip.exe install --quiet -r requirements.txt & .\venv\Scripts\pip.exe install --quiet pystray pillow - Log "Abhaengigkeiten installiert." + Log "Abhängigkeiten installiert." # 4. HandBrakeCLI 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 Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep 2 -# Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die -# Installation je nach Zielverzeichnis persoenlich oder maschinenweit war. +# Autostart-Verknüpfung entfernen - BEIDE möglichen Orte, weil die +# Installation je nach Zielverzeichnis persönlich oder maschinenweit war. foreach (`$ordner in @("Startup", "CommonStartup")) { try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} } -# ... und die 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 try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch {} Set-Location (Split-Path `$PSScriptRoot -Parent) @@ -287,55 +349,55 @@ Remove-Item -Recurse -Force `$PSScriptRoot "@ 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 # mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER 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 # $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 - # 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 - # Verknuepfung dort selbst sehen und loeschen. Zwei Fliegen. + # Verknüpfung dort selbst sehen und löschen. Zwei Fliegen. if ($chkAuto.Checked) { try { # 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: - # der Installer laeuft dafuer ohnehin erhoeht, und bei einer - # Erhoehung ueber ein FREMDES Administratorkonto waere der + # der Installer läuft dafür ohnehin erhöht, und bei einer + # Erhöhung über ein FREMDES Administratorkonto wäre der # 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") $programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86") - $fuerAlle = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") ` + $maschinenweit = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") ` -or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase")) - $autostartOrdner = if ($fuerAlle) { + $autostartDir = if ($maschinenweit) { [Environment]::GetFolderPath("CommonStartup") } else { [Environment]::GetFolderPath("Startup") } - $verknuepfung = Join-Path $autostartOrdner "RippyWorker.lnk" + $lnkPfad = Join-Path $autostartDir "RippyWorker.lnk" $wsh = New-Object -ComObject WScript.Shell - $lnk = $wsh.CreateShortcut($verknuepfung) + $lnk = $wsh.CreateShortcut($lnkPfad) $lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat") $lnk.WorkingDirectory = $InstallDir $lnk.Description = "Rippy Encoding-Worker" $lnk.Save() - if ($fuerAlle) { - Log "Autostart eingerichtet (fuer alle Benutzer dieser Maschine)." + if ($maschinenweit) { + Log "Autostart eingerichtet (für alle Benutzer dieser Maschine)." } else { - Log "Autostart eingerichtet (fuer dich, startet bei der Anmeldung)." + Log "Autostart eingerichtet (für dich, startet bei der Anmeldung)." } } catch { # Nur eine Warnung: der Worker selbst ist fertig und startbar. Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." - Log " Der Worker ist trotzdem fertig installiert und laeuft." - Log " Von Hand: start-tray.bat in den Autostart-Ordner verknuepfen" + Log " Der Worker ist trotzdem fertig installiert und läuft." + Log " Von Hand: start-tray.bat in den Autostart-Ordner verknüpfen" Log " (Windows-Taste + R, dann shell:startup eingeben)." } } diff --git a/deploy/worker-windows/install.ps1 b/deploy/worker-windows/install.ps1 index 64531d9..69cc3d8 100644 --- a/deploy/worker-windows/install.ps1 +++ b/deploy/worker-windows/install.ps1 @@ -1,211 +1,211 @@ -# Rippy: Nativer Windows-Transcode-Worker - Installation OHNE Docker. -# -# 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 -# 2. Laedt den Worker-Code direkt von deiner Rippy-Instanz (/api/worker-setup/paket) -# 3. Laedt HandBrakeCLI (gepinnte Version, offizielles GitHub-Release), -# falls nicht schon im PATH oder im Ordner vorhanden -# 4. Erzeugt start-worker.bat - Doppelklick startet den Worker -# -# Der Worker bedient NUR die Kompressions-Queue (transcode) - gerippt wird -# weiterhin auf der Rippy-Hauptmaschine (dort haengt das Laufwerk). -# -# Aufruf (PowerShell): -# .\install.ps1 -RippyHost 192.168.178.162 [-WorkerName mein-pc] [-Autostart] -# -# -Autostart registriert eine Aufgabe "RippyWorker" (Start bei Anmeldung). - -param( - [Parameter(Mandatory = $true)] [string]$RippyHost, - [string]$WorkerName = $env:COMPUTERNAME.ToLower(), - [switch]$Autostart, - # Zielverzeichnis. Standard ist "Programme", wie bei jedem anderen Programm - # (Commander-Wunsch 25.07.2026). Dorthin schreiben braucht Adminrechte - - # PowerShell also "Als Administrator" starten, oder hier einen Ordner im - # eigenen Profil angeben. - [string]$InstallDir = (Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker") -) - -$ErrorActionPreference = "Stop" -# HandBrake-Version: die NEUESTE offizielle (GitHub-Release) - so passt der -# Windows-Worker zu dem, was der Update-Check im UI meldet (frueher war hier -# eine feste 1.9.2 hardcodiert, was verwirrte). Faellt auf eine bekannte -# Version zurueck, falls die GitHub-API gerade nicht erreichbar ist. -$HandBrakeVersion = "1.11.2" -try { - $rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" ` - -TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" } - if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") } - Write-Host "Neueste HandBrake-Version: $HandBrakeVersion" -} catch { Write-Host "GitHub-API nicht erreichbar - nutze HandBrake $HandBrakeVersion" } -$HandBrakeUrl = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip" - -Write-Host "== Rippy Windows-Worker Installation ==" -ForegroundColor Cyan - -# 1. Python finden (py-Launcher oder python im PATH) -$python = $null -foreach ($kandidat in @("py -3", "python")) { - try { - $version = Invoke-Expression "$kandidat --version" 2>&1 - if ("$version" -match "Python 3\.(\d+)") { - if ([int]$Matches[1] -ge 10) { $python = $kandidat; break } - } - } catch {} -} -if (-not $python) { - Write-Host "FEHLER: Python 3.10+ nicht gefunden." -ForegroundColor Red - Write-Host "Installieren mit: winget install Python.Python.3.12 (dann Skript erneut ausfuehren)" - exit 1 -} -Write-Host "Python gefunden: $python" - -# 2. Zielordner + Worker-Code von der Rippy-Instanz laden -$ziel = $InstallDir -try { - New-Item -ItemType Directory -Force $ziel -ErrorAction Stop | Out-Null - $probe = Join-Path $ziel ".schreibtest" - Set-Content -Path $probe -Value "x" -ErrorAction Stop - Remove-Item -Force $probe -ErrorAction SilentlyContinue -} catch { - Write-Host "" - Write-Host "FEHLER: In '$ziel' darf nicht geschrieben werden." -ForegroundColor Red - Write-Host "Das ist normal bei Ordnern unter 'Programme'. Zwei Wege:" -ForegroundColor Red - Write-Host " 1. PowerShell als Administrator starten und erneut aufrufen, ODER" -ForegroundColor Red - Write-Host " 2. ein eigenes Ziel angeben, z. B.:" -ForegroundColor Red - Write-Host " -InstallDir `"`$env:LOCALAPPDATA\Rippy Worker`"" -ForegroundColor Red - exit 1 -} -Write-Host "Ziel: $ziel" -Set-Location $ziel - -Write-Host "Lade Worker-Code von http://$RippyHost/api/worker-setup/paket ..." -Invoke-WebRequest "http://$RippyHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing -Expand-Archive worker.zip -DestinationPath . -Force -Remove-Item worker.zip - -# 3. venv + Abhaengigkeiten -if (-not (Test-Path "venv")) { - Write-Host "Lege Python-Umgebung an ..." - Invoke-Expression "$python -m venv venv" -} -& .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip -& .\venv\Scripts\pip.exe install --quiet -r requirements.txt -# Nur fuer den Windows-Worker: Tray-Symbol (Status, Start/Stopp, Beenden) -& .\venv\Scripts\pip.exe install --quiet pystray pillow -Write-Host "Python-Abhaengigkeiten installiert." - -# 4. HandBrakeCLI besorgen (falls noetig) -$hb = Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue -if (-not $hb -and -not (Test-Path ".\HandBrakeCLI.exe")) { - Write-Host "Lade HandBrakeCLI $HandBrakeVersion (offizielles GitHub-Release) ..." - Invoke-WebRequest $HandBrakeUrl -OutFile hb.zip -UseBasicParsing - Expand-Archive hb.zip -DestinationPath . -Force - Remove-Item hb.zip -} -if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not $hb) { - Write-Host "FEHLER: HandBrakeCLI.exe fehlt." -ForegroundColor Red - exit 1 -} -Write-Host "HandBrakeCLI bereit." - -# 5. Start-Skript erzeugen -$bat = @" -@echo off -rem Rippy Windows-Transcode-Worker - verbindet sich mit $RippyHost -cd /d "%~dp0" -set REDIS_URL=redis://${RippyHost}:6379/0 -set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy -set API_URL=http://${RippyHost}:8000 -set WORKER_NAME=$WorkerName -set PATH=%~dp0;%PATH% -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 - -# 6. Tray-Start (empfohlen): Symbol neben der Uhr, kein Konsolenfenster -$trayBat = @" -@echo off -rem Rippy-Worker mit Tray-Symbol - verbindet sich mit $RippyHost -cd /d "%~dp0" -set REDIS_URL=redis://${RippyHost}:6379/0 -set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy -set API_URL=http://${RippyHost}:8000 -set WORKER_NAME=$WorkerName -set RIPPY_TRAY_HOST=$RippyHost -set PATH=%~dp0;%PATH% -start "" venv\Scripts\pythonw.exe tray.py -"@ -Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII -Write-Host "start-worker.bat + start-tray.bat erzeugt." - -# 7. Deinstaller - MUSS-Kriterium: rueckstandsfrei entfernbar -$uninstall = @" -# Rippy-Worker DEINSTALLIEREN: stoppt Prozesse, entfernt Autostart, -# meldet den Worker in Rippy ab und loescht diesen Ordner komplett. -param([switch]`$Force) -if (-not `$Force) { - `$antwort = Read-Host "Rippy-Worker '$WorkerName' wirklich deinstallieren? (j/n)" - if (`$antwort -ne "j") { Write-Host "Abgebrochen."; exit } -} -Get-CimInstance Win32_Process | Where-Object { - `$_.ExecutablePath -like "`$PSScriptRoot*" -} | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } -Start-Sleep 2 -# Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die -# Installation je nach Zielverzeichnis persoenlich oder maschinenweit war. -foreach (`$ordner in @("Startup", "CommonStartup")) { - try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} -} -# ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden. -schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null -try { - Invoke-RestMethod -Method Delete "http://$RippyHost/api/workers/$WorkerName" -TimeoutSec 5 | Out-Null - Write-Host "Worker-Eintrag in Rippy entfernt." -} catch { Write-Host "Hinweis: Eintrag in Rippy ggf. von Hand loeschen (Einstellungen -> Worker)." } -Set-Location (Split-Path `$PSScriptRoot -Parent) -Remove-Item -Recurse -Force `$PSScriptRoot -Write-Host "Rippy-Worker deinstalliert." -ForegroundColor Green -"@ -Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII -Write-Host "uninstall.ps1 erzeugt." - -# 8. Optional: Autostart bei Anmeldung (mit Tray) -# -# Ueber den Autostart-ORDNER, nicht ueber schtasks: Eine Aufgabe im -# Wurzelordner der Aufgabenplanung anzulegen verlangt Administratorrechte, und -# der Installer laeuft normal ohne. Auf dem Commander-PC scheiterte das am -# 25.07.2026 mit "FEHLER: Zugriff verweigert." - und weil oben -# $ErrorActionPreference = "Stop" steht, riss es die ganze Installation mit, -# obwohl nur der Autostart fehlte. Der Autostart-Ordner braucht nie Adminrechte. -if ($Autostart) { - try { - # Unter "Programme" = Installation fuer die Maschine, also Autostart fuer - # alle Benutzer. Sonst persoenlich. Siehe install-gui.ps1 fuer die - # ausfuehrliche Begruendung (Rechtelage bei fremdem Admin-Konto). - $programme = [Environment]::GetFolderPath("ProgramFiles") - $fuerAlle = $ziel.StartsWith($programme, "OrdinalIgnoreCase") - $autostartOrdner = if ($fuerAlle) { - [Environment]::GetFolderPath("CommonStartup") - } else { - [Environment]::GetFolderPath("Startup") - } - $wsh = New-Object -ComObject WScript.Shell - $lnk = $wsh.CreateShortcut((Join-Path $autostartOrdner "RippyWorker.lnk")) - $lnk.TargetPath = (Join-Path $ziel "start-tray.bat") - $lnk.WorkingDirectory = $ziel - $lnk.Description = "Rippy Encoding-Worker" - $lnk.Save() - Write-Host "Autostart eingerichtet (Verknuepfung im Autostart-Ordner)." - } catch { - Write-Host "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." -ForegroundColor Yellow - Write-Host " Der Worker ist trotzdem fertig installiert." -ForegroundColor Yellow - Write-Host " Von Hand: start-tray.bat in shell:startup verknuepfen." -ForegroundColor Yellow - } -} - -Write-Host "" -Write-Host "FERTIG." -ForegroundColor Green -Write-Host " Starten (mit Tray-Symbol): $ziel\start-tray.bat" -Write-Host " Starten (Konsole/Debug): $ziel\start-worker.bat" -Write-Host " Deinstallieren: $ziel\uninstall.ps1" -Write-Host "Der Worker erscheint in Rippy unter Einstellungen -> Worker als '$WorkerName'." +# Rippy: Nativer Windows-Transcode-Worker - Installation OHNE Docker. +# +# Was das Skript tut (alles nach -InstallDir, Standard "C:\Program Files\Rippy Worker"): +# 1. Prüft Python (3.10+), legt ein venv an, installiert die Abhängigkeiten +# 2. Lädt den Worker-Code direkt von deiner Rippy-Instanz (/api/worker-setup/paket) +# 3. Lädt HandBrakeCLI (gepinnte Version, offizielles GitHub-Release), +# falls nicht schon im PATH oder im Ordner vorhanden +# 4. Erzeugt start-worker.bat - Doppelklick startet den Worker +# +# Der Worker bedient NUR die Kompressions-Queue (transcode) - gerippt wird +# weiterhin auf der Rippy-Hauptmaschine (dort hängt das Laufwerk). +# +# Aufruf (PowerShell): +# .\install.ps1 -RippyHost 192.168.178.162 [-WorkerName mein-pc] [-Autostart] +# +# -Autostart registriert eine Aufgabe "RippyWorker" (Start bei Anmeldung). + +param( + [Parameter(Mandatory = $true)] [string]$RippyHost, + [string]$WorkerName = $env:COMPUTERNAME.ToLower(), + [switch]$Autostart, + # Zielverzeichnis. Standard ist "Programme", wie bei jedem anderen Programm + # (Commander-Wunsch 25.07.2026). Dorthin schreiben braucht Adminrechte - + # PowerShell also "Als Administrator" starten, oder hier einen Ordner im + # eigenen Profil angeben. + [string]$InstallDir = (Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker") +) + +$ErrorActionPreference = "Stop" +# HandBrake-Version: die NEUESTE offizielle (GitHub-Release) - so passt der +# Windows-Worker zu dem, was der Update-Check im UI meldet (früher war hier +# eine feste 1.9.2 hardcodiert, was verwirrte). Fällt auf eine bekannte +# Version zurück, falls die GitHub-API gerade nicht erreichbar ist. +$HandBrakeVersion = "1.11.2" +try { + $rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" ` + -TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" } + if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") } + Write-Host "Neueste HandBrake-Version: $HandBrakeVersion" +} catch { Write-Host "GitHub-API nicht erreichbar - nutze HandBrake $HandBrakeVersion" } +$HandBrakeUrl = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip" + +Write-Host "== Rippy Windows-Worker Installation ==" -ForegroundColor Cyan + +# 1. Python finden (py-Launcher oder python im PATH) +$python = $null +foreach ($kandidat in @("py -3", "python")) { + try { + $version = Invoke-Expression "$kandidat --version" 2>&1 + if ("$version" -match "Python 3\.(\d+)") { + if ([int]$Matches[1] -ge 10) { $python = $kandidat; break } + } + } catch {} +} +if (-not $python) { + Write-Host "FEHLER: Python 3.10+ nicht gefunden." -ForegroundColor Red + Write-Host "Installieren mit: winget install Python.Python.3.12 (dann Skript erneut ausführen)" + exit 1 +} +Write-Host "Python gefunden: $python" + +# 2. Zielordner + Worker-Code von der Rippy-Instanz laden +$ziel = $InstallDir +try { + New-Item -ItemType Directory -Force $ziel -ErrorAction Stop | Out-Null + $probe = Join-Path $ziel ".schreibtest" + Set-Content -Path $probe -Value "x" -ErrorAction Stop + Remove-Item -Force $probe -ErrorAction SilentlyContinue +} catch { + Write-Host "" + Write-Host "FEHLER: In '$ziel' darf nicht geschrieben werden." -ForegroundColor Red + Write-Host "Das ist normal bei Ordnern unter 'Programme'. Zwei Wege:" -ForegroundColor Red + Write-Host " 1. PowerShell als Administrator starten und erneut aufrufen, ODER" -ForegroundColor Red + Write-Host " 2. ein eigenes Ziel angeben, z. B.:" -ForegroundColor Red + Write-Host " -InstallDir `"`$env:LOCALAPPDATA\Rippy Worker`"" -ForegroundColor Red + exit 1 +} +Write-Host "Ziel: $ziel" +Set-Location $ziel + +Write-Host "Lade Worker-Code von http://$RippyHost/api/worker-setup/paket ..." +Invoke-WebRequest "http://$RippyHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing +Expand-Archive worker.zip -DestinationPath . -Force +Remove-Item worker.zip + +# 3. venv + Abhängigkeiten +if (-not (Test-Path "venv")) { + Write-Host "Lege Python-Umgebung an ..." + Invoke-Expression "$python -m venv venv" +} +& .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip +& .\venv\Scripts\pip.exe install --quiet -r requirements.txt +# Nur für den Windows-Worker: Tray-Symbol (Status, Start/Stopp, Beenden) +& .\venv\Scripts\pip.exe install --quiet pystray pillow +Write-Host "Python-Abhängigkeiten installiert." + +# 4. HandBrakeCLI besorgen (falls nötig) +$hb = Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue +if (-not $hb -and -not (Test-Path ".\HandBrakeCLI.exe")) { + Write-Host "Lade HandBrakeCLI $HandBrakeVersion (offizielles GitHub-Release) ..." + Invoke-WebRequest $HandBrakeUrl -OutFile hb.zip -UseBasicParsing + Expand-Archive hb.zip -DestinationPath . -Force + Remove-Item hb.zip +} +if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not $hb) { + Write-Host "FEHLER: HandBrakeCLI.exe fehlt." -ForegroundColor Red + exit 1 +} +Write-Host "HandBrakeCLI bereit." + +# 5. Start-Skript erzeugen +$bat = @" +@echo off +rem Rippy Windows-Transcode-Worker - verbindet sich mit $RippyHost +cd /d "%~dp0" +set REDIS_URL=redis://${RippyHost}:6379/0 +set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy +set API_URL=http://${RippyHost}:8000 +set WORKER_NAME=$WorkerName +set PATH=%~dp0;%PATH% +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 + +# 6. Tray-Start (empfohlen): Symbol neben der Uhr, kein Konsolenfenster +$trayBat = @" +@echo off +rem Rippy-Worker mit Tray-Symbol - verbindet sich mit $RippyHost +cd /d "%~dp0" +set REDIS_URL=redis://${RippyHost}:6379/0 +set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy +set API_URL=http://${RippyHost}:8000 +set WORKER_NAME=$WorkerName +set RIPPY_TRAY_HOST=$RippyHost +set PATH=%~dp0;%PATH% +start "" venv\Scripts\pythonw.exe tray.py +"@ +Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII +Write-Host "start-worker.bat + start-tray.bat erzeugt." + +# 7. Deinstaller - MUSS-Kriterium: rückstandsfrei entfernbar +$uninstall = @" +# Rippy-Worker DEINSTALLIEREN: stoppt Prozesse, entfernt Autostart, +# meldet den Worker in Rippy ab und löscht diesen Ordner komplett. +param([switch]`$Force) +if (-not `$Force) { + `$antwort = Read-Host "Rippy-Worker '$WorkerName' wirklich deinstallieren? (j/n)" + if (`$antwort -ne "j") { Write-Host "Abgebrochen."; exit } +} +Get-CimInstance Win32_Process | Where-Object { + `$_.ExecutablePath -like "`$PSScriptRoot*" +} | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } +Start-Sleep 2 +# Autostart-Verknüpfung entfernen - BEIDE möglichen Orte, weil die +# Installation je nach Zielverzeichnis persönlich oder maschinenweit war. +foreach (`$ordner in @("Startup", "CommonStartup")) { + try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} +} +# ... und die geplante Aufgabe aus älteren Installationen, falls vorhanden. +schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null +try { + Invoke-RestMethod -Method Delete "http://$RippyHost/api/workers/$WorkerName" -TimeoutSec 5 | Out-Null + Write-Host "Worker-Eintrag in Rippy entfernt." +} catch { Write-Host "Hinweis: Eintrag in Rippy ggf. von Hand löschen (Einstellungen -> Worker)." } +Set-Location (Split-Path `$PSScriptRoot -Parent) +Remove-Item -Recurse -Force `$PSScriptRoot +Write-Host "Rippy-Worker deinstalliert." -ForegroundColor Green +"@ +Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII +Write-Host "uninstall.ps1 erzeugt." + +# 8. Optional: Autostart bei Anmeldung (mit Tray) +# +# Über den Autostart-ORDNER, nicht über schtasks: Eine Aufgabe im +# Wurzelordner der Aufgabenplanung anzulegen verlangt Administratorrechte, und +# der Installer läuft normal ohne. Auf dem Commander-PC scheiterte das am +# 25.07.2026 mit "FEHLER: Zugriff verweigert." - und weil oben +# $ErrorActionPreference = "Stop" steht, riss es die ganze Installation mit, +# obwohl nur der Autostart fehlte. Der Autostart-Ordner braucht nie Adminrechte. +if ($Autostart) { + try { + # Unter "Programme" = Installation für die Maschine, also Autostart für + # alle Benutzer. Sonst persönlich. Siehe install-gui.ps1 für die + # ausführliche Begründung (Rechtelage bei fremdem Admin-Konto). + $programme = [Environment]::GetFolderPath("ProgramFiles") + $maschinenweit = $ziel.StartsWith($programme, "OrdinalIgnoreCase") + $autostartDir = if ($maschinenweit) { + [Environment]::GetFolderPath("CommonStartup") + } else { + [Environment]::GetFolderPath("Startup") + } + $wsh = New-Object -ComObject WScript.Shell + $lnk = $wsh.CreateShortcut((Join-Path $autostartDir "RippyWorker.lnk")) + $lnk.TargetPath = (Join-Path $ziel "start-tray.bat") + $lnk.WorkingDirectory = $ziel + $lnk.Description = "Rippy Encoding-Worker" + $lnk.Save() + Write-Host "Autostart eingerichtet (Verknüpfung im Autostart-Ordner)." + } catch { + Write-Host "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." -ForegroundColor Yellow + Write-Host " Der Worker ist trotzdem fertig installiert." -ForegroundColor Yellow + Write-Host " Von Hand: start-tray.bat in shell:startup verknüpfen." -ForegroundColor Yellow + } +} + +Write-Host "" +Write-Host "FERTIG." -ForegroundColor Green +Write-Host " Starten (mit Tray-Symbol): $ziel\start-tray.bat" +Write-Host " Starten (Konsole/Debug): $ziel\start-worker.bat" +Write-Host " Deinstallieren: $ziel\uninstall.ps1" +Write-Host "Der Worker erscheint in Rippy unter Einstellungen -> Worker als '$WorkerName'." diff --git a/docker/api/main.py b/docker/api/main.py index c7f6605..185e74a 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -58,9 +58,9 @@ async def startup_event(): asyncio.create_task(asyncio.to_thread(remount)) 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 - # 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()) # Worker-Erreichbarkeit im Hintergrund pingen (siehe _ping_knoten) — sonst # 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)? 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 samt Trennzeichen. Erwartet werden normalisierte Container-Pfade mit „/". """ @@ -1383,7 +1383,7 @@ async def set_keystore(request: Request): rohdaten = await request.body() 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. fehler = makemkv_daten.private_data_pruefen(rohdaten) if fehler: diff --git a/docker/api/makemkv_daten.py b/docker/api/makemkv_daten.py index 936bb37..bcf02c2 100644 --- a/docker/api/makemkv_daten.py +++ b/docker/api/makemkv_daten.py @@ -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): 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: makemkvcon unter LINUX ruft die Disc-Schluessel nie ab. @@ -10,35 +10,35 @@ MakeMKV liest die Disc — sondern: Gemessen, nicht vermutet: * Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit - leerem UND mit gefuelltem Schluesselspeicher, mit und ohne --noscan, mit - dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Pruefung - (update.conf geloescht, Meldung 5074 belegt den Web-Kontakt). Immer: + leerem UND mit gefülltem Schlüsselspeicher, mit und ohne --noscan, mit + dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Prüfung + (update.conf gelöscht, Meldung 5074 belegt den Web-Kontakt). Immer: keine Verbindung, kein Schluessel. * Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading 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"). - * Der Code dafuer steckt auch im Linux-Binary: die Meldungsvorlage - "Downloading latest %1 to %2 ..." steht in makemkvcon. Sie loest nur nie + * Der Code dafür steckt auch im Linux-Binary: die Meldungsvorlage + "Downloading latest %1 to %2 ..." steht in makemkvcon. Sie löst nur nie aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet. FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst, 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 aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der 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. 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. 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=34022 * 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 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 @@ -72,14 +72,14 @@ DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV") # GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert. 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 # eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt. MAX_KEYDB_BYTES = 64 * 1024 * 1024 # Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit # 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*=") @@ -94,9 +94,9 @@ def keydb_pfad(daten_dir: str = None) -> str: 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 macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI "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: - """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 — - 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(): return "Die Datei ist leer." if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES: return ( - "Die Datei ist groesser als " + "Die Datei ist größer als " f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg." ) 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: eintraege = zaehle_disc_eintraege(datei.read()) except OSError: - pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem + pass # Datei da, aber unlesbar: Größe/Datum stimmen trotzdem return { "vorhanden": True, "pfad": pfad, @@ -163,7 +163,7 @@ def keydb_status(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. - 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. """ pfad = keydb_pfad(daten_dir) @@ -244,32 +244,32 @@ def dumps_auflisten(daten_dir: str = None) -> list: return liste -# --- Schluesselspeicher (_private_data.tar) ------------------------------- +# --- Schlüsselspeicher (_private_data.tar) ------------------------------- # -# Das ist MakeMKVs eigener Speicher fuer die "Hashed Keys": ein tar-Archiv mit -# hkd_*.bin-Eintraegen. Unter Windows fuellt MakeMKV es selbst (Meldung 3338), +# Das ist MakeMKVs eigener Speicher für die "Hashed Keys": ein tar-Archiv mit +# hkd_*.bin-Eintraegen. Unter Windows füllt MakeMKV es selbst (Meldung 3338), # 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" -# 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 -# client_max_body_size in docker/ui/nginx.conf: waere die Grenze hier hoeher, -# wuerde nginx den Upload abweisen, bevor die API ihn ueberhaupt sieht. +# client_max_body_size in docker/ui/nginx.conf: wäre die Grenze hier höher, +# würde nginx den Upload abweisen, bevor die API ihn überhaupt sieht. MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024 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) def zaehle_schluessel(rohdaten: bytes) -> int: """Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar). - Das ist die ehrliche Kennzahl fuer "wie viele Disc-Schluessel kennt diese - Installation". Ein frischer, leerer Speicher enthaelt nur eine + Das ist die ehrliche Kennzahl für "wie viele Disc-Schluessel kennt diese + Installation". Ein frischer, leerer Speicher enthält nur eine Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede unbekannte UHD-Disc scheitert. """ @@ -281,20 +281,20 @@ def zaehle_schluessel(rohdaten: bytes) -> int: 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 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). """ if not rohdaten: return "Die Datei ist leer." if len(rohdaten) > MAX_PRIVATE_DATA_BYTES: return ( - "Die Datei ist groesser als " + "Die Datei ist größer als " f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein " - "MakeMKV-Schluesselspeicher." + "MakeMKV-Schlüsselspeicher." ) try: with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv: @@ -308,13 +308,13 @@ def private_data_pruefen(rohdaten: bytes) -> str: return ( "In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). " "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 "" 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) try: 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: - """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. """ 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). 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. - 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. """ zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")] diff --git a/docker/api/makemkv_key.py b/docker/api/makemkv_key.py index b65f44e..23fc520 100644 --- a/docker/api/makemkv_key.py +++ b/docker/api/makemkv_key.py @@ -1,22 +1,22 @@ """Automatische Erneuerung des MakeMKV-Beta-Keys. 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 -faellt Blu-ray/UHD-Ripping in den 30-Tage-Testmodus; DVD-Ripping bleibt frei. +t=1053) wechselt etwa monatlich und läuft zum Monatsende ab. Ohne gültigen Key +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). 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 (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 — NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine. -Richtigstellung 25.07.2026: Hier stand frueher, MakeMKV ziehe den AACS-Schluessel -via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt fuer Blu-ray, aber -NICHT fuer 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung, +Richtigstellung 25.07.2026: Hier stand früher, MakeMKV ziehe den AACS-Schluessel +via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt für Blu-ray, aber +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 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: """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.raise_for_status() return extract_key(antwort.text) 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.""" import db 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: """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 await asyncio.sleep(start_verzoegerung_s) while True: diff --git a/docker/api/mounts.py b/docker/api/mounts.py index 2e48729..de943db 100644 --- a/docker/api/mounts.py +++ b/docker/api/mounts.py @@ -76,11 +76,11 @@ def schreibtest(pfad: str) -> bool: def _stale_mounts_loesen(ziel: str) -> int: """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.: - 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 - 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. """ geloest = 0 diff --git a/docker/api/test_api_smoke.py b/docker/api/test_api_smoke.py index 742091d..3ce19ec 100644 --- a/docker/api/test_api_smoke.py +++ b/docker/api/test_api_smoke.py @@ -24,8 +24,8 @@ def test_main_importierbar_und_routen_verdrahtet(): # Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht # mehr online nach). Ohne diese Routen ist die Seite im UI tot. "/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}", - # Der Hauptweg fuer 4K-UHD (25.07.2026): makemkvcon holt Disc-Schluessel - # unter Linux nie selbst, sie kommen von Hand ueber diesen Endpunkt. + # Der Hauptweg für 4K-UHD (25.07.2026): makemkvcon holt Disc-Schluessel + # unter Linux nie selbst, sie kommen von Hand über diesen Endpunkt. "/system/keystore", ): 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(): - """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 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 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): """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 - 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 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(): - """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.""" import main @@ -127,11 +127,11 @@ def test_ping_takt_ist_kuerzer_als_die_haltbarkeit(): 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 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 entfernt - und war danach eine Endlosschleife je Verbindung ohne jeden Verbraucher. Wer echtes Push will, braucht BEIDE Seiten (Server UND ein diff --git a/docker/api/test_makemkv_daten.py b/docker/api/test_makemkv_daten.py index b32cd3f..451e6d5 100644 --- a/docker/api/test_makemkv_daten.py +++ b/docker/api/test_makemkv_daten.py @@ -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 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(): - # Kommentar- und Leerzeilen duerfen NICHT mitgezaehlt werden, sonst meldet - # das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthaelt. + # Kommentar- und Leerzeilen dürfen NICHT mitgezaehlt werden, sonst meldet + # das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthält. assert zaehle_disc_eintraege(_GUELTIG) == 2 @@ -51,8 +51,8 @@ def test_keydb_pruefen_meldet_leere_datei(): def test_keydb_pruefen_erkennt_html(): - # Haeufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite - # eines Downloads im Feld. MakeMKV wuerde dann still weiter meckern. + # Häufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite + # eines Downloads im Feld. MakeMKV würde dann still weiter meckern. fehler = keydb_pruefen("\n404 Not Found\n") 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(): - # "" heisst laut Vertrag: alles in Ordnung, darf geschrieben werden. + # "" heißt laut Vertrag: alles in Ordnung, darf geschrieben werden. assert keydb_pruefen(_GUELTIG) == "" 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. assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True def test_ist_aacs_dump_blockt_pfad_tricks(): - # Der Download-Endpunkt haengt den Namen an das Datenverzeichnis — ein - # durchgelassenes ".." oder ein Pfadtrenner waere ein Ausbruch. + # Der Download-Endpunkt hängt den Namen an das Datenverzeichnis — ein + # durchgelassenes ".." oder ein Pfadtrenner wäre ein Ausbruch. assert ist_aacs_dump("../x.tgz") is False assert ist_aacs_dump("../../etc/passwd.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(): - # Regression: bis 25.07.2026 wurde die Datei komplett ueberschrieben. Mit - # dem jetzt persistenten Datenverzeichnis waere app_UpdateEnable vor jedem + # Regression: bis 25.07.2026 wurde die Datei komplett überschrieben. Mit + # dem jetzt persistenten Datenverzeichnis wäre app_UpdateEnable vor jedem # Rip weg gewesen. alt = 'app_UpdateEnable = "1"\napp_Key = "T-alt"\napp_DestinationDir = "/tmp"\n' neu = settings_conf_zusammenfuehren(alt, "T-neu") diff --git a/docker/api/test_makemkv_key.py b/docker/api/test_makemkv_key.py index aa8f3d9..ecfd257 100644 --- a/docker/api/test_makemkv_key.py +++ b/docker/api/test_makemkv_key.py @@ -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 # 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(): - # 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 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). treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text") assert treffer == _KEY diff --git a/docker/ui/src/components/FirstRunWizard.tsx b/docker/ui/src/components/FirstRunWizard.tsx index 8d81bdb..ac157b0 100644 --- a/docker/ui/src/components/FirstRunWizard.tsx +++ b/docker/ui/src/components/FirstRunWizard.tsx @@ -37,12 +37,12 @@ const ENCODER_LABELS: Record = { // braucht roh rund 40 GB, eine 4K-UHD bis 100 GB. 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) { if (schwach) { // 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 - // als H.265, dafuer etwas groesser). + // verlustfrei behalten und für den Rest H.264 nehmen (deutlich schneller + // als H.265, dafür etwas größer). return { transcodePreset: 'HQ 1080p30 Surround', transcodePresetBluray: 'HQ 1080p30 Surround', @@ -72,7 +72,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) { const [keyWarnung, setKeyWarnung] = useState(null) // 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. useEffect(() => { let laeuft = true @@ -112,8 +112,8 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) { transcodeEnabled, ...presets, }) - // Erst SPEICHERN, dann pruefen: /metadata/status liest die Keys aus der - // Datenbank, nicht aus dem Formular. Ein falsch kopierter Key faellt so + // Erst SPEICHERN, dann prüfen: /metadata/status liest die Keys aus der + // Datenbank, nicht aus dem Formular. Ein falsch kopierter Key fällt so // sofort auf, statt erst beim ersten Rip als „Unknown Disc". if (tmdbKey.trim() || omdbKey.trim()) { try { @@ -131,7 +131,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) { return // nicht wegklicken, damit die Warnung gelesen wird } } 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') diff --git a/docker/ui/src/components/WorkerVerwaltung.tsx b/docker/ui/src/components/WorkerVerwaltung.tsx index e878a54..777e80c 100644 --- a/docker/ui/src/components/WorkerVerwaltung.tsx +++ b/docker/ui/src/components/WorkerVerwaltung.tsx @@ -88,7 +88,7 @@ export default function WorkerVerwaltung() { : [ // Standard-Ziel ist "C:\Program Files\Rippy Worker" - dorthin darf nur // 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)`, `iwr http://${hostFuerBefehl}/api/worker-setup/windows -OutFile install-rippy-worker.ps1`, `powershell -ExecutionPolicy Bypass -File .\\install-rippy-worker.ps1 \``, diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx index db848be..b23de01 100644 --- a/docker/ui/src/pages/Settings.tsx +++ b/docker/ui/src/pages/Settings.tsx @@ -72,8 +72,8 @@ interface WorkerInfo { encoders: string[] // keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg // in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich - // rippt, zaehlt, deshalb steht die Angabe pro Worker und nicht global. - // cpu_kerne/cpu_simd: Rechenleistung fuers Software-Encodieren. Ohne diese + // rippt, zählt, deshalb steht die Angabe pro Worker und nicht global. + // 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 // Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne). // handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst. @@ -101,7 +101,7 @@ interface StorageZiel { 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 { vorhanden: boolean pfad: string @@ -125,7 +125,7 @@ interface SystemInfo { 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 { if (!bytes) return '0 B' 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 -// 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. function zeitLesbar(iso: string): string { if (!iso) return 'unbekannt' @@ -158,7 +158,7 @@ export default function SettingsPage() { const [updatesBusy, setUpdatesBusy] = useState(false) // KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine // 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(null) const [keydbBusy, setKeydbBusy] = useState(false) const [dumps, setDumps] = useState([]) @@ -200,7 +200,7 @@ export default function SettingsPage() { // letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen // widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung. // 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([])) } @@ -208,7 +208,7 @@ export default function SettingsPage() { setKeydbBusy(true) try { // 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() await api.post('/system/keydb', { inhalt }) keydbLaden() @@ -436,9 +436,9 @@ export default function SettingsPage() { {/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen. Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe - build_abcde_cmd) und es gibt auch keine Oberflaeche, um eine - Teilmenge zu waehlen — eine Audio-CD wurde also immer - vollstaendig gerippt, egal wie der Schalter stand. Statt einer + build_abcde_cmd) und es gibt auch keine Oberfläche, um eine + Teilmenge zu wählen — eine Audio-CD wurde also immer + vollständig gerippt, egal wie der Schalter stand. Statt einer Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}

💿 Eine Audio-CD wird immer vollständig gerippt — alle Tracks als @@ -892,9 +892,9 @@ export default function SettingsPage() { )} {/* - Nur das Verzeichnis des rippenden Workers zaehlt — deshalb je Worker. - Die Zahl der Disc-Schluessel ist die entscheidende Angabe fuer 4K-UHD: - 0 heisst, dass JEDE unbekannte UHD-Disc scheitert. + Nur das Verzeichnis des rippenden Workers zählt — deshalb je Worker. + Die Zahl der Disc-Schluessel ist die entscheidende Angabe für 4K-UHD: + 0 heißt, dass JEDE unbekannte UHD-Disc scheitert. */} {w.info?.schluessel && w.info.schluessel !== '0' && w.info.schluessel !== 'unbekannt' && ( @@ -919,7 +919,7 @@ export default function SettingsPage() { Disc-Schluessel-Datenbank mit. Nachgemessen: falsch — MakeMKV liefert gar keine Schluessel mit, es holt sie zur Laufzeit. Und die Linux-Version holt sie nie (kein einziger Verbindungsversuch, auf - beiden Maschinen verglichen). Deshalb der Schluesselspeicher unten. + beiden Maschinen verglichen). Deshalb der Schlüsselspeicher unten. */}

MakeMKV 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 unter Linux NIE selbst, die Windows-Version tut es (Meldung 3338, 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, - die auch MakeMKV selbst nicht kennt. Der Regelfall laeuft ueber den - Schluesselspeicher im Block darueber. Dateiname GROSS geschrieben, unter + die auch MakeMKV selbst nicht kennt. Der Regelfall läuft über den + Schlüsselspeicher im Block darüber. Dateiname GROSS geschrieben, unter Linux case-sensitiv. Rippy stellt nur den Platz bereit. */}

@@ -1107,7 +1107,7 @@ export default function SettingsPage() { {/* 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. */}
diff --git a/docker/worker/db.py b/docker/worker/db.py index 39b6e85..9b5c5c4 100644 --- a/docker/worker/db.py +++ b/docker/worker/db.py @@ -203,7 +203,7 @@ def zaehle_online_worker(sekunden: int = 120) -> int: Die Zombie-Erkennung vergleicht das mit der Zahl der Celery-Antworten: 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 diff --git a/docker/worker/makemkv_daten.py b/docker/worker/makemkv_daten.py index 936bb37..bcf02c2 100644 --- a/docker/worker/makemkv_daten.py +++ b/docker/worker/makemkv_daten.py @@ -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): 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: makemkvcon unter LINUX ruft die Disc-Schluessel nie ab. @@ -10,35 +10,35 @@ MakeMKV liest die Disc — sondern: Gemessen, nicht vermutet: * Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit - leerem UND mit gefuelltem Schluesselspeicher, mit und ohne --noscan, mit - dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Pruefung - (update.conf geloescht, Meldung 5074 belegt den Web-Kontakt). Immer: + leerem UND mit gefülltem Schlüsselspeicher, mit und ohne --noscan, mit + dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Prüfung + (update.conf gelöscht, Meldung 5074 belegt den Web-Kontakt). Immer: keine Verbindung, kein Schluessel. * Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading 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"). - * Der Code dafuer steckt auch im Linux-Binary: die Meldungsvorlage - "Downloading latest %1 to %2 ..." steht in makemkvcon. Sie loest nur nie + * Der Code dafür steckt auch im Linux-Binary: die Meldungsvorlage + "Downloading latest %1 to %2 ..." steht in makemkvcon. Sie löst nur nie aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet. FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst, 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 aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der 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. 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. 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=34022 * 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 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 @@ -72,14 +72,14 @@ DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV") # GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert. 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 # eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt. MAX_KEYDB_BYTES = 64 * 1024 * 1024 # Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit # 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*=") @@ -94,9 +94,9 @@ def keydb_pfad(daten_dir: str = None) -> str: 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 macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI "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: - """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 — - 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(): return "Die Datei ist leer." if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES: return ( - "Die Datei ist groesser als " + "Die Datei ist größer als " f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg." ) 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: eintraege = zaehle_disc_eintraege(datei.read()) except OSError: - pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem + pass # Datei da, aber unlesbar: Größe/Datum stimmen trotzdem return { "vorhanden": True, "pfad": pfad, @@ -163,7 +163,7 @@ def keydb_status(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. - 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. """ pfad = keydb_pfad(daten_dir) @@ -244,32 +244,32 @@ def dumps_auflisten(daten_dir: str = None) -> list: return liste -# --- Schluesselspeicher (_private_data.tar) ------------------------------- +# --- Schlüsselspeicher (_private_data.tar) ------------------------------- # -# Das ist MakeMKVs eigener Speicher fuer die "Hashed Keys": ein tar-Archiv mit -# hkd_*.bin-Eintraegen. Unter Windows fuellt MakeMKV es selbst (Meldung 3338), +# Das ist MakeMKVs eigener Speicher für die "Hashed Keys": ein tar-Archiv mit +# hkd_*.bin-Eintraegen. Unter Windows füllt MakeMKV es selbst (Meldung 3338), # 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" -# 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 -# client_max_body_size in docker/ui/nginx.conf: waere die Grenze hier hoeher, -# wuerde nginx den Upload abweisen, bevor die API ihn ueberhaupt sieht. +# client_max_body_size in docker/ui/nginx.conf: wäre die Grenze hier höher, +# würde nginx den Upload abweisen, bevor die API ihn überhaupt sieht. MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024 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) def zaehle_schluessel(rohdaten: bytes) -> int: """Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar). - Das ist die ehrliche Kennzahl fuer "wie viele Disc-Schluessel kennt diese - Installation". Ein frischer, leerer Speicher enthaelt nur eine + Das ist die ehrliche Kennzahl für "wie viele Disc-Schluessel kennt diese + Installation". Ein frischer, leerer Speicher enthält nur eine Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede unbekannte UHD-Disc scheitert. """ @@ -281,20 +281,20 @@ def zaehle_schluessel(rohdaten: bytes) -> int: 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 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). """ if not rohdaten: return "Die Datei ist leer." if len(rohdaten) > MAX_PRIVATE_DATA_BYTES: return ( - "Die Datei ist groesser als " + "Die Datei ist größer als " f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein " - "MakeMKV-Schluesselspeicher." + "MakeMKV-Schlüsselspeicher." ) try: with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv: @@ -308,13 +308,13 @@ def private_data_pruefen(rohdaten: bytes) -> str: return ( "In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). " "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 "" 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) try: 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: - """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. """ 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). 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. - 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. """ zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")] diff --git a/docker/worker/ripping.py b/docker/worker/ripping.py index 68e8257..1f7b11b 100644 --- a/docker/worker/ripping.py +++ b/docker/worker/ripping.py @@ -223,9 +223,9 @@ def parse_msg(zeile: str): fertig zusammengesetzte Klartext. 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 ..."). - Deshalb eine Regex, die die Anfuehrungszeichen respektiert. + Deshalb eine Regex, die die Anführungszeichen respektiert. """ treffer = _MSG_RE.match(zeile.strip()) if not treffer: @@ -409,7 +409,7 @@ def _handbrake_schleife(process, output_path: str, abbruch_cb=None, progress_cb= try: for line in process.stdout: # 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). if 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 Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft 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 nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten). """ diff --git a/docker/worker/tasks.py b/docker/worker/tasks.py index d1362c8..c78094c 100644 --- a/docker/worker/tasks.py +++ b/docker/worker/tasks.py @@ -104,7 +104,7 @@ def unter_wurzel(pfad: str, wurzel: str) -> bool: """Liegt `pfad` wirklich unterhalb von `wurzel` (oder IST es die Wurzel)? 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 Container-Pfade, auch wenn ein nativer Windows-Worker das Modul lädt. diff --git a/docker/worker/test_makemkv_daten_worker.py b/docker/worker/test_makemkv_daten_worker.py index b285cbd..7f00dd0 100644 --- a/docker/worker/test_makemkv_daten_worker.py +++ b/docker/worker/test_makemkv_daten_worker.py @@ -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): 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 noch funktionierende Weg ist eine selbst mitgebrachte KEYDB.cfg im -Datenverzeichnis. Damit haengt einiges an diesen kleinen Funktionen: erkennen wir -die Datei falsch, meldet das UI "alles gut", waehrend MakeMKV weiter scheitert. +Datenverzeichnis. Damit hängt einiges an diesen kleinen Funktionen: erkennen wir +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 libaacs (AGENTS Regel D, externe Schnittstellen nie aus dem Kopf): 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 pytest Testdateien unter ihrem blossen Dateinamen — zwei Dateien namens 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 @@ -47,10 +47,10 @@ zaehle_schluessel = _modul.zaehle_schluessel 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 - 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 tarfile @@ -67,7 +67,7 @@ def _tar_mit(namen): def test_zaehle_schluessel_zaehlt_nur_hkd_eintraege(): # Nur hkd_*.bin sind Disc-Schluessel. Index- und sdf-Dateien gehoeren zum # 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([ "hkd_00000059.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(): # 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. leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"]) 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() assert worker == api, ( "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 @@ -142,7 +142,7 @@ A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D4 = BLADE RUNNER 2049 def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen(): """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". """ 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. 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" assert zaehle_disc_eintraege(zu_kurz) == 0 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(" \n\n ") != "" @@ -173,7 +173,7 @@ def test_keydb_pruefen_meldet_leere_datei(): def test_keydb_pruefen_erkennt_html_fehlerseite(): """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. """ html = "\n

404 Not Found

\n" @@ -184,12 +184,12 @@ def test_keydb_pruefen_erkennt_html_fehlerseite(): def test_keydb_pruefen_meldet_text_ohne_disc_zeile(): # 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 != "" 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) == "" @@ -200,9 +200,9 @@ def test_ist_aacs_dump_akzeptiert_echten_namen(): def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab(): - """Der Download-Endpunkt haengt den Namen an das Datenverzeichnis an — - ohne diese Pruefung koennte man sich damit aus dem Verzeichnis heraus - lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts fuer die + """Der Download-Endpunkt hängt den Namen an das Datenverzeichnis an — + ohne diese Prüfung könnte man sich damit aus dem Verzeichnis heraus + lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts für die Liste.""" assert ist_aacs_dump("../ausbruch.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(): """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 - persistenten Datenverzeichnis waere damit bei jedem Containerstart und vor + und tasks.py die settings.conf komplett überschrieben. Mit dem jetzt + persistenten Datenverzeichnis wäre damit bei jedem Containerstart und vor jedem Rip alles andere weg — allen voran app_UpdateEnable.""" alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\napp_DefaultSelectionString = "+sel:all"\n' neu = settings_conf_zusammenfuehren(alt, "T-neuerSchluessel") diff --git a/docker/worker/test_medien.py b/docker/worker/test_medien.py index 4731990..359517b 100644 --- a/docker/worker/test_medien.py +++ b/docker/worker/test_medien.py @@ -114,15 +114,15 @@ def test_pfad_lokal_uebersetzt_fuer_windows_worker(): assert pfad_lokal("/app/temp/raw/abc", "") == "/app/temp/raw/abc" -# Die beiden frueheren Tests zu _original_aufheben sind nach -# test_original_aufheben.py gewandert und dort erweitert. Sie pruefen jetzt das +# Die beiden früheren Tests zu _original_aufheben sind nach +# test_original_aufheben.py gewandert und dort erweitert. Sie prüfen jetzt das # richtige Kriterium: Vorher taeuschten sie per gefaelschtem os.stat # „verschiedene Dateisysteme" vor — genau die Annahme, an der der Schutz in der # Praxis scheiterte (st_dev war identisch, os.rename trotzdem EXDEV). 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 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/movies" 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({}, "/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" def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein(): - """Befund 25.07.2026: Elf Stellen prueften mit nacktem startswith(). - „/app/media-boese/x" beginnt mit „/app/media", liegt aber ausserhalb.""" + """Befund 25.07.2026: Elf Stellen prüften mit nacktem startswith(). + „/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb.""" import tasks 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("", "/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 @@ -163,7 +163,7 @@ def test_zielbasis_lehnt_praefix_ausbruch_ab(): import tasks 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("/etc", "bluray") != "/etc" diff --git a/docker/worker/test_original_aufheben.py b/docker/worker/test_original_aufheben.py index 7a3185b..e4570f6 100644 --- a/docker/worker/test_original_aufheben.py +++ b/docker/worker/test_original_aufheben.py @@ -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 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), os.rename scheitert zwischen ihnen trotzdem mit EXDEV, weil der Kernel den -Mount vergleicht und nicht das Geraet. Die Platzpruefung wurde deshalb -uebersprungen und shutil.move kopierte 75 GB bei 37 GB frei. +Mount vergleicht und nicht das Gerät. Die Platzpruefung wurde deshalb +übersprungen und shutil.move kopierte 75 GB bei 37 GB frei. """ import errno @@ -46,7 +46,7 @@ def _lege_rohdaten_an(tmp_path, groesse=2048): 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) 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): - """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. """ raw, final = _lege_rohdaten_an(tmp_path) diff --git a/docker/worker/test_ripping_helpers.py b/docker/worker/test_ripping_helpers.py index 727b0cf..1a06dd0 100644 --- a/docker/worker/test_ripping_helpers.py +++ b/docker/worker/test_ripping_helpers.py @@ -113,7 +113,7 @@ def test_parse_msg_trennt_code_und_klartext(): def test_parse_msg_liest_die_uhd_fehlermeldung(): """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.""" 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"' @@ -125,9 +125,9 @@ def test_parse_msg_liest_die_uhd_fehlermeldung(): 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 - 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.""" zeile = ( '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(): - # 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. 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 @@ -188,8 +188,8 @@ def test_abcde_config_enthaelt_zielverzeichnis(): def test_preset_fuer_nimmt_das_preset_des_disc_typs(): - # Befund 25.07.2026: vorher galt EIN Preset fuer alles — eine 4K-UHD wurde - # damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip waere + # 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 wäre # die 4K-Aufloesung still verlorengegangen. 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(): # 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 alt = {"transcodePreset": "HQ 1080p30 Surround"} assert preset_fuer("uhd", 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" @@ -269,7 +269,7 @@ def test_handbrake_prueft_abbruch_bei_jeder_zeile_nicht_nur_bei_fortschritt(): assert ergebnis["status"] == "cancelled" 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. 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("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", None) is True 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 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 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(): - """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 brauchbares Preset bekommen statt eines Fehlers.""" from ripping import DEFAULT_HB_PRESET, PRESET_KEINE, preset_fuer assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE, "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 def test_reservewert_kollidiert_mit_keinem_echten_preset(): """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 assert PRESET_KEINE == "keine" diff --git a/install.sh b/install.sh index a713f6f..cea7985 100755 --- a/install.sh +++ b/install.sh @@ -3,15 +3,15 @@ # Rippy einrichten — ein Befehl, keine Handarbeit. # # 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) # * Ablage- und MakeMKV-Verzeichnis anlegen # * 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 # -# Es ist absichtlich WIEDERHOLBAR: mehrfach ausfuehren aendert nichts kaputt. -# Es aendert NIE einen Wert, der schon in der .env steht. +# Es ist absichtlich WIEDERHOLBAR: mehrfach ausführen ändert nichts kaputt. +# Es ändert NIE einen Wert, der schon in der .env steht. # # Aufruf: sudo ./install.sh # sudo ./install.sh --nur-pruefen (nichts anfassen, nur berichten) @@ -33,7 +33,7 @@ titel() { printf '\n\033[1m%s\033[0m\n' "$*"; } echo "======================================================================" echo " Rippy — Einrichtung" 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 @@ -43,7 +43,7 @@ titel "1/6 Voraussetzungen" if [ "$(uname -s)" != "Linux" ]; then 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 " durchreichen — deshalb geht es dort grundsaetzlich nicht.)" + rot " durchreichen — deshalb geht es dort grundsätzlich nicht.)" exit 1 fi gruen " OK Linux erkannt ($(uname -r))" @@ -71,14 +71,14 @@ fi if [ "$(id -u)" = "0" ]; then gruen " OK Als root gestartet" 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. - 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)." else rot " X Bitte mit sudo starten: sudo ./install.sh" 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 fi @@ -89,11 +89,11 @@ fi # --------------------------------------------------------------------------- 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 -# 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 -# 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="" SG="" LAUFWERK="" @@ -118,14 +118,14 @@ if [ -n "$SR" ]; then gruen " $SR + $SG (SCSI-Adresse abgeglichen, nicht geraten)" else gelb " ! Kein optisches Laufwerk gefunden." - gelb " Rippy laesst sich trotzdem installieren und kann dann als reine" - gelb " KOMPRIMIER-Maschine fuer andere Rippy-Instanzen dienen." - gelb " Zum Rippen fehlt das Laufwerk. Haeufigste Ursachen:" + gelb " Rippy lässt sich trotzdem installieren und kann dann als reine" + gelb " KOMPRIMIER-Maschine für andere Rippy-Instanzen dienen." + gelb " Zum Rippen fehlt das Laufwerk. Häufigste Ursachen:" gelb " - In einer VM: Laufwerk per USB-Passthrough durchreichen" gelb " (Proxmox: qm set -usb0 host=:,usb3=1)" gelb " NICHT als emuliertes CD-ROM (media=cdrom) — das kann keine" 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 # --------------------------------------------------------------------------- @@ -136,20 +136,20 @@ for d in "$MEDIA_DIR" "$MAKEMKV_DIR"; do if [ -d "$d" ]; then gruen " OK $d liegt schon da" elif [ "$NUR_PRUEFEN" = 1 ]; then - gelb " ! $d fehlt (wuerde angelegt)" + gelb " ! $d fehlt (würde angelegt)" else mkdir -p "$d" && gruen " OK $d angelegt" fi done 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 # --------------------------------------------------------------------------- -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. # Auf den meisten systemd-Hosts ist / schon rshared und es ist nichts zu tun. propagation_ok() { @@ -159,19 +159,19 @@ propagation_ok() { if propagation_ok; then gruen " OK $MEDIA_DIR ist bereits 'shared' — nichts zu tun" elif [ "$NUR_PRUEFEN" = 1 ]; then - gelb " ! Propagation fehlt (wuerde eingerichtet)" + gelb " ! Propagation fehlt (würde eingerichtet)" else mount --bind "$MEDIA_DIR" "$MEDIA_DIR" 2>/dev/null mount --make-rshared "$MEDIA_DIR" 2>/dev/null if propagation_ok; then gruen " OK Propagation eingerichtet" - # Neustart-fest machen, sonst ist es nach dem naechsten Reboot wieder weg - # und das Einhaengen aus dem UI scheitert stillschweigend. + # Neustart-fest machen, sonst ist es nach dem nächsten Reboot wieder weg + # und das Einhängen aus dem UI scheitert stillschweigend. 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 cat > "$UNIT" </dev/null 2>&1 \ && gruen " OK Neustart-fest gemacht ($(basename "$UNIT"))" \ || gelb " ! Konnte nicht neustart-fest gemacht werden — nach einem Reboot" - gelb " ggf. dieses Skript erneut ausfuehren." + gelb " ggf. dieses Skript erneut ausführen." fi else gelb " ! Propagation liess sich nicht einrichten." - gelb " Rippy laeuft trotzdem — nur das Einhaengen von NAS-Freigaben AUS DEM UI" - gelb " funktioniert dann nicht. Freigaben stattdessen auf dem Host einhaengen," + gelb " Rippy läuft trotzdem — nur das Einhängen von NAS-Freigaben AUS DEM UI" + gelb " funktioniert dann nicht. Freigaben stattdessen auf dem Host einhängen," gelb " z. B. unter $MEDIA_DIR/nas. Betrifft nur klassisches Docker mit" gelb " SYS_ADMIN — nicht rootless/Podman." fi @@ -207,7 +207,7 @@ cd "$(dirname "$0")" || exit 1 if [ ! -f .env ]; then if [ "$NUR_PRUEFEN" = 1 ]; then - gelb " ! .env fehlt (wuerde aus .env.example erzeugt)" + gelb " ! .env fehlt (würde aus .env.example erzeugt)" else cp .env.example .env && gruen " OK .env aus .env.example erzeugt" fi @@ -218,10 +218,10 @@ fi # Setzt einen Wert NUR, wenn er noch nicht (unkommentiert) gesetzt ist. setze_wenn_leer() { 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 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 sed -i "/^#*${schluessel}=/d" .env printf '%s=%s\n' "$schluessel" "$wert" >> .env @@ -236,9 +236,9 @@ fi setze_wenn_leer MAKEMKV_DATA_HOST "$MAKEMKV_DIR" 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 # startet Docker mit einem Pfad, den es nicht gibt, und die Meldung ist kryptisch. 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-) [ -n "${wirksam:-}" ] || wirksam="$standard" 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 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 gelb " Es wurde auch kein anderes Laufwerk gefunden — siehe Schritt 2." fi @@ -264,9 +264,9 @@ fi titel "6/6 Bauen und starten" if [ "$NUR_PRUEFEN" = 1 ]; then - gelb " ! Uebersprungen (Nur-Pruefen-Modus)." + gelb " ! Übersprungen (Nur-Prüfen-Modus)." 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 fi @@ -274,7 +274,7 @@ echo " Der erste Bau dauert je nach Internet 3-10 Minuten. Nur beim ersten echo "" if ! $COMPOSE up -d --build; then 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 " - Port 80 schon belegt: ss -tlnp | grep ':80 '" 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 "======================================================================" -gruen " Fertig. Rippy laeuft." +gruen " Fertig. Rippy läuft." echo "======================================================================" echo "" -echo " Jetzt im Browser oeffnen: http://$IP" +echo " Jetzt im Browser öffnen: http://$IP" echo "" echo " Dort startet der Einrichtungs-Assistent. Er zeigt dir, ob Laufwerk," echo " Worker und Platz in Ordnung sind, und stellt die Kompression passend"