Files
rippy/rippy-windows/src/kern/ablauf/auftrag.ts
T
HitonabiandClaude Fable 5.1 3f7f962dff feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft
danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer
die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge,
Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen:
Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression
und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab
Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme
nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den
Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben.
Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder
Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der
Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs
Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die
Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die
Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet.
Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird
nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck,
bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline,
Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit
aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware.

WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom
Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte
Kompression waren seine Wahl).

GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg-
Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format,
TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an
diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength,
MakeMKV-Auswahlregel laut forum.makemkv.com t=4386.

348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche
folgt im naechsten Schritt.

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

178 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Der Kompressions-Auftrag (5.4.0) — PUR, ohne Laufwerk und ohne Netz.
//
// Er ist das Bindeglied zwischen zwei Phasen, die seit 5.4.0 getrennt
// laufen: Der RIP belegt das Laufwerk und endet, sobald die Roh-Dateien
// liegen; die KOMPRESSION läuft danach in einer Warteschlange und braucht
// nur noch diese Beschreibung. Deshalb wird sie als JSON in der Datenbank
// gemerkt — so kann „Wiederholen ab Kompression" sie Tage später wieder
// aufnehmen, ohne die Disc noch einmal zu lesen (40 Minuten je Blu-ray).
import type { TitelRolle } from '../../gemeinsam/nachrichten'
import type { Zuordnung } from '../metadaten/zuordnung'
export interface RohDatei {
pfad: string
titelNr: number
rolle: TitelRolle
dauerS: number
/** Größe der Roh-Datei — gewichtet den Fortschrittsbalken. */
groesseBytes: number
/** Sprachen je Datei (leer = die globale Einstellung gilt). */
audioSprachen: string[]
untertitelSprachen: string[]
}
export interface KompressionsAuftrag {
vorgangId: string
laufwerk: string
discTyp: string
rohOrdner: string
dateien: RohDatei[]
/** Schnappschuss der Metadaten-Zuordnung zum Zeitpunkt des Rips. */
zuordnung: Zuordnung | null
/** Staffel (Nutzer > Disc > 1) und erste Folge (nur vom Nutzer). */
staffel: number | null
ersteFolge: number | null
discNr: number | null
fingerprint: string | null
lesefehler: boolean
/** Klartext aus dem Rip („Übersprungen: …", „Nicht gesichert: …"). */
hinweise: string[]
/** Beginn des Rips (ISO) — die Bibliothek merkt sich diesen Zeitpunkt. */
begonnen: string
/** Preset-Vorgabe beim Wiederholen; '' = die Einstellung gilt. */
preset: string
/** Beim Wiederholen: der Zielordner des ERSTEN Durchlaufs — die neue
* Fassung ersetzt die alte, statt einen zweiten Ordner anzulegen.
* '' = Zielordner wird frisch bestimmt. */
zielOrdner: string
}
export function auftragZuJson(auftrag: KompressionsAuftrag): string {
return JSON.stringify(auftrag)
}
/** Liest einen gemerkten Auftrag zurück — null, wenn er nicht die Form hat
* (eine Datenbank aus einer späteren Fassung, ein Fremdeintrag). */
export function auftragAusJson(text: string): KompressionsAuftrag | null {
let roh: unknown
try {
roh = JSON.parse(text)
} catch {
return null
}
if (typeof roh !== 'object' || roh === null) return null
const a = roh as Record<string, unknown>
if (typeof a.vorgangId !== 'string' || typeof a.rohOrdner !== 'string' || !Array.isArray(a.dateien)) return null
const dateien: RohDatei[] = []
for (const d of a.dateien) {
if (typeof d !== 'object' || d === null) return null
const e = d as Record<string, unknown>
if (typeof e.pfad !== 'string') return null
dateien.push({
pfad: e.pfad,
titelNr: typeof e.titelNr === 'number' ? e.titelNr : 0,
rolle: e.rolle === 'extra' || e.rolle === 'folge' ? e.rolle : 'hauptfilm',
dauerS: typeof e.dauerS === 'number' ? e.dauerS : 0,
groesseBytes: typeof e.groesseBytes === 'number' ? e.groesseBytes : 0,
audioSprachen: Array.isArray(e.audioSprachen) ? e.audioSprachen.map(String) : [],
untertitelSprachen: Array.isArray(e.untertitelSprachen) ? e.untertitelSprachen.map(String) : [],
})
}
return {
vorgangId: a.vorgangId,
laufwerk: typeof a.laufwerk === 'string' ? a.laufwerk : '',
discTyp: typeof a.discTyp === 'string' ? a.discTyp : 'bluray',
rohOrdner: a.rohOrdner,
dateien,
zuordnung: typeof a.zuordnung === 'object' && a.zuordnung !== null ? (a.zuordnung as Zuordnung) : null,
staffel: typeof a.staffel === 'number' ? a.staffel : null,
ersteFolge: typeof a.ersteFolge === 'number' ? a.ersteFolge : null,
discNr: typeof a.discNr === 'number' ? a.discNr : null,
fingerprint: typeof a.fingerprint === 'string' ? a.fingerprint : null,
lesefehler: a.lesefehler === true,
hinweise: Array.isArray(a.hinweise) ? a.hinweise.map(String) : [],
begonnen: typeof a.begonnen === 'string' ? a.begonnen : '',
preset: typeof a.preset === 'string' ? a.preset : '',
zielOrdner: typeof a.zielOrdner === 'string' ? a.zielOrdner : '',
}
}
/** Was HandBrake für EINE Datei bekommt. */
export interface SprachWahl {
audio: string[]
untertitel: string[]
/** Die erste Untertitelspur vorwählen — weil der Ton nicht in der
* Wunschsprache ist. */
untertitelStandard: boolean
}
/**
* Sprachwahl je Datei (pur): Was der Nutzer am Titel gewählt hat, schlägt
* die globale Einstellung. Und die Regel des Commanders (01.09.2026):
* „japanischer Ton und braucht dringend die deutschen untertitel" —
* spielt die ERSTE Tonspur nicht in der Wunschsprache (der ersten
* globalen Tonsprache), wird die Untertitelspur der Wunschsprache nach
* vorn gestellt und als Standard markiert. HandBrake ordnet die Spuren in
* Listen-Reihenfolge und `--subtitle-default=1` trifft die erste (gemessen,
* komprimieren/handbrake.ts).
*/
export function sprachwahl(
datei: { audioSprachen: readonly string[]; untertitelSprachen: readonly string[] },
global: { audio: readonly string[]; untertitel: readonly string[] },
): SprachWahl {
const audio = datei.audioSprachen.length > 0 ? [...datei.audioSprachen] : [...global.audio]
let untertitel = datei.untertitelSprachen.length > 0 ? [...datei.untertitelSprachen] : [...global.untertitel]
const wunsch = global.audio[0] ?? ''
const tonFremd = wunsch.length > 0 && audio.length > 0 && audio[0] !== wunsch
const untertitelStandard = tonFremd && untertitel.includes(wunsch)
if (untertitelStandard) untertitel = [wunsch, ...untertitel.filter((u) => u !== wunsch)]
return { audio, untertitel, untertitelStandard }
}
/** Folgen-Nummern von Hand: fortlaufend ab der ersten, in Disc-Reihenfolge. */
export function folgenNummern(anzahl: number, ersteFolge: number): number[] {
const start = Math.max(1, Math.trunc(ersteFolge))
return Array.from({ length: Math.max(0, anzahl) }, (_, i) => start + i)
}
/**
* Durchgehender Fortschritt über mehrere Teile, nach Gewicht (pur).
* Vorher sprang der Balken je Titel auf null zurück — bei 2 × 16 GB stand
* er nach der Hälfte wieder bei 0 %. Fehlt ein Gewicht (0), zählen alle
* Teile gleich. `prozent` < 0 heißt „vom laufenden Teil noch keine Angabe".
*/
export function gewichteterFortschritt(gewichte: readonly number[], index: number, prozent: number): number {
if (gewichte.length === 0) return -1
const gleich = gewichte.some((g) => !(g > 0))
const g = gleich ? gewichte.map(() => 1) : [...gewichte]
const gesamt = g.reduce((s, w) => s + w, 0)
if (gesamt <= 0) return -1
const i = Math.min(Math.max(0, index), g.length - 1)
const erledigt = g.slice(0, i).reduce((s, w) => s + w, 0)
const laufend = prozent < 0 ? 0 : (g[i] * Math.min(100, prozent)) / 100
return Math.max(0, Math.min(100, Math.floor(((erledigt + laufend) / gesamt) * 100)))
}
/** Passen die von Hand gesetzten Folgen zu den TMDb-Laufzeiten? Nur eine
* Warnung — der Nutzer weiß, was auf seiner Disc ist. Rückgabe: Klartext
* je Abweichung (leer = alles plausibel oder nichts zum Vergleichen). */
export function folgenPlausibel(
dateiSekunden: readonly number[],
nummern: readonly number[],
laufzeiten: ReadonlyArray<readonly [nummer: number, sekunden: number]>,
toleranz = 0.15,
): string[] {
const warnungen: string[] = []
for (let i = 0; i < nummern.length; i++) {
const soll = laufzeiten.find(([nr]) => nr === nummern[i])
const ist = dateiSekunden[i] ?? 0
if (soll === undefined || soll[1] <= 0 || ist <= 0) continue
if (Math.abs(ist - soll[1]) / soll[1] > toleranz) {
warnungen.push(
`Folge ${nummern[i]}: Datei ${Math.round(ist / 60)} min, TMDb ${Math.round(soll[1] / 60)} min — bitte nachsehen.`,
)
}
}
return warnungen
}