0.12.0-16 misc fixes
This commit is contained in:
@@ -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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user