Files
rippy/rippy-windows/test/automatik.test.ts
T
HitonabiandClaude Fable 5.1 f5dab5d1c1 feat(v5): 5.6.0 — Vorgangs-Leiste, mehrere Filme je Disc, Filmnamen für Ordner und Dateien, Untertitel-Vorwahl je Extra, Restzeit, Hell-Modus
Die sechs Punkte des Commanders vom 02.09.2026 abends und seine Entscheide:

* Restzeit der Kompression aus HandBrakes eigener ETA-Klammer (Formatstring
  aus dem Binary 1.11.2 gelesen), KompressionStand.restS.
* Kachel zeigt nach dem Rip nur noch eine Zeile — Balken, Prozent, Ziel in
  der Kompressions-Karte und im Verlauf ("nur in der Kompressions-Karte").
* Kopfzeile bleibt beim Scrollen stehen (sticky), Sektionswahl darunter.
* Untertitel-Vorwahl je Extra ("NUR für Extras, IMMER OPTIONAL"): welche
  Untertitel-Spur beim Abspielen an ist — weiche Spur, nie eingebrannt;
  RipAuftragTitel/RohDatei.untertitelVorwahl, sprachwahl.
* Mehrere Filme je Disc: "anderer Film …" je Titel mit TMDb-Suche;
  filme.ts filmGruppen trennt am Rip-Ende in eigene Roh-Ordner, Vorgänge
  und Kompressions-Aufträge mit voller Zuordnung (erkennung.zuordnungAus).
* Roh-Ordner und Dateien tragen den Filmnamen (filme.ts rohOrdnerName,
  ablage/dateinamen.ts: Akira (1988).mkv, - Fassung n, - Extra nn).
* Hell-Modus: Einstellung design (dunkel/hell/system), data-design auf
  <html>, Token --color-tinte statt white/-Alpha-Klassen.
* Vorgangs-Leiste (Commander: "Phase 1–4 plus Komprimierung — zu einem
  machen"): gemeinsam/vorgangsleiste.ts + uebersicht/VorgangsLeiste.tsx,
  vier Stationen (Rettung als Zwischenstation), Segment-Balken in der
  aktiven Station; Kachel, Kompressions-Karte, Verlauf. PHASE-x/4-Texte weg.
* Automatik-Kasten leert sich, sobald ein Rip startet.

Beweise: 446 Tests (neu: filme, dateinamen, vorgangsleiste; erweitert:
auftrag, handbrake, automatik, ablegen), drei Typprüfungen, Klick-Beweis
33 Schritte inkl. Vorwahl, anderer Film, Vorgangs-Leiste, Restzeit,
Hell-Modus (Bilder beweise/klick/01…11).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 19:17:37 +02:00

211 lines
9.5 KiB
TypeScript

// 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('leeren: Countdown weg und ein leerer Stand ans Fenster — der Kasten verschwindet (5.6.0)', async () => {
const a = aufbau(5)
await a.dienst.pruefen(geraet(), zuordnung(0.99), null)
a.dienst.leeren('G')
expect(a.gestopptZahl()).toBe(1)
expect(a.nachrichten[a.nachrichten.length - 1]).toMatchObject({ aktiv: false, sekunden: 0, text: '' })
// Ohne bekannten Stand wird nichts gesendet — kein leerer Kasten aus dem Nichts.
const b = aufbau(5)
b.dienst.leeren('G')
expect(b.nachrichten).toHaveLength(0)
})
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)
})
})