Ampel / ampel (push) Successful in 1m34s
Update-Strecke: Quellenkette (Hersteller 525 → Forum → Archiv, höchste gewinnt, 31.08. nachgemessen), MZ- + Versions-Ressourcen-Prüfung (GuinpinSoft inc, gemessen), Download in .neu, Start übers Haupt (shell/UAC — Installer hängt bewusst NICHT an der Leine), Warte-Poll bis die EXE-Version wechselt, dann Schlüssel neu prüfen. Ehrlicher Zweig: installierte == neueste Fassung → kein sinnloser Download. Fenster: Laufwerks-Kachel als Bühne (TMDb-Backdrop, Poster, Beschreibung, Phasen-Label, Restzeit, aufklappbares Protokoll), Bibliothek als Poster-Regal (DB-Schema 3: poster_pfad, Migration), Schlüssel-Karte mit Update-Knopf und Beschaffungs-Balken, MakeMKV-Version als Badge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
136 lines
4.9 KiB
TypeScript
136 lines
4.9 KiB
TypeScript
// 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<Record<string, unknown>>
|
|
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()
|
|
}
|
|
}
|