docs: KONZEPT-WINDOWS — Rippy v5 als eigenstaendiges Electron-Programm
Die heutige Windows-Fassung ist der Docker-Code in einer EXE: PyInstaller
buendelt docker/api und docker/worker, uvicorn laeuft auf 127.0.0.1:7788,
pywebview stellt ein Fenster davor. Elf Release-Kandidaten in drei Tagen,
und die Funde sind fast alle derselbe Typ — Linux-Annahmen, die unter
Windows etwas anderes bedeuten (timeout ls -d, shutil.which, /app,
/dev/{name}, F: ohne Trenner).
Das Konzept beschreibt den Neubau als Windows-Programm: Electron 44,
TypeScript durchgehend, Einzelplatz, kein Python, kein HTTP-Server.
Vier Entscheide des Commanders (30.08.2026) sind eingearbeitet:
alles TypeScript/Node · Docker bleibt unangetastet · reiner Einzelplatz ·
eigener Branch.
Vor der ersten Zeile Konzept gemessen (Regel D), Belege in beweise/:
* Win32 aus Node an G: (HL-DT-ST BD-RE BU40N) — Laufwerkssuche,
CreateFileW, QUERY_PROPERTY (Modell + Serial), CHECK_VERIFY2,
GET_LENGTH_INFO (33,76 GB -> Blu-ray). IOCTL_CDROM_DISK_TYPE
antwortet mit Fehler 50 — derselbe Befund wie im Python-Treiber.
* Prozess-Leine (Job Object): Kind stirbt mit dem per taskkill /F
ohne /T abgeschossenen Elternprozess. Die Messung aus rc10, in
Node nachgestellt.
* node:sqlite ist in Node 24 eingebaut — in Electron noch ungeprueft,
ausdruecklich als offener Punkt markiert.
Akutestes Risiko im Dokument: MakeMKVs freier Beta-Key laeuft Ende
September 2026 ab, die Kaufseite fuer die Dauerlizenz ist seit Mai defekt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9050fea3f0
commit
cb811bec28
@@ -0,0 +1,771 @@
|
|||||||
|
# KONZEPT — Rippy für Windows
|
||||||
|
|
||||||
|
> **Rippy v5.** Ein eigenständiges Windows-Programm, gebaut mit Electron und
|
||||||
|
> TypeScript. Grüne Wiese: kein Docker-Code, kein Python, keine Container-Reste.
|
||||||
|
>
|
||||||
|
> Stand: 30.08.2026 · Branch `worktree-windows-electron`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Leseanleitung
|
||||||
|
|
||||||
|
| § | Frage |
|
||||||
|
|---|-------|
|
||||||
|
| 1 | Warum wird neu gebaut statt repariert? |
|
||||||
|
| 2 | Was hat der Commander entschieden? |
|
||||||
|
| 3 | Was wurde gemessen, bevor entschieden wurde? |
|
||||||
|
| 4–5 | Wie ist das Programm aufgebaut, und was passiert, wenn eine Disc reingeht? |
|
||||||
|
| 6–9 | Features, Oberfläche, Setup, Werkzeuge |
|
||||||
|
| 10 | Was kommt aus dem alten Rippy mit? |
|
||||||
|
| 11–13 | Etappen, Risiken, was NICHT gebaut wird |
|
||||||
|
|
||||||
|
**Was dieses Dokument NICHT tut:** Es erfindet keine Messungen. Alles, was hier
|
||||||
|
als Zahl oder Verhalten steht, ist entweder in § 3 an dieser Maschine gemessen,
|
||||||
|
im alten Rippy belegt (`SAVEPOINT.md`, `AGENTS.md`) oder ausdrücklich als
|
||||||
|
**Annahme** markiert. Die Trennung ist wichtig, weil dieses Projekt schon
|
||||||
|
mehrfach teuer dafür bezahlt hat, dass jemand eine plausible Erklärung für eine
|
||||||
|
gemessene gehalten hat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Warum neu gebaut wird
|
||||||
|
|
||||||
|
Die heutige Windows-Fassung ist **kein Windows-Programm**. Sie ist der
|
||||||
|
Docker-Code in einer EXE:
|
||||||
|
|
||||||
|
```
|
||||||
|
RippySetup.exe (PyInstaller)
|
||||||
|
├─ docker/api/ → FastAPI, uvicorn auf 127.0.0.1:7788
|
||||||
|
├─ docker/worker/ → der Celery-Worker, nur ohne Celery
|
||||||
|
├─ docker/ui/dist/ → dieselbe React-Oberfläche wie im Browser
|
||||||
|
└─ pywebview → ein Fenster davor
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Ergebnis steht im `SAVEPOINT.md`: **elf Release-Kandidaten in drei Tagen.**
|
||||||
|
Und die Funde sind fast alle vom selben Typ — Linux-Annahmen, die unter Windows
|
||||||
|
etwas anderes bedeuten:
|
||||||
|
|
||||||
|
| Fund | Was passierte |
|
||||||
|
|------|---------------|
|
||||||
|
| `timeout 4 ls -d <pfad>` als Verzeichnisprüfung | Unter Windows gibt es `timeout.exe`, aber sie kennt weder `ls` noch `-d`. Antwort war **„weg" für jedes Verzeichnis** — Rohdaten waren grundsätzlich unsichtbar. Dazu blitzten Konsolenfenster auf. |
|
||||||
|
| `shutil.which("makemkvcon")` | Findet unter Windows nie etwas — Programme liegen in `Program Files`, nicht im PATH. Die 4K-Schlüssel-Automatik lief deshalb **nie** an. |
|
||||||
|
| `os.path.isdir("/app")` | Rippy hielt sich für einen fremden Docker-Worker. Zweimal gefunden, an zwei Stellen. |
|
||||||
|
| Routen `/dev/{name}` | Das UI ruft mit `G` auf, gebaut war `/dev/G`. **Auswerfen und Disc-Scan antworteten immer mit 404.** |
|
||||||
|
| `F:\` wurde zu `F:` | Unter Windows ist `F:` ohne Trenner der *aktuelle Ordner* auf F, nicht dessen Wurzel. 16,5 GB Rohschnitt unsichtbar; der Dialog bot nur „Neu rippen" an. |
|
||||||
|
| `makemkvcon` überlebte den Elternprozess | Windows räumt Kindprozesse nicht auf. Zwei Waisen hielten das Laufwerk fest. |
|
||||||
|
| Zwei Speicher für dieselben Ordner | Die Oberfläche schrieb `outputDir` in die Datenbank, der Betrieb las `storage.medien` aus einer Konfigurationsdatei, die niemand schrieb. Eingestellt `E:\Rippy`, angezeigt `C:\Users\...\Videos\Rippy`. |
|
||||||
|
|
||||||
|
Diese Liste ist nicht das Problem — sie ist das **Symptom**. Jeder einzelne
|
||||||
|
Fund war eine Stelle, an der Code, der für einen Linux-Container geschrieben
|
||||||
|
wurde, unter Windows etwas anderes bedeutet. Solange die Grundlage ein
|
||||||
|
umgebogener Container ist, ist der nächste Fund dieser Sorte nur eine Frage der
|
||||||
|
Zeit.
|
||||||
|
|
||||||
|
**Die Entscheidung ist deshalb nicht „das ist schlecht gebaut", sondern: das
|
||||||
|
ist am falschen Ort gebaut.** Ein Windows-Programm fängt bei Windows an, nicht
|
||||||
|
bei einem Container, dem man Windows beibringt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Die Entscheidungen (Commander, 30.08.2026)
|
||||||
|
|
||||||
|
Vier Fragen wurden gestellt, vier sind beantwortet. Sie stehen hier, damit
|
||||||
|
später nachvollziehbar ist, was Entscheidung war und was Vorschlag.
|
||||||
|
|
||||||
|
### Entscheid 1 — Alles TypeScript/Node
|
||||||
|
|
||||||
|
Es gibt **keinen Python-Anteil** in Rippy für Windows. Kein Sidecar, keine
|
||||||
|
mitgelieferte Laufzeit, keine IPC-Brücke zwischen zwei Sprachen. Eine Sprache,
|
||||||
|
eine Werkzeugkette, ein Setup.
|
||||||
|
|
||||||
|
*Was das kostet:* Die rund 18.000 Zeilen Python werden nicht übernommen. Was
|
||||||
|
übernommen wird, ist ihr **Wissen** — die Parser-Regeln, die gemessenen
|
||||||
|
Fallen, die Kommandozeilen (§ 10). Das ist der teure Teil an ihnen, und der
|
||||||
|
wandert vollständig mit.
|
||||||
|
|
||||||
|
*Was das bringt:* Der ganze Fehlertyp aus § 1 kann nicht mehr entstehen. Es
|
||||||
|
gibt keinen Container-Pfad, den jemand vergessen könnte, weil es nie einen gab.
|
||||||
|
|
||||||
|
### Entscheid 2 — Die Docker-Version bleibt unangetastet
|
||||||
|
|
||||||
|
Rippy auf der VM läuft weiter wie heute. Es wird nicht angefasst, nicht
|
||||||
|
migriert, nicht mitgepflegt. Zwei getrennte Produkte, keine geteilten Module.
|
||||||
|
|
||||||
|
*Begründung:* Geteilte Module zwischen zwei Betriebssystemen sind genau die
|
||||||
|
Konstruktion, an der v1 gelitten hat — drei Dateien lagen byte-identisch
|
||||||
|
doppelt im Repo, und `db.py` schrieb es sogar in den Kopfkommentar: *„Wer die
|
||||||
|
Struktur ändert, ändert BEIDE Dateien."*
|
||||||
|
|
||||||
|
### Entscheid 3 — Reiner Einzelplatz
|
||||||
|
|
||||||
|
Ein Rechner macht alles: erkennen, rippen, komprimieren, ablegen. Kein Server,
|
||||||
|
keine Netz-Queue, keine Worker-Verwaltung, keine Pfad-Übersetzung zwischen
|
||||||
|
Maschinen.
|
||||||
|
|
||||||
|
*Was damit ersatzlos wegfällt:* Celery, Redis, Postgres, die Worker-Tabelle,
|
||||||
|
`worker_direct`, die Lease-Mechanik über Netz, `RIPPY_PATH_MAP`, die
|
||||||
|
Zombie-Erkennung, die Mount-Wache, das Rate-Limit (es gibt keinen fremden
|
||||||
|
Client mehr), `/worker-setup/*`, die Encoder-Auswahl je Knoten.
|
||||||
|
|
||||||
|
Grob geschätzt sind das **40 % der heutigen Komplexität** — und zwar der Teil,
|
||||||
|
der die meisten Fehlerbilder erzeugt hat.
|
||||||
|
|
||||||
|
*Was bleibt:* Die Ablage darf trotzdem auf dem NAS liegen. Ein UNC-Pfad
|
||||||
|
(`\\nas\medien`) oder ein verbundenes Netzlaufwerk ist für Rippy nur ein Pfad.
|
||||||
|
Windows mountet, Rippy prüft — dieselbe Trennung wie in Entscheid 3 des
|
||||||
|
v2-Konzepts, nur ohne Container drumherum.
|
||||||
|
|
||||||
|
### Entscheid 4 — Eigener Branch im selben Gitea-Repo
|
||||||
|
|
||||||
|
Ein Git-Worktree unter `.claude/worktrees/windows-electron` auf dem Branch
|
||||||
|
`worktree-windows-electron`. `main` bleibt unberührt und jederzeit deploybar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Was gemessen wurde, bevor entschieden wurde
|
||||||
|
|
||||||
|
Electron bedeutet: Die Arbeit macht Node, nicht Python. Damit hängt das ganze
|
||||||
|
Konzept an einer Frage, die vorher niemand geprüft hat:
|
||||||
|
|
||||||
|
> **Kann Node überhaupt alles, was Rippy am Laufwerk braucht?**
|
||||||
|
|
||||||
|
Wäre die Antwort nein, wäre das Konzept wertlos. Also gemessen — am
|
||||||
|
30.08.2026, auf diesem Rechner, am echten Laufwerk.
|
||||||
|
|
||||||
|
### 3.1 Die Werkzeuglage
|
||||||
|
|
||||||
|
```
|
||||||
|
Node v24.19.0
|
||||||
|
npm 11.17.0
|
||||||
|
Electron 44.0.0 (Chromium 152.0.7977.54 · Node 24.18.1)
|
||||||
|
WebView2 151.0.4129.107
|
||||||
|
Laufwerk G: HL-DT-ST BD-RE BU40N USB Device (Medium eingelegt)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Win32 aus Node — die Laufwerks-Steuerung
|
||||||
|
|
||||||
|
`koffi` 3.1.6 installiert, dieselben Steuercodes wie in
|
||||||
|
`src/rippy/drives/win_ioctl.py` (die dort aus dem `CTL_CODE`-Makro hergeleitet
|
||||||
|
sind, nicht abgeschrieben). Nur lesende Aufrufe, nichts ausgeworfen:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. GetLogicalDrives + GetDriveTypeW -> optische Laufwerke: [ 'G' ]
|
||||||
|
2. CreateFileW \\.\G: (Zugriff 0) -> offen
|
||||||
|
CreateFileW \\.\G: (GENERIC_READ) -> offen
|
||||||
|
3. IOCTL_STORAGE_QUERY_PROPERTY -> Hersteller: HL-DT-ST | Modell: BD-RE BU40N
|
||||||
|
| Rev: 1.03 | Serial: 0025114C0149
|
||||||
|
4. IOCTL_STORAGE_CHECK_VERIFY2 -> Medium eingelegt
|
||||||
|
5. IOCTL_CDROM_DISK_TYPE -> Win32-Fehler 50
|
||||||
|
6. IOCTL_DISK_GET_LENGTH_INFO -> 33.759.690.752 Bytes (33,76 GB) => Blu-ray
|
||||||
|
```
|
||||||
|
|
||||||
|
**Punkt 5 ist kein Fehlschlag, sondern eine Bestätigung.** Fehler 50 ist
|
||||||
|
`ERROR_NOT_SUPPORTED` — genau das, was auch der Python-Treiber an diesem
|
||||||
|
Laufwerk sieht (`SAVEPOINT.md`, rc11). Node verhält sich identisch. Und die
|
||||||
|
Lehre daraus steht schon im alten Code: **Die Disc-Einordnung darf sich nicht
|
||||||
|
auf `IOCTL_CDROM_DISK_TYPE` verlassen — die Größe ist der verlässliche Weg.**
|
||||||
|
|
||||||
|
### 3.3 Die Prozess-Leine — überlebt makemkvcon einen Absturz?
|
||||||
|
|
||||||
|
Der Befund aus rc10: Windows räumt Kindprozesse nicht auf. Ein hart beendeter
|
||||||
|
Rippy hinterlässt ein `makemkvcon`, das das Laufwerk festhält. Die Lösung dort
|
||||||
|
war eine Windows-Arbeitsgruppe (Job Object) mit
|
||||||
|
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`.
|
||||||
|
|
||||||
|
In Node nachgestellt — Job Object gesetzt, Kind gestartet, Eltern mit
|
||||||
|
`taskkill /F` **ohne** `/T` abgeschossen (genau das, was bei einem Absturz und
|
||||||
|
beim Drüber-Installieren passiert):
|
||||||
|
|
||||||
|
```
|
||||||
|
1. CreateJobObjectW -> Arbeitsgruppe angelegt
|
||||||
|
2. SetInformationJobObject -> KILL_ON_JOB_CLOSE gesetzt
|
||||||
|
3. AssignProcessToJobObject -> dieser Prozess hängt in der Gruppe
|
||||||
|
4. Kindprozess gestartet, PID 16040
|
||||||
|
|
||||||
|
vor dem Abschuss: Eltern lebt: True Kind lebt: True
|
||||||
|
taskkill /F /PID 29488 -> ERFOLGREICH
|
||||||
|
danach: Eltern lebt: False Kind lebt: False
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Datenbank ohne native Abhängigkeit
|
||||||
|
|
||||||
|
```
|
||||||
|
node:sqlite eingebaut -> geht
|
||||||
|
```
|
||||||
|
|
||||||
|
Node 24 bringt SQLite mit (`node:sqlite`, `DatabaseSync`). Keine
|
||||||
|
`better-sqlite3`-Kompilierung, kein `node-gyp`, kein Visual-Studio-Build in der
|
||||||
|
Bau-Kette.
|
||||||
|
|
||||||
|
> ⚠️ **Ungeprüft:** Electron kompiliert sein Node mit eigenen Flags. Ob
|
||||||
|
> `node:sqlite` in Electron 44 vorhanden ist, ist an *dieser* Stelle **nicht**
|
||||||
|
> gemessen — nur in Node 24.19.0 selbst. Das ist der erste Handgriff in
|
||||||
|
> Etappe W-0. Rückfallebene: `better-sqlite3` (bewährt, aber nativ und damit
|
||||||
|
> eine Kompilierung in der Bau-Kette).
|
||||||
|
|
||||||
|
### 3.5 Ein Nebenbefund für die Bau-Kette
|
||||||
|
|
||||||
|
`npm` 11 blockiert Install-Skripte fremder Pakete standardmäßig:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm warn allow-scripts koffi@3.1.6 (install: node ./cnoke.cjs -P . --prebuild --release)
|
||||||
|
```
|
||||||
|
|
||||||
|
`koffi` lädt trotzdem — es bringt vorgebaute Binärdateien mit. Aber die
|
||||||
|
Warnung gehört in die Bau-Anleitung, sonst sucht jemand eines Tages an der
|
||||||
|
falschen Stelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Aufbau
|
||||||
|
|
||||||
|
### 4.1 Drei Teile, drei Prozesse
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Rippy.exe — Electron Main-Prozess │
|
||||||
|
│ ├─ Tray-Symbol, Fensterverwaltung, Autostart, Selbst-Update │
|
||||||
|
│ └─ hält die Prozess-Leine (§ 3.3) — ALLES stirbt mit ihm │
|
||||||
|
└───────────┬─────────────────────────────┬────────────────────┘
|
||||||
|
│ IPC │ MessagePort
|
||||||
|
┌───────────▼──────────────┐ ┌──────────▼───────────────────────┐
|
||||||
|
│ Fenster (Renderer) │ │ Kern (utilityProcess) │
|
||||||
|
│ React + Tailwind │◄──┤ die GANZE Arbeit │
|
||||||
|
│ zeigt an, sonst nichts │ │ ├─ Laufwerkswache (koffi/Win32) │
|
||||||
|
│ │ │ ├─ Ablauf-Steuerung │
|
||||||
|
│ kein Node-Zugriff │ │ ├─ Auftragstabelle (SQLite) │
|
||||||
|
│ (contextIsolation) │ │ └─ Werkzeuge als Kindprozesse: │
|
||||||
|
│ │ │ makemkvcon · HandBrakeCLI │
|
||||||
|
└──────────────────────────┘ └──────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warum drei und nicht einer:**
|
||||||
|
|
||||||
|
1. **Ein Rip dauert 30–90 Minuten.** Läuft er im Main-Prozess, friert die
|
||||||
|
Oberfläche ein — Electrons eigene Dokumentation sagt das ausdrücklich.
|
||||||
|
2. **Der Kern darf abstürzen, ohne das Fenster mitzureißen.** Dann steht dort
|
||||||
|
„Der Kern ist abgestürzt, ich starte ihn neu" statt eines toten Fensters.
|
||||||
|
3. **Das Fenster darf zugehen, ohne den Rip zu beenden.** Rippy lebt dann im
|
||||||
|
Tray weiter — genau wie heute, nur ohne den Umweg über einen HTTP-Server.
|
||||||
|
|
||||||
|
**Warum `utilityProcess` und nicht `child_process.fork`:** Electrons eigene
|
||||||
|
Empfehlung. `utilityProcess` kann einen `MessagePort` direkt zum Renderer
|
||||||
|
aufbauen — der Fortschritt eines Rips geht damit ohne Umweg über den
|
||||||
|
Main-Prozess ins Fenster.
|
||||||
|
|
||||||
|
**Was es NICHT gibt:** keinen HTTP-Server, keinen Port, kein `localhost:7788`,
|
||||||
|
keine REST-API, kein SSE. Fenster und Kern reden über Electrons IPC. Ein
|
||||||
|
lokaler Webserver in einer Einzelplatz-Anwendung ist eine offene Tür ohne
|
||||||
|
Gegenwert — und er war der Grund für das Rate-Limit, den Proxy-Kummer und die
|
||||||
|
Polling-Last im alten Rippy.
|
||||||
|
|
||||||
|
### 4.2 Ordner
|
||||||
|
|
||||||
|
```
|
||||||
|
rippy-windows/
|
||||||
|
├── package.json
|
||||||
|
├── electron.vite.config.ts
|
||||||
|
├── src/
|
||||||
|
│ ├── haupt/ Electron Main
|
||||||
|
│ │ ├── index.ts Start, Einzelinstanz-Sperre, Leine setzen
|
||||||
|
│ │ ├── fenster.ts Fenster anlegen, Zustand merken
|
||||||
|
│ │ ├── tray.ts Symbol + Kontextmenü
|
||||||
|
│ │ ├── autostart.ts HKCU\...\Run
|
||||||
|
│ │ └── update.ts electron-updater
|
||||||
|
│ │
|
||||||
|
│ ├── kern/ utilityProcess — die Arbeit
|
||||||
|
│ │ ├── laufwerk/
|
||||||
|
│ │ │ ├── win32.ts DER EINZIGE ORT MIT koffi
|
||||||
|
│ │ │ ├── wache.ts Einwurf/Auswurf erkennen
|
||||||
|
│ │ │ └── disc.ts Typ, Label, Größe, TOC
|
||||||
|
│ │ ├── metadaten/
|
||||||
|
│ │ │ ├── tmdb.ts tvdb.ts omdb.ts musicbrainz.ts
|
||||||
|
│ │ │ ├── discmerkmale.ts Label + Laufzeiten + BDMV-Titel
|
||||||
|
│ │ │ └── zuordnung.ts Treffer + Sicherheitsgrad
|
||||||
|
│ │ ├── rip/
|
||||||
|
│ │ │ ├── makemkv.ts Kommandobau + Aufruf
|
||||||
|
│ │ │ ├── parser.ts TINFO/SINFO/PRGV/MSG (rein, testbar)
|
||||||
|
│ │ │ ├── audiocd.ts Roh-Sektoren + FLAC
|
||||||
|
│ │ │ └── iso.ts Datendiscs 1:1 sichern
|
||||||
|
│ │ ├── komprimieren/
|
||||||
|
│ │ │ ├── handbrake.ts Kommandobau + Aufruf + Fortschritt
|
||||||
|
│ │ │ ├── encoder.ts GEMESSEN, nicht behauptet
|
||||||
|
│ │ │ └── presets.ts
|
||||||
|
│ │ ├── ablage/
|
||||||
|
│ │ │ ├── struktur.ts Jellyfin/Emby/Kodi/Plex
|
||||||
|
│ │ │ ├── nfo.ts poster.ts medienserver.ts
|
||||||
|
│ │ ├── ablauf/
|
||||||
|
│ │ │ ├── pipeline.ts die Verkettung — NUR HIER
|
||||||
|
│ │ │ ├── zustand.ts Zustandsmaschine
|
||||||
|
│ │ │ └── auftraege.ts Warteschlange, Slots
|
||||||
|
│ │ ├── werkzeuge/
|
||||||
|
│ │ │ ├── katalog.ts wo liegt makemkvcon/HandBrakeCLI/flac
|
||||||
|
│ │ │ ├── beschaffen.ts holen, prüfen, einrichten
|
||||||
|
│ │ │ └── leine.ts Job Object (§ 3.3)
|
||||||
|
│ │ └── speicher/
|
||||||
|
│ │ ├── db.ts SQLite
|
||||||
|
│ │ ├── einstellungen.ts EINE Quelle (§ 6.7)
|
||||||
|
│ │ └── bibliothek.ts
|
||||||
|
│ │
|
||||||
|
│ ├── gemeinsam/ Typen + Ereignis-Schema — Fenster UND Kern
|
||||||
|
│ └── fenster/ React
|
||||||
|
│
|
||||||
|
├── bau/ electron-builder, NSIS-Skript, Icon
|
||||||
|
└── test/ Vitest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Sechs Regeln, die den Aufbau tragen
|
||||||
|
|
||||||
|
Jede ist aus einem bezahlten Fehler abgeleitet. Sie sind mechanisch prüfbar
|
||||||
|
und bekommen je einen Wächter-Test.
|
||||||
|
|
||||||
|
**R1 — Win32 lebt nur in `laufwerk/win32.ts`.**
|
||||||
|
Kein `koffi`-Import irgendwo sonst. Ein Wächter-Test greift bei jedem anderen
|
||||||
|
Fundort. *(Grund: In v1 rief `main.py` `eject()` direkt auf — deshalb brauchte
|
||||||
|
der API-Container Geräte-Zugriff und `SYS_ADMIN`.)*
|
||||||
|
|
||||||
|
**R2 — Ein Fehlschlag löscht nie einen Zustand.**
|
||||||
|
Kein `catch { return [] }`. Wer nichts Neues weiß, behält, was er wusste.
|
||||||
|
*(Grund: Fünfmal `catch(() => [])` im alten UI — jeder verpasste Abruf hieß
|
||||||
|
„es gibt keine Jobs", die Liste leerte sich im Sekundentakt. Der Commander
|
||||||
|
meldete es als „wird oft neu geladen".)*
|
||||||
|
|
||||||
|
**R3 — Auswerfen heißt: entriegeln, auswerfen, nachsehen.**
|
||||||
|
In dieser Reihenfolge. Kein Rückgabewert gilt als Beweis. *(Grund: `CDROMEJECT`
|
||||||
|
quittiert auf einem verriegelten Laufwerk Erfolg und tut nichts. MakeMKV
|
||||||
|
verriegelt die Tür während des Rips.)*
|
||||||
|
|
||||||
|
**R4 — Jede Hintergrundschleife meldet ihren Fehler.**
|
||||||
|
Kein stilles `catch {}`. *(Grund: Ein `except Exception: pass` in einer
|
||||||
|
Vorrats-Schleife hat einmal eine Stunde gekostet.)*
|
||||||
|
|
||||||
|
**R5 — Externe Schnittstellen werden gemessen, nicht erinnert.**
|
||||||
|
Jeder Kommandozeilen-Schalter, jeder IOCTL-Code trägt im Kommentar, wo er
|
||||||
|
herkommt. *(Grund: `--audio-codec` gibt es bei HandBrakeCLI nicht — es heißt
|
||||||
|
`-E`/`--aencoder`. Ein unbekannter Schalter ist für HandBrake kein Fehler:
|
||||||
|
Rückgabewert 0. Das kostete einen fertigen 16,5-GB-Rip, und ein Test hatte den
|
||||||
|
Fehler festgeschrieben statt ihn zu finden.)*
|
||||||
|
|
||||||
|
**R6 — Alles läuft an der Leine.**
|
||||||
|
Jeder Werkzeugaufruf hängt in der Arbeitsgruppe aus § 3.3. Ausgenommen ist nur
|
||||||
|
das Aufräum-Skript der Deinstallation — es muss Rippy überleben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Der Ablauf — was passiert, wenn eine Disc reingeht
|
||||||
|
|
||||||
|
Der rote Faden des Commanders: **so viel Arbeit abnehmen wie möglich.**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Disc rein
|
||||||
|
│ WM_DEVICECHANGE / DBT_DEVICEARRIVAL (Ereignis, kein Poll)
|
||||||
|
│ Rückfallebene: 3-Sekunden-Takt, falls die Nachricht ausbleibt
|
||||||
|
▼
|
||||||
|
2. Was ist das?
|
||||||
|
│ Größe (33,76 GB → Blu-ray) · Audio-TOC? · Dateisystem lesbar?
|
||||||
|
│ → Film / Serie / Musik / Daten
|
||||||
|
▼
|
||||||
|
3. Was ist da drauf? ← DER SCHRITT, DER HEUTE ZU DÜNN IST
|
||||||
|
│ Disc-Label · Titel-Laufzeiten · BDMV-Titel · DVD-IFO · CD-DiscID
|
||||||
|
│ → TMDb / TVDb / OMDb / MusicBrainz
|
||||||
|
│ → Treffer mit SICHERHEITSGRAD
|
||||||
|
▼
|
||||||
|
4. Sicher genug?
|
||||||
|
│ ja → sofort losrippen, nur Bescheid sagen (Zero-Click)
|
||||||
|
│ nein → fragen: Fenster hoch, Vorschläge zur Wahl
|
||||||
|
▼
|
||||||
|
5. Rippen makemkvcon, verlustfrei, Fortschritt aus PRGV
|
||||||
|
▼
|
||||||
|
6. Komprimieren HandBrakeCLI, Encoder GEMESSEN, Preset je Disc-Typ
|
||||||
|
▼
|
||||||
|
7. Ablegen Zielstruktur · NFO · Poster/Fanart
|
||||||
|
▼
|
||||||
|
8. Melden Medienserver anstoßen · Benachrichtigung · Auswerfen
|
||||||
|
▼
|
||||||
|
9. Merken Bibliothek fortschreiben — beim nächsten Mal erkannt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was an Schritt 3 neu ist.** Heute nimmt Rippy im Wesentlichen das Disc-Label
|
||||||
|
und die längste Laufzeit. Das reicht bei „PIRATES_OF_THE_CARIBBEAN", aber nicht
|
||||||
|
bei „LOGICAL_VOLUME_ID" — und dann fragt Rippy, obwohl die Antwort auf der
|
||||||
|
Disc steht. Vier Quellen kommen dazu:
|
||||||
|
|
||||||
|
| Quelle | Wo | Was sie liefert |
|
||||||
|
|--------|-----|-----------------|
|
||||||
|
| **BDMV-Metadaten** | `BDMV/META/DL/bdmt_*.xml` auf der Blu-ray | Der vom Studio hinterlegte Disc-Titel, oft mit Sprachvarianten. **Deutlich besser als das Volume-Label.** |
|
||||||
|
| **Titel-Struktur** | makemkvcon `info` | Die Anzahl und Länge der Titel ist ein Fingerabdruck: 1 langer + viele kurze = Film; 6–13 gleich lange = Serienstaffel. |
|
||||||
|
| **DVD-Struktur** | `VIDEO_TS`, IFO-Dateien | Kapitelzahl und Titelsatz-Aufbau |
|
||||||
|
| **CD-DiscID** | Audio-TOC | Bereits gebaut und bewiesen (rc8): Spurlage → MusicBrainz → Album, Interpret, Jahr, jeder Titel. |
|
||||||
|
|
||||||
|
Dazu ein **Sicherheitsgrad**, der ehrlich ist. Rippy sagt nicht „das ist
|
||||||
|
Akira", sondern „das ist sehr wahrscheinlich Akira (1988)" — und fragt nur,
|
||||||
|
wenn es das nicht sagen kann. Die Schwelle ist einstellbar; wer will, lässt
|
||||||
|
Rippy immer fragen, und wer will, lässt es immer durchlaufen.
|
||||||
|
|
||||||
|
**Und wenn Rippy sich irrt:** Der Titel lässt sich nachträglich ändern, und die
|
||||||
|
Ablage wandert mit. Das ist der Unterschied zwischen „falsch geraten" und
|
||||||
|
„falsch abgelegt".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Die Features
|
||||||
|
|
||||||
|
### 6.1 Automatische Erkennung (§ 5, Schritt 2–4)
|
||||||
|
|
||||||
|
Siehe oben. Kernsatz: **Rippy fragt nur, wenn es wirklich nicht weiß.**
|
||||||
|
|
||||||
|
### 6.2 Rippen
|
||||||
|
|
||||||
|
`makemkvcon` im Robot-Modus (`--robot --messages=-stdout --progress=-same`),
|
||||||
|
verlustfrei. Der Parser (TINFO/SINFO/PRGV/MSG) wandert aus dem Python-Code
|
||||||
|
nach TypeScript — er ist reine Textverarbeitung und damit vollständig
|
||||||
|
testbar, ohne Laufwerk.
|
||||||
|
|
||||||
|
Was aus dem alten Rippy zwingend mitkommt:
|
||||||
|
|
||||||
|
- **Lesefehler heißen Lesefehler.** MakeMKV kann 1 von 2 Titeln sichern und
|
||||||
|
sich trotzdem mit 0 beenden. Rippy warnt nach dem Rip — auch und gerade dann,
|
||||||
|
wenn er als Erfolg endete — und nennt die **MSG-Nummer**: MakeMKVs Texte sind
|
||||||
|
übersetzt, die Nummern nicht.
|
||||||
|
- **Erst nachsehen, dann rippen.** Liegt keine Disc drin, sagt Rippy das in
|
||||||
|
einem Satz, statt zwei Minuten in makemkvcon zu laufen und mit Code 11 zu
|
||||||
|
enden. Aber: Wenn das Nachsehen selbst scheitert, wird trotzdem gerippt —
|
||||||
|
„ich weiß es nicht" darf nie zu „es geht nicht" werden.
|
||||||
|
- **Die Ausgabe binär lesen.** makemkvcon mischt Kodierungen; ein `ü` im Pfad
|
||||||
|
hat die Kompression einmal getötet (`UnicodeDecodeError` auf Byte 0x81).
|
||||||
|
|
||||||
|
### 6.3 Audio-CDs
|
||||||
|
|
||||||
|
Der Weg aus rc8, bewiesen: Roh-Sektoren über Win32 lesen (eine Audio-CD hat
|
||||||
|
kein Dateisystem — die `Track01.cda` sind 44-Byte-Platzhalter), dann durch
|
||||||
|
`flac.exe`. Dazu MusicBrainz über die DiscID.
|
||||||
|
|
||||||
|
Zwei Fallen, beide gemessen und beide dokumentiert: Die DiscID rechnet **mit**
|
||||||
|
den 150 Frames Vorlauf, das Lesen **ohne**. Und die Leseadresse zählt in
|
||||||
|
2048er-Einheiten, obwohl ein Audio-Sektor 2352 Bytes hat.
|
||||||
|
|
||||||
|
### 6.4 ISO-Backup für Datendiscs *(neu)*
|
||||||
|
|
||||||
|
Die einzige echte Lücke gegenüber ARM. Wird eine Disc weder als Film noch als
|
||||||
|
Musik erkannt, wird sie 1:1 gesichert statt abgelehnt: sequenziell aus
|
||||||
|
`\\.\G:` lesen, Länge aus `IOCTL_DISK_GET_LENGTH_INFO` (in § 3.2 gemessen:
|
||||||
|
liefert exakte Bytes). Kein Fremdwerkzeug nötig.
|
||||||
|
|
||||||
|
### 6.5 Mehrere Laufwerke gleichzeitig *(neu)*
|
||||||
|
|
||||||
|
Laufwerke sind eigenständige Objekte mit stabiler Kennung (Seriennummer, nicht
|
||||||
|
Buchstabe — `0025114C0149` in § 3.2 gemessen). Ein Rip-Auftrag belegt genau
|
||||||
|
ein Laufwerk; die Zahl gleichzeitiger Rips und Kompressionen ist einstellbar.
|
||||||
|
|
||||||
|
> ⚠️ **Annahme, zu messen bevor sie festgeschrieben wird:** Mehrere
|
||||||
|
> `makemkvcon`-Prozesse teilen sich ein Datenverzeichnis
|
||||||
|
> (`_private_data.tar`, `settings.conf`). Der Entwurf sieht deshalb vor:
|
||||||
|
> **Rip-Phase parallel, Schlüssel-Phase serialisiert.** Das ist aus der
|
||||||
|
> Aktenlage abgeleitet, nicht gemessen — und gehört an zwei Laufwerken geprüft,
|
||||||
|
> bevor die Grenze im Code steht.
|
||||||
|
|
||||||
|
### 6.6 Bibliothek *(neu)*
|
||||||
|
|
||||||
|
Eine Ansicht über alles Gerippte: Cover, Größe, Datum, Ablageort, Dauer des
|
||||||
|
Vorgangs. Und der praktische Teil: **Rippy erkennt beim Einlegen, wenn eine
|
||||||
|
Disc schon einmal durchgelaufen ist** (über die Disc-Merkmale aus § 5) und
|
||||||
|
fragt nach, statt stumm zu doppeln.
|
||||||
|
|
||||||
|
### 6.7 Arbeitsverzeichnis und Ablage — eine Quelle
|
||||||
|
|
||||||
|
Der Fund aus rc11: Die Oberfläche schrieb in die Datenbank, der Betrieb las
|
||||||
|
aus einer Konfigurationsdatei, die niemand schrieb. Eingestellt `E:\Rippy`,
|
||||||
|
angezeigt `C:\Users\...\Videos\Rippy`.
|
||||||
|
|
||||||
|
In v5 gibt es **genau einen Speicher für Einstellungen** (`speicher/einstellungen.ts`,
|
||||||
|
SQLite). Ein Wächter-Test prüft mechanisch, dass kein zweiter Ort entsteht.
|
||||||
|
|
||||||
|
Dazu die Pfad-Regeln aus rc11, als reine Funktionen mit Tests:
|
||||||
|
- Eine Laufwerkswurzel behält ihren Trenner (`F:\`, nicht `F:`)
|
||||||
|
- UNC-Wurzeln bleiben absolut
|
||||||
|
- Verzeichnisprüfungen sind **windows-nativ** — kein `timeout ls -d`
|
||||||
|
- Pro Rip darf ein anderes Arbeitsverzeichnis gewählt werden, und **gesucht
|
||||||
|
wird später unter der Wahl des Jobs**, nicht unter der heutigen Einstellung
|
||||||
|
|
||||||
|
### 6.8 Selbst-Update *(neu)*
|
||||||
|
|
||||||
|
`electron-updater` gegen die Releases im Gitea. Rippy prüft beim Start und
|
||||||
|
danach täglich, lädt im Hintergrund und installiert beim nächsten Start —
|
||||||
|
**nie mitten in einem Rip**.
|
||||||
|
|
||||||
|
Die Quelle steht in der Konfiguration und ist vorbelegt mit dem Gitea des
|
||||||
|
Commanders. Ein nicht erreichbarer Update-Server ist kein Fehler, sondern eine
|
||||||
|
Zeile im Protokoll.
|
||||||
|
|
||||||
|
### 6.9 Erst-Einrichtung *(neu gedacht)*
|
||||||
|
|
||||||
|
Der Befund des Commanders zur heutigen Fassung: *„Dann hätte ich beim Setup
|
||||||
|
auch erwartet, dass ein echtes Setup passiert — wo will ich das hinspeichern,
|
||||||
|
ein Pre-Requirement-Check usw. Da kommt gar nichts, Rippy geht einfach auf."*
|
||||||
|
|
||||||
|
Der Assistent läuft **nach** der Installation, im Programm selbst, in fünf
|
||||||
|
Schritten:
|
||||||
|
|
||||||
|
| Schritt | Was passiert |
|
||||||
|
|---------|--------------|
|
||||||
|
| 1 Willkommen | Was Rippy tut, in vier Sätzen |
|
||||||
|
| 2 Prüfung | Laufwerke · Werkzeuge · Plattenplatz · Internet · WebView2 — **jeder Befund sagt, was zu tun ist** |
|
||||||
|
| 3 Werkzeuge | Was fehlt, wird geholt (§ 9) — mit Fortschritt, nicht mit einem eingefrorenen Fenster |
|
||||||
|
| 4 Ordner | Ablage und Arbeitsverzeichnis, mit echten Windows-Ordnerdialogen und Platzanzeige |
|
||||||
|
| 5 Automatik | Wie viel soll Rippy allein entscheiden? Je Disc-Typ einstellbar. |
|
||||||
|
|
||||||
|
Zwei Regeln, die aus dem alten Setup übernommen werden:
|
||||||
|
|
||||||
|
- **Nur ein Fehler blockiert.** Eine Warnung, die den Knopf sperrt, ist
|
||||||
|
Bevormundung; ein Fehler, der nur warnt, ist eine Falle. Kein optisches
|
||||||
|
Laufwerk ist eine **Warnung** — eine reine Komprimier-Maschine ist ein
|
||||||
|
vorgesehener Betriebsfall.
|
||||||
|
- **Ein Setup meldet nie Erfolg, während ein Pflichtwerkzeug fehlt.** Der
|
||||||
|
Bestand wird VOR und NACH dem Versuch gelesen; berichtet wird, was danach
|
||||||
|
wirklich da ist.
|
||||||
|
|
||||||
|
### 6.10 Was unverändert erhalten bleibt
|
||||||
|
|
||||||
|
Alles, was der Commander heute benutzt:
|
||||||
|
|
||||||
|
Kompression je Disc-Typ abwählbar (4K verlustfrei durchreichen) · Preset-Wahl
|
||||||
|
je Typ · Sprachwahl für Ton und Untertitel vor dem Rip · Original behalten ·
|
||||||
|
Vollautomatik · Nur-Hauptfeature · automatischer Auswurf · Medienserver-Refresh
|
||||||
|
(Jellyfin/Emby/Plex/Kodi) · NFO + Poster + Fanart · Serien-Staffelablage mit
|
||||||
|
Episoden-Zuordnung per Laufzeitabgleich (**nur bei eindeutiger Zuordnung wird
|
||||||
|
umbenannt**) · Webhook-Benachrichtigungen (Discord/Slack/ntfy/generisch) ·
|
||||||
|
Live-Log · Job-Detailansicht · Wiederholen ab Rip oder ab Kompression ·
|
||||||
|
4K-Schlüsselkette in drei Stufen · Restzeit-Schätzung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Die Oberfläche
|
||||||
|
|
||||||
|
**Entscheid:** Neu bauen, Look behalten.
|
||||||
|
|
||||||
|
Wiedererkennbar bleibt die Design-Sprache — dunkel/hell, die Kachel-Anordnung,
|
||||||
|
das Kino-Banner, die Farbwelt. Neu ist, dass die Oberfläche eine
|
||||||
|
**Einzelplatz-Anwendung** bedient statt eines Docker-Clusters.
|
||||||
|
|
||||||
|
**Was verschwindet:** Worker-Verwaltung · Container-Platte · „Prüfen:
|
||||||
|
`docker compose ps`" · Speicherziele-Einhängen · Pfad-Map · Worker-Setup-Paket ·
|
||||||
|
Encoder-Auswahl je Knoten · alles, was einen zweiten Rechner voraussetzt.
|
||||||
|
|
||||||
|
**Was dazukommt:**
|
||||||
|
|
||||||
|
- Echte **Windows-Ordnerdialoge** statt eines nachgebauten Dateibrowsers im
|
||||||
|
Web. Das ist ein spürbarer Unterschied im Alltag: Der Nutzer sieht seine
|
||||||
|
Netzlaufwerke, seine Schnellzugriffe, seine gewohnte Oberfläche.
|
||||||
|
- **Tray-Symbol mit Zustand** — auf einen Blick: läuft ein Rip, wie weit,
|
||||||
|
wie lange noch. Rechtsklick: Öffnen, Pause, Beenden.
|
||||||
|
- **Windows-Benachrichtigungen** (echte Toasts, nicht Browser-Popups), mit
|
||||||
|
Klick direkt in den Job.
|
||||||
|
- **Fortschritt in der Taskleiste** — Windows kann einen Fortschrittsbalken
|
||||||
|
über dem Symbol zeigen. Bei einem 90-Minuten-Rip ist das genau die
|
||||||
|
Information, die man will, ohne das Fenster zu öffnen.
|
||||||
|
- **Die Bibliothek** als eigene Ansicht (§ 6.6).
|
||||||
|
|
||||||
|
**Was die Oberfläche NIE tut:** Eine Liste auf leer setzen, weil eine Nachricht
|
||||||
|
ausblieb (R2). Bei einem Aussetzer bleibt der letzte Stand stehen, und ein
|
||||||
|
Banner sagt, dass gerade nichts Neues kommt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Setup, Autostart, Deinstallation
|
||||||
|
|
||||||
|
**Entscheid: pro Benutzer, ohne Administratorrechte, Windows 10 (ab 1809) und 11, x64.**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Installer | `electron-builder` → NSIS, ein `RippySetup.exe` |
|
||||||
|
| Ziel | `%LOCALAPPDATA%\Programs\Rippy` — kein UAC-Dialog |
|
||||||
|
| Autostart | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` |
|
||||||
|
| Update | `electron-updater`, NSIS-Ziel (§ 6.8) |
|
||||||
|
| Deinstallation | Über die Windows-Programmliste, räumt **wirklich** auf |
|
||||||
|
|
||||||
|
**Warum kein Windows-Dienst:** Ein Dienst startet vor der Anmeldung — klingt
|
||||||
|
besser, kostet aber bei jeder Installation und jedem Update eine
|
||||||
|
Administrator-Abfrage, und er kann kein Tray-Symbol zeigen (keine
|
||||||
|
Benutzersitzung). Für einen Rechner, an dem jemand sitzt und Discs einlegt, ist
|
||||||
|
der Autostart-Eintrag der richtige Weg.
|
||||||
|
|
||||||
|
**Drei Dinge, die aus den alten Fehlern übernommen werden:**
|
||||||
|
|
||||||
|
1. **Drüber-Installieren funktioniert.** Rippy läuft fast immer (Autostart) —
|
||||||
|
der Installer beendet die laufende Fassung, bevor er kopiert. Sonst landet
|
||||||
|
die neue Version als `Rippy.exe.neu` daneben. *(Genau das ist passiert.)*
|
||||||
|
2. **Die Deinstallation räumt die WebView-Reste weg.** Beim letzten Mal blieben
|
||||||
|
**148 Dateien** liegen, weil die Renderer-Hilfsprozesse den Zwischenspeicher
|
||||||
|
offen hielten — und der Deinstallierer meldete trotzdem „Rippy wurde
|
||||||
|
entfernt."
|
||||||
|
3. **Das Aufräum-Skript darf keine Laufwerkswurzel löschen.** Wenn der
|
||||||
|
Installationsort aus der Registry eine Wurzel wäre, löschte die
|
||||||
|
Deinstallation das Laufwerk. Nicht beobachtet — aber nicht wiedergutzumachen.
|
||||||
|
|
||||||
|
> ⚠️ **SmartScreen.** Eine unsignierte EXE bekommt beim ersten Start eine
|
||||||
|
> Warnung („Der Computer wurde geschützt"). Ein Code-Signing-Zertifikat kostet
|
||||||
|
> etwa 200–400 € im Jahr. Für den Eigengebrauch ist die Warnung hinnehmbar;
|
||||||
|
> das gehört in die Anleitung, nicht unter den Teppich. **Offener Punkt für den
|
||||||
|
> Commander.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Die Werkzeuge
|
||||||
|
|
||||||
|
Der Entscheid vom 28.08.2026 gilt unverändert: *„handbrake und makemkv MÜSSEN
|
||||||
|
mitgeliefert werden oder während des Setups separat installiert werden."* Die
|
||||||
|
Wege unterscheiden sich, und der Grund ist die **Lizenz**, nicht die
|
||||||
|
Bequemlichkeit.
|
||||||
|
|
||||||
|
| | Weg | Warum |
|
||||||
|
|---|---|---|
|
||||||
|
| **HandBrakeCLI** | **mitgeliefert** (~35 MB) | GPL-2 erlaubt die Weitergabe, solange Lizenztext und Quellverweis dabei sind. Damit komprimiert Rippy auch ohne Internet. |
|
||||||
|
| **FLAC** | **mitgeliefert** (~2 MB) | Xiph-Lizenz (BSD-artig), Weitergabe erlaubt. ⚠️ `flac.exe` braucht `libFLAC.dll` daneben — mit nur der EXE endet jeder Aufruf ohne eine Zeile Ausgabe. |
|
||||||
|
| **MakeMKV** | **beim Einrichten geholt** | Proprietär — die Lizenz erlaubt Dritten keine Weitergabe. Rippy lädt die offizielle Datei vom Hersteller und startet dessen Installer. |
|
||||||
|
|
||||||
|
**Die Bezugskette für MakeMKV** (aus dem 28.08.-Entscheid, unverändert):
|
||||||
|
eigene Quelle → Hersteller-Seite → Hersteller-Forum → Internet Archive. Mit
|
||||||
|
zwei Feinheiten, beide gemessen: **Die höchste Versionsnummer gewinnt, nicht
|
||||||
|
die erste** (das Archiv meldet sonst 1.18.2 statt 1.18.4), und **jeder Versuch
|
||||||
|
bekommt eine knappe Zeitgrenze** (8 s), weil das Forum in zwei von drei
|
||||||
|
Abrufen ausfiel.
|
||||||
|
|
||||||
|
MakeMKV signiert seinen Installer nicht (`NotSigned`, an der echten Datei
|
||||||
|
gemessen) — geprüft wird deshalb die Versions-Ressource
|
||||||
|
(`CompanyName: GuinpinSoft inc`). Das ersetzt keine Signatur, fängt aber ab,
|
||||||
|
was hier wirklich droht: eine Fehlerseite mit `.exe`-Namen, ein abgebrochener
|
||||||
|
Download, eine falsche Fassung.
|
||||||
|
|
||||||
|
### 9.1 Der Beta-Key — ein Termin, kein Detail
|
||||||
|
|
||||||
|
> **MakeMKVs freier Beta-Key ist bis Ende September 2026 gültig. Die Kaufseite
|
||||||
|
> für die Dauerlizenz ist seit Mai 2026 defekt.**
|
||||||
|
|
||||||
|
Das trifft Rippy in etwa vier Wochen, und ohne gültigen Key geht **keine
|
||||||
|
Blu-ray und keine 4K-Disc** mehr auf (DVDs laufen weiter).
|
||||||
|
|
||||||
|
Rippy v5 geht damit so um:
|
||||||
|
|
||||||
|
1. **Der Key wird automatisch geholt** (wie heute) — aus dem Forum-Thread, in
|
||||||
|
dem der Hersteller ihn veröffentlicht.
|
||||||
|
2. **Rippy zeigt das Ablaufdatum im Dashboard** und warnt **sieben Tage
|
||||||
|
vorher** — statt dass ein Rip mitten in der Nacht scheitert und niemand
|
||||||
|
weiß, warum.
|
||||||
|
3. **Ein abgelaufener Key ist eine klare Meldung**, kein rätselhafter
|
||||||
|
MakeMKV-Fehlercode.
|
||||||
|
4. **Ein eingetragener Dauerlizenz-Schlüssel schlägt alles.** Wer eine Lizenz
|
||||||
|
hat, trägt sie ein und ist raus aus dem Thema.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Was aus dem alten Rippy mitkommt
|
||||||
|
|
||||||
|
**Kein Code. Aber jede teuer bezahlte Erkenntnis.**
|
||||||
|
|
||||||
|
Das ist der Kern des Grüne-Wiese-Ansatzes: Der Python-Code wird nicht portiert,
|
||||||
|
aber er ist die beste verfügbare Dokumentation darüber, wie sich MakeMKV,
|
||||||
|
HandBrake und Windows-Laufwerke wirklich verhalten. Diese Tabelle ist die
|
||||||
|
Arbeitsliste für den Neubau.
|
||||||
|
|
||||||
|
| Herkunft | Was übernommen wird | Wohin |
|
||||||
|
|----------|--------------------|-------|
|
||||||
|
| `ripping.py` | TINFO/SINFO/PRGV/MSG-Parser, Kommandobau, `laengster_titel`, `episoden_titel`, Sprach-Zusammenfassung | `rip/parser.ts`, `rip/makemkv.ts` |
|
||||||
|
| `win_ioctl.py` | Die hergeleiteten Steuercodes samt Herleitung — **in § 3.2 aus Node bestätigt** | `laufwerk/win32.ts` |
|
||||||
|
| `windows.py`, `winlauf.py` | Zugriffsgründe je Fehlernummer, `CREATE_NO_WINDOW`, Standby verhindern, die Prozess-Leine | `laufwerk/`, `werkzeuge/leine.ts` |
|
||||||
|
| `caps.py` | Encoder **messen** statt behaupten (`HandBrakeCLI --help` auswerten) | `komprimieren/encoder.ts` |
|
||||||
|
| `handbrake_aufruf.py` | `--aencoder` (nicht `--audio-codec`), Container aus dem Preset erzwingen, letzte 12 Zeilen puffern, `unknown option`-Erkennung | `komprimieren/handbrake.ts` |
|
||||||
|
| `medien.py` | Zielstruktur, NFO-Schema, Episoden-Zuordnung per Laufzeit, Medienserver-Refresh | `ablage/` |
|
||||||
|
| `prescan.py`, `clients/*` | TMDb/TVDb/OMDb/MusicBrainz/Jikan-Abfragen, Zwischenspeicher, Confidence | `metadaten/` |
|
||||||
|
| `pfade.py` | Laufwerkswurzeln, UNC, `naechster_vorhandener` **ohne Endlosschleife** | `speicher/pfade.ts` |
|
||||||
|
| `musicbrainz_cd.py`, `audio_cd.py` | DiscID-Rechnung (mit/ohne 150 Frames), 2048-vs-2352-Falle | `rip/audiocd.ts` |
|
||||||
|
| `presets.py`, `eta.py`, `phasen.py` | Preset-Zuordnung je Disc-Typ, Restzeit, Phasenmodell | `komprimieren/`, `ablauf/` |
|
||||||
|
| `beschaffen.py`, `katalog.py` | Suchreihenfolge für Werkzeuge, Registry-Auswertung, Ausweichquellen | `werkzeuge/` |
|
||||||
|
| `AGENTS.md` | Die sechs Regeln aus § 4.3 | Wächter-Tests |
|
||||||
|
|
||||||
|
**Die Tests wandern sinngemäß mit.** Von den 977 heutigen Tests ist der größte
|
||||||
|
Teil reine Textverarbeitung (Parser, Pfade, Kommandobau) — die Testfälle
|
||||||
|
gelten unverändert, nur die Sprache wechselt. Das ist kein Nebeneffekt,
|
||||||
|
sondern die Absicherung: Ein neu geschriebener Parser, der dieselben
|
||||||
|
Testfälle besteht wie der alte, hat dieselben Fallen abgedeckt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Etappen
|
||||||
|
|
||||||
|
**Grundregel:** Jede Etappe endet mit grüner Ampel und einem Programm, das man
|
||||||
|
starten kann. Kein großer Umbau, nach dem monatelang nichts geht.
|
||||||
|
|
||||||
|
| # | Titel | Inhalt | Fertig, wenn |
|
||||||
|
|---|-------|--------|--------------|
|
||||||
|
| **W-0** | **Gerüst** | Electron 44 + TypeScript + Vite + Vitest, drei Prozesse, IPC, `node:sqlite` in Electron **prüfen** (§ 3.4), Prozess-Leine, CI-Ampel | Ein Fenster geht auf, der Kern läuft, ein Testlauf ist grün |
|
||||||
|
| **W-1** | **Laufwerk** | `laufwerk/win32.ts` aus dem § 3.2-Beweis, Wache über `WM_DEVICECHANGE`, Disc-Typ, Verriegeln, Auswerfen mit Nachsehen | Disc rein → Rippy sagt korrekt, was es ist. Auswerfen wirkt (nachgemessen, nicht geglaubt) |
|
||||||
|
| **W-2** | **Rippen** | makemkvcon-Aufruf, Parser, Fortschritt, Abbruch, Lesefehler-Warnung, Werkzeug-Beschaffung | Eine DVD und eine Blu-ray laufen durch, verlustfrei |
|
||||||
|
| **W-3** | **Komprimieren + Ablegen** | HandBrake, Encoder gemessen, Presets, Zielstruktur, NFO, Poster, Medienserver | Ein Film liegt fertig in Jellyfin und ist dort sichtbar |
|
||||||
|
| **W-4** | **Metadaten-Automatik** | Die vier Quellen aus § 5, Sicherheitsgrad, Nachkorrektur | Fünf verschiedene Discs richtig erkannt, ohne Nachfrage |
|
||||||
|
| **W-5** | **Oberfläche** | Dashboard, Einstellungen, Logs, Bibliothek, Tray, Windows-Toasts, Taskleisten-Fortschritt | Der Commander bedient Rippy einen Abend lang ohne Rückfrage |
|
||||||
|
| **W-6** | **Setup + Update** | NSIS, Autostart, Erst-Einrichtung, Selbst-Update, Deinstallation | Frischer Rechner: Setup → Disc rein → MKV raus. Und: drüber-installieren geht |
|
||||||
|
| **W-7** | **Der Rest** | Audio-CD, ISO-Backup, mehrere Laufwerke, 4K-Schlüsselkette | Je Feature ein Nachweis |
|
||||||
|
|
||||||
|
Reihenfolge-Begründung: W-1 bis W-3 sind die Kette, ohne die Rippy nichts tut.
|
||||||
|
W-4 und W-5 machen sie bequem. W-6 macht sie weitergebbar. W-7 sind die
|
||||||
|
Ergänzungen, die ohne die Kette keinen Sinn hätten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Risiken und offene Punkte
|
||||||
|
|
||||||
|
| Risiko | Bewertung | Behandlung |
|
||||||
|
|--------|-----------|------------|
|
||||||
|
| **MakeMKV-Beta-Key läuft Ende September 2026 ab** | hoch × sicher | § 9.1 — Ablaufdatum sichtbar, Warnung 7 Tage vorher, klare Meldung, Dauerlizenz eintragbar. **Das ist der akuteste Punkt im ganzen Dokument.** |
|
||||||
|
| `node:sqlite` in Electron ungeprüft | mittel × unbekannt | Erster Handgriff in W-0. Rückfallebene `better-sqlite3` |
|
||||||
|
| Neubau dauert länger als eine Reparatur | sicher | Deshalb Entscheid 2: Die Docker-Version läuft weiter. Es entsteht keine Lücke |
|
||||||
|
| Electron-Paket ~150–200 MB | sicher, gering | Ein einmaliger Download. Der Gegenwert ist eine Werkzeugkette statt zweier |
|
||||||
|
| SmartScreen bei unsignierter EXE | mittel | § 8 — Anleitung, oder Zertifikat. **Entscheidung offen** |
|
||||||
|
| koffi + Electron + npm-Install-Sperre | niedrig | § 3.5 gemessen: koffi lädt mit vorgebauten Binärdateien. In W-0 im Electron-Kontext gegenprüfen |
|
||||||
|
| Parallel-Rip vs. MakeMKV-Datenverzeichnis | mittel | § 6.5 — **messen, bevor die Grenze festgeschrieben wird** |
|
||||||
|
| Ein neu geschriebener Parser bringt neue Fehler | mittel | Die alten Testfälle wandern mit (§ 10). Ein Parser, der sie besteht, kennt dieselben Fallen |
|
||||||
|
| 4K-UHD-Schlüssel | gelöst | Unter Windows holt makemkvcon sie selbst — der strukturelle Vorteil dieser Plattform, gemessen 25.07.2026 |
|
||||||
|
|
||||||
|
### Offene Punkte für den Commander
|
||||||
|
|
||||||
|
1. **Code-Signing-Zertifikat** — ja oder nein? (~200–400 €/Jahr, sonst
|
||||||
|
SmartScreen-Warnung beim ersten Start)
|
||||||
|
2. **Update-Quelle** — Gitea-Releases auf `192.168.178.153`, oder ein Ordner
|
||||||
|
im Netz?
|
||||||
|
3. **Der alte Windows-Weg** — bleibt `RippySetup.exe` (PyInstaller) parallel
|
||||||
|
installierbar, bis v5 W-6 erreicht hat? *(Empfehlung: ja, und v5 bekommt
|
||||||
|
einen eigenen Installationsordner, damit sich die beiden nicht überschreiben.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Was NICHT gebaut wird
|
||||||
|
|
||||||
|
- **Kein Python.** Auch nicht „nur für den einen Parser".
|
||||||
|
- **Kein HTTP-Server, kein Port, kein localhost.** Fenster und Kern reden über
|
||||||
|
IPC.
|
||||||
|
- **Keine Docker-Kompatibilität.** Kein geteiltes Modul, kein gemeinsames
|
||||||
|
Schema, keine Container-Pfade.
|
||||||
|
- **Kein Linux, kein macOS.** „100 % für Windows gebaut" heißt: Wo eine
|
||||||
|
Windows-Lösung besser ist als eine portable, gewinnt die Windows-Lösung.
|
||||||
|
- **Kein ARM-Fork.** Rippy bleibt ein Eigenbau.
|
||||||
|
- **Keine Auth.** Einzelplatz, kein Netzdienst — es gibt nichts zu schützen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anhang — Verhältnis zu den bestehenden Konzepten
|
||||||
|
|
||||||
|
`KONZEPT.md` (v1) und `KONZEPT-V2.md` (v2) **bleiben gültig für die
|
||||||
|
Docker-Version**. Dieses Dokument ersetzt sie nicht, es steht daneben.
|
||||||
|
|
||||||
|
Drei Punkte aus v2 werden für Windows bewusst anders entschieden — jeweils mit
|
||||||
|
Begründung:
|
||||||
|
|
||||||
|
| v2 sagt | v5 macht | Warum |
|
||||||
|
|---------|----------|-------|
|
||||||
|
| Entscheid 4: *kein Electron, pywebview + WebView2* (8 MB statt 150 MB) | **Electron** | Die Rechnung war richtig und die Schlussfolgerung trotzdem falsch: Sie hat nur die Fenster-Frage betrachtet. Der Preis für pywebview war nicht 0 MB, sondern **eine zweite Werkzeugkette** — PyInstaller, hidden imports, `pythonnet`, und ein Kern, der aus Container-Code bestand. Electron kostet 150 MB und schenkt uns *eine* Sprache von der Oberfläche bis zum Laufwerk. |
|
||||||
|
| Eine Anwendung, drei Verdrahtungen (Ports/Treiber) | **Ein Windows-Programm** | Die Ports-Architektur ist richtig für ein Produkt, das drei Betriebsarten tragen muss. Bei Entscheid 2 (Docker bleibt getrennt) trägt sie nichts mehr — sie wäre Abstraktion auf Vorrat. |
|
||||||
|
| REST + SSE als Schnittstelle | **IPC** | Ein lokaler Webserver in einer Einzelplatz-Anwendung ist eine offene Tür ohne Gegenwert. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Rippy v5 · Konzept · 30.08.2026 · Branch `worktree-windows-electron`*
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Beweise — die Messungen zu KONZEPT-WINDOWS.md § 3
|
||||||
|
|
||||||
|
Diese zwei Skripte sind die Grundlage für die Entscheidung, Rippy v5 in
|
||||||
|
TypeScript/Node zu bauen. Sie beantworten die einzige Frage, an der das ganze
|
||||||
|
Konzept hängt:
|
||||||
|
|
||||||
|
> **Kann Node überhaupt alles, was Rippy am Laufwerk braucht?**
|
||||||
|
|
||||||
|
Wäre die Antwort nein gewesen, wäre das Konzept wertlos gewesen. Deshalb sind
|
||||||
|
sie zuerst gelaufen — vor der ersten Zeile Konzept.
|
||||||
|
|
||||||
|
`AGENTS.md` Regel D: *„Externe Schnittstellen NIE aus dem Kopf."*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nachstellen
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install koffi
|
||||||
|
node laufwerk.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Gemessen am 30.08.2026 mit Node v24.19.0, koffi 3.1.6, an einem
|
||||||
|
HL-DT-ST BD-RE BU40N (USB) mit eingelegter Blu-ray.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `laufwerk.js` — Win32-Laufwerkszugriff
|
||||||
|
|
||||||
|
Ruft dieselben Steuercodes auf wie `src/rippy/drives/win_ioctl.py` im
|
||||||
|
Docker-Zweig. **Nur lesend** — kein Auswerfen, kein Verriegeln.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. GetLogicalDrives + GetDriveTypeW -> optische Laufwerke: [ 'G' ]
|
||||||
|
2. CreateFileW \\.\G: (Zugriff 0) -> offen
|
||||||
|
CreateFileW \\.\G: (GENERIC_READ) -> offen
|
||||||
|
3. IOCTL_STORAGE_QUERY_PROPERTY -> Hersteller: HL-DT-ST | Modell: BD-RE BU40N
|
||||||
|
| Rev: 1.03 | Serial: 0025114C0149
|
||||||
|
4. IOCTL_STORAGE_CHECK_VERIFY2 -> Medium eingelegt
|
||||||
|
5. IOCTL_CDROM_DISK_TYPE -> Win32-Fehler 50
|
||||||
|
6. IOCTL_DISK_GET_LENGTH_INFO -> 33.759.690.752 Bytes (33,76 GB) => Blu-ray
|
||||||
|
```
|
||||||
|
|
||||||
|
**Punkt 5 ist kein Fehlschlag.** Fehler 50 ist `ERROR_NOT_SUPPORTED` — genau
|
||||||
|
das, was der Python-Treiber an diesem Laufwerk auch sieht (`SAVEPOINT.md`,
|
||||||
|
v4.0-rc11, Abschnitt „Das Laufwerk"). Node verhält sich identisch. Die Lehre
|
||||||
|
steht im alten Code und gilt weiter: **Die Disc-Einordnung darf sich nicht auf
|
||||||
|
`IOCTL_CDROM_DISK_TYPE` verlassen — die Größe ist der verlässliche Weg.**
|
||||||
|
|
||||||
|
Punkt 2 zeigt die Unterscheidung, die in rc11 teuer war: `Zugriff 0` fragt das
|
||||||
|
**Gerät**, `GENERIC_READ` fragt das **Medium**. Bei einer gestörten Disc
|
||||||
|
scheitert das zweite und das erste geht weiter — wer nur eins probiert, hält
|
||||||
|
ein antwortendes Laufwerk für tot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `leine.js` — die Prozess-Leine (Job Object)
|
||||||
|
|
||||||
|
Stellt den Befund aus `SAVEPOINT.md` v4.0-rc10 nach: Windows räumt
|
||||||
|
Kindprozesse **nicht** auf. Ein hart beendeter Rippy hinterlässt ein
|
||||||
|
`makemkvcon`, das das Laufwerk festhält, und jeder spätere Rip scheitert.
|
||||||
|
|
||||||
|
Das Skript setzt eine Arbeitsgruppe mit `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`,
|
||||||
|
startet ein langlebiges Kind und lässt sich dann hart abschießen —
|
||||||
|
`taskkill /F` **ohne** `/T`, also genau das, was bei einem Absturz und beim
|
||||||
|
Drüber-Installieren passiert.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. CreateJobObjectW -> Arbeitsgruppe angelegt
|
||||||
|
2. SetInformationJobObject -> KILL_ON_JOB_CLOSE gesetzt
|
||||||
|
3. AssignProcessToJobObject -> dieser Prozess hängt in der Gruppe
|
||||||
|
4. Kindprozess gestartet, PID 16040
|
||||||
|
|
||||||
|
vor dem Abschuss: Eltern lebt: True Kind lebt: True
|
||||||
|
taskkill /F /PID 29488 -> ERFOLGREICH
|
||||||
|
danach: Eltern lebt: False Kind lebt: False
|
||||||
|
```
|
||||||
|
|
||||||
|
Nachstellen (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$p = Start-Process node -ArgumentList "leine.js" -PassThru -RedirectStandardOutput out.txt
|
||||||
|
Start-Sleep 3
|
||||||
|
$kind = (Select-String -Path out.txt -Pattern 'KIND-PID=(\d+)').Matches[0].Groups[1].Value
|
||||||
|
taskkill /F /PID $p.Id
|
||||||
|
Start-Sleep 2
|
||||||
|
Get-Process -Id $kind -ErrorAction SilentlyContinue # muss LEER sein
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zwei weitere Messungen aus § 3, ohne eigenes Skript
|
||||||
|
|
||||||
|
**`node:sqlite` ist in Node 24 eingebaut:**
|
||||||
|
|
||||||
|
```
|
||||||
|
node -e "const s=require('node:sqlite'); const db=new s.DatabaseSync(':memory:'); db.exec('CREATE TABLE t (a TEXT)'); db.prepare('INSERT INTO t VALUES (?)').run('geht'); console.log(db.prepare('SELECT a FROM t').get().a)"
|
||||||
|
geht
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Das ist in **Node 24.19.0** gemessen, nicht in Electron. Electron baut sein
|
||||||
|
Node mit eigenen Flags — ob `node:sqlite` dort vorhanden ist, ist der erste
|
||||||
|
Handgriff in Etappe W-0.
|
||||||
|
|
||||||
|
**npm 11 blockiert Install-Skripte:**
|
||||||
|
|
||||||
|
```
|
||||||
|
npm warn allow-scripts koffi@3.1.6 (install: node ./cnoke.cjs -P . --prebuild --release)
|
||||||
|
```
|
||||||
|
|
||||||
|
koffi lädt trotzdem — es bringt vorgebaute Binärdateien mit. Die Warnung
|
||||||
|
gehört in die Bau-Anleitung, sonst sucht eines Tages jemand an der falschen
|
||||||
|
Stelle.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Beweis-Test: Kann Node/koffi dieselben Win32-Aufrufe wie Rippys Python-Treiber?
|
||||||
|
// NUR LESEND — kein Auswerfen, kein Verriegeln.
|
||||||
|
const koffi = require('koffi');
|
||||||
|
|
||||||
|
// ── CTL_CODE aus winioctl.h, eins zu eins wie in src/rippy/drives/win_ioctl.py
|
||||||
|
const ctl = (typ, fn, methode, zugriff) =>
|
||||||
|
((typ << 16) | (zugriff << 14) | (fn << 2) | methode) >>> 0;
|
||||||
|
|
||||||
|
const FILE_DEVICE_CD_ROM = 0x02, FILE_DEVICE_DISK = 0x07, FILE_DEVICE_MASS_STORAGE = 0x2d;
|
||||||
|
const IOCTL_STORAGE_CHECK_VERIFY2 = ctl(FILE_DEVICE_MASS_STORAGE, 0x0200, 0, 0);
|
||||||
|
const IOCTL_STORAGE_QUERY_PROPERTY = ctl(FILE_DEVICE_MASS_STORAGE, 0x0500, 0, 0);
|
||||||
|
const IOCTL_CDROM_DISK_TYPE = ctl(FILE_DEVICE_CD_ROM, 0x0010, 0, 0);
|
||||||
|
const IOCTL_DISK_GET_LENGTH_INFO = ctl(FILE_DEVICE_DISK, 0x0017, 0, 1);
|
||||||
|
|
||||||
|
const kernel32 = koffi.load('kernel32.dll');
|
||||||
|
const GetLogicalDrives = kernel32.func('uint32_t __stdcall GetLogicalDrives()');
|
||||||
|
const GetDriveTypeW = kernel32.func('uint32_t __stdcall GetDriveTypeW(str16 lpRootPathName)');
|
||||||
|
const CreateFileW = kernel32.func('void* __stdcall CreateFileW(str16 lpFileName, uint32_t dwDesiredAccess, uint32_t dwShareMode, void *lpSecurityAttributes, uint32_t dwCreationDisposition, uint32_t dwFlagsAndAttributes, void *hTemplateFile)');
|
||||||
|
const CloseHandle = kernel32.func('bool __stdcall CloseHandle(void *hObject)');
|
||||||
|
const GetLastError = kernel32.func('uint32_t __stdcall GetLastError()');
|
||||||
|
const DeviceIoControl = kernel32.func('bool __stdcall DeviceIoControl(void *hDevice, uint32_t dwIoControlCode, void *lpInBuffer, uint32_t nInBufferSize, _Out_ void *lpOutBuffer, uint32_t nOutBufferSize, _Out_ uint32_t *lpBytesReturned, void *lpOverlapped)');
|
||||||
|
|
||||||
|
const GENERIC_READ = 0x80000000, FILE_SHARE_READ = 1, FILE_SHARE_WRITE = 2, OPEN_EXISTING = 3;
|
||||||
|
const DRIVE_CDROM = 5;
|
||||||
|
|
||||||
|
// ── 1. Laufwerke finden (die Windows-Entsprechung von glob /dev/sr*)
|
||||||
|
const maske = GetLogicalDrives();
|
||||||
|
const optische = [];
|
||||||
|
for (let i = 0; i < 26; i++) {
|
||||||
|
if (!(maske & (1 << i))) continue;
|
||||||
|
const buchstabe = String.fromCharCode(65 + i);
|
||||||
|
if (GetDriveTypeW(buchstabe + ':\\') === DRIVE_CDROM) optische.push(buchstabe);
|
||||||
|
}
|
||||||
|
console.log('1. GetLogicalDrives + GetDriveTypeW -> optische Laufwerke:', optische);
|
||||||
|
if (!optische.length) { console.log(' Kein optisches Laufwerk — Test endet hier.'); process.exit(0); }
|
||||||
|
|
||||||
|
const lw = optische[0];
|
||||||
|
|
||||||
|
// ── 2. Gerät öffnen. Zugriff 0 = nur das GERÄT fragen, nicht das Medium.
|
||||||
|
// Genau die Unterscheidung aus SAVEPOINT rc11: Bei gestörtem Medium
|
||||||
|
// scheitert GENERIC_READ, Zugriff 0 geht weiter.
|
||||||
|
function oeffnen(zugriff) {
|
||||||
|
const h = CreateFileW('\\\\.\\' + lw + ':', zugriff,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, 0, null);
|
||||||
|
const adr = koffi.address(h);
|
||||||
|
// INVALID_HANDLE_VALUE ist -1, als vorzeichenlose 64-Bit-Zahl 0xFFFF...
|
||||||
|
if (adr === 0n || adr === 0xffffffffffffffffn) return { h: null, fehler: GetLastError() };
|
||||||
|
return { h, fehler: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ohneMedium = oeffnen(0);
|
||||||
|
console.log(`2. CreateFileW \\\\.\\${lw}: (Zugriff 0) -> ` +
|
||||||
|
(ohneMedium.h ? 'offen' : 'Win32-Fehler ' + ohneMedium.fehler));
|
||||||
|
const mitLesen = oeffnen(GENERIC_READ);
|
||||||
|
console.log(` CreateFileW \\\\.\\${lw}: (GENERIC_READ) -> ` +
|
||||||
|
(mitLesen.h ? 'offen' : 'Win32-Fehler ' + mitLesen.fehler));
|
||||||
|
|
||||||
|
const h = ohneMedium.h;
|
||||||
|
if (!h) { console.log(' Gerät nicht ansprechbar — Test endet hier.'); process.exit(0); }
|
||||||
|
|
||||||
|
const rueck = Buffer.alloc(4);
|
||||||
|
|
||||||
|
// ── 3. Hersteller/Modell/Serial (STORAGE_DEVICE_DESCRIPTOR)
|
||||||
|
const anfrage = Buffer.alloc(12);
|
||||||
|
anfrage.writeUInt32LE(0, 0); // PropertyId = StorageDeviceProperty
|
||||||
|
anfrage.writeUInt32LE(0, 4); // QueryType = PropertyStandardQuery
|
||||||
|
const antwort = Buffer.alloc(1024);
|
||||||
|
if (DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY, anfrage, 12, antwort, 1024, rueck, null)) {
|
||||||
|
const text = (off) => {
|
||||||
|
const start = antwort.readUInt32LE(off);
|
||||||
|
if (!start || start >= antwort.length) return '';
|
||||||
|
let ende = start; while (ende < antwort.length && antwort[ende] !== 0) ende++;
|
||||||
|
return antwort.toString('latin1', start, ende).trim();
|
||||||
|
};
|
||||||
|
console.log('3. IOCTL_STORAGE_QUERY_PROPERTY -> Hersteller:', text(12),
|
||||||
|
'| Modell:', text(16), '| Rev:', text(20), '| Serial:', text(24));
|
||||||
|
} else {
|
||||||
|
console.log('3. IOCTL_STORAGE_QUERY_PROPERTY -> Win32-Fehler', GetLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Liegt ein Medium drin? (ERROR_NOT_READY = 21 heißt: leer)
|
||||||
|
const okVerify = DeviceIoControl(h, IOCTL_STORAGE_CHECK_VERIFY2, null, 0, null, 0, rueck, null);
|
||||||
|
console.log('4. IOCTL_STORAGE_CHECK_VERIFY2 -> ' +
|
||||||
|
(okVerify ? 'Medium eingelegt' : 'kein Medium (Win32-Fehler ' + GetLastError() + ')'));
|
||||||
|
|
||||||
|
// ── 5. Audio- oder Datenspur?
|
||||||
|
const typBuf = Buffer.alloc(1);
|
||||||
|
if (DeviceIoControl(h, IOCTL_CDROM_DISK_TYPE, null, 0, typBuf, 1, rueck, null)) {
|
||||||
|
const t = typBuf[0];
|
||||||
|
console.log('5. IOCTL_CDROM_DISK_TYPE -> ' +
|
||||||
|
(t === 1 ? 'Audio-CD' : t === 2 ? 'Daten-Disc' : 'Wert ' + t));
|
||||||
|
} else {
|
||||||
|
console.log('5. IOCTL_CDROM_DISK_TYPE -> Win32-Fehler', GetLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6. Größe des Mediums (für die Unterscheidung DVD / BD / UHD)
|
||||||
|
const laenge = Buffer.alloc(8);
|
||||||
|
if (mitLesen.h && DeviceIoControl(mitLesen.h, IOCTL_DISK_GET_LENGTH_INFO, null, 0, laenge, 8, rueck, null)) {
|
||||||
|
const bytes = laenge.readBigUInt64LE(0);
|
||||||
|
const gb = Number(bytes) / 1e9;
|
||||||
|
console.log('6. IOCTL_DISK_GET_LENGTH_INFO -> ' + bytes + ' Bytes (' + gb.toFixed(2) + ' GB)' +
|
||||||
|
' => Einordnung: ' + (gb > 60 ? 'UHD' : gb > 9.5 ? 'Blu-ray' : gb > 1 ? 'DVD' : 'CD'));
|
||||||
|
} else {
|
||||||
|
console.log('6. IOCTL_DISK_GET_LENGTH_INFO -> Win32-Fehler', GetLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(h);
|
||||||
|
if (mitLesen.h) CloseHandle(mitLesen.h);
|
||||||
|
console.log('\nAlle Handles geschlossen. Nichts ausgeworfen, nichts verriegelt.');
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Beweis-Test 2: Kann Node eine Windows-Arbeitsgruppe (Job Object) setzen,
|
||||||
|
// die Kindprozesse mitreisst, wenn der Elternprozess HART beendet wird?
|
||||||
|
// Das ist der Mechanismus aus SAVEPOINT v4.0-rc10 — ohne ihn ueberlebt
|
||||||
|
// makemkvcon Rippy und haelt das Laufwerk fest.
|
||||||
|
const koffi = require('koffi');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const kernel32 = koffi.load('kernel32.dll');
|
||||||
|
const CreateJobObjectW = kernel32.func('void* __stdcall CreateJobObjectW(void *lpJobAttributes, str16 lpName)');
|
||||||
|
const SetInformationJobObject = kernel32.func('bool __stdcall SetInformationJobObject(void *hJob, int JobObjectInformationClass, void *lpJobObjectInformation, uint32_t cbJobObjectInformationLength)');
|
||||||
|
const AssignProcessToJobObject = kernel32.func('bool __stdcall AssignProcessToJobObject(void *hJob, void *hProcess)');
|
||||||
|
const GetCurrentProcess = kernel32.func('void* __stdcall GetCurrentProcess()');
|
||||||
|
const GetLastError = kernel32.func('uint32_t __stdcall GetLastError()');
|
||||||
|
|
||||||
|
// JobObjectExtendedLimitInformation = 9 (winnt.h)
|
||||||
|
const JobObjectExtendedLimitInformation = 9;
|
||||||
|
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 (winnt.h)
|
||||||
|
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
|
||||||
|
|
||||||
|
const job = CreateJobObjectW(null, null);
|
||||||
|
if (koffi.address(job) === 0n) {
|
||||||
|
console.log('CreateJobObjectW fehlgeschlagen, Win32-Fehler', GetLastError());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('1. CreateJobObjectW -> Arbeitsgruppe angelegt');
|
||||||
|
|
||||||
|
// JOBOBJECT_EXTENDED_LIMIT_INFORMATION ist 144 Byte auf x64.
|
||||||
|
// LimitFlags liegt im eingebetteten BASIC_LIMIT_INFORMATION bei Offset 16.
|
||||||
|
const info = Buffer.alloc(144);
|
||||||
|
info.writeUInt32LE(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, 16);
|
||||||
|
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, info, 144)) {
|
||||||
|
console.log('SetInformationJobObject fehlgeschlagen, Win32-Fehler', GetLastError());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('2. SetInformationJobObject -> KILL_ON_JOB_CLOSE gesetzt');
|
||||||
|
|
||||||
|
if (!AssignProcessToJobObject(job, GetCurrentProcess())) {
|
||||||
|
console.log('AssignProcessToJobObject fehlgeschlagen, Win32-Fehler', GetLastError());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('3. AssignProcessToJobObject -> dieser Prozess haengt in der Gruppe');
|
||||||
|
|
||||||
|
// Ein langlebiges Kind starten — der Platzhalter fuer makemkvcon.
|
||||||
|
const kind = spawn('ping', ['-n', '300', '127.0.0.1'], { stdio: 'ignore' });
|
||||||
|
console.log('4. Kindprozess gestartet, PID ' + kind.pid);
|
||||||
|
console.log('ELTERN-PID=' + process.pid);
|
||||||
|
console.log('KIND-PID=' + kind.pid);
|
||||||
|
|
||||||
|
// Offen halten, bis uns jemand hart abschiesst.
|
||||||
|
setTimeout(() => { console.log('Zeit abgelaufen'); process.exit(0); }, 60000);
|
||||||
Reference in New Issue
Block a user