Ampel / ampel (push) Failing after 1m22s
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>
191 lines
7.0 KiB
TypeScript
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)
|
|
}
|