feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie veröffentlicht wurde): Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge, Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts, Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts. Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/, uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte), einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/ (ErstEinrichtung, PosterHintergrund). Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs); LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update; Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown; Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag; Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem Poster-Grid. Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung, fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit 29 Schritten inklusive Erst-Einrichtung (zweiter Start mit RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6678c81592
commit
3dc6798c79
@@ -0,0 +1,198 @@
|
||||
// Tests der Automatik (kern/dienste/automatik.ts, 5.5.0): Rippys Vorschlag
|
||||
// als Auftrag, Staffel/Folge aus Disc und Serien-Merker, und der Ablauf
|
||||
// mit Fake-Uhr — sicher → Titel lesen → Countdown → Rip; unsicher → nur
|
||||
// sagen, warum; Stopp und Jetzt.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AutomatikStand, GeraeteInfo, KernNachricht, RipAuftragOptionen, RipAuftragTitel, TitelStand } from '../src/gemeinsam/nachrichten'
|
||||
import type { Protokoll } from '../src/gemeinsam/protokoll'
|
||||
import { AUTOMATIK_SICHER, AutomatikDienst, auftragAusVorschlag, optionenAus, type AutomatikAbhaengigkeiten } from '../src/kern/dienste/automatik'
|
||||
import type { KernKontext } from '../src/kern/kontext'
|
||||
import type { Zuordnung } from '../src/kern/metadaten/zuordnung'
|
||||
|
||||
function titel(nr: number, gewaehlt: boolean, rolle: 'hauptfilm' | 'extra' | 'folge' = 'hauptfilm'): TitelStand['titel'][number] {
|
||||
return { nr, dauerS: 3000 + nr, groesseBytes: 1e9 * (nr + 1), kapitel: 12, gewaehlt, rolle, grund: gewaehlt ? 'Hauptfilm' : 'zu kurz', sprachen: { audio: [], untertitel: [] } }
|
||||
}
|
||||
|
||||
function stand(teile: TitelStand['titel'], unsicher = false, fehler = ''): TitelStand {
|
||||
return { id: 'G', laeuft: false, titel: teile, einordnung: unsicher ? 'zwei fast gleich lange Titel — bitte nachsehen' : 'Film: ein Hauptfilm', unsicher, fehler }
|
||||
}
|
||||
|
||||
function geraet(typ: GeraeteInfo['typ'] = 'bluray'): GeraeteInfo {
|
||||
return { id: 'G', name: 'G:', typ, pfad: 'G:\\', status: 'ready', modell: 'Fake', seriennummer: '', groesseBytes: 4e10, grund: '' }
|
||||
}
|
||||
|
||||
function zuordnung(confidence: number): Zuordnung {
|
||||
return { titel: 'Akira', jahr: 1988, confidence, meta: {} as never }
|
||||
}
|
||||
|
||||
describe('auftragAusVorschlag', () => {
|
||||
it('nimmt nur die vorausgewählten Titel, mit Rolle, Dauer und Größe — Sprachen leer (Einstellung gilt)', () => {
|
||||
const auftrag = auftragAusVorschlag(stand([titel(0, true), titel(1, false), titel(2, true, 'extra')]))
|
||||
expect(auftrag).toEqual<RipAuftragTitel[]>([
|
||||
{ nr: 0, rolle: 'hauptfilm', dauerS: 3000, groesseBytes: 1e9 },
|
||||
{ nr: 2, rolle: 'extra', dauerS: 3002, groesseBytes: 3e9 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('optionenAus', () => {
|
||||
it('ohne alles: nichts vorgeben', () => {
|
||||
expect(optionenAus(null, null)).toEqual<RipAuftragOptionen>({ staffel: null, ersteFolge: null })
|
||||
})
|
||||
it('die Staffel der Disc schlägt den Merker', () => {
|
||||
expect(optionenAus({ staffel: 2, discNr: 2 }, { staffel: 1, naechsteFolge: 5 })).toEqual({ staffel: 2, ersteFolge: 5 })
|
||||
})
|
||||
it('Disc 1 fängt nie beim Merker an — sonst begänne die neue Ausgabe bei Folge 5', () => {
|
||||
expect(optionenAus({ staffel: null, discNr: 1 }, { staffel: 1, naechsteFolge: 5 })).toEqual({ staffel: 1, ersteFolge: null })
|
||||
})
|
||||
it('Disc-Nummer unbekannt oder > 1: ab der gemerkten Folge weiter', () => {
|
||||
expect(optionenAus({ staffel: null, discNr: null }, { staffel: 1, naechsteFolge: 3 })).toEqual({ staffel: 1, ersteFolge: 3 })
|
||||
expect(optionenAus({ staffel: null, discNr: 3 }, { staffel: 1, naechsteFolge: 9 })).toEqual({ staffel: 1, ersteFolge: 9 })
|
||||
})
|
||||
})
|
||||
|
||||
function aufbau(sekunden = 3) {
|
||||
const nachrichten: AutomatikStand[] = []
|
||||
const hinweise: string[] = []
|
||||
const kontext = {
|
||||
db: null,
|
||||
dbStatus: { ok: false, fehler: 'Test' },
|
||||
anFenster: (n: KernNachricht) => {
|
||||
if (n.art === 'automatik') nachrichten.push(n.stand)
|
||||
},
|
||||
anHauptUndFenster: () => {},
|
||||
hinweis: (t: string) => hinweise.push(t),
|
||||
fehler: () => {},
|
||||
protokoll: { schreiben: () => {} } as unknown as Protokoll,
|
||||
smoke: true,
|
||||
} as unknown as KernKontext
|
||||
const gestartet: Array<{ id: string; auftrag: RipAuftragTitel[]; optionen: RipAuftragOptionen }> = []
|
||||
const benachrichtigt: string[] = []
|
||||
let tick: (() => void) | null = null
|
||||
let gestoppt = 0
|
||||
let an = true
|
||||
let ripLaeuft = false
|
||||
let titelStand: TitelStand = stand([titel(0, true), titel(1, false)])
|
||||
let angaben: { staffel: number | null; discNr: number | null } | null = null
|
||||
let merker: { staffel: number; naechsteFolge: number } | null = null
|
||||
const abh: AutomatikAbhaengigkeiten = {
|
||||
an: () => an,
|
||||
sekunden: () => sekunden,
|
||||
ripLaeuft: () => ripLaeuft,
|
||||
titelLesen: async () => titelStand,
|
||||
ripStarten: (id, auftrag, optionen) => gestartet.push({ id, auftrag, optionen }),
|
||||
discAngaben: () => angaben,
|
||||
serienMerker: () => merker,
|
||||
benachrichtigen: (t, x) => benachrichtigt.push(`${t}: ${x}`),
|
||||
takt: (fn) => {
|
||||
tick = fn
|
||||
return 1 as unknown as NodeJS.Timeout
|
||||
},
|
||||
taktStoppen: () => {
|
||||
gestoppt += 1
|
||||
tick = null
|
||||
},
|
||||
}
|
||||
const dienst = new AutomatikDienst(kontext, abh)
|
||||
return {
|
||||
dienst,
|
||||
nachrichten,
|
||||
hinweise,
|
||||
gestartet,
|
||||
benachrichtigt,
|
||||
tick: () => {
|
||||
if (tick === null) throw new Error('kein Takt')
|
||||
tick()
|
||||
},
|
||||
gestopptZahl: () => gestoppt,
|
||||
setAn: (w: boolean) => (an = w),
|
||||
setRipLaeuft: (w: boolean) => (ripLaeuft = w),
|
||||
setTitel: (s: TitelStand) => (titelStand = s),
|
||||
setAngaben: (a: typeof angaben) => (angaben = a),
|
||||
setMerker: (m: typeof merker) => (merker = m),
|
||||
}
|
||||
}
|
||||
|
||||
describe('AutomatikDienst', () => {
|
||||
it('sicher erkannt, nicht gerippt: liest Titel, zählt herunter, startet mit Rippys Vorschlag', async () => {
|
||||
const a = aufbau(3)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.95), null)
|
||||
expect(a.nachrichten[0].text).toContain('liest die Titel')
|
||||
expect(a.nachrichten[1]).toMatchObject({ aktiv: true, sekunden: 3 })
|
||||
a.tick()
|
||||
a.tick()
|
||||
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: true, sekunden: 1 })
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
a.tick()
|
||||
expect(a.gestartet).toHaveLength(1)
|
||||
expect(a.gestartet[0].auftrag).toEqual([{ nr: 0, rolle: 'hauptfilm', dauerS: 3000, groesseBytes: 1e9 }])
|
||||
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, text: 'Automatik: Rip gestartet (1 Titel).' })
|
||||
expect(a.benachrichtigt[0]).toContain('Automatik hat auf Laufwerk G gestartet')
|
||||
expect(a.gestopptZahl()).toBe(1)
|
||||
})
|
||||
it('unsichere Erkennung: nichts passiert von selbst — nur der Grund', async () => {
|
||||
const a = aufbau()
|
||||
await a.dienst.pruefen(geraet(), zuordnung(AUTOMATIK_SICHER - 0.01), null)
|
||||
expect(a.nachrichten).toHaveLength(1)
|
||||
expect(a.nachrichten[0]).toMatchObject({ aktiv: false })
|
||||
expect(a.nachrichten[0].text).toContain('nicht sicher genug')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
})
|
||||
it('schon gerippt: wartet und nennt das Datum', async () => {
|
||||
const a = aufbau()
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), '2026-08-30T10:00:00Z')
|
||||
expect(a.nachrichten[0].text).toContain('schon gerippt (2026-08-30)')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
})
|
||||
it('aus, CD, unbekannte Disc oder laufender Rip: gar nichts', async () => {
|
||||
const a = aufbau()
|
||||
a.setAn(false)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.setAn(true)
|
||||
await a.dienst.pruefen(geraet('cd'), zuordnung(0.99), null)
|
||||
await a.dienst.pruefen(geraet('unknown'), zuordnung(0.99), null)
|
||||
a.setRipLaeuft(true)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
expect(a.nachrichten).toHaveLength(0)
|
||||
})
|
||||
it('unsicherer Titel-Lauf (zwei fast gleich lange Titel): kein Countdown, die Einordnung wird gesagt', async () => {
|
||||
const a = aufbau()
|
||||
a.setTitel(stand([titel(0, true), titel(1, false)], true))
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
expect(a.nachrichten[a.nachrichten.length - 1].text).toContain('bitte nachsehen')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
})
|
||||
it('Stopp beendet den Countdown mit Grund; Jetzt überspringt ihn', async () => {
|
||||
const a = aufbau(30)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.dienst.stopp('G', 'Automatik gestoppt — du entscheidest.')
|
||||
expect(a.gestopptZahl()).toBe(1)
|
||||
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, text: 'Automatik gestoppt — du entscheidest.' })
|
||||
expect(() => a.tick()).toThrow('kein Takt')
|
||||
// Jetzt braucht einen aktiven Countdown und den gemerkten Titel-Stand.
|
||||
a.dienst.jetzt('G')
|
||||
expect(a.gestartet).toHaveLength(0)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.dienst.titelMerken('G', stand([titel(0, true), titel(1, false)]))
|
||||
a.dienst.jetzt('G')
|
||||
expect(a.gestartet).toHaveLength(1)
|
||||
})
|
||||
it('Serie: Staffel/Folge aus Disc und Merker, Merker nur mit gemerkter Zuordnung', async () => {
|
||||
const a = aufbau(1)
|
||||
a.setTitel(stand([titel(0, true, 'folge'), titel(1, true, 'folge')]))
|
||||
a.setAngaben({ staffel: null, discNr: 2 })
|
||||
a.setMerker({ staffel: 1, naechsteFolge: 3 })
|
||||
a.dienst.zuordnungMerken('G', zuordnung(0.99))
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
a.tick()
|
||||
expect(a.gestartet[0].optionen).toEqual({ staffel: 1, ersteFolge: 3 })
|
||||
})
|
||||
it('Disc raus: Countdown weg, Stand weg', async () => {
|
||||
const a = aufbau(5)
|
||||
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
|
||||
expect(a.dienst.stand('G')).not.toBeNull()
|
||||
a.dienst.discEntnommen('G')
|
||||
expect(a.dienst.stand('G')).toBeNull()
|
||||
expect(a.gestopptZahl()).toBe(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user