feat(v5): jede Spur einzeln anhaken — Codec und Kanäle je Tonspur, Untertitel genauso

WAS: Der Auswahl-Dialog zeigt je Titel jede Ton- und Untertitelspur, die
der Info-Lauf gemessen hat („German · DTS-HD MA · Surround 5.1", „German ·
DD · Stereo (Kommentar)", „German · PGS (nur erzwungene)"), vorbelegt wie
bisher (je Wunschsprache die erste Tonspur, alle Untertitel der Wunsch-
sprachen) — angehakt wird einzeln. Die Untertitel-Vorwahl je Extra ist
jetzt eine Spur. Der Parser liest dazu SINFO-Attribute 2 (Anordnung),
22 (Flags) und markiert abgeleitete Spuren (Bit 0x800, an der Kizuna-
Disc gemessen): DTS-Kerne werden nicht angeboten, „nur erzwungene"-
Untertitel schon. Die Ablage scannt die Roh-Datei und findet die
gewählten MakeMKV-Spuren gestuft wieder (Sprache + Codec-Familie +
Kanäle → … → Sprache), HandBrake bekommt `--audio 1,3 --subtitle 2,1
--subtitle-default=1`; Fehlendes wird gemeldet, scheitert der Scan,
gelten die Sprachen der gewählten Spuren als Liste. MakeMKVs Auswahl-
regel lässt Stereo-Doppel jetzt stehen (nur -sel:havecore); Rippys alte
Regel wird in der Registry ersetzt, eine fremde bleibt.
Dazu F31 (Durchsicht): `--aencoder copy` ohne `--audio-copy-mask`
verengt HandBrakeCLI 1.11.2 auf „copy:aac" — AC3/DTS/TrueHD wurden seit
5.0 in AAC umgerechnet. Beide Wege setzen die Maske jetzt ausdrücklich
(gemessen 12.09.2026: AC3 bleibt AC3, Passthru-Zeile nennt alle Codecs).
WARUM: Commander-Entscheid 12.09.2026 („Jede Spur einzeln anhaken") —
bisher ließ sich je Sprache nur „alles oder nichts" wählen, und der
Kommentar-Ton fehlte in der Roh-Datei (havemulti). Gemessen am
mitgelieferten HandBrakeCLI mit ffmpeg-gebauter Quelle
(test/messung.spuren.test.ts, RIPPY_MESSUNG=1): --audio 1,3 → jpn und
deu-Kommentar, --subtitle 2,1 --subtitle-default=1 → eng (default) vor
deu, --subtitle none → keine Spur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 22:11:27 +02:00
co-authored by Claude Opus 5
parent fc2d16ca38
commit e7e33e9021
18 changed files with 1098 additions and 60 deletions
+50 -6
View File
@@ -46,8 +46,12 @@ import {
folgenNummern,
folgenPlausibel,
gewichteterFortschritt,
sprachlistenAusSpuren,
sprachwahl,
spurwahl,
type KompressionsAuftrag,
type RohDatei,
type SpurWahl,
} from './auftrag'
export interface AblageUmgebung {
@@ -103,6 +107,24 @@ export function dateiVerschieben(von: string, nach: string): void {
}
}
/** Die Sprachlisten aus den gewählten Spuren, als Felder für sprachwahl. */
function sprachlistenAusSpurenFuerWahl(datei: RohDatei): { audioSprachen: string[]; untertitelSprachen: string[] } {
const listen = sprachlistenAusSpuren(datei)
return { audioSprachen: listen.audio, untertitelSprachen: listen.untertitel }
}
/** Der Zusatz in der Fortschrittszeile: „ · Ton 1,3 · Untertitel 2 (vorgewählt)". */
function spurText(spurNr: SpurWahl | null, wahl: { audio: string[]; untertitel: string[]; untertitelStandard: boolean }): string {
if (spurNr !== null) {
return `${spurNr.audioNr.length > 0 ? ` · Ton Spur ${spurNr.audioNr.join(',')}` : ' · Ton: erste Spur'}${
spurNr.untertitelNr.length > 0 ? ` · Untertitel Spur ${spurNr.untertitelNr.join(',')}${spurNr.untertitelStandard > 0 ? ' (vorgewählt)' : ''}` : ''
}`
}
return `${wahl.audio.length > 0 ? ` · Ton ${wahl.audio.join(',')}` : ''}${
wahl.untertitel.length > 0 ? ` · Untertitel ${wahl.untertitel.join(',')}${wahl.untertitelStandard ? ' (vorgewählt)' : ''}` : ''
}`
}
function groesseVon(pfad: string): number {
try {
return statSync(pfad).size
@@ -239,13 +261,34 @@ export async function komprimierenUndAblegen(
continue
}
const wahl = sprachwahl(datei, global)
let wahl = sprachwahl(datei, global)
// 5.7.0: Spuren EINZELN gewählt — die Roh-Datei wird gescannt und jede
// gewählte MakeMKV-Spur als HandBrake-Spurnummer wiedergefunden
// (auftrag.ts spurwahl). Scheitert der Scan, gelten die Sprachen der
// gewählten Spuren als Liste (wie 5.4.05.6.x).
const einzeln = datei.audioSpuren !== undefined || datei.untertitelSpuren !== undefined
let spurNr: SpurWahl | null = null
// Die Roh-Datei fragen, ob die gewünschten Sprachen wirklich drin sind
// (scan.ts). Nur wenn der Nutzer etwas JE TITEL gewählt hat — die
// globale Vorgabe war schon immer „was da ist".
if (datei.audioSprachen.length > 0 || datei.untertitelSprachen.length > 0) {
if (einzeln || datei.audioSprachen.length > 0 || datei.untertitelSprachen.length > 0) {
const spuren = await scannen(handbrake, datei.pfad)
if (spuren !== null) {
if (spuren !== null && einzeln) {
spurNr = spurwahl(datei, spuren, global.audio[0] ?? '')
for (const f of spurNr?.fehlend ?? []) {
hinweise.push(`${basename(datei.pfad)}: ${f} ist NICHT in der Roh-Datei — MakeMKV hat sie nicht mitgenommen (Auswahlregel prüfen).`)
}
if (spurNr !== null && spurNr.audioNr.length === 0 && (datei.audioSpuren?.length ?? 0) > 0) {
// Keine gewählte Tonspur gefunden → lieber Sprachlisten als eine
// stumme Datei.
spurNr = null
wahl = sprachwahl({ ...datei, ...sprachlistenAusSpurenFuerWahl(datei) }, global)
hinweise.push(`${basename(datei.pfad)}: keine der gewählten Tonspuren gefunden — Sprachen ${wahl.audio.join(',') || 'nach Einstellung'} statt Spuren.`)
}
} else if (spuren === null && einzeln) {
wahl = sprachwahl({ ...datei, ...sprachlistenAusSpurenFuerWahl(datei) }, global)
hinweise.push(`${basename(datei.pfad)}: Spuren der Roh-Datei ließen sich nicht messen — Sprachen ${[...wahl.audio, ...wahl.untertitel].join(',') || 'nach Einstellung'} statt einzelner Spuren.`)
} else if (spuren !== null) {
const fehlt = fehlendeSprachen(spuren, wahl)
if (fehlt.audio.length > 0) {
hinweise.push(
@@ -265,9 +308,7 @@ export async function komprimierenUndAblegen(
phase: 'komprimiert',
prozent: gewichteterFortschritt(gewichte, i, 0),
restS: -1,
text: `Komprimiere ${i + 1}/${dateien.length}: ${basename(datei.pfad)} („${preset}")${
wahl.audio.length > 0 ? ` · Ton ${wahl.audio.join(',')}` : ''
}${wahl.untertitel.length > 0 ? ` · Untertitel ${wahl.untertitel.join(',')}${wahl.untertitelStandard ? ' (vorgewählt)' : ''}` : ''}`,
text: `Komprimiere ${i + 1}/${dateien.length}: ${basename(datei.pfad)} („${preset}")${spurText(spurNr, wahl)}`,
})
const encode = await komprimieren({
programm: handbrake,
@@ -277,6 +318,9 @@ export async function komprimierenUndAblegen(
audioSprachen: wahl.audio,
untertitelSprachen: wahl.untertitel,
untertitelStandard: wahl.untertitelStandard,
...(spurNr !== null
? { audioNr: spurNr.audioNr, untertitelNr: spurNr.untertitelNr, untertitelStandardNr: spurNr.untertitelStandard }
: {}),
// Beim Wiederholen darf die neue Fassung die alte gleichen Namens
// ersetzen (ohne Arbeitsordner schreibt HandBrake direkt ins Ziel).
ueberschreiben: zwischen.length === 0 && ersetzt.has(zielDatei),
+74 -1
View File
@@ -6,7 +6,9 @@
// 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 { MetaVorschlag, TitelRolle } from '../../gemeinsam/nachrichten'
import type { MetaVorschlag, SpurAngabe, TitelRolle } from '../../gemeinsam/nachrichten'
import { istSpurAngabe } from '../../gemeinsam/nachrichten'
import { spurenZuordnen, sprachenDerSpuren, type HbSpurAngabe } from '../../gemeinsam/spuren'
import type { Zuordnung } from '../metadaten/zuordnung'
export interface RohDatei {
@@ -21,6 +23,11 @@ export interface RohDatei {
untertitelSprachen: string[]
/** 5.6.0 (Extras): vorgewählte Untertitel-Sprache; null = keine; fehlend = Regel. */
untertitelVorwahl?: string | null
/** 5.7.0: die GEWÄHLTEN Spuren (einzeln); fehlend = Sprachlisten gelten. */
audioSpuren?: SpurAngabe[]
untertitelSpuren?: SpurAngabe[]
/** 5.7.0: vorgewählte Untertitel-SPUR (MakeMKV-Nummer); null = keine. */
untertitelVorwahlSpur?: number | null
/** 5.6.0: der Film, zu dem diese Datei gehört, wenn nicht die Disc-Zuordnung. */
film?: MetaVorschlag
}
@@ -86,6 +93,9 @@ export function auftragAusJson(text: string): KompressionsAuftrag | null {
audioSprachen: Array.isArray(e.audioSprachen) ? e.audioSprachen.map(String) : [],
untertitelSprachen: Array.isArray(e.untertitelSprachen) ? e.untertitelSprachen.map(String) : [],
...(e.untertitelVorwahl === null || typeof e.untertitelVorwahl === 'string' ? { untertitelVorwahl: e.untertitelVorwahl } : {}),
...(Array.isArray(e.audioSpuren) && e.audioSpuren.every(istSpurAngabe) ? { audioSpuren: e.audioSpuren } : {}),
...(Array.isArray(e.untertitelSpuren) && e.untertitelSpuren.every(istSpurAngabe) ? { untertitelSpuren: e.untertitelSpuren } : {}),
...(e.untertitelVorwahlSpur === null || typeof e.untertitelVorwahlSpur === 'number' ? { untertitelVorwahlSpur: e.untertitelVorwahlSpur } : {}),
...(filmAus(e.film) !== null ? { film: filmAus(e.film)! } : {}),
})
}
@@ -162,6 +172,69 @@ export function sprachwahl(
return { audio, untertitel, untertitelStandard }
}
/** Was HandBrake bekommt, wenn Spuren EINZELN gewählt wurden (5.7.0). */
export interface SpurWahl {
/** HandBrake-Spurnummern (1-basiert, Scan-Reihenfolge). */
audioNr: number[]
untertitelNr: number[]
/** Index (1-basiert) der vorgewählten Untertitelspur in `untertitelNr`, 0 = keine. */
untertitelStandard: number
/** Klartext für die Hinweise: gewählte Spuren, die nicht in der Datei sind. */
fehlend: string[]
}
/**
* Spurwahl je Datei (pur, 5.7.0): Die gewählten MakeMKV-Spuren werden in
* HandBrakes Scan der Roh-Datei wiedergefunden (spurenZuordnen). Die Regel
* von 5.4.0 gilt weiter: Spielt die erste gewählte Tonspur nicht in der
* Wunschsprache, wird eine gewählte Untertitelspur der Wunschsprache
* vorgewählt — außer der Nutzer hat eine Vorwahl gesetzt (Spur) oder
* ausdrücklich keine (null). Liefert null, wenn keine Spuren gewählt sind
* (dann gilt die Sprachwahl). */
export function spurwahl(
datei: Pick<RohDatei, 'audioSpuren' | 'untertitelSpuren' | 'untertitelVorwahlSpur'>,
scan: { audio: readonly HbSpurAngabe[]; untertitel: readonly HbSpurAngabe[] },
wunschSprache: string,
): SpurWahl | null {
if (datei.audioSpuren === undefined && datei.untertitelSpuren === undefined) return null
const audio = spurenZuordnen(datei.audioSpuren ?? [], scan.audio)
const untertitel = spurenZuordnen(datei.untertitelSpuren ?? [], scan.untertitel)
const gefundeneUntertitel = (datei.untertitelSpuren ?? []).filter((s) => !untertitel.fehlend.includes(s))
// Wer wird vorgewählt?
let standardSpur: SpurAngabe | undefined
if (datei.untertitelVorwahlSpur === null) {
standardSpur = undefined
} else if (typeof datei.untertitelVorwahlSpur === 'number') {
standardSpur = gefundeneUntertitel.find((s) => s.nr === datei.untertitelVorwahlSpur)
} else {
const ersteTon = (datei.audioSpuren ?? []).find((s) => !audio.fehlend.includes(s))
const tonFremd = wunschSprache.length > 0 && ersteTon !== undefined && ersteTon.lang.toLowerCase() !== wunschSprache
if (tonFremd) standardSpur = gefundeneUntertitel.find((s) => s.lang.toLowerCase() === wunschSprache)
}
// Die vorgewählte Spur nach vorn — HandBrakes --subtitle-default zählt
// die AUSGABE-Reihenfolge, und die folgt der Liste.
let untertitelNr = untertitel.nummern
let untertitelStandard = 0
if (standardSpur !== undefined) {
const i = gefundeneUntertitel.indexOf(standardSpur)
if (i >= 0) {
untertitelNr = [untertitel.nummern[i], ...untertitel.nummern.filter((_, k) => k !== i)]
untertitelStandard = 1
}
}
const fehlend = [
...audio.fehlend.map((s) => `Tonspur ${s.sprache || s.lang}${s.codec.length > 0 ? ` ${s.codec}` : ''}`),
...untertitel.fehlend.map((s) => `Untertitel ${s.sprache || s.lang}${s.beschreibung.includes('erzwungen') ? ' (nur erzwungene)' : ''}`),
]
return { audioNr: audio.nummern, untertitelNr, untertitelStandard, fehlend }
}
/** Die Sprachlisten aus gewählten Spuren — der Rückfall, wenn der Scan der
* Roh-Datei scheitert (pur). */
export function sprachlistenAusSpuren(datei: Pick<RohDatei, 'audioSpuren' | 'untertitelSpuren'>): { audio: string[]; untertitel: string[] } {
return { audio: sprachenDerSpuren(datei.audioSpuren ?? []), untertitel: sprachenDerSpuren(datei.untertitelSpuren ?? []) }
}
/** 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))
@@ -575,6 +575,10 @@ export class Pipeline {
audioSprachen: a?.audioSprachen ?? [],
untertitelSprachen: a?.untertitelSprachen ?? [],
...(a?.untertitelVorwahl === null || typeof a?.untertitelVorwahl === 'string' ? { untertitelVorwahl: a.untertitelVorwahl } : {}),
// 5.7.0: die einzeln gewählten Spuren reisen mit in die Ablage.
...(a?.audioSpuren !== undefined ? { audioSpuren: a.audioSpuren } : {}),
...(a?.untertitelSpuren !== undefined ? { untertitelSpuren: a.untertitelSpuren } : {}),
...(a?.untertitelVorwahlSpur === null || typeof a?.untertitelVorwahlSpur === 'number' ? { untertitelVorwahlSpur: a.untertitelVorwahlSpur } : {}),
...(a?.film !== undefined ? { film: a.film } : {}),
}
})
+3 -1
View File
@@ -17,7 +17,7 @@ import { OmdbClient } from '../metadaten/omdb'
import { TmdbClient } from '../metadaten/tmdb'
import { LEERER_TREFFER, ausDetails, nachpruefen, zuordnen, type Zuordnung } from '../metadaten/zuordnung'
import { ABGELOEST, titelInfoLesen } from '../rip/makemkv'
import { sprachenFuerTitel, type TitelInfo } from '../rip/parser'
import { spurenFuerTitel, sprachenFuerTitel, type TitelInfo } from '../rip/parser'
import { strukturHinweis, titelVorauswahl } from '../rip/titelwahl'
export interface ErkennungAbhaengigkeiten {
@@ -348,6 +348,8 @@ export class ErkennungDienst {
...t,
// Die Sprachen JE TITEL reisen mit (5.4.0) — der Dialog bietet sie zum Anhaken an.
sprachen: sprachenFuerTitel(info.streams.get(t.nr)),
// 5.7.0: und jede Spur einzeln.
spuren: spurenFuerTitel(info.streams.get(t.nr)),
}))
const stand = senden({
id,
@@ -15,6 +15,13 @@
// im Fehlerfall die einzige Auskunft.
// - Abbruch bei JEDER Zeile prüfen, nicht nur bei Prozent-Änderung —
// sonst sieht „Abbrechen" bei einem 4K-Encode minutenlang tot aus.
// - `--aencoder copy` OHNE `--audio-copy-mask` kopiert NUR AAC: HandBrake-
// CLI 1.11.2 setzt die Kopiermaske dann auf „copy:aac" — obwohl das
// Preset alle Codecs erlaubt — und rechnet AC3/DTS/TrueHD in AAC um
// („Auto Passthru: allowed codecs are AAC … passthru not possible for
// track 1, using fallback"). Gemessen 12.09.2026 (Durchsicht, F31):
// mit ausdrücklicher Maske bleibt AC3 AC3. Seit 5.0 bis 5.6.1 wurde
// also jede Tonspur verlustbehaftet umgerechnet.
import { werkzeugSpawn } from '../werkzeuge/aufruf'
import { existsSync, renameSync, unlinkSync } from 'node:fs'
// win32-Pfadregeln ausdrücklich — die CI prüft diese Logik auf Linux.
@@ -33,6 +40,13 @@ export interface HandbrakeArgsOptionen {
/** Die ERSTE gewählte Untertitelspur beim Abspielen vorwählen (5.4.0):
* japanischer Ton, deutsche Untertitel sollen von selbst an sein. */
untertitelStandard?: boolean
/** 5.7.0: Spuren EINZELN — HandBrake-Spurnummern (`-a`/`-s`, 1-basiert,
* Scan-Reihenfolge). Gesetzt, gelten die Sprachlisten nicht. Eine leere
* Liste heißt: keine Spur dieser Art. `untertitelStandardNr` ist der
* 1-basierte Index in `untertitelNr` (0 = keine Vorwahl). */
audioNr?: readonly number[]
untertitelNr?: readonly number[]
untertitelStandardNr?: number
}
// Sprach-Schalter am mitgelieferten HandBrake 1.11 GEMESSEN (01.09.2026,
@@ -47,16 +61,47 @@ export interface HandbrakeArgsOptionen {
// zusätzlich forced=1 (so schreibt es HandBrakes Muxer; Jellyfin zeigt
// sie damit von selbst)
// -N deu allein fügt KEINE Untertitelspur hinzu (nur die Vorwahl-Logik)
// Spur-Schalter GEMESSEN (12.09.2026, Testdatei jpn 5.1/deu 2.0/deu 2.0
// „Kommentar" AC3 + deu/eng SRT, Preset „H.265 MKV 1080p30"):
// --audio 1,3 → jpn und deu-Kommentar, in dieser Reihenfolge
// --subtitle 2,1 --subtitle-default=1 → eng (default=1) vor deu
// --subtitle none → keine Untertitelspur (ohne --audio: die erste Tonspur)
// --audio-copy-mask aac,ac3,eac3,dtshd,dts,mp3,truehd,flac → AC3 bleibt AC3
/** Was HandBrake verlustfrei durchreichen darf — genau die Maske der
* mitgelieferten Presets („copy:aac" … „copy:flac"), ausdrücklich, weil
* die CLI sie sonst auf AAC verengt (siehe Kopf). */
export const KOPIERMASKE = 'aac,ac3,eac3,dtshd,dts,mp3,truehd,flac'
export function buildHandbrakeArgs(optionen: HandbrakeArgsOptionen): string[] {
const args = ['--input', optionen.eingabe, '--output', optionen.ausgabe, '--preset', optionen.preset]
const format = FORMATE[extname(optionen.ausgabe).toLowerCase()]
if (format !== undefined) args.push('--format', format)
// 5.7.0: Spuren einzeln — `--audio 1,3` und `--subtitle 2` (HandBrakeCLI
// --help: „-a, --audio <string> Select audio track(s), separated by
// commas", „-s, --subtitle <string> Select subtitle track(s)"; am
// mitgelieferten 1.11.2 gemessen, siehe test/messung.spuren.test.ts).
if (optionen.audioNr !== undefined || optionen.untertitelNr !== undefined) {
const audioNr = optionen.audioNr ?? []
// Ohne gewählte Tonspur bleibt HandBrakes Vorgabe: die erste Spur —
// ein stummer Film wäre nie gewollt (der Dialog sagt das dazu).
if (audioNr.length > 0) args.push('--audio', audioNr.join(','))
args.push('--aencoder', 'copy', '--audio-copy-mask', KOPIERMASKE, '--audio-fallback', 'av_aac')
const untertitelNr = optionen.untertitelNr ?? []
if (untertitelNr.length > 0) {
args.push('--subtitle', untertitelNr.join(','))
if ((optionen.untertitelStandardNr ?? 0) > 0) args.push(`--subtitle-default=${optionen.untertitelStandardNr}`)
} else {
args.push('--subtitle', 'none')
}
return args
}
const audio = (optionen.audioSprachen ?? []).filter((s) => s.length > 0)
if (audio.length > 0) args.push('--audio-lang-list', audio.join(','))
args.push('--first-audio')
// `copy` reicht die gewählten Tonspuren verlustfrei durch; `av_aac` ist
// der dokumentierte Rückfall, wenn ein Codec nicht kopierbar ist.
args.push('--aencoder', 'copy', '--audio-fallback', 'av_aac')
// `copy` reicht die gewählten Tonspuren verlustfrei durch — aber nur mit
// ausdrücklicher Kopiermaske (F31, siehe Kopf); `av_aac` ist der
// dokumentierte Rückfall, wenn ein Codec nicht kopierbar ist.
args.push('--aencoder', 'copy', '--audio-copy-mask', KOPIERMASKE, '--audio-fallback', 'av_aac')
const untertitel = (optionen.untertitelSprachen ?? []).filter((s) => s.length > 0)
if (untertitel.length > 0) args.push('--subtitle-lang-list', untertitel.join(','))
args.push('--all-subtitles')
+18 -3
View File
@@ -18,9 +18,21 @@
import { werkzeugSpawn } from '../werkzeuge/aufruf'
import { textVon } from '../rip/parser'
/** Eine Spur laut HandBrake-Scan. 5.7.0: dazu CodecName („dtshd", „dca",
* „truehd", „ac3" …), ChannelCount und bei Untertiteln Source/SourceName
* („PGS") — am 12.09.2026 am mitgelieferten HandBrake 1.11.2 gemessen
* (AudioList[].CodecName/ChannelCount/ChannelLayout/TrackNumber). */
export interface HbSpur {
nr: number
lang: string
beschreibung: string
codec: string
kanaele: number
}
export interface HbSpuren {
audio: Array<{ nr: number; lang: string; beschreibung: string }>
untertitel: Array<{ nr: number; lang: string; beschreibung: string }>
audio: HbSpur[]
untertitel: HbSpur[]
dauerS: number
}
@@ -60,14 +72,17 @@ export function spurenAusScan(ausgabe: string): HbSpuren | null {
const liste = (daten as { TitleList?: unknown }).TitleList
if (!Array.isArray(liste) || liste.length === 0) return null
const titel = liste[0] as Record<string, unknown>
const spuren = (feld: unknown): Array<{ nr: number; lang: string; beschreibung: string }> => {
const spuren = (feld: unknown): HbSpur[] => {
if (!Array.isArray(feld)) return []
return feld.map((s, i) => {
const eintrag = (typeof s === 'object' && s !== null ? s : {}) as Record<string, unknown>
const kanaele = eintrag.ChannelCount
return {
nr: i + 1,
lang: String(eintrag.LanguageCode ?? '').toLowerCase(),
beschreibung: String(eintrag.Description ?? eintrag.Language ?? ''),
codec: String(eintrag.CodecName ?? eintrag.SourceName ?? eintrag.Source ?? eintrag.Format ?? ''),
kanaele: typeof kanaele === 'number' && Number.isFinite(kanaele) ? kanaele : 0,
}
})
}
+33 -3
View File
@@ -134,8 +134,18 @@ export interface StreamInfo {
codec: string
kanaele: string
beschreibung: string
/** Attribut 2: „Surround 5.1", „Stereo" (5.7.0). */
layout: string
/** Attribut 22: Flags. Bit 0x800 heißt „abgeleitet" — an der Kizuna-Disc
* gemessen (03.09.2026): 2304 (0x900) am DTS-Kern der DTS-HD-Spur, 6144
* (0x1800) an der „nur erzwungene"-Fassung eines Untertitels, 1024 und 0
* an den vollen Spuren (5.7.0). */
flags: number
}
/** Bit 0x800 in Attribut 22: eine von MakeMKV abgeleitete Spur. */
export const STREAM_FLAG_ABGELEITET = 0x800
export interface TitelStreams {
audio: StreamInfo[]
untertitel: StreamInfo[]
@@ -160,17 +170,19 @@ export function parseStreamInfo(ausgabe: string): Map<number, TitelStreams> {
}
let eintrag = streams.get(sNr)
if (eintrag === undefined) {
eintrag = { nr: sNr, typ: '', typCode: 0, lang: '', sprache: '', codec: '', kanaele: '', beschreibung: '' }
eintrag = { nr: sNr, typ: '', typCode: 0, lang: '', sprache: '', codec: '', kanaele: '', beschreibung: '', layout: '', flags: 0 }
streams.set(sNr, eintrag)
}
if (attr === 1) {
eintrag.typ = wert
eintrag.typCode = code
}
else if (attr === 2) eintrag.layout = wert
else if (attr === 3) eintrag.lang = wert
else if (attr === 4) eintrag.sprache = wert
else if (attr === 6) eintrag.codec = wert
else if (attr === 14) eintrag.kanaele = wert
else if (attr === 22 && /^\d+$/.test(wert)) eintrag.flags = Number(wert)
else if (attr === 30) eintrag.beschreibung = wert
}
@@ -192,8 +204,8 @@ export function parseStreamInfo(ausgabe: string): Map<number, TitelStreams> {
// EINE Definition für Kern und Fenster (5.4.0): Der Auswahl-Dialog zeigt
// die Sprachen je Titel — deshalb lebt der Typ in gemeinsam/.
import type { SprachEintrag } from '../../gemeinsam/nachrichten'
export type { SprachEintrag }
import type { SprachEintrag, SpurAngabe } from '../../gemeinsam/nachrichten'
export type { SprachEintrag, SpurAngabe }
/** Welche Sprachen hat die DISC insgesamt? Über alle Titel vereinigt,
* Reihenfolge stabil (wie auf der Disc). */
@@ -219,6 +231,24 @@ export function sprachenZusammenfassen(streamsJeTitel: Map<number, TitelStreams>
return { audio: sammeln('audio'), untertitel: sammeln('untertitel') }
}
/** Jede Spur EINES Titels als SpurAngabe (5.7.0, für den Auswahl-Dialog):
* Ton und Untertitel getrennt, in Disc-Reihenfolge, abgeleitete Spuren
* markiert. Ohne Streams leer. */
export function spurenFuerTitel(streams: TitelStreams | undefined): { audio: SpurAngabe[]; untertitel: SpurAngabe[] } {
const angabe = (s: StreamInfo): SpurAngabe => ({
nr: s.nr,
lang: s.lang,
sprache: s.sprache.length > 0 ? s.sprache : s.lang,
codec: s.codec,
kanaele: s.kanaele,
layout: s.layout,
beschreibung: s.beschreibung,
abgeleitet: (s.flags & STREAM_FLAG_ABGELEITET) !== 0,
})
if (streams === undefined) return { audio: [], untertitel: [] }
return { audio: streams.audio.map(angabe), untertitel: streams.untertitel.map(angabe) }
}
/** Die Sprachen EINES Titels (5.4.0, für den Auswahl-Dialog) — dieselbe
* Zusammenfassung wie oben, nur auf einen Titel beschränkt. Ohne Streams
* (Titel unbekannt) leer, nicht erfunden. */
@@ -22,12 +22,20 @@
// eigene hat, behält sie — wie bei app_UpdateEnable in schluessel.ts).
// Abgelegt wird wie der Schlüssel in HKCU\Software\MakeMKV (Windows liest
// dort, die settings.conf ist der Linux-Weg).
//
// 5.7.0: Seit der Dialog jede Spur einzeln anbietet, dürfen auch die
// Stereo-Doppel bleiben (havemulti — „Deutsch · DD · 2.0 (Kommentar)"
// neben „Deutsch · DTS-HD MA · 5.1"): Nur die abgeleiteten DTS-Kerne
// (havecore) fallen weg, die zeigt der Dialog gar nicht erst. Stand
// Rippys ALTE Regel in der Registry, wird sie ersetzt; eine fremde bleibt.
import { spawnSync } from 'node:child_process'
export const REGISTRY_PFAD = 'HKCU\\Software\\MakeMKV'
export const REGEL_NAME = 'app_DefaultSelectionString'
/** Alles behalten, Doppel und Kerne weg — sprachunabhängig. */
export const RIPPY_REGEL = '-sel:all,+sel:all,-sel:(havemulti|havecore),=100:all'
/** Alles behalten, nur die Kerne weg — sprachunabhängig (5.7.0). */
export const RIPPY_REGEL = '-sel:all,+sel:all,-sel:havecore,=100:all'
/** Rippys frühere Regeln (5.4.05.6.1) — werden beim Start ersetzt. */
export const RIPPY_REGELN_ALT: readonly string[] = ['-sel:all,+sel:all,-sel:(havemulti|havecore),=100:all']
export interface SpurauswahlStand {
/** 'gesetzt' (von Rippy, jetzt) | 'eigen' (der Nutzer hat eine eigene) |
@@ -37,6 +45,16 @@ export interface SpurauswahlStand {
text: string
}
/** Was mit einer vorgefundenen Regel geschieht (pur, testbar):
* 'rippy' = steht schon · 'ersetzen' = Rippys alte Regel · 'eigen' =
* fremd, bleibt · 'setzen' = keine da. */
export function regelUrteil(bisher: string | null): 'rippy' | 'ersetzen' | 'eigen' | 'setzen' {
if (bisher === null || bisher.length === 0) return 'setzen'
if (bisher === RIPPY_REGEL) return 'rippy'
if (RIPPY_REGELN_ALT.includes(bisher)) return 'ersetzen'
return 'eigen'
}
function reg(args: string[]): { ok: boolean; ausgabe: string } {
const ergebnis = spawnSync('reg', args, { windowsHide: true, timeout: 15_000, encoding: 'utf8' })
return { ok: ergebnis.status === 0, ausgabe: (ergebnis.stdout ?? '') + (ergebnis.stderr ?? '') }
@@ -56,14 +74,15 @@ export function spurauswahlSicherstellen(): SpurauswahlStand {
}
const vorhanden = reg(['query', REGISTRY_PFAD, '/v', REGEL_NAME])
const bisher = vorhanden.ok ? regelAusQuery(vorhanden.ausgabe) : null
if (bisher !== null && bisher.length > 0) {
if (bisher === RIPPY_REGEL) {
return { stand: 'rippy', regel: bisher, text: 'MakeMKV behält alle Sprachen (Rippys Auswahlregel steht).' }
}
const urteil = regelUrteil(bisher)
if (urteil === 'rippy') {
return { stand: 'rippy', regel: bisher ?? '', text: 'MakeMKV behält alle Spuren (Rippys Auswahlregel steht).' }
}
if (urteil === 'eigen') {
return {
stand: 'eigen',
regel: bisher,
text: `MakeMKV hat eine eigene Auswahlregel („${bisher}") — Rippy lässt sie stehen. Fehlen Sprachen in der Roh-Datei, liegt es daran.`,
regel: bisher ?? '',
text: `MakeMKV hat eine eigene Auswahlregel („${bisher}") — Rippy lässt sie stehen. Fehlen Spuren in der Roh-Datei, liegt es daran.`,
}
}
const gesetzt = reg(['add', REGISTRY_PFAD, '/v', REGEL_NAME, '/t', 'REG_SZ', '/d', RIPPY_REGEL, '/f'])
@@ -73,6 +92,9 @@ export function spurauswahlSicherstellen(): SpurauswahlStand {
return {
stand: 'gesetzt',
regel: RIPPY_REGEL,
text: 'MakeMKV-Auswahlregel gesetzt: alle Sprachen kommen in die Roh-Datei (Stereo-Doppel und DTS-Kerne nicht).',
text:
urteil === 'ersetzen'
? 'MakeMKV-Auswahlregel erneuert: jede Spur kommt in die Roh-Datei, auch Stereo-Fassungen und Kommentare (nur DTS-Kerne nicht).'
: 'MakeMKV-Auswahlregel gesetzt: jede Spur kommt in die Roh-Datei (nur DTS-Kerne nicht).',
}
}