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
@@ -9,6 +9,10 @@
|
||||
// vorgewählt ist (weiche Spur, nie eingebrannt, immer optional) — und je
|
||||
// Titel „anderer Film …": Double Feature, Trilogie-Disc, Bonus-Film bekommen
|
||||
// eigenen Roh-Ordner, Vorgang und Zielordner.
|
||||
// 5.7.0 (Commander, 12.09.2026): JEDE Spur einzeln anhaken — „Deutsch ·
|
||||
// DTS-HD MA · 5.1" und „Deutsch · DD · 2.0 (Kommentar)" getrennt, Untertitel
|
||||
// genauso. Die Sprach-Chips bleiben nur als Rückfall, wenn ein Titel keine
|
||||
// einzelnen Spuren meldet.
|
||||
import { useState } from 'react'
|
||||
import { sichererName, zielVorschau } from '../../gemeinsam/ablagepfad'
|
||||
import { platzUrteil } from '../../gemeinsam/platz'
|
||||
@@ -19,9 +23,11 @@ import type {
|
||||
MetaVorschlag,
|
||||
SpeicherStand,
|
||||
SprachEintrag,
|
||||
SpurAngabe,
|
||||
TitelRolle,
|
||||
TitelStand,
|
||||
} from '../../gemeinsam/nachrichten'
|
||||
import { angeboten, sprachenDerSpuren, spurenVorbelegt, spurLabel } from '../../gemeinsam/spuren'
|
||||
import { DialogRahmen, IconDisc, KNOPF_GOLD, datumKurz, dauerText, gb, gbKurz, posterUrlKlein, sprachenVorbelegt } from '../bausteine'
|
||||
import { metadatenSuchen, ripStarten, titelAbbrechen } from '../kernverbindung'
|
||||
|
||||
@@ -64,6 +70,53 @@ function SprachChips({
|
||||
)
|
||||
}
|
||||
|
||||
/** Spur-Zeilen eines Titels (5.7.0): jede Ton- bzw. Untertitelspur einzeln
|
||||
* anhaken, mit Codec und Kanälen, wie MakeMKV sie gemessen hat. Außerhalb
|
||||
* des Auswahl-Labels (ein Haken IM Label würde den Titel mit umschalten). */
|
||||
function SpurZeilen({
|
||||
name,
|
||||
art,
|
||||
vorhanden,
|
||||
gewaehlt,
|
||||
aufWechsel,
|
||||
}: {
|
||||
name: string
|
||||
art: 'audio' | 'untertitel'
|
||||
vorhanden: readonly SpurAngabe[]
|
||||
gewaehlt: readonly number[]
|
||||
aufWechsel: (nr: number) => void
|
||||
}) {
|
||||
const zeilen = vorhanden.filter((s) => angeboten(s, art))
|
||||
if (zeilen.length === 0) return null
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-0.5" data-spuren={art}>
|
||||
<span className="font-mono text-[10px] tracking-wider text-nebel">{name}</span>
|
||||
{zeilen.map((s) => {
|
||||
const an = gewaehlt.includes(s.nr)
|
||||
return (
|
||||
<label
|
||||
key={s.nr}
|
||||
className={`inline-flex cursor-pointer items-center gap-1.5 font-mono text-[10.5px] ${an ? 'text-gold-hell' : 'text-nebel hover:text-dunst'}`}
|
||||
title={s.beschreibung.length > 0 ? s.beschreibung : spurLabel(s)}
|
||||
>
|
||||
<input type="checkbox" checked={an} onChange={() => aufWechsel(s.nr)} className="accent-[oklch(0.83_0.11_75)]" />
|
||||
{spurLabel(s)}
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Was Rippys Regel für diesen Titel vorwählen würde (dieselbe Regel wie
|
||||
* kern/ablauf/auftrag.ts spurwahl, 5.7.0): Spielt die erste gewählte
|
||||
* Tonspur nicht in der Wunschsprache, die erste gewählte Untertitelspur
|
||||
* der Wunschsprache — sonst nichts. */
|
||||
function regelVorwahlSpur(audio: readonly SpurAngabe[], untertitel: readonly SpurAngabe[], wunsch: string): SpurAngabe | undefined {
|
||||
if (wunsch.length === 0 || audio.length === 0 || audio[0].lang.toLowerCase() === wunsch) return undefined
|
||||
return untertitel.find((s) => s.lang.toLowerCase() === wunsch)
|
||||
}
|
||||
|
||||
/** Was Rippys Regel für diesen Titel vorwählen würde (dieselbe Regel wie
|
||||
* kern/ablauf/auftrag.ts sprachwahl) — '' wenn nichts. */
|
||||
function regelVorwahl(audio: readonly string[], untertitel: readonly string[], wunsch: string): string {
|
||||
@@ -105,6 +158,12 @@ export function TitelWahlDialog({
|
||||
// 5.6.0: Untertitel-Vorwahl je Extra — fehlt: Rippys Regel; null: keine;
|
||||
// sonst die Sprache. Immer optional, nie eingebrannt.
|
||||
const [vorwahl, setVorwahl] = useState<Record<number, string | null>>({})
|
||||
// 5.7.0: Spuren je Titel EINZELN (MakeMKV-Stream-Nummern). Was hier nicht
|
||||
// drinsteht, ist vorbelegt: je Wunschsprache die erste Tonspur, alle
|
||||
// Untertitel der Wunschsprachen (gemeinsam/spuren.ts). Dazu die
|
||||
// Untertitel-Vorwahl je Extra als SPUR — fehlt: Regel; null: keine.
|
||||
const [spuren, setSpuren] = useState<Record<number, { audio: number[]; untertitel: number[] }>>({})
|
||||
const [vorwahlSpur, setVorwahlSpur] = useState<Record<number, number | null>>({})
|
||||
// 5.6.0: Titel, die zu einem anderen Film gehören.
|
||||
const [filme, setFilme] = useState<Record<number, MetaVorschlag>>({})
|
||||
const [filmSuche, setFilmSuche] = useState<{ nr: number; text: string } | null>(null)
|
||||
@@ -130,6 +189,31 @@ export function TitelWahlDialog({
|
||||
const liste = jetzt[art].includes(lang) ? jetzt[art].filter((l) => l !== lang) : [...jetzt[art], lang]
|
||||
setSprachen({ ...sprachen, [t.nr]: { ...jetzt, [art]: liste } })
|
||||
}
|
||||
const wunschListe = (schluessel: string): string[] =>
|
||||
(einstellungen[schluessel] ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter((s) => s.length > 0)
|
||||
/** Meldet dieser Titel einzelne Spuren? Sonst gelten die Sprach-Chips. */
|
||||
const spurenDa = (t: DiscTitelVorschlag): boolean => t.spuren !== undefined && (t.spuren.audio.length > 0 || t.spuren.untertitel.length > 0)
|
||||
const spurenVon = (t: DiscTitelVorschlag): { audio: number[]; untertitel: number[] } =>
|
||||
spuren[t.nr] ?? {
|
||||
audio: spurenVorbelegt(t.spuren?.audio ?? [], wunschListe('audioSprachen'), 'audio'),
|
||||
untertitel: spurenVorbelegt(t.spuren?.untertitel ?? [], wunschListe('untertitelSprachen'), 'untertitel'),
|
||||
}
|
||||
const spurWechsel = (t: DiscTitelVorschlag, art: 'audio' | 'untertitel', nr: number): void => {
|
||||
const jetzt = spurenVon(t)
|
||||
const liste = jetzt[art].includes(nr) ? jetzt[art].filter((n) => n !== nr) : [...jetzt[art], nr].sort((a, b) => a - b)
|
||||
setSpuren({ ...spuren, [t.nr]: { ...jetzt, [art]: liste } })
|
||||
}
|
||||
/** Die gewählten Spuren als Angaben, in Disc-Reihenfolge. */
|
||||
const gewaehlteSpuren = (t: DiscTitelVorschlag): { audio: SpurAngabe[]; untertitel: SpurAngabe[] } => {
|
||||
const nrn = spurenVon(t)
|
||||
return {
|
||||
audio: (t.spuren?.audio ?? []).filter((s) => nrn.audio.includes(s.nr)),
|
||||
untertitel: (t.spuren?.untertitel ?? []).filter((s) => nrn.untertitel.includes(s.nr)),
|
||||
}
|
||||
}
|
||||
const umschalten = (nr: number): void => {
|
||||
const neu = new Set(aktiv)
|
||||
if (neu.has(nr)) neu.delete(nr)
|
||||
@@ -231,6 +315,10 @@ export function TitelWahlDialog({
|
||||
const film = filme[t.nr]
|
||||
const regelWahl = regelVorwahl(wahl.audio.length > 0 ? wahl.audio : t.sprachen.audio.map((s) => s.lang), wahl.untertitel, wunschSprache)
|
||||
const vorwahlWert = t.nr in vorwahl ? (vorwahl[t.nr] === null ? 'keine' : vorwahl[t.nr]!) : 'regel'
|
||||
const spurNrn = spurenVon(t)
|
||||
const spurWahl = gewaehlteSpuren(t)
|
||||
const regelSpur = regelVorwahlSpur(spurWahl.audio, spurWahl.untertitel, wunschSprache)
|
||||
const vorwahlSpurWert = t.nr in vorwahlSpur ? (vorwahlSpur[t.nr] === null ? 'keine' : String(vorwahlSpur[t.nr])) : 'regel'
|
||||
return (
|
||||
<div key={t.nr} className={`rounded-lg border ${an ? 'border-gold/40 bg-gold/8' : 'border-tinte/8 bg-samt/40'}`}>
|
||||
<label className="flex cursor-pointer items-center gap-3 p-2">
|
||||
@@ -255,10 +343,56 @@ export function TitelWahlDialog({
|
||||
<span className="min-w-0 flex-1 truncate text-[11px] text-nebel">{t.grund}</span>
|
||||
)}
|
||||
</label>
|
||||
{/* Spuren je Titel (5.7.0): jede Ton- und Untertitelspur, die
|
||||
der Info-Lauf an DIESEM Titel gemessen hat — einzeln
|
||||
anhaken, was in die Datei soll. */}
|
||||
{an && spurenDa(t) && (
|
||||
<div className="flex flex-wrap items-start gap-x-6 gap-y-1.5 border-t border-tinte/8 px-2 py-1.5 pl-9">
|
||||
<SpurZeilen name="TON" art="audio" vorhanden={t.spuren.audio} gewaehlt={spurNrn.audio} aufWechsel={(nr) => spurWechsel(t, 'audio', nr)} />
|
||||
<SpurZeilen
|
||||
name="UNTERTITEL"
|
||||
art="untertitel"
|
||||
vorhanden={t.spuren.untertitel}
|
||||
gewaehlt={spurNrn.untertitel}
|
||||
aufWechsel={(nr) => spurWechsel(t, 'untertitel', nr)}
|
||||
/>
|
||||
{spurNrn.audio.length === 0 && t.spuren.audio.some((s) => angeboten(s, 'audio')) && (
|
||||
<span className="font-mono text-[10.5px] text-warn">keine Tonspur gewählt — HandBrake nimmt die erste Spur</span>
|
||||
)}
|
||||
{/* 5.6.0/5.7.0: Vorwahl NUR bei Extras, IMMER optional — eine
|
||||
weiche Spur, die der Spieler von selbst einblendet;
|
||||
eingebrannt wird nie. Hier als SPUR. */}
|
||||
{rolle === 'extra' && spurWahl.untertitel.length > 0 && (
|
||||
<label className="inline-flex items-center gap-1.5" title="Weiche Untertitel-Spur, die beim Abspielen schon an ist — nichts wird eingebrannt. Leer lassen ist immer erlaubt.">
|
||||
<span className="font-mono text-[10px] tracking-wider text-nebel">BEIM ABSPIELEN AN</span>
|
||||
<select
|
||||
value={vorwahlSpurWert}
|
||||
onChange={(e) => {
|
||||
const w = e.target.value
|
||||
const neu = { ...vorwahlSpur }
|
||||
if (w === 'regel') delete neu[t.nr]
|
||||
else neu[t.nr] = w === 'keine' ? null : Number(w)
|
||||
setVorwahlSpur(neu)
|
||||
}}
|
||||
className="rounded-md border border-tinte/15 bg-samt/70 px-1.5 py-0.5 font-mono text-[10.5px] text-dunst"
|
||||
>
|
||||
<option value="regel">{regelSpur !== undefined ? `Rippys Regel (${spurLabel(regelSpur)})` : 'Rippys Regel (keine)'}</option>
|
||||
<option value="keine">keine</option>
|
||||
{spurWahl.untertitel.map((u) => (
|
||||
<option key={u.nr} value={String(u.nr)}>
|
||||
{spurLabel(u)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Sprachen je Titel (5.4.0): Was der Info-Lauf an DIESEM
|
||||
Titel gemessen hat — anhaken, was in die Datei soll.
|
||||
Der Commander-Fall: japanischer Ton, deutsche Untertitel. */}
|
||||
{an && (t.sprachen.audio.length > 0 || t.sprachen.untertitel.length > 0) && (
|
||||
Der Commander-Fall: japanischer Ton, deutsche Untertitel.
|
||||
Seit 5.7.0 nur noch der Rückfall ohne einzelne Spuren. */}
|
||||
{an && !spurenDa(t) && (t.sprachen.audio.length > 0 || t.sprachen.untertitel.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-tinte/8 px-2 py-1.5 pl-9">
|
||||
<SprachChips name="TON" vorhanden={t.sprachen.audio} gewaehlt={wahl.audio} aufWechsel={(lang) => sprachWechsel(t, 'audio', lang)} />
|
||||
<SprachChips name="UNTERTITEL" vorhanden={t.sprachen.untertitel} gewaehlt={wahl.untertitel} aufWechsel={(lang) => sprachWechsel(t, 'untertitel', lang)} />
|
||||
@@ -484,6 +618,24 @@ export function TitelWahlDialog({
|
||||
gewaehlteTitel.map((t) => {
|
||||
const wahl = sprachenVon(t)
|
||||
const rolle = rolleVon(t)
|
||||
// 5.7.0: Spuren einzeln — die Sprachlisten reisen abgeleitet
|
||||
// mit (Rückfall des Kerns, wenn der Scan der Roh-Datei
|
||||
// scheitert; und die alten Hinweise bleiben stimmig).
|
||||
if (spurenDa(t)) {
|
||||
const spurWahl = gewaehlteSpuren(t)
|
||||
return {
|
||||
nr: t.nr,
|
||||
rolle,
|
||||
dauerS: t.dauerS,
|
||||
groesseBytes: t.groesseBytes,
|
||||
audioSprachen: sprachenDerSpuren(spurWahl.audio),
|
||||
untertitelSprachen: sprachenDerSpuren(spurWahl.untertitel),
|
||||
audioSpuren: spurWahl.audio,
|
||||
untertitelSpuren: spurWahl.untertitel,
|
||||
...(rolle === 'extra' && t.nr in vorwahlSpur ? { untertitelVorwahlSpur: vorwahlSpur[t.nr] } : {}),
|
||||
...(filme[t.nr] !== undefined ? { film: filme[t.nr] } : {}),
|
||||
}
|
||||
}
|
||||
return {
|
||||
nr: t.nr,
|
||||
rolle,
|
||||
|
||||
Reference in New Issue
Block a user