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:
co-authored by
Claude Opus 5
parent
fc2d16ca38
commit
e7e33e9021
@@ -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.0–5.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),
|
||||
|
||||
@@ -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 } : {}),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.0–5.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).',
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user