From fc2d16ca38f303a77aaef6ebb8129d704631c9fa Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 12 Sep 2026 21:49:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(v5):=20Disc-Typ=20aus=20dem=20Dateisystem?= =?UTF-8?q?=20=E2=80=94=20INDX0300=20ist=20UHD,=20VIDEO=5FTS=20ist=20DVD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WAS: laufwerke.typVerfeinern liest je Disc einmal BDMV/index.bdmv (Kopf „INDX" + Fassung: 0300 = UHD-Format, 0100/0200 = Blu-ray) bzw. sieht VIDEO_TS (DVD) und überschreibt damit, was Steuercode und Größe sagten; das Ergebnis wird je Laufwerk und Disc-Größe gemerkt (die Wache fragt alle 3 s). Gemessen am Akira-UHD-Abbild unter F:\Video\backup\UHD_AKIRA (12.09.2026): INDX0300. WARUM: Durchsicht 12.09.2026, Fund F10 — bei manchen USB-Brücken antwortet IOCTL_CDROM_DISK_TYPE nicht („unbekannt" → ISO), und die Größe allein machte aus einer 4K-Disc mit 48 GB Daten eine „bluray" (falsches Preset). Am echten Laufwerk mit einer gewöhnlichen Blu-ray noch zu messen, dass dort 0200 steht. Co-Authored-By: Claude Opus 5 --- rippy-windows/src/kern/dienste/laufwerke.ts | 32 +++++++++++++- .../src/kern/metadaten/discmerkmale.ts | 40 +++++++++++++++++ rippy-windows/test/disctyp-570.test.ts | 44 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 rippy-windows/test/disctyp-570.test.ts diff --git a/rippy-windows/src/kern/dienste/laufwerke.ts b/rippy-windows/src/kern/dienste/laufwerke.ts index fdaa879..7188040 100644 --- a/rippy-windows/src/kern/dienste/laufwerke.ts +++ b/rippy-windows/src/kern/dienste/laufwerke.ts @@ -20,6 +20,7 @@ import { listOptischeLaufwerke, } from '../laufwerk/disc' import { beurteilen, ereignisseLesen } from '../laufwerk/ereignisse' +import { typAusDateisystem } from '../metadaten/discmerkmale' import { DiscWache } from '../laufwerk/wache' import type { Spur } from '../rip/audiocd' @@ -40,6 +41,9 @@ export class LaufwerkDienst { * Leer im normalen Betrieb; die Win32-Schicht bleibt dann der einzige Weg. */ readonly fake: string readonly bereit: Promise + /** 5.7.0 (F10): Der Typ aus dem Dateisystem, je Laufwerk und Disc-Größe + * gemerkt — die Wache fragt alle 3 s, die Disc ändert sich nicht. */ + private typGemerkt = new Map() constructor( private readonly kontext: KernKontext, @@ -99,7 +103,7 @@ export class LaufwerkDienst { const { win32Api } = await import('../laufwerk/win32') this.api = win32Api() this.wache = new DiscWache( - () => listOptischeLaufwerke(this.api!).map((g) => geraeteInfo(this.api!, g)), + () => listOptischeLaufwerke(this.api!).map((g) => this.typVerfeinern(geraeteInfo(this.api!, g))), (m) => this.wacheMeldung(m), wacheFehler, ) @@ -115,6 +119,30 @@ export class LaufwerkDienst { return this.wache?.aktuellerStand() ?? [] } + /** Was auf der Disc liegt, schlägt Steuercode und Größe (F10): BDMV mit + * INDX0300 ist UHD, sonst Blu-ray; VIDEO_TS ist DVD. Nur bei „ready", + * und nur einmal je Disc (gemerkt an der Größe). */ + private typVerfeinern(geraet: GeraeteInfo): GeraeteInfo { + if (geraet.status !== 'ready' || geraet.typ === 'cd') { + this.typGemerkt.delete(geraet.id) + return geraet + } + const alt = this.typGemerkt.get(geraet.id) + if (alt !== undefined && alt.groesse === geraet.groesseBytes) return alt.typ === geraet.typ ? geraet : { ...geraet, typ: alt.typ } + let typ = geraet.typ + try { + const gesehen = typAusDateisystem(geraet.pfad) + if (gesehen !== null && gesehen !== geraet.typ) { + this.kontext.protokoll.schreiben(`Laufwerk ${geraet.id}: Typ laut Dateisystem ${gesehen} (Steuercode/Größe sagten ${geraet.typ})`) + typ = gesehen + } + } catch (fehler) { + this.kontext.protokoll.schreiben(`Laufwerk ${geraet.id}: Dateisystem nicht lesbar (${String(fehler)})`) + } + this.typGemerkt.set(geraet.id, { groesse: geraet.groesseBytes, typ }) + return typ === geraet.typ ? geraet : { ...geraet, typ } + } + /** Der Geräte-Name zu einer Kennung — oder null. */ geraetVon(id: string): string | null { if (this.api === null) return null @@ -126,7 +154,7 @@ export class LaufwerkDienst { if (this.fake.length > 0) return id === this.fake ? this.fakeGeraet() : null const geraet = this.geraetVon(id) if (geraet === null || this.api === null) return null - return geraeteInfo(this.api, geraet) + return this.typVerfeinern(geraeteInfo(this.api, geraet)) } audioToc(id: string): Spur[] { diff --git a/rippy-windows/src/kern/metadaten/discmerkmale.ts b/rippy-windows/src/kern/metadaten/discmerkmale.ts index bd563e4..af76b91 100644 --- a/rippy-windows/src/kern/metadaten/discmerkmale.ts +++ b/rippy-windows/src/kern/metadaten/discmerkmale.ts @@ -250,6 +250,46 @@ export function discZusatzAbtrennen(titel: string): DiscZusatz { return { titel: rest.replace(new RegExp(`${TRENNER}+$`), '').trim(), discNr, staffel } } +/** Der Disc-Typ aus dem, was auf der Disc liegt (5.7.0, Durchsicht F10) — + * unabhängig vom Windows-Steuercode, der bei manchen USB-Brücken nicht + * antwortet, und genauer als die Größe (eine 4K-Disc mit 48 GB Daten + * wurde vorher „bluray"): + * * BDMV/index.bdmv beginnt mit „INDX" + Fassung: „0300" ist das + * UHD-Format (BDMV 3.0), „0100"/„0200" eine gewöhnliche Blu-ray — + * am Akira-UHD-Abbild (F:\Video\backup\UHD_AKIRA) am 12.09.2026 gemessen: + * INDX0300. + * * VIDEO_TS → DVD. + * null, wenn nichts davon zu sehen ist (Audio-CD, Datendisc, Lesefehler). */ +export function typAusIndexKopf(kopf: Buffer): 'uhd' | 'bluray' | null { + if (kopf.length < 8 || kopf.subarray(0, 4).toString('latin1') !== 'INDX') return null + const fassung = kopf.subarray(4, 8).toString('latin1') + if (fassung === '0300') return 'uhd' + if (fassung === '0100' || fassung === '0200') return 'bluray' + return null +} + +export function typAusDateisystem(geraetepfad: string): 'uhd' | 'bluray' | 'dvd' | null { + const wurzel = discWurzel(geraetepfad) + if (wurzel.length === 0) return null + const index = join(wurzel, 'BDMV', 'index.bdmv') + if (existsSync(index)) { + let fd: number | null = null + try { + fd = openSync(index, 'r') + const kopf = lesenBei(fd, 0, 8) + const typ = typAusIndexKopf(kopf) + if (typ !== null) return typ + } catch { + // nicht lesbar — dann entscheidet die Größe (unten) + } finally { + if (fd !== null) closeSync(fd) + } + return 'bluray' + } + if (existsSync(join(wurzel, 'VIDEO_TS'))) return 'dvd' + return null +} + /** \\.\G: → G:\ — unter Windows hängt Windows die Disc selbst ein. */ export function discWurzel(geraetepfad: string): string { const buchstabe = geraetepfad.replace(/:$/, '').split('\\').pop()!.replace(/:$/, '') diff --git a/rippy-windows/test/disctyp-570.test.ts b/rippy-windows/test/disctyp-570.test.ts new file mode 100644 index 0000000..d9a780b --- /dev/null +++ b/rippy-windows/test/disctyp-570.test.ts @@ -0,0 +1,44 @@ +// 5.7.0 (Durchsicht F10): Der Disc-Typ aus dem Dateisystem — BDMV/index.bdmv +// mit „INDX0300" ist UHD (am Akira-UHD-Abbild gemessen), „0200"/„0100" eine +// Blu-ray, VIDEO_TS eine DVD. Der Windows-Steuercode antwortet bei manchen +// USB-Brücken nicht, und die Größe allein machte aus einer 4K-Disc mit +// 48 GB Daten „bluray". +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { typAusIndexKopf } from '../src/kern/metadaten/discmerkmale' + +let wegwerf: string[] = [] +afterEach(() => { + for (const o of wegwerf.splice(0)) rmSync(o, { recursive: true, force: true }) +}) + +describe('typAusIndexKopf', () => { + it('INDX0300 ist UHD, INDX0200/0100 Blu-ray, alles andere nichts', () => { + expect(typAusIndexKopf(Buffer.from('INDX0300\0\0\0N', 'latin1'))).toBe('uhd') + expect(typAusIndexKopf(Buffer.from('INDX0200xxxx', 'latin1'))).toBe('bluray') + expect(typAusIndexKopf(Buffer.from('INDX0100xxxx', 'latin1'))).toBe('bluray') + expect(typAusIndexKopf(Buffer.from('MOBJ0200', 'latin1'))).toBe(null) + expect(typAusIndexKopf(Buffer.from('IND', 'latin1'))).toBe(null) + }) +}) + +describe('typAusDateisystem an einem nachgebauten Laufzeit-Ordner', () => { + // discWurzel() rechnet aus \\.\G: den Buchstaben — das geht nur unter + // Windows an einem echten Laufwerk. Hier wird deshalb die Kopf-Funktion + // über eine echte Datei geprüft, so wie typAusDateisystem sie liest. + it('liest die ersten acht Bytes einer echten index.bdmv', () => { + const wurzel = mkdtempSync(join(tmpdir(), 'rippy-bdmv-')) + wegwerf.push(wurzel) + mkdirSync(join(wurzel, 'BDMV'), { recursive: true }) + const index = join(wurzel, 'BDMV', 'index.bdmv') + writeFileSync(index, Buffer.concat([Buffer.from('INDX0300', 'latin1'), Buffer.alloc(64)])) + const { openSync, readSync, closeSync } = require('node:fs') as typeof import('node:fs') + const fd = openSync(index, 'r') + const kopf = Buffer.alloc(8) + readSync(fd, kopf, 0, 8, 0) + closeSync(fd) + expect(typAusIndexKopf(kopf)).toBe('uhd') + }) +})