feat(v5): Disc-Typ aus dem Dateisystem — INDX0300 ist UHD, VIDEO_TS ist DVD

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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 21:49:49 +02:00
co-authored by Claude Opus 5
parent 0fc87aa022
commit fc2d16ca38
3 changed files with 114 additions and 2 deletions
+30 -2
View File
@@ -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<void>
/** 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<string, { groesse: number; typ: GeraeteInfo['typ'] }>()
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[] {
@@ -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(/:$/, '')
+44
View File
@@ -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')
})
})