0.12.0-16 misc fixes

This commit is contained in:
2026-03-30 14:37:23 +00:00
parent 76b020d5c0
commit 5284dd2a43
30 changed files with 6955 additions and 896 deletions
+492 -54
View File
@@ -65,21 +65,246 @@ function isBurnedSubtitleTrack(track) {
);
}
function isForcedOnlySubtitleTrack(track) {
const summary = `${track?.title || ''} ${track?.description || ''} ${track?.languageLabel || ''}`.toLowerCase();
return Boolean(
track?.forcedTrack
|| /forced only/.test(summary)
|| /nur erzwungen/.test(summary)
|| /\berzwungen\b/.test(summary)
);
function isDuplicateSubtitleTrack(track) {
return Boolean(track?.duplicate);
}
function hasForcedSubtitleAvailable(track) {
const sourceTrackIds = normalizeTrackIdList(
Array.isArray(track?.forcedSourceTrackIds) ? track.forcedSourceTrackIds : []
function normalizeTrackIdSequence(values, options = {}) {
const list = Array.isArray(values) ? values : [];
const dedupe = options?.dedupe !== false;
const seen = new Set();
const output = [];
for (const value of list) {
const normalized = normalizeTrackId(value);
if (normalized === null) {
continue;
}
const key = String(normalized);
if (dedupe && seen.has(key)) {
continue;
}
if (dedupe) {
seen.add(key);
}
output.push(normalized);
}
return output;
}
function normalizeSubtitleLanguage(value) {
const raw = String(value || '').trim().toLowerCase();
if (!raw) {
return 'und';
}
if (raw.length >= 3) {
return raw.slice(0, 3);
}
return raw;
}
function normalizeSubtitleLanguageOrder(rawOrder = []) {
const list = Array.isArray(rawOrder) ? rawOrder : [];
const seen = new Set();
const output = [];
for (const item of list) {
const language = normalizeSubtitleLanguage(item);
if (!language || seen.has(language)) {
continue;
}
seen.add(language);
output.push(language);
}
return output;
}
function normalizeSubtitleVariantSelectionMap(rawSelection) {
const map = {};
if (!rawSelection || typeof rawSelection !== 'object') {
return map;
}
for (const [language, value] of Object.entries(rawSelection)) {
const normalizedLanguage = normalizeSubtitleLanguage(language);
map[normalizedLanguage] = {
full: Boolean(value?.full),
forced: Boolean(value?.forced)
};
}
return map;
}
function normalizeSubtitleConfidenceValue(value) {
const raw = String(value || '').trim().toLowerCase();
if (raw === 'high' || raw === 'medium' || raw === 'low') {
return raw;
}
return 'low';
}
function subtitleConfidenceScore(value) {
const normalized = normalizeSubtitleConfidenceValue(value);
if (normalized === 'high') {
return 3;
}
if (normalized === 'medium') {
return 2;
}
return 1;
}
function resolveSubtitleTrackVariantFlags(track) {
const subtitleType = String(track?.subtitleType || '').trim().toLowerCase();
const isForcedOnly = Boolean(
track?.isForcedOnly
?? track?.subtitlePreviewForcedOnly
?? track?.forcedOnly
?? subtitleType === 'forced'
);
return Boolean(track?.forcedAvailable || sourceTrackIds.length > 0);
const fullHasForced = !isForcedOnly && Boolean(
track?.fullHasForced
?? track?.subtitleFullHasForced
?? track?.hasForcedVariant
);
return { isForcedOnly, fullHasForced };
}
function compareForcedSubtitleCandidate(a, b) {
const defaultDiff = Number(Boolean(b?.defaultFlag)) - Number(Boolean(a?.defaultFlag));
if (defaultDiff !== 0) {
return defaultDiff;
}
const confidenceDiff = subtitleConfidenceScore(b?.confidence) - subtitleConfidenceScore(a?.confidence);
if (confidenceDiff !== 0) {
return confidenceDiff;
}
if (a.id !== b.id) {
return a.id - b.id;
}
return a.originalIndex - b.originalIndex;
}
function compareFullSubtitleCandidate(a, b) {
const defaultDiff = Number(Boolean(b?.defaultFlag)) - Number(Boolean(a?.defaultFlag));
if (defaultDiff !== 0) {
return defaultDiff;
}
if (a.id !== b.id) {
return a.id - b.id;
}
return a.originalIndex - b.originalIndex;
}
function resolveDeterministicSubtitleCliSelectionForPreview({
subtitleTracks,
subtitleVariantSelection,
subtitleLanguageOrder
}) {
const tracks = (Array.isArray(subtitleTracks) ? subtitleTracks : [])
.map((track, index) => {
const id = normalizeTrackId(track?.id ?? track?.sourceTrackId);
if (id === null) {
return null;
}
if (isBurnedSubtitleTrack(track) || isDuplicateSubtitleTrack(track)) {
return null;
}
const flags = resolveSubtitleTrackVariantFlags(track);
return {
id,
language: normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und'),
isForcedOnly: flags.isForcedOnly,
fullHasForced: flags.fullHasForced,
defaultFlag: Boolean(track?.defaultFlag ?? track?.subtitlePreviewDefaultTrack ?? track?.defaultTrack),
confidence: normalizeSubtitleConfidenceValue(track?.sourceConfidence || track?.confidence),
selectedForEncode: Boolean(track?.selectedForEncode),
originalIndex: index
};
})
.filter(Boolean);
const grouped = new Map();
for (const track of tracks) {
if (!grouped.has(track.language)) {
grouped.set(track.language, []);
}
grouped.get(track.language).push(track);
}
const normalizedVariantSelection = normalizeSubtitleVariantSelectionMap(subtitleVariantSelection || {});
const hasExplicitVariantSelection = Object.keys(normalizedVariantSelection).length > 0;
const normalizedLanguageOrder = normalizeSubtitleLanguageOrder(subtitleLanguageOrder || []);
const sortedLanguages = Array.from(grouped.entries())
.map(([language, languageTracks]) => ({
language,
minId: languageTracks.reduce((minValue, track) => Math.min(minValue, track.id), Number.MAX_SAFE_INTEGER)
}))
.sort((a, b) => a.minId - b.minId || a.language.localeCompare(b.language))
.map((item) => item.language);
const order = [];
const seen = new Set();
const pushLanguage = (language) => {
if (!grouped.has(language) || seen.has(language)) {
return;
}
seen.add(language);
order.push(language);
};
normalizedLanguageOrder.forEach(pushLanguage);
sortedLanguages.forEach(pushLanguage);
const subtitleTrackIds = [];
const subtitleForcedTrackIndexes = [];
for (const language of order) {
const languageTracks = grouped.get(language) || [];
const forcedOnly = languageTracks.filter((track) => track.isForcedOnly).sort(compareForcedSubtitleCandidate)[0] || null;
const fullTracks = languageTracks.filter((track) => !track.isForcedOnly).sort(compareFullSubtitleCandidate);
const bestFull = fullTracks[0] || null;
const bestFullHasForced = fullTracks.filter((track) => track.fullHasForced).sort(compareFullSubtitleCandidate)[0] || null;
const explicit = normalizedVariantSelection[language] || null;
const fallbackFull = languageTracks.some((track) => track.selectedForEncode && !track.isForcedOnly) || Boolean(bestFull);
const fallbackForced = languageTracks.some((track) => track.selectedForEncode && track.isForcedOnly) || Boolean(forcedOnly);
const requestedFull = explicit
? Boolean(explicit.full)
: (hasExplicitVariantSelection ? false : fallbackFull);
const requestedForced = explicit
? Boolean(explicit.forced)
: (hasExplicitVariantSelection ? false : fallbackForced);
if (!requestedFull && !requestedForced) {
continue;
}
if (forcedOnly) {
subtitleTrackIds.push(forcedOnly.id);
if (requestedFull && bestFull) {
subtitleTrackIds.push(bestFull.id);
}
continue;
}
if (bestFullHasForced) {
if (requestedFull && requestedForced) {
subtitleTrackIds.push(bestFullHasForced.id);
subtitleTrackIds.push(bestFullHasForced.id);
subtitleForcedTrackIndexes.push(subtitleTrackIds.length);
} else if (requestedForced) {
subtitleTrackIds.push(bestFullHasForced.id);
subtitleForcedTrackIndexes.push(subtitleTrackIds.length);
} else if (requestedFull) {
subtitleTrackIds.push(bestFullHasForced.id);
}
continue;
}
if (requestedFull && bestFull) {
subtitleTrackIds.push(bestFull.id);
}
}
return {
subtitleTrackIds,
subtitleForcedTrackIndexes
};
}
function splitArgs(input) {
@@ -191,6 +416,8 @@ function buildHandBrakeCommandPreview({
title,
selectedAudioTrackIds,
selectedSubtitleTrackIds,
selectedSubtitleVariantSelection = {},
selectedSubtitleLanguageOrder = [],
commandOutputPath = null,
presetOverride = null
}) {
@@ -211,7 +438,19 @@ function buildHandBrakeCommandPreview({
? Math.trunc(rawMappedTitleId)
: null;
const selectedSubtitleSet = new Set(normalizeTrackIdList(selectedSubtitleTrackIds).map((id) => String(id)));
const resolvedSubtitleSelection = resolveDeterministicSubtitleCliSelectionForPreview({
subtitleTracks: Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [],
subtitleVariantSelection: selectedSubtitleVariantSelection,
subtitleLanguageOrder: selectedSubtitleLanguageOrder
});
const normalizedSelectedVariantSelection = normalizeSubtitleVariantSelectionMap(selectedSubtitleVariantSelection || {});
const hasExplicitSelectedVariantSelection = Object.keys(normalizedSelectedVariantSelection).length > 0;
const subtitleTrackIds = resolvedSubtitleSelection.subtitleTrackIds.length > 0
? resolvedSubtitleSelection.subtitleTrackIds
: (hasExplicitSelectedVariantSelection
? []
: normalizeTrackIdSequence(selectedSubtitleTrackIds, { dedupe: false }));
const selectedSubtitleSet = new Set(normalizeTrackIdList(subtitleTrackIds).map((id) => String(id)));
const selectedSubtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).filter((track) => {
const id = normalizeTrackId(track?.id);
return id !== null && selectedSubtitleSet.has(String(id));
@@ -223,10 +462,6 @@ function buildHandBrakeCommandPreview({
const subtitleDefaultTrackId = normalizeTrackIdList(
selectedSubtitleTracks.filter((track) => Boolean(track?.subtitlePreviewDefaultTrack || track?.defaultTrack)).map((track) => track?.id)
)[0] || null;
const subtitleForcedTrackId = normalizeTrackIdList(
selectedSubtitleTracks.filter((track) => Boolean(track?.subtitlePreviewForced || track?.forced)).map((track) => track?.id)
)[0] || null;
const subtitleForcedOnly = selectedSubtitleTracks.some((track) => Boolean(track?.subtitlePreviewForcedOnly || track?.forcedOnly));
const baseArgs = [
'-i',
@@ -248,7 +483,7 @@ function buildHandBrakeCommandPreview({
'-a',
normalizeTrackIdList(selectedAudioTrackIds).join(',') || 'none',
'-s',
normalizeTrackIdList(selectedSubtitleTrackIds).join(',') || 'none'
subtitleTrackIds.length > 0 ? subtitleTrackIds.join(',') : 'none'
];
if (subtitleBurnTrackId !== null) {
@@ -257,10 +492,8 @@ function buildHandBrakeCommandPreview({
if (subtitleDefaultTrackId !== null) {
overrideArgs.push(`--subtitle-default=${subtitleDefaultTrackId}`);
}
if (subtitleForcedTrackId !== null) {
overrideArgs.push(`--subtitle-forced=${subtitleForcedTrackId}`);
} else if (subtitleForcedOnly) {
overrideArgs.push('--subtitle-forced');
if (resolvedSubtitleSelection.subtitleForcedTrackIndexes.length > 0) {
overrideArgs.push(`--subtitle-forced=${resolvedSubtitleSelection.subtitleForcedTrackIndexes.join(',')}`);
}
const finalArgs = [...baseArgs, ...filteredExtra, ...overrideArgs];
@@ -637,11 +870,191 @@ function TrackList({
type = 'generic',
allowSelection = false,
selectedTrackIds = [],
selectedSubtitleVariantSelection = {},
selectedSubtitleLanguageOrder = [],
onToggleTrack = null,
onToggleSubtitleVariant = null,
audioSelector = null
}) {
const orderedTracks = (Array.isArray(tracks) ? [...tracks] : [])
.sort((a, b) => {
const aId = normalizeTrackId(a?.id) ?? Number.MAX_SAFE_INTEGER;
const bId = normalizeTrackId(b?.id) ?? Number.MAX_SAFE_INTEGER;
if (aId !== bId) {
return aId - bId;
}
return String(a?.language || '').localeCompare(String(b?.language || ''));
});
if (type === 'subtitle') {
const selectedVariants = normalizeSubtitleVariantSelectionMap(selectedSubtitleVariantSelection || {});
const sortedSubtitleTracks = orderedTracks
.filter((track) => !isDuplicateSubtitleTrack(track));
const renderVariantRow = ({
key,
checked,
disabled,
onChange,
text,
confidence = null,
indent = false
}) => {
const confidenceColor = confidence === 'high'
? '#2e7d32'
: confidence === 'medium'
? '#f57c00'
: '#c62828';
return (
<div key={key} className="media-track-item" style={indent ? { paddingLeft: '1.4rem' } : null}>
<label className="readonly-check-row">
<input
type="checkbox"
checked={checked}
onChange={onChange}
readOnly={disabled}
disabled={disabled}
/>
<span>
{text}
{confidence ? <span style={{ color: confidenceColor, marginLeft: '0.35rem' }}></span> : null}
</span>
</label>
</div>
);
};
const resolveSubtitleActionInfo = (track, selected) => {
const base = String(track?.subtitlePreviewSummary || track?.subtitleActionSummary || '').trim();
if (allowSelection) {
return selected ? 'Übernehmen' : 'Nicht übernommen';
}
return base || (selected ? 'Übernehmen' : 'Nicht übernommen');
};
return (
<div>
<h4>{title}</h4>
{!tracks || tracks.length === 0 ? (
<p>Keine Einträge.</p>
) : (
<div className="media-track-list">
{sortedSubtitleTracks.map((track) => {
const burned = isBurnedSubtitleTrack(track);
const language = normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und');
const displayLanguage = toLang2(track?.language || track?.languageLabel || 'und');
const displayCodec = simplifyCodec('subtitle', track?.format, track?.description || track?.title);
const flags = resolveSubtitleTrackVariantFlags(track);
const confidenceRaw = String(track?.sourceConfidence || '').trim().toLowerCase();
const confidence = confidenceRaw === 'high' || confidenceRaw === 'medium' || confidenceRaw === 'low'
? confidenceRaw
: 'low';
const languageSelection = selectedVariants[language] || { full: false, forced: false };
if (flags.isForcedOnly) {
const checked = allowSelection ? Boolean(languageSelection.forced) : Boolean(track?.selectedForEncode);
const disabled = !allowSelection || burned;
const actionInfo = resolveSubtitleActionInfo(track, checked);
return (
<div key={`${title}-${track.id}-forced-only-group`} className="media-track-item">
{renderVariantRow({
key: `${title}-${track.id}-forced-only`,
checked,
disabled,
onChange: (event) => {
if (disabled || typeof onToggleSubtitleVariant !== 'function') {
return;
}
onToggleSubtitleVariant(language, 'forced', event.target.checked);
},
text: `#${track.id} | ${displayLanguage} | Automatische Übersetzungen (${displayCodec})`,
confidence
})}
<small className="track-action-note">Untertitel: {actionInfo}</small>
</div>
);
}
if (flags.fullHasForced) {
const checkedFull = allowSelection ? Boolean(languageSelection.full) : Boolean(track?.selectedForEncode);
const checkedForced = allowSelection ? Boolean(languageSelection.forced) : false;
const disabled = !allowSelection || burned;
const actionInfo = resolveSubtitleActionInfo(track, checkedFull || checkedForced);
return (
<div key={`${title}-${track.id}-combo`} className="media-track-item">
<div className="media-track-item">
<small className="track-action-note">{`#${track.id} | ${displayLanguage}`}</small>
</div>
{renderVariantRow({
key: `${title}-${track.id}-combo-full`,
checked: checkedFull,
disabled,
onChange: (event) => {
if (disabled || typeof onToggleSubtitleVariant !== 'function') {
return;
}
onToggleSubtitleVariant(language, 'full', event.target.checked);
},
text: `Komplette Untertitel (${displayCodec})`,
indent: true
})}
{renderVariantRow({
key: `${title}-${track.id}-combo-forced`,
checked: checkedForced,
disabled,
onChange: (event) => {
if (disabled || typeof onToggleSubtitleVariant !== 'function') {
return;
}
onToggleSubtitleVariant(language, 'forced', event.target.checked);
},
text: `Automatische Übersetzungen (${displayCodec})`,
confidence,
indent: true
})}
<small className="track-action-note">Untertitel: {actionInfo}</small>
</div>
);
}
const checked = allowSelection ? Boolean(languageSelection.full) : Boolean(track?.selectedForEncode);
const disabled = !allowSelection || burned;
const actionInfo = resolveSubtitleActionInfo(track, checked);
return (
<div key={`${title}-${track.id}-full-group`} className="media-track-item">
{renderVariantRow({
key: `${title}-${track.id}-full`,
checked,
disabled,
onChange: (event) => {
if (disabled || typeof onToggleSubtitleVariant !== 'function') {
return;
}
onToggleSubtitleVariant(language, 'full', event.target.checked);
},
text: `#${track.id} | ${displayLanguage} | Komplette Untertitel (${displayCodec})`
})}
<small className="track-action-note">Untertitel: {actionInfo}</small>
</div>
);
})}
</div>
)}
<div className="subtitle-footnote-list">
<small className="track-action-note">
Automatische Übersetzungen werden nur eingeblendet, wenn im Film eine andere Sprache gesprochen wird. Komplette Untertitel zeigen alle Dialoge an. Blu-ray Untertitel liegen meist als PGS (Bildformat) vor.
</small>
<small className="track-action-note">
Confidence-Index (nur für Automatische Übersetzungen): <span style={{ color: '#2e7d32' }}></span> high = explizites Track-Signal erkannt, <span style={{ color: '#f57c00' }}></span> medium = "forced" im Titel erkannt, <span style={{ color: '#c62828' }}></span> low = heuristische Einschätzung.
</small>
</div>
</div>
);
}
const selectedIds = normalizeTrackIdList(selectedTrackIds);
const checkedTrackOrder = (Array.isArray(tracks) ? tracks : [])
const checkedTrackOrder = orderedTracks
.map((track) => normalizeTrackId(track?.id))
.filter((trackId, index) => {
if (trackId === null) {
@@ -650,7 +1063,7 @@ function TrackList({
if (allowSelection) {
return selectedIds.includes(trackId);
}
const track = tracks[index];
const track = orderedTracks[index];
return Boolean(track?.selectedForEncode);
});
@@ -661,11 +1074,10 @@ function TrackList({
<p>Keine Einträge.</p>
) : (
<div className="media-track-list">
{tracks.map((track) => {
{orderedTracks.map((track) => {
const trackId = normalizeTrackId(track.id);
const burned = type === 'subtitle' ? isBurnedSubtitleTrack(track) : false;
const checked = allowSelection
? (trackId !== null && selectedIds.includes(trackId) && !(type === 'subtitle' && burned))
? (trackId !== null && selectedIds.includes(trackId))
: Boolean(track.selectedForEncode);
const selectedIndex = trackId !== null
? checkedTrackOrder.indexOf(trackId)
@@ -686,23 +1098,14 @@ function TrackList({
return base || buildAudioActionPreviewSummary(track, selectedIndex, audioSelector);
})()
: 'Nicht übernommen')
: type === 'subtitle'
? (checked
? (() => {
const base = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
return /^nicht übernommen$/i.test(base) ? 'Übernehmen' : (base || 'Übernehmen');
})()
: 'Nicht übernommen')
: null;
: null;
const displayLanguage = toLang2(track.language || track.languageLabel || 'und');
const displayHint = track.description || track.title;
const displayCodec = simplifyCodec(type, track.format, displayHint);
const displayChannelCount = channelCount(track.channels);
const displayAudioTitle = audioChannelLabel(track.channels);
const audioVariant = type === 'audio' ? extractAudioVariant(displayHint) : '';
const disabled = !allowSelection || (type === 'subtitle' && burned);
const forcedOnlyTrack = type === 'subtitle' ? isForcedOnlySubtitleTrack(track) : false;
const forcedAvailable = type === 'subtitle' ? hasForcedSubtitleAvailable(track) : false;
const disabled = !allowSelection;
let displayText = `#${track.id} | ${displayLanguage} | ${displayCodec}`;
if (type === 'audio') {
@@ -716,14 +1119,6 @@ function TrackList({
displayText += ` | ${audioVariant}`;
}
}
if (type === 'subtitle' && burned) {
displayText += ' | burned';
} else if (type === 'subtitle' && forcedOnlyTrack) {
displayText += ' | forced-only';
} else if (type === 'subtitle' && forcedAvailable) {
displayText += ' | forced verfügbar';
}
return (
<div key={`${title}-${track.id}`} className="media-track-item">
<label className="readonly-check-row">
@@ -739,7 +1134,9 @@ function TrackList({
readOnly={disabled}
disabled={disabled}
/>
<span>{displayText}</span>
<span>
{displayText}
</span>
</label>
{actionInfo ? <small className="track-action-note">Encode: {actionInfo}</small> : null}
</div>
@@ -804,6 +1201,7 @@ export default function MediaInfoReviewPanel({
allowTrackSelection = false,
trackSelectionByTitle = {},
onTrackSelectionChange = null,
onSubtitleVariantSelectionChange = null,
availableScripts = [],
availableChains = [],
preEncodeItems = [],
@@ -934,7 +1332,7 @@ export default function MediaInfoReviewPanel({
<div><strong>Audio Copy-Mask:</strong> {(effectiveAudioSelector?.copyMask || []).join(', ') || '-'}</div>
<div><strong>Audio Fallback:</strong> {effectiveAudioSelector?.fallbackEncoder || '-'}</div>
<div><strong>Subtitle Auswahl:</strong> {review.selectors?.subtitle?.mode || '-'}</div>
<div><strong>Subtitle Flags:</strong> {review.selectors?.subtitle?.forcedOnly ? 'forced-only' : '-'}{review.selectors?.subtitle?.burnBehavior === 'first' ? ' + burned(first)' : ''}</div>
<div><strong>Subtitle Flags:</strong> {review.selectors?.subtitle?.burnBehavior === 'first' ? 'burned(first)' : '-'}</div>
</div>
{review.partial ? (
@@ -1213,18 +1611,44 @@ export default function MediaInfoReviewPanel({
const titleSelectionEntry = trackSelectionByTitle?.[title.id] || trackSelectionByTitle?.[String(title.id)] || {};
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
const selectableSubtitleTrackIds = subtitleTracks
.filter((track) => !isBurnedSubtitleTrack(track))
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null);
const selectableSubtitleTrackIdSet = new Set(selectableSubtitleTrackIds.map((id) => String(id)));
const sortedSelectableSubtitleTracks = subtitleTracks
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
.slice()
.sort((a, b) => (normalizeTrackId(a?.id) || Number.MAX_SAFE_INTEGER) - (normalizeTrackId(b?.id) || Number.MAX_SAFE_INTEGER));
const defaultAudioTrackIds = (Array.isArray(title.audioTracks) ? title.audioTracks : [])
.filter((track) => Boolean(track?.selectedByRule))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null);
const defaultSubtitleTrackIds = subtitleTracks
.filter((track) => Boolean(track?.selectedByRule) && !isBurnedSubtitleTrack(track))
.filter((track) => Boolean(track?.selectedByRule) && !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null);
const defaultSubtitleVariantSelection = {};
const defaultSubtitleLanguageOrder = [];
const subtitleLanguageOrderSeen = new Set();
for (const track of sortedSelectableSubtitleTracks) {
const language = normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und');
if (!subtitleLanguageOrderSeen.has(language)) {
subtitleLanguageOrderSeen.add(language);
defaultSubtitleLanguageOrder.push(language);
}
if (!Boolean(track?.selectedByRule)) {
continue;
}
if (!defaultSubtitleVariantSelection[language]) {
defaultSubtitleVariantSelection[language] = { full: false, forced: false };
}
const flags = resolveSubtitleTrackVariantFlags(track);
if (flags.isForcedOnly) {
defaultSubtitleVariantSelection[language].forced = true;
} else {
defaultSubtitleVariantSelection[language].full = true;
}
}
const selectedAudioTrackIds = normalizeTrackIdList(
Array.isArray(titleSelectionEntry?.audioTrackIds)
? titleSelectionEntry.audioTrackIds
@@ -1235,6 +1659,14 @@ export default function MediaInfoReviewPanel({
? titleSelectionEntry.subtitleTrackIds
: defaultSubtitleTrackIds
).filter((id) => selectableSubtitleTrackIdSet.has(String(id)));
const selectedSubtitleVariantSelection = normalizeSubtitleVariantSelectionMap(
titleSelectionEntry?.subtitleVariantSelection || defaultSubtitleVariantSelection
);
const selectedSubtitleLanguageOrder = normalizeSubtitleLanguageOrder(
Array.isArray(titleSelectionEntry?.subtitleLanguageOrder)
? titleSelectionEntry.subtitleLanguageOrder
: defaultSubtitleLanguageOrder
);
const allowTrackSelectionForTitle = Boolean(
allowTrackSelection
&& allowTitleSelection
@@ -1302,15 +1734,19 @@ export default function MediaInfoReviewPanel({
/>
<TrackList
title={`Subtitles (Titel #${title.id})`}
tracks={allowTrackSelectionForTitle ? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track)) : subtitleTracks}
tracks={allowTrackSelectionForTitle
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
: subtitleTracks}
type="subtitle"
allowSelection={allowTrackSelectionForTitle}
selectedTrackIds={selectedSubtitleTrackIds}
onToggleTrack={(trackId, checked) => {
if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') {
selectedSubtitleVariantSelection={selectedSubtitleVariantSelection}
selectedSubtitleLanguageOrder={selectedSubtitleLanguageOrder}
onToggleSubtitleVariant={(language, variant, checked) => {
if (!allowTrackSelectionForTitle || typeof onSubtitleVariantSelectionChange !== 'function') {
return;
}
onTrackSelectionChange(title.id, 'subtitle', trackId, checked);
onSubtitleVariantSelectionChange(title.id, language, variant, checked);
}}
/>
</div>
@@ -1320,6 +1756,8 @@ export default function MediaInfoReviewPanel({
title,
selectedAudioTrackIds,
selectedSubtitleTrackIds,
selectedSubtitleVariantSelection,
selectedSubtitleLanguageOrder,
commandOutputPath,
presetOverride: effectivePresetOverride
});