0.13.0 DVD Series
This commit is contained in:
@@ -411,6 +411,46 @@ function shellQuote(value) {
|
||||
return `'${raw.replace(/'/g, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function transliterateForFilename(input) {
|
||||
return String(input || '')
|
||||
.replace(/ä/g, 'ae').replace(/Ä/g, 'Ae')
|
||||
.replace(/ö/g, 'oe').replace(/Ö/g, 'Oe')
|
||||
.replace(/ü/g, 'ue').replace(/Ü/g, 'Ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
.replace(/å/g, 'a').replace(/Å/g, 'A')
|
||||
.replace(/æ/g, 'ae').replace(/Æ/g, 'Ae')
|
||||
.replace(/ø/g, 'o').replace(/Ø/g, 'O')
|
||||
.replace(/ð/g, 'd').replace(/Ð/g, 'D')
|
||||
.replace(/þ/g, 'th').replace(/Þ/g, 'Th')
|
||||
.normalize('NFD')
|
||||
.replace(/\p{M}+/gu, '')
|
||||
.replace(/[^\x00-\x7F]/g, '');
|
||||
}
|
||||
|
||||
function sanitizePathSegment(value) {
|
||||
return transliterateForFilename(String(value || ''))
|
||||
.replace(/[^a-zA-Z0-9 ().[\]_-]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sanitizeOutputPathForCommand(pathValue) {
|
||||
const raw = String(pathValue || '').trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const normalizedPath = raw.replace(/\\/g, '/');
|
||||
const hasLeadingSlash = normalizedPath.startsWith('/');
|
||||
const segments = normalizedPath
|
||||
.split('/')
|
||||
.map((segment) => sanitizePathSegment(segment))
|
||||
.filter(Boolean);
|
||||
if (segments.length === 0) {
|
||||
return hasLeadingSlash ? '/' : null;
|
||||
}
|
||||
return `${hasLeadingSlash ? '/' : ''}${segments.join('/')}`;
|
||||
}
|
||||
|
||||
function buildHandBrakeCommandPreview({
|
||||
review,
|
||||
title,
|
||||
@@ -433,7 +473,12 @@ function buildHandBrakeCommandPreview({
|
||||
const extraArgs = presetOverride !== null
|
||||
? String(presetOverride.extraArgs || '').trim()
|
||||
: String(review?.selectors?.extraArgs || '').trim();
|
||||
const rawMappedTitleId = Number(review?.handBrakeTitleId);
|
||||
const rawMappedTitleId = Number(
|
||||
title?.handBrakeTitleId
|
||||
?? title?.titleIndex
|
||||
?? title?.id
|
||||
?? review?.handBrakeTitleId
|
||||
);
|
||||
const mappedTitleId = Number.isFinite(rawMappedTitleId) && rawMappedTitleId > 0
|
||||
? Math.trunc(rawMappedTitleId)
|
||||
: null;
|
||||
@@ -463,11 +508,12 @@ function buildHandBrakeCommandPreview({
|
||||
selectedSubtitleTracks.filter((track) => Boolean(track?.subtitlePreviewDefaultTrack || track?.defaultTrack)).map((track) => track?.id)
|
||||
)[0] || null;
|
||||
|
||||
const sanitizedOutputPath = sanitizeOutputPathForCommand(commandOutputPath);
|
||||
const baseArgs = [
|
||||
'-i',
|
||||
inputPath || '<encode-input>',
|
||||
'-o',
|
||||
String(commandOutputPath || '').trim() || '<encode-output>'
|
||||
String(sanitizedOutputPath || '').trim() || '<encode-output>'
|
||||
];
|
||||
|
||||
if (mappedTitleId !== null) {
|
||||
@@ -1195,13 +1241,22 @@ export default function MediaInfoReviewPanel({
|
||||
review,
|
||||
presetDisplayValue = '',
|
||||
commandOutputPath = null,
|
||||
commandOutputPathByTitle = {},
|
||||
selectedEncodeTitleId = null,
|
||||
selectedEncodeTitleIds = [],
|
||||
allowTitleSelection = false,
|
||||
compactTitleSelection = false,
|
||||
onSelectEncodeTitle = null,
|
||||
onToggleEncodeTitle = null,
|
||||
allowTrackSelection = false,
|
||||
trackSelectionByTitle = {},
|
||||
onTrackSelectionChange = null,
|
||||
onSubtitleVariantSelectionChange = null,
|
||||
selectedMetadataEpisodes = [],
|
||||
selectedEpisodeFillStart = null,
|
||||
onEpisodeFillStartChange = null,
|
||||
episodeAssignmentsByTitle = {},
|
||||
onEpisodeAssignmentChange = null,
|
||||
availableScripts = [],
|
||||
availableChains = [],
|
||||
preEncodeItems = [],
|
||||
@@ -1225,10 +1280,68 @@ export default function MediaInfoReviewPanel({
|
||||
}
|
||||
|
||||
const titles = review.titles || [];
|
||||
const currentSelectedId = normalizeTitleId(selectedEncodeTitleId) || normalizeTitleId(review.encodeInputTitleId);
|
||||
const encodeInputTitle = titles.find((item) => item.id === currentSelectedId) || null;
|
||||
const processedFiles = Number(review.processedFiles || titles.length || 0);
|
||||
const totalFiles = Number(review.totalFiles || titles.length || 0);
|
||||
const candidateTitles = compactTitleSelection && Array.isArray(review?.handBrakeTitleCandidates)
|
||||
? review.handBrakeTitleCandidates
|
||||
.map((candidate) => {
|
||||
const id = normalizeTitleId(candidate?.handBrakeTitleId);
|
||||
if (id === null) return null;
|
||||
const durationSeconds = Number(candidate?.durationSeconds || 0);
|
||||
const durationMinutes = Number.isFinite(durationSeconds) && durationSeconds > 0
|
||||
? Number((durationSeconds / 60).toFixed(2))
|
||||
: Number(candidate?.durationMinutes || 0);
|
||||
return {
|
||||
id,
|
||||
fileName: `Titel #${id}`,
|
||||
durationSeconds: Number.isFinite(durationSeconds) ? durationSeconds : 0,
|
||||
durationMinutes: Number.isFinite(durationMinutes) ? durationMinutes : 0,
|
||||
sizeBytes: Number(candidate?.sizeBytes || 0),
|
||||
audioTrackCount: Number(candidate?.audioTrackCount || 0),
|
||||
subtitleTrackCount: Number(candidate?.subtitleTrackCount || 0),
|
||||
audioTracks: [],
|
||||
subtitleTracks: [],
|
||||
selectedForEncode: false
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const displayTitles = candidateTitles && candidateTitles.length > 0 ? candidateTitles : titles;
|
||||
const selectedTitleIds = normalizeTrackIdList([
|
||||
...normalizeTrackIdList(selectedEncodeTitleIds),
|
||||
...normalizeTrackIdList(review?.selectedTitleIds || []),
|
||||
...displayTitles.filter((item) => Boolean(item?.selectedForEncode)).map((item) => item?.id)
|
||||
]);
|
||||
const currentSelectedId = normalizeTitleId(selectedEncodeTitleId)
|
||||
|| normalizeTitleId(review.encodeInputTitleId)
|
||||
|| selectedTitleIds[0]
|
||||
|| null;
|
||||
const selectedTitleIdSet = new Set(selectedTitleIds.map((id) => String(id)));
|
||||
const encodeInputTitle = displayTitles.find((item) => item.id === currentSelectedId) || null;
|
||||
const normalizedEpisodeRows = (Array.isArray(selectedMetadataEpisodes) ? selectedMetadataEpisodes : [])
|
||||
.map((episode) => {
|
||||
const episodeId = Number(episode?.episodeId ?? episode?.id ?? 0);
|
||||
const episodeNumber = Number(episode?.episodeNumber ?? episode?.number ?? 0);
|
||||
const seasonNumber = Number(episode?.seasonNumber ?? episode?.season ?? 0);
|
||||
const episodeTitle = String(episode?.episodeTitle || episode?.name || episode?.title || '').trim();
|
||||
return {
|
||||
episodeId: Number.isFinite(episodeId) && episodeId > 0 ? Math.trunc(episodeId) : null,
|
||||
episodeNumber: Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null,
|
||||
seasonNumber: Number.isFinite(seasonNumber) && seasonNumber > 0 ? Math.trunc(seasonNumber) : null,
|
||||
episodeTitle: episodeTitle || null
|
||||
};
|
||||
})
|
||||
.filter((episode) => episode.episodeId || episode.episodeNumber !== null);
|
||||
const episodeFillOptions = normalizedEpisodeRows.map((episode) => {
|
||||
const seasonLabel = episode.seasonNumber !== null ? `S${String(episode.seasonNumber).padStart(2, '0')}` : 'S??';
|
||||
const episodeLabel = episode.episodeNumber !== null ? `E${String(episode.episodeNumber).padStart(2, '0')}` : 'E??';
|
||||
const titleLabel = episode.episodeTitle || '(ohne Titel)';
|
||||
const value = String(episode.episodeId || episode.episodeNumber || '');
|
||||
return {
|
||||
label: `${seasonLabel}${episodeLabel} - ${titleLabel}`,
|
||||
value
|
||||
};
|
||||
}).filter((option) => option.value);
|
||||
const processedFiles = Number(review.processedFiles || displayTitles.length || 0);
|
||||
const totalFiles = Number(review.totalFiles || displayTitles.length || 0);
|
||||
const playlistRecommendation = review.playlistRecommendation || null;
|
||||
const rawPreset = String(review.selectors?.preset || '').trim();
|
||||
const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)';
|
||||
@@ -1280,9 +1393,170 @@ export default function MediaInfoReviewPanel({
|
||||
const handlePreDrop = makeHandleDrop(preEncodeItems, onReorderPreEncodeItem);
|
||||
const handlePostDrop = makeHandleDrop(postEncodeItems, onReorderPostEncodeItem);
|
||||
|
||||
const resolveTitleSelectionState = (title) => {
|
||||
const titleSelectionEntry = trackSelectionByTitle?.[title.id] || trackSelectionByTitle?.[String(title.id)] || {};
|
||||
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||
const selectableSubtitleTrackIds = subtitleTracks
|
||||
.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) && !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
|
||||
: defaultAudioTrackIds
|
||||
);
|
||||
const selectedSubtitleTrackIds = normalizeTrackIdList(
|
||||
Array.isArray(titleSelectionEntry?.subtitleTrackIds)
|
||||
? 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
|
||||
);
|
||||
return {
|
||||
titleSelectionEntry,
|
||||
subtitleTracks,
|
||||
selectedAudioTrackIds,
|
||||
selectedSubtitleTrackIds,
|
||||
selectedSubtitleVariantSelection,
|
||||
selectedSubtitleLanguageOrder
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeTrackSignatureText = (value) => String(value || '').trim().toLowerCase();
|
||||
const buildAudioSignature = (track) => [
|
||||
normalizeTrackSignatureText(track?.language || track?.languageLabel || 'und'),
|
||||
normalizeTrackSignatureText(track?.format || track?.codecName || ''),
|
||||
normalizeTrackSignatureText(track?.channels || ''),
|
||||
normalizeTrackSignatureText(track?.description || track?.title || '')
|
||||
].join('|');
|
||||
const buildSubtitleSignature = (track) => {
|
||||
const flags = resolveSubtitleTrackVariantFlags(track);
|
||||
return [
|
||||
normalizeTrackSignatureText(track?.language || track?.languageLabel || 'und'),
|
||||
normalizeTrackSignatureText(track?.format || track?.codecName || ''),
|
||||
flags.isForcedOnly ? 'forced' : 'full',
|
||||
flags.fullHasForced ? 'with_forced_variant' : 'plain'
|
||||
].join('|');
|
||||
};
|
||||
|
||||
const selectedTitlesForGlobal = selectedTitleIds.length > 0
|
||||
? titles.filter((title) => selectedTitleIdSet.has(String(normalizeTitleId(title?.id))))
|
||||
: titles;
|
||||
const masterGlobalTitle = selectedTitlesForGlobal[0] || titles[0] || null;
|
||||
const globalMasterSelectionState = masterGlobalTitle ? resolveTitleSelectionState(masterGlobalTitle) : null;
|
||||
const globalAudioTrackMaps = new Map();
|
||||
const globalSubtitleLanguagesByTitle = new Map();
|
||||
const globalTrackCompatibility = (() => {
|
||||
if (!masterGlobalTitle || selectedTitlesForGlobal.length < 2) {
|
||||
return { audio: false, subtitle: false };
|
||||
}
|
||||
const masterAudioTracks = Array.isArray(masterGlobalTitle.audioTracks) ? masterGlobalTitle.audioTracks : [];
|
||||
const masterSubtitleTracks = (Array.isArray(masterGlobalTitle.subtitleTracks) ? masterGlobalTitle.subtitleTracks : [])
|
||||
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track));
|
||||
const masterAudioSignatures = masterAudioTracks.map((track) => buildAudioSignature(track)).sort();
|
||||
const masterSubtitleSignatures = masterSubtitleTracks.map((track) => buildSubtitleSignature(track)).sort();
|
||||
const masterSubtitleLanguages = Array.from(new Set(masterSubtitleTracks.map((track) =>
|
||||
normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und')
|
||||
))).sort();
|
||||
|
||||
let audioCompatible = true;
|
||||
let subtitleCompatible = true;
|
||||
for (const title of selectedTitlesForGlobal) {
|
||||
const titleId = normalizeTitleId(title?.id);
|
||||
if (!titleId) {
|
||||
continue;
|
||||
}
|
||||
const audioMap = new Map();
|
||||
const titleAudioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
||||
const titleAudioSignatures = titleAudioTracks.map((track) => {
|
||||
const signature = buildAudioSignature(track);
|
||||
const trackId = normalizeTrackId(track?.id);
|
||||
if (trackId !== null && !audioMap.has(signature)) {
|
||||
audioMap.set(signature, trackId);
|
||||
}
|
||||
return signature;
|
||||
}).sort();
|
||||
globalAudioTrackMaps.set(titleId, audioMap);
|
||||
if (titleAudioSignatures.join('||') !== masterAudioSignatures.join('||')) {
|
||||
audioCompatible = false;
|
||||
}
|
||||
|
||||
const subtitleTracks = (Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [])
|
||||
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track));
|
||||
const subtitleSignatures = subtitleTracks.map((track) => buildSubtitleSignature(track)).sort();
|
||||
const subtitleLanguages = Array.from(new Set(subtitleTracks.map((track) =>
|
||||
normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und')
|
||||
))).sort();
|
||||
globalSubtitleLanguagesByTitle.set(titleId, subtitleLanguages);
|
||||
if (subtitleSignatures.join('||') !== masterSubtitleSignatures.join('||')) {
|
||||
subtitleCompatible = false;
|
||||
}
|
||||
if (subtitleLanguages.join('||') !== masterSubtitleLanguages.join('||')) {
|
||||
subtitleCompatible = false;
|
||||
}
|
||||
}
|
||||
return { audio: audioCompatible, subtitle: subtitleCompatible };
|
||||
})();
|
||||
const showGlobalEpisodeTrackControls = Boolean(
|
||||
allowTrackSelection
|
||||
&& allowTitleSelection
|
||||
&& masterGlobalTitle
|
||||
&& selectedTitlesForGlobal.length > 1
|
||||
&& globalTrackCompatibility.audio
|
||||
&& globalTrackCompatibility.subtitle
|
||||
);
|
||||
const useEpisodeTrackAccordion = Boolean(
|
||||
masterGlobalTitle
|
||||
&& selectedTitlesForGlobal.length > 1
|
||||
&& globalTrackCompatibility.audio
|
||||
&& globalTrackCompatibility.subtitle
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="media-review-wrap">
|
||||
{allowUserPresetSelection && (
|
||||
{!compactTitleSelection && allowUserPresetSelection && (
|
||||
<div className="user-preset-selector" style={{ marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.4rem', fontWeight: 600 }}>
|
||||
Encode-Preset auswählen
|
||||
@@ -1315,9 +1589,10 @@ export default function MediaInfoReviewPanel({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="media-review-meta">
|
||||
<div>
|
||||
<strong>Preset:</strong>{' '}
|
||||
{!compactTitleSelection ? (
|
||||
<div className="media-review-meta">
|
||||
<div>
|
||||
<strong>Preset:</strong>{' '}
|
||||
{effectivePresetOverride
|
||||
? (effectivePresetOverride.handbrakePreset || '(kein Preset)')
|
||||
: presetLabel}
|
||||
@@ -1345,13 +1620,14 @@ export default function MediaInfoReviewPanel({
|
||||
<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?.burnBehavior === 'first' ? 'burned(first)' : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{review.partial ? (
|
||||
{!compactTitleSelection && review.partial ? (
|
||||
<small>Zwischenstand: {processedFiles}/{totalFiles} Datei(en) analysiert.</small>
|
||||
) : null}
|
||||
|
||||
{playlistRecommendation ? (
|
||||
{!compactTitleSelection && playlistRecommendation ? (
|
||||
<div className="playlist-recommendation-box">
|
||||
<small>
|
||||
<strong>Empfehlung:</strong> {playlistRecommendation.playlistFile || '-'}
|
||||
@@ -1361,7 +1637,7 @@ export default function MediaInfoReviewPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{Array.isArray(review.notes) && review.notes.length > 0 ? (
|
||||
{!compactTitleSelection && Array.isArray(review.notes) && review.notes.length > 0 ? (
|
||||
<div className="media-review-notes">
|
||||
{review.notes.map((note, idx) => (
|
||||
<small key={`${idx}-${note}`}>{note}</small>
|
||||
@@ -1370,7 +1646,7 @@ export default function MediaInfoReviewPanel({
|
||||
) : null}
|
||||
|
||||
{/* Pre-Encode Items (scripts + chains unified) */}
|
||||
{(allowEncodeItemSelection || preEncodeItems.length > 0) ? (
|
||||
{!compactTitleSelection && (allowEncodeItemSelection || preEncodeItems.length > 0) ? (
|
||||
<div className="post-script-box">
|
||||
<h4>Pre-Encode Ausführungen (optional)</h4>
|
||||
{scriptCatalog.length === 0 && chainCatalog.length === 0 ? (
|
||||
@@ -1491,7 +1767,8 @@ export default function MediaInfoReviewPanel({
|
||||
) : null}
|
||||
|
||||
{/* Post-Encode Items (scripts + chains unified) */}
|
||||
<div className="post-script-box">
|
||||
{!compactTitleSelection ? (
|
||||
<div className="post-script-box">
|
||||
<h4>Post-Encode Ausführungen (optional)</h4>
|
||||
{scriptCatalog.length === 0 && chainCatalog.length === 0 ? (
|
||||
<small>Keine Skripte oder Ketten konfiguriert. In den Settings anlegen.</small>
|
||||
@@ -1606,22 +1883,140 @@ export default function MediaInfoReviewPanel({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<small>Ausführung nach erfolgreichem Encode, strikt nacheinander (Drag-and-Drop möglich).</small>
|
||||
</div>
|
||||
<small>Ausführung nach erfolgreichem Encode, strikt nacheinander (Drag-and-Drop möglich).</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<h4>Titel</h4>
|
||||
{episodeFillOptions.length > 0 ? (
|
||||
<div className="episode-fill-box">
|
||||
<div className="episode-fill-field">
|
||||
<label>Episodentitel befüllen ab Episode</label>
|
||||
<Dropdown
|
||||
value={String(selectedEpisodeFillStart || '').trim() || null}
|
||||
options={episodeFillOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
onChange={(event) => onEpisodeFillStartChange?.(event?.value || null)}
|
||||
placeholder="Episode auswählen …"
|
||||
disabled={!allowTitleSelection}
|
||||
style={{ width: '100%' }}
|
||||
filter
|
||||
showClear
|
||||
/>
|
||||
<small>
|
||||
Startet bei der gewählten Episode und befüllt alle ausgewählten DVD-Titel der Reihe nach.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showGlobalEpisodeTrackControls ? (
|
||||
<div className="episode-global-selection-box">
|
||||
<h4>Spurauswahl für alle Episoden</h4>
|
||||
<small>Gilt für alle aktuell gewählten Episoden. Einzelne Episoden können danach weiterhin manuell überschrieben werden.</small>
|
||||
<div className="media-track-grid">
|
||||
{globalTrackCompatibility.audio && globalMasterSelectionState ? (
|
||||
<TrackList
|
||||
title="Tonspuren (alle Episoden)"
|
||||
tracks={masterGlobalTitle?.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={allowTrackSelection}
|
||||
selectedTrackIds={globalMasterSelectionState.selectedAudioTrackIds}
|
||||
audioSelector={effectiveAudioSelector}
|
||||
onToggleTrack={(masterTrackId, checked) => {
|
||||
if (!allowTrackSelection || typeof onTrackSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
const masterTrack = (Array.isArray(masterGlobalTitle?.audioTracks) ? masterGlobalTitle.audioTracks : [])
|
||||
.find((track) => normalizeTrackId(track?.id) === normalizeTrackId(masterTrackId));
|
||||
if (!masterTrack) {
|
||||
return;
|
||||
}
|
||||
const signature = buildAudioSignature(masterTrack);
|
||||
for (const title of selectedTitlesForGlobal) {
|
||||
const titleId = normalizeTitleId(title?.id);
|
||||
if (!titleId) {
|
||||
continue;
|
||||
}
|
||||
const titleTrackMap = globalAudioTrackMaps.get(titleId);
|
||||
const mappedTrackId = titleTrackMap?.get(signature) ?? null;
|
||||
if (!mappedTrackId) {
|
||||
continue;
|
||||
}
|
||||
onTrackSelectionChange(titleId, 'audio', mappedTrackId, checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<h4>Tonspuren (alle Episoden)</h4>
|
||||
<small>Globale Auswahl nicht verfügbar, da die Tonspur-Layouts zwischen Episoden abweichen.</small>
|
||||
</div>
|
||||
)}
|
||||
{globalTrackCompatibility.subtitle && globalMasterSelectionState ? (
|
||||
<TrackList
|
||||
title="Subtitles (alle Episoden)"
|
||||
tracks={(Array.isArray(masterGlobalTitle?.subtitleTracks) ? masterGlobalTitle.subtitleTracks : [])
|
||||
.filter((track) => !isBurnedSubtitleTrack(track))}
|
||||
type="subtitle"
|
||||
allowSelection={allowTrackSelection}
|
||||
selectedTrackIds={globalMasterSelectionState.selectedSubtitleTrackIds}
|
||||
selectedSubtitleVariantSelection={globalMasterSelectionState.selectedSubtitleVariantSelection}
|
||||
selectedSubtitleLanguageOrder={globalMasterSelectionState.selectedSubtitleLanguageOrder}
|
||||
onToggleSubtitleVariant={(language, variant, checked) => {
|
||||
if (!allowTrackSelection || typeof onSubtitleVariantSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
const normalizedLanguage = normalizeSubtitleLanguage(language);
|
||||
for (const title of selectedTitlesForGlobal) {
|
||||
const titleId = normalizeTitleId(title?.id);
|
||||
if (!titleId) {
|
||||
continue;
|
||||
}
|
||||
const subtitleLanguages = globalSubtitleLanguagesByTitle.get(titleId) || [];
|
||||
if (!subtitleLanguages.includes(normalizedLanguage)) {
|
||||
continue;
|
||||
}
|
||||
onSubtitleVariantSelectionChange(titleId, normalizedLanguage, variant, checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<h4>Subtitles (alle Episoden)</h4>
|
||||
<small>Globale Auswahl nicht verfügbar, da die Untertitel-Layouts zwischen Episoden abweichen.</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="media-title-list">
|
||||
{titles.length === 0 ? (
|
||||
{displayTitles.length === 0 ? (
|
||||
<p>Keine Titel analysiert.</p>
|
||||
) : titles.map((title) => {
|
||||
const titleEligible = title?.eligibleForEncode !== false;
|
||||
) : displayTitles.map((title) => {
|
||||
const normalizedTitleId = normalizeTitleId(title.id);
|
||||
const titleChecked = allowTitleSelection
|
||||
? (currentSelectedId !== null
|
||||
? currentSelectedId === normalizeTitleId(title.id)
|
||||
? (selectedTitleIdSet.size > 0
|
||||
? selectedTitleIdSet.has(String(normalizedTitleId))
|
||||
: Boolean(title.selectedForEncode))
|
||||
: Boolean(title.selectedForEncode);
|
||||
const titleIsPrimary = currentSelectedId !== null && currentSelectedId === normalizedTitleId;
|
||||
const titleEpisodeAssignment = episodeAssignmentsByTitle?.[normalizedTitleId]
|
||||
|| episodeAssignmentsByTitle?.[String(normalizedTitleId)]
|
||||
|| null;
|
||||
const episodeInputValue = String(
|
||||
titleEpisodeAssignment?.episodeTitle
|
||||
|| title?.episodeTitle
|
||||
|| ''
|
||||
);
|
||||
const titleSelectionEntry = trackSelectionByTitle?.[title.id] || trackSelectionByTitle?.[String(title.id)] || {};
|
||||
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||
const audioCount = Number.isFinite(Number(title?.audioTrackCount))
|
||||
? Number(title.audioTrackCount)
|
||||
: (Array.isArray(title.audioTracks) ? title.audioTracks.length : 0);
|
||||
const subtitleCount = Number.isFinite(Number(title?.subtitleTrackCount))
|
||||
? Number(title.subtitleTrackCount)
|
||||
: subtitleTracks.length;
|
||||
const selectableSubtitleTrackIds = subtitleTracks
|
||||
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
|
||||
.map((track) => normalizeTrackId(track?.id))
|
||||
@@ -1691,21 +2086,52 @@ export default function MediaInfoReviewPanel({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={titleChecked}
|
||||
onChange={() => {
|
||||
if (!allowTitleSelection || typeof onSelectEncodeTitle !== 'function') {
|
||||
onChange={(event) => {
|
||||
if (!allowTitleSelection) {
|
||||
return;
|
||||
}
|
||||
onSelectEncodeTitle(normalizeTitleId(title.id));
|
||||
const checked = Boolean(event?.target?.checked);
|
||||
if (typeof onToggleEncodeTitle === 'function') {
|
||||
onToggleEncodeTitle(normalizedTitleId, checked);
|
||||
}
|
||||
if (checked && typeof onSelectEncodeTitle === 'function') {
|
||||
onSelectEncodeTitle(normalizedTitleId);
|
||||
}
|
||||
}}
|
||||
readOnly={!allowTitleSelection}
|
||||
disabled={!allowTitleSelection}
|
||||
/>
|
||||
<span>
|
||||
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} | {formatBytes(title.sizeBytes)}
|
||||
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)}
|
||||
{audioCount > 0 ? ` | Audio: ${audioCount}` : ''}
|
||||
{subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''}
|
||||
{title.encodeInput ? ' | Encode-Input' : ''}
|
||||
{titleIsPrimary ? ' | Primär' : ''}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{titleChecked ? (
|
||||
<div className="episode-title-input-row">
|
||||
<label htmlFor={`episode-title-${title.id}`}>Episodentitel</label>
|
||||
<input
|
||||
id={`episode-title-${title.id}`}
|
||||
className="p-inputtext p-component"
|
||||
type="text"
|
||||
value={episodeInputValue}
|
||||
onChange={(event) => {
|
||||
if (!allowTitleSelection || typeof onEpisodeAssignmentChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onEpisodeAssignmentChange(normalizedTitleId, {
|
||||
episodeTitle: event?.target?.value || ''
|
||||
});
|
||||
}}
|
||||
placeholder="Episodentitel"
|
||||
disabled={!allowTitleSelection}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{title.playlistFile || title.playlistEvaluationLabel || title.playlistSegmentCommand ? (
|
||||
<div className="playlist-info-box">
|
||||
<small>
|
||||
@@ -1729,40 +2155,85 @@ export default function MediaInfoReviewPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="media-track-grid">
|
||||
<TrackList
|
||||
title={`Tonspuren (Titel #${title.id})`}
|
||||
tracks={title.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
selectedTrackIds={selectedAudioTrackIds}
|
||||
audioSelector={effectiveAudioSelector}
|
||||
onToggleTrack={(trackId, checked) => {
|
||||
if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onTrackSelectionChange(title.id, 'audio', trackId, checked);
|
||||
}}
|
||||
/>
|
||||
<TrackList
|
||||
title={`Subtitles (Titel #${title.id})`}
|
||||
tracks={allowTrackSelectionForTitle
|
||||
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
|
||||
: subtitleTracks}
|
||||
type="subtitle"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
selectedTrackIds={selectedSubtitleTrackIds}
|
||||
selectedSubtitleVariantSelection={selectedSubtitleVariantSelection}
|
||||
selectedSubtitleLanguageOrder={selectedSubtitleLanguageOrder}
|
||||
onToggleSubtitleVariant={(language, variant, checked) => {
|
||||
if (!allowTrackSelectionForTitle || typeof onSubtitleVariantSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onSubtitleVariantSelectionChange(title.id, language, variant, checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{titleChecked ? (() => {
|
||||
{!compactTitleSelection && useEpisodeTrackAccordion ? (
|
||||
<details className="episode-track-accordion">
|
||||
<summary>Tonspuren und Untertitel</summary>
|
||||
<div className="media-track-grid">
|
||||
<TrackList
|
||||
title={`Tonspuren (Titel #${title.id})`}
|
||||
tracks={title.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
selectedTrackIds={selectedAudioTrackIds}
|
||||
audioSelector={effectiveAudioSelector}
|
||||
onToggleTrack={(trackId, checked) => {
|
||||
if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onTrackSelectionChange(title.id, 'audio', trackId, checked);
|
||||
}}
|
||||
/>
|
||||
<TrackList
|
||||
title={`Subtitles (Titel #${title.id})`}
|
||||
tracks={allowTrackSelectionForTitle
|
||||
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
|
||||
: subtitleTracks}
|
||||
type="subtitle"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
selectedTrackIds={selectedSubtitleTrackIds}
|
||||
selectedSubtitleVariantSelection={selectedSubtitleVariantSelection}
|
||||
selectedSubtitleLanguageOrder={selectedSubtitleLanguageOrder}
|
||||
onToggleSubtitleVariant={(language, variant, checked) => {
|
||||
if (!allowTrackSelectionForTitle || typeof onSubtitleVariantSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onSubtitleVariantSelectionChange(title.id, language, variant, checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
) : (!compactTitleSelection ? (
|
||||
<div className="media-track-grid">
|
||||
<TrackList
|
||||
title={`Tonspuren (Titel #${title.id})`}
|
||||
tracks={title.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
selectedTrackIds={selectedAudioTrackIds}
|
||||
audioSelector={effectiveAudioSelector}
|
||||
onToggleTrack={(trackId, checked) => {
|
||||
if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onTrackSelectionChange(title.id, 'audio', trackId, checked);
|
||||
}}
|
||||
/>
|
||||
<TrackList
|
||||
title={`Subtitles (Titel #${title.id})`}
|
||||
tracks={allowTrackSelectionForTitle
|
||||
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
|
||||
: subtitleTracks}
|
||||
type="subtitle"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
selectedTrackIds={selectedSubtitleTrackIds}
|
||||
selectedSubtitleVariantSelection={selectedSubtitleVariantSelection}
|
||||
selectedSubtitleLanguageOrder={selectedSubtitleLanguageOrder}
|
||||
onToggleSubtitleVariant={(language, variant, checked) => {
|
||||
if (!allowTrackSelectionForTitle || typeof onSubtitleVariantSelectionChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
onSubtitleVariantSelectionChange(title.id, language, variant, checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null)}
|
||||
{!compactTitleSelection && titleChecked ? (() => {
|
||||
const resolvedCommandOutputPath = String(
|
||||
commandOutputPathByTitle?.[normalizeTitleId(title?.id)]
|
||||
|| commandOutputPathByTitle?.[String(normalizeTitleId(title?.id))]
|
||||
|| commandOutputPath
|
||||
|| ''
|
||||
).trim() || null;
|
||||
const commandPreview = buildHandBrakeCommandPreview({
|
||||
review,
|
||||
title,
|
||||
@@ -1770,7 +2241,7 @@ export default function MediaInfoReviewPanel({
|
||||
selectedSubtitleTrackIds,
|
||||
selectedSubtitleVariantSelection,
|
||||
selectedSubtitleLanguageOrder,
|
||||
commandOutputPath,
|
||||
commandOutputPath: resolvedCommandOutputPath,
|
||||
presetOverride: effectivePresetOverride
|
||||
});
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user