Files
rippy/rippy-windows/src/kern/werkzeuge/katalog.ts
T
HitonabiandClaude Fable 5 844bca37ff
Ampel / ampel (push) Failing after 1m22s
feat(v5): Etappe W-2 — Rippen: Parser, makemkvcon-Lauf, Werkzeug-Katalog
rip/parser.ts: die reine Textverarbeitung mit allen bezahlten Fallen aus
dem Docker-Zweig — quelle() (dev:G: statt dev:\.\G:, der 404-Verwandte),
textVon() (UTF-8 streng, sonst cp1252 — nie wieder 'Öffnen'), parseMsg
(Regex statt split — Meldungen mit Komma), PRGV (-1 vs. echtes 0 %),
TINFO/SINFO (Sprache aus Attr 3/4, NICHT 28/29), laengsterTitel mit
Play-All-Schutz, episodenTitel, KRITISCHE_CODES (sprachfeste NUMMERN).

rip/makemkv.ts: Kommandobau + Lauf — ZeilenLeser (binaer, halbe Zeilen,
je Zeile dekodiert), RipAuswertung (pur), rippen() mit AbortSignal,
Lesefehler-Flag AUCH bei Rueckgabewert 0. werkzeuge/katalog.ts: die
Suchreihenfolge samt %VAR%-Gross/Klein-Falle und Registry-Rueckfall.
ablauf/rippen.ts: ein Rip je Laufwerk, Status-Push, 'erst nachsehen,
dann rippen' (unbekannt heisst trotzdem rippen). Fenster: Rippen-Knopf,
Fortschrittsbalken, Abbrechen; Auswurf waehrend des Rips abgelehnt.

Gemessen am echten System (30.08.2026): Katalog findet
makemkvcon64 (Program Files x86), HandBrakeCLI und flac (beide im
rc11-Werkzeugordner LOCALAPPDATA\Rippy\tools). NEUER BEFUND: Nach dem
abgebrochenen rc11-Rip vom Mittag sieht MakeMKV GAR KEIN Laufwerk mehr
(MSG 5042, leere DRV-Liste, mit und ohne --noscan), waehrend die
Storage-IOCTLs sauber antworten — MakeMKV spricht SCSI direkt. 5042 ist
jetzt ein KRITISCHER_CODE mit Klartext-Abhilfe. Der volle Rip-Beweis
(messung.rip.test.ts liegt bereit) braucht deshalb erst eine Hand am
Geraet: Disc neu einlegen oder USB ab-/anstecken.

96 Tests gruen, Smoke gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:12:44 +02:00

191 lines
7.0 KiB
TypeScript

// Wo Rippys Werkzeuge liegen — und wie man sie findet (KONZEPT § 9).
// Die Suchreihenfolge und ihre Fallen stammen aus tools/katalog.py, dort am
// Commander-PC gemessen:
//
// 1. Ausdrücklich eingestellt — wer einen Pfad einträgt, will genau den.
// 2. Rippys eigener Werkzeug-Ordner (mitgeliefert bzw. selbst geholt) —
// VOR dem System, damit eine gepflegte Fassung eine alte Installation
// schlägt.
// 3. PATH — der „selbst eingetragen"-Fall.
// 4. Bekannte Installationsorte (%ProgramFiles%…) — der Normalfall.
// 5. Uninstall-Zweig der Registry: Bei MakeMKV ist `InstallLocation` LEER
// (28.08.2026 nachgesehen), aber `DisplayIcon` zeigt in den
// Programmordner.
//
// ⚠ %VAR%-Entfaltung MUSS Groß/Klein ignorieren: os-Umgebungen liefern
// PROGRAMFILES(X86), das Muster sagt %ProgramFiles(x86)% — der strikte
// Vergleich ließ die bekannten Orte auf einer echten Maschine NIE greifen
// (katalog.py, 28.08.2026).
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { delimiter, join } from 'node:path'
export interface Werkzeug {
titel: string
zweck: string
programme: readonly string[]
orte: readonly string[]
/** Teilstring des DisplayName im Uninstall-Zweig der Registry. */
anzeige: string
pflicht: boolean
}
export const WERKZEUGE: Record<string, Werkzeug> = {
makemkv: {
titel: 'MakeMKV',
zweck: 'Liest DVDs, Blu-rays und 4K-UHDs — der einzige Weg durch AACS.',
programme: ['makemkvcon64.exe', 'makemkvcon.exe'],
orte: ['%ProgramFiles(x86)%\\MakeMKV', '%ProgramFiles%\\MakeMKV'],
anzeige: 'MakeMKV',
pflicht: true,
},
handbrake: {
titel: 'HandBrakeCLI',
zweck: 'Komprimiert den verlustfreien Rip auf Arbeitsgröße.',
programme: ['HandBrakeCLI.exe'],
orte: ['%ProgramFiles%\\HandBrake', '%ProgramFiles(x86)%\\HandBrake'],
anzeige: 'HandBrake',
pflicht: false, // ohne HandBrake geht Rippen, nur ohne Kompression
},
flac: {
titel: 'FLAC',
zweck: 'Wandelt die von der Audio-CD gelesenen Rohdaten in FLAC.',
programme: ['flac.exe'],
orte: ['%ProgramFiles%\\FLAC', '%ProgramFiles(x86)%\\FLAC'],
anzeige: 'FLAC',
pflicht: false, // ohne FLAC läuft alles außer Audio-CDs
},
}
/** Rippys eigener Werkzeug-Ordner — dort landet Mitgeliefertes/Geholtes. */
export function werkzeugOrdner(umgebung: NodeJS.ProcessEnv = process.env): string {
const wurzel = umgebung['LOCALAPPDATA'] ?? umgebung['USERPROFILE'] ?? '.'
return join(wurzel, 'Rippy', 'tools')
}
/** %ProgramFiles%\X → C:\Program Files\X — Groß/Klein wird IGNORIERT.
* Bleibt ein % stehen, ist der Kandidat unbrauchbar → ''. */
export function entfalten(pfad: string, umgebung: NodeJS.ProcessEnv): string {
let ergebnis = pfad
for (const [name, wert] of Object.entries(umgebung)) {
if (wert === undefined) continue
const marke = `%${name}%`.toUpperCase()
for (;;) {
const stelle = ergebnis.toUpperCase().indexOf(marke)
if (stelle < 0) break
ergebnis = ergebnis.slice(0, stelle) + wert + ergebnis.slice(stelle + marke.length)
}
}
return ergebnis.includes('%') ? '' : ergebnis
}
/** Alle Kandidaten-Pfade in Suchreihenfolge — reine Funktion, prüft nichts. */
export function kandidaten(
name: string,
eigenerOrdner: string,
umgebung: NodeJS.ProcessEnv,
): string[] {
const werkzeug = WERKZEUGE[name]
const pfade: string[] = []
for (const programm of werkzeug.programme) {
pfade.push(join(eigenerOrdner, name, programm))
pfade.push(join(eigenerOrdner, programm))
}
for (const ort of werkzeug.orte) {
const entfaltet = entfalten(ort, umgebung)
if (entfaltet.length === 0) continue
for (const programm of werkzeug.programme) {
pfade.push(join(entfaltet, programm))
}
}
return pfade
}
function imPfadSuchen(programme: readonly string[], umgebung: NodeJS.ProcessEnv): string {
const pfadVar = umgebung['PATH'] ?? umgebung['Path'] ?? ''
for (const ordner of pfadVar.split(delimiter)) {
if (ordner.length === 0) continue
for (const programm of programme) {
const kandidat = join(ordner, programm)
if (existsSync(kandidat)) return kandidat
}
}
return ''
}
/** Registry-Rückfall: DisplayIcon aus dem Uninstall-Zweig. Bei MakeMKV ist
* InstallLocation leer — DisplayIcon zeigt auf eine Datei im
* Programmordner (28.08.2026 nachgesehen). Über reg.exe gelesen, damit
* kein zweiter koffi-Ort entsteht (R1). */
export function ausUninstallRegistry(
anzeige: string,
programme: readonly string[],
ausfuehren: (wurzel: string) => string = regQuery,
): string {
const wurzeln = [
'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
]
for (const wurzel of wurzeln) {
const ausgabe = ausfuehren(wurzel)
// reg.exe gruppiert je Unterschlüssel: Kopfzeile, dann NAME TYP WERT.
let passt = false
for (const zeile of ausgabe.split(/\r?\n/)) {
if (zeile.startsWith('HK')) {
passt = false
continue
}
const wert = /^\s*(\S+)\s+REG_SZ\s+(.+)$/.exec(zeile)
if (wert === null) continue
if (wert[1] === 'DisplayName') {
passt = wert[2].includes(anzeige)
} else if (passt && (wert[1] === 'DisplayIcon' || wert[1] === 'UninstallString')) {
const ordner = wert[2].replace(/^"|"$/g, '').replace(/[^\\]*$/, '')
for (const programm of programme) {
const kandidat = join(ordner, programm)
if (existsSync(kandidat)) return kandidat
}
}
}
}
return ''
}
function regQuery(wurzel: string): string {
const ergebnis = spawnSync('reg', ['query', wurzel, '/s'], {
encoding: 'utf8',
windowsHide: true,
timeout: 15_000,
maxBuffer: 32 * 1024 * 1024,
})
return ergebnis.stdout ?? ''
}
/** Der Pfad zum Werkzeug — oder '' wenn es nicht da ist. Prüf-Funktionen
* einspritzbar, damit die Suchreihenfolge ohne Installationen testbar ist. */
export function finden(
name: string,
eingestellt = '',
eigenerOrdner: string = werkzeugOrdner(),
umgebung: NodeJS.ProcessEnv = process.env,
existiert: (pfad: string) => boolean = existsSync,
registry: typeof ausUninstallRegistry = ausUninstallRegistry,
): string {
// 1. Ausdrücklich eingestellt — auch wenn der Pfad (noch) fehlt, wird
// NICHT still etwas anderes genommen: Wer einen Pfad einträgt, will
// genau den, und ein Tippfehler soll als Fehler sichtbar werden.
if (eingestellt.trim().length > 0) {
return existiert(eingestellt.trim()) ? eingestellt.trim() : ''
}
// 2 + 4. Eigener Ordner, dann bekannte Orte.
for (const kandidat of kandidaten(name, eigenerOrdner, umgebung)) {
if (existiert(kandidat)) return kandidat
}
// 3. PATH.
const imPfad = imPfadSuchen(WERKZEUGE[name].programme, umgebung)
if (imPfad.length > 0) return imPfad
// 5. Registry-Rückfall.
return registry(WERKZEUGE[name].anzeige, WERKZEUGE[name].programme)
}