// Der EINZIGE Ort, der die Datenbank anfasst — § 6.7: EINE Quelle für // Einstellungen. Ein Wächter-Test (test/waechter.test.ts) prüft mechanisch, // dass 'node:sqlite' nirgendwo sonst importiert wird. Der Fund hinter der // Regel: In rc11 schrieb die Oberfläche in die Datenbank, der Betrieb las aus // einer Konfigurationsdatei, die niemand schrieb. // // node:sqlite statt better-sqlite3: am 30.08.2026 in Electron 44 im // utilityProcess gemessen (KONZEPT-WINDOWS.md § 3.4, beweise/sqlite-*.js) — // keine native Kompilierung, kein node-gyp in der Bau-Kette. import { DatabaseSync } from 'node:sqlite' import { mkdirSync } from 'node:fs' import { dirname } from 'node:path' import type { BibliothekEintrag } from '../../gemeinsam/nachrichten' export const SCHEMA_VERSION = 3 export class Datenbank { readonly pfad: string private db: DatabaseSync /** * Öffnet (oder erzeugt) die Datenbank und legt das Schema an. * Wirft bei nicht anlegbarem Pfad — der Aufrufer meldet den Fehler * LAUT weiter (R4), statt mit einer stillen Ersatz-Datenbank zu starten. */ constructor(pfad: string) { this.pfad = pfad if (pfad !== ':memory:') { mkdirSync(dirname(pfad), { recursive: true }) } this.db = new DatabaseSync(pfad) this.db.exec( `CREATE TABLE IF NOT EXISTS meta ( schluessel TEXT PRIMARY KEY, wert TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS einstellungen ( schluessel TEXT PRIMARY KEY, wert TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS bibliothek ( fingerprint TEXT PRIMARY KEY, titel TEXT NOT NULL, jahr INTEGER, typ TEXT NOT NULL, groesse_bytes INTEGER NOT NULL, ablageort TEXT NOT NULL, datum TEXT NOT NULL, dauer_s INTEGER NOT NULL, poster_pfad TEXT NOT NULL DEFAULT '' );`, ) // Schema 2 → 3: Bestands-Datenbanken haben die Bibliothek noch ohne // Poster-Spalte — nachrüsten, ohne Einträge anzufassen. const spalten = this.db.prepare(`PRAGMA table_info(bibliothek)`).all() as Array<{ name: string }> if (!spalten.some((s) => s.name === 'poster_pfad')) { this.db.exec(`ALTER TABLE bibliothek ADD COLUMN poster_pfad TEXT NOT NULL DEFAULT ''`) } this.db .prepare(`INSERT OR REPLACE INTO meta (schluessel, wert) VALUES ('schema_version', ?)`) .run(String(SCHEMA_VERSION)) } /** Liest eine Einstellung; unbekannter Schlüssel ist `null`, kein Fehler. */ einstellungLesen(schluessel: string): string | null { const zeile = this.db .prepare(`SELECT wert FROM einstellungen WHERE schluessel = ?`) .get(schluessel) as { wert: string } | undefined return zeile === undefined ? null : zeile.wert } einstellungSchreiben(schluessel: string, wert: string): void { this.db .prepare(`INSERT OR REPLACE INTO einstellungen (schluessel, wert) VALUES (?, ?)`) .run(schluessel, wert) } schemaVersion(): number { const zeile = this.db .prepare(`SELECT wert FROM meta WHERE schluessel = 'schema_version'`) .get() as { wert: string } | undefined return zeile === undefined ? 0 : Number(zeile.wert) } // ── Bibliothek (§ 6.6): alles Gerippte, wiedererkennbar am // Disc-Fingerabdruck ──────────────────────────────────────────────── bibliothekEintragen(eintrag: BibliothekEintrag): void { this.db .prepare( `INSERT OR REPLACE INTO bibliothek (fingerprint, titel, jahr, typ, groesse_bytes, ablageort, datum, dauer_s, poster_pfad) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( eintrag.fingerprint, eintrag.titel, eintrag.jahr, eintrag.typ, eintrag.groesseBytes, eintrag.ablageort, eintrag.datum, eintrag.dauerS, eintrag.posterPfad, ) } bibliothekListe(): BibliothekEintrag[] { const zeilen = this.db .prepare(`SELECT * FROM bibliothek ORDER BY datum DESC`) .all() as Array> return zeilen.map((z) => ({ fingerprint: String(z.fingerprint), titel: String(z.titel), jahr: z.jahr === null ? null : Number(z.jahr), typ: String(z.typ), groesseBytes: Number(z.groesse_bytes), ablageort: String(z.ablageort), datum: String(z.datum), dauerS: Number(z.dauer_s), posterPfad: String(z.poster_pfad ?? ''), })) } /** Datum des früheren Rips dieser Disc — oder null (neu). */ bibliothekSchonGerippt(fingerprint: string): string | null { const zeile = this.db .prepare(`SELECT datum FROM bibliothek WHERE fingerprint = ?`) .get(fingerprint) as { datum: string } | undefined return zeile === undefined ? null : zeile.datum } schliessen(): void { this.db.close() } }