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
+36 -14
View File
@@ -124,16 +124,26 @@ await schritt('Titel-Lauf liefert drei Titel', async () => {
await dialog().getByText('Titel 2', { exact: true }).waitFor()
})
await schritt('Sprachen je Titel: jpn/deu Ton, deu/eng Untertitel als Chips', async () => {
await dialog().getByRole('button', { name: /jpn/ }).first().waitFor()
await dialog().getByRole('button', { name: /✓ deu/ }).first().waitFor()
await schritt('5.7.0: Spuren je Titel einzeln — Codec und Kanäle, der DTS-Kern bleibt unsichtbar', async () => {
await dialog().getByLabel('Japanese · DTS-HD MA · Surround 5.1').first().waitFor()
await dialog().getByLabel('German · DD · Surround 5.1').first().waitFor()
await dialog().getByLabel('German · DD · Stereo (Kommentar)').first().waitFor()
await dialog().getByLabel('German · PGS (nur erzwungene)').first().waitFor()
if ((await dialog().getByLabel('Japanese · DTS · Surround 5.1').count()) !== 0) throw new Error('der abgeleitete DTS-Kern wird angeboten')
// Vorbelegt: je Wunschsprache (deu,eng) die erste Tonspur → nur German DD 5.1;
// Untertitel: alle deutschen (auch „nur erzwungene"), englisch nicht.
if (!(await dialog().getByLabel('German · DD · Surround 5.1').first().isChecked())) throw new Error('German 5.1 nicht vorbelegt')
if (await dialog().getByLabel('German · DD · Stereo (Kommentar)').first().isChecked()) throw new Error('Kommentar-Spur vorbelegt')
if (await dialog().getByLabel('Japanese · DTS-HD MA · Surround 5.1').first().isChecked()) throw new Error('Japanisch vorbelegt, obwohl Deutsch da ist')
if (!(await dialog().getByLabel('German · PGS (nur erzwungene)').first().isChecked())) throw new Error('erzwungene Untertitel nicht vorbelegt')
if (await dialog().getByLabel('English · PGS').first().isChecked()) throw new Error('englische Untertitel vorbelegt')
})
await schritt('Titel 0 bekommt japanischen Ton (deu abwählen, jpn anwählen)', async () => {
const chips = dialog().locator('button', { hasText: /^✓ deu$|^deu$/ })
await chips.first().click()
await dialog().getByRole('button', { name: /^jpn$/ }).first().click()
await dialog().getByRole('button', { name: /^✓ jpn$/ }).first().waitFor()
await schritt('Titel 0 bekommt japanischen Ton (German 5.1 abhaken, Japanese anhaken)', async () => {
await dialog().getByLabel('German · DD · Surround 5.1').first().uncheck()
await dialog().getByText('keine Tonspur gewählt — HandBrake nimmt die erste Spur').waitFor()
await dialog().getByLabel('Japanese · DTS-HD MA · Surround 5.1').first().check()
if ((await dialog().getByText('keine Tonspur gewählt').count()) !== 0) throw new Error('Warnung bleibt trotz japanischer Spur')
})
await schritt('„Hauptinhalt + Extras" wählt beide langen Titel, der Schnipsel bleibt draußen', async () => {
@@ -153,7 +163,9 @@ await schritt('5.6.0: Untertitel-Vorwahl erscheint NUR bei Extras, „anderer Fi
await rollen.nth(1).selectOption('extra')
await dialog().getByText('BEIM ABSPIELEN AN').waitFor()
const vorwahl = dialog().locator('select').nth(2)
await vorwahl.selectOption('deu')
// 5.7.0: Rippys Regel nennt die Spur (japanischer Ton → deutsche
// Untertitel), die Optionen sind Spuren — gewählt wird per Nummer (5).
await vorwahl.selectOption('5')
const knoepfe = await dialog().getByRole('button', { name: 'anderer Film …' }).count()
if (knoepfe < 2) throw new Error(`nur ${knoepfe} „anderer Film"-Knöpfe`)
await dialog().getByRole('button', { name: 'anderer Film …' }).first().click()
@@ -279,22 +291,32 @@ await schritt('Werkzeuge haben Durchsuchen, Allgemein und Update sind eigene Kar
})
await bild('einstellungen')
await schritt('Roh-Dateien löschen → „gelöscht"', async () => {
await seite.getByRole('button', { name: 'Roh-Dateien löschen' }).first().click()
await schritt('Roh-Dateien löschen → Rückfrage → „Ja, löschen" → „gelöscht"', async () => {
await seite.getByRole('button', { name: 'Roh-Dateien löschen' }).first().click()
await seite.getByRole('button', { name: 'Ja, löschen' }).first().click()
await seite.getByText(/Roh: gelöscht/).first().waitFor()
})
await schritt('5.6.0: Hell-Modus über Einstellungen → Allgemein, data-design wechselt', async () => {
await seite.locator('#allgemein').locator('select').first().selectOption('hell')
await seite.locator('#allgemein').getByLabel('Erscheinungsbild').selectOption('hell')
await seite.locator('html[data-design="hell"]').waitFor()
})
await bild('hell-modus')
await schritt('zurück in den Kinosaal (dunkel)', async () => {
await seite.locator('#allgemein').locator('select').first().selectOption('dunkel')
await seite.locator('#allgemein').getByLabel('Erscheinungsbild').selectOption('dunkel')
await seite.locator('html[data-design="dunkel"]').waitFor()
})
await schritt('5.7.0: „Titel lesen — höchstens" steht auf 15 Minuten, 30 lässt sich wählen', async () => {
const regler = seite.locator('#allgemein').getByLabel('Titel lesen — höchstens')
if ((await regler.inputValue()) !== '15') throw new Error(`Vorgabe ist ${await regler.inputValue()}, nicht 15`)
await regler.selectOption('30')
// Der Wert geht zum Kern und kommt als Einstellung zurück — erst dann
// zeigt das (kontrollierte) Feld 30.
await regler.locator('option[value="30"]:checked').waitFor({ state: 'attached' })
})
await schritt('Ereignisse nennen die Kompression', async () => {
await seite.getByRole('button', { name: 'Übersicht' }).click()
await seite.getByText(/Kompression „/).first().waitFor()
@@ -357,6 +379,6 @@ await schritt('Fertig: der Knopf Los gehts', async () => {
})
await bild('einrichtung-fertig')
console.log('KLICK: OK — Speicher, Rippen, Dialog, Sprachen, Folgen, Platz, Rip, Warteschlange, Kompression, Arbeitsordner, Ablage, Verlauf, Bilanz, Roh-Dateien, Einstellungen, Vorwahl, anderer Film, Vorgangs-Leiste, Restzeit, Hell-Modus, Erst-Einrichtung: alles geklickt.')
console.log('KLICK: OK — Speicher, Rippen, Dialog, Spuren einzeln, Folgen, Platz, Rip, Warteschlange, Kompression, Arbeitsordner, Ablage, Verlauf, Bilanz, Roh-Dateien, Einstellungen, Vorwahl, anderer Film, Vorgangs-Leiste, Restzeit, Hell-Modus, Erst-Einrichtung: alles geklickt.')
await app.close().catch(() => {})
process.exit(0)
+9 -4
View File
@@ -47,16 +47,21 @@ if (args.includes('--scan')) {
out('"MainFeature": 0,')
out('"TitleList": [')
out('{')
// 5.7.0: CodecName und ChannelCount wie das echte 1.11.2 (gemessen
// 12.09.2026) — dieselben Spuren, die fake-makemkvcon meldet, ohne den
// DTS-Kern (den nimmt MakeMKVs Regel nicht mit).
out('"AudioList": [')
out('{ "Description": "Japanese (AAC LC, 2.0 ch)", "Language": "Japanese", "LanguageCode": "jpn", "TrackNumber": 1 },')
out('{ "Description": "Deutsch (AAC LC, 2.0 ch)", "Language": "Deutsch", "LanguageCode": "deu", "TrackNumber": 2 }')
out('{ "ChannelCount": 6, "ChannelLayout": 1551, "CodecName": "dtshd", "Description": "Japanese (DTS-HD MA, 5.1 ch)", "Language": "Japanese", "LanguageCode": "jpn", "TrackNumber": 1 },')
out('{ "ChannelCount": 6, "ChannelLayout": 1551, "CodecName": "ac3", "Description": "Deutsch (AC3, 5.1 ch)", "Language": "Deutsch", "LanguageCode": "deu", "TrackNumber": 2 },')
out('{ "ChannelCount": 2, "ChannelLayout": 3, "CodecName": "ac3", "Description": "Deutsch (AC3, 2.0 ch)", "Language": "Deutsch", "LanguageCode": "deu", "TrackNumber": 3 }')
out('],')
out('"Duration": { "Hours": 0, "Minutes": 52, "Seconds": 47, "Ticks": 542070 },')
out('"Index": 1,')
out(`"Name": ${JSON.stringify(eingabe)},`)
out('"SubtitleList": [')
out('{ "Format": "bitmap", "Language": "Deutsch", "LanguageCode": "deu", "SourceName": "PGS", "TrackNumber": 1 },')
out('{ "Format": "bitmap", "Language": "English", "LanguageCode": "eng", "SourceName": "PGS", "TrackNumber": 2 }')
out('{ "Format": "bitmap", "Language": "Deutsch", "LanguageCode": "deu", "Source": 5, "SourceName": "PGS", "TrackNumber": 1 },')
out('{ "Format": "bitmap", "Language": "Deutsch", "LanguageCode": "deu", "Source": 5, "SourceName": "PGS", "TrackNumber": 2 },')
out('{ "Format": "bitmap", "Language": "English", "LanguageCode": "eng", "Source": 5, "SourceName": "PGS", "TrackNumber": 3 }')
out(']')
out('}')
out(']')
+21 -12
View File
@@ -31,18 +31,27 @@ if (befehl === 'info') {
out(`TINFO:${t.nr},11,0,"${t.bytes}"`)
out(`SINFO:${t.nr},0,1,6201,"Video"`)
if (t.nr < 2) {
out(`SINFO:${t.nr},1,1,6202,"Audio"`)
out(`SINFO:${t.nr},1,3,0,"jpn"`)
out(`SINFO:${t.nr},1,4,0,"Japanese"`)
out(`SINFO:${t.nr},2,1,6202,"Audio"`)
out(`SINFO:${t.nr},2,3,0,"deu"`)
out(`SINFO:${t.nr},2,4,0,"German"`)
out(`SINFO:${t.nr},3,1,6203,"Subtitles"`)
out(`SINFO:${t.nr},3,3,0,"deu"`)
out(`SINFO:${t.nr},3,4,0,"German"`)
out(`SINFO:${t.nr},4,1,6203,"Subtitles"`)
out(`SINFO:${t.nr},4,3,0,"eng"`)
out(`SINFO:${t.nr},4,4,0,"English"`)
// 5.7.0: jede Spur mit Codec (6), Kanälen (14), Anordnung (2), Flags
// (22) und Beschreibung (30), wie an der Kizuna-Disc gemessen — samt
// dem abgeleiteten DTS-Kern (Flag 0x800), den der Dialog NICHT zeigt,
// und der „nur erzwungene"-Fassung eines Untertitels, die er zeigt.
const spur = (nr, code, typ, lang, name, codec, kanaele, layout, flags, text) => {
out(`SINFO:${t.nr},${nr},1,${code},"${typ}"`)
if (layout) out(`SINFO:${t.nr},${nr},2,0,"${layout}"`)
out(`SINFO:${t.nr},${nr},3,0,"${lang}"`)
out(`SINFO:${t.nr},${nr},4,0,"${name}"`)
out(`SINFO:${t.nr},${nr},6,0,"${codec}"`)
if (kanaele) out(`SINFO:${t.nr},${nr},14,0,"${kanaele}"`)
out(`SINFO:${t.nr},${nr},22,0,"${flags}"`)
out(`SINFO:${t.nr},${nr},30,0,"${text}"`)
}
spur(1, 6202, 'Audio', 'jpn', 'Japanese', 'DTS-HD MA', '6', 'Surround 5.1', 1024, 'DTS-HD MA Surround 5.1 Japanese')
spur(2, 6202, 'Audio', 'jpn', 'Japanese', 'DTS', '6', 'Surround 5.1', 2304, 'DTS Surround 5.1 Japanese')
spur(3, 6202, 'Audio', 'deu', 'German', 'DD', '6', 'Surround 5.1', 0, 'DD Surround 5.1 German')
spur(4, 6202, 'Audio', 'deu', 'German', 'DD', '2', 'Stereo', 0, 'DD Stereo German (Kommentar)')
spur(5, 6203, 'Untertitel', 'deu', 'German', 'PGS', '', '', 0, 'PGS German')
spur(6, 6203, 'Untertitel', 'deu', 'German', 'PGS', '', '', 6144, 'PGS German (nur erzwungene)')
spur(7, 6203, 'Untertitel', 'eng', 'English', 'PGS', '', '', 0, 'PGS English')
}
}
process.exit(0)