import { Button } from 'primereact/button'; import { Dropdown } from 'primereact/dropdown'; function formatDuration(minutes) { const value = Number(minutes || 0); if (!Number.isFinite(value)) { return '-'; } return `${value.toFixed(2)} min`; } function formatBytes(bytes) { const value = Number(bytes || 0); if (!Number.isFinite(value) || value <= 0) { return '-'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = value; let index = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } return `${size.toFixed(2)} ${units[index]}`; } function normalizeTrackId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function normalizePositiveInt(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function formatTwoDigit(value) { const normalized = normalizePositiveInt(value); if (normalized === null) { return '??'; } return String(normalized).padStart(2, '0'); } function buildEpisodeRangeToken(start, end) { const startNormalized = normalizePositiveInt(start); const endNormalized = normalizePositiveInt(end); if (!startNormalized) { return '??'; } if (!endNormalized || endNormalized <= startNormalized) { return formatTwoDigit(startNormalized); } return `${formatTwoDigit(startNormalized)}-${formatTwoDigit(endNormalized)}`; } function buildEpisodePartsToken(start, end) { const startNormalized = normalizePositiveInt(start) || 1; const endNormalized = normalizePositiveInt(end) || startNormalized; const span = Math.max(1, endNormalized - startNormalized + 1); if (span <= 1) { return '1'; } if (span === 2) { return '1+2'; } return `1-${span}`; } function stripEpisodePartSuffix(value) { const source = String(value || '').replace(/\s+/g, ' ').trim(); if (!source) { return ''; } const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)\s*$/i; let normalized = source; let changed = false; for (let i = 0; i < 2; i += 1) { const next = normalized.replace(suffixPattern, '').replace(/[-–—:.,\s]+$/g, '').trim(); if (!next || next === normalized) { break; } normalized = next; changed = true; } return changed ? (normalized || source) : source; } function normalizeEpisodeFillTitle(value, isMulti = false) { const source = String(value || '').replace(/\s+/g, ' ').trim(); if (!source) { return ''; } if (!isMulti) { return source; } return stripEpisodePartSuffix(source) || source; } function resolveTitleDurationSecondsForFill(title = null) { const durationSeconds = Number(title?.durationSeconds || 0); if (Number.isFinite(durationSeconds) && durationSeconds > 0) { return durationSeconds; } const durationMinutes = Number(title?.durationMinutes || 0); if (Number.isFinite(durationMinutes) && durationMinutes > 0) { return durationMinutes * 60; } return 0; } function medianPositiveNumber(values = []) { const numbers = (Array.isArray(values) ? values : []) .map((value) => Number(value)) .filter((value) => Number.isFinite(value) && value > 0) .sort((a, b) => a - b); if (numbers.length === 0) { return 0; } const middle = Math.floor(numbers.length / 2); if (numbers.length % 2 === 1) { return numbers[middle]; } return (numbers[middle - 1] + numbers[middle]) / 2; } function estimateEpisodeSpanForFill(title = null, allTitles = []) { const titleDuration = resolveTitleDurationSecondsForFill(title); if (!Number.isFinite(titleDuration) || titleDuration <= 0) { return 1; } const durations = (Array.isArray(allTitles) ? allTitles : []) .map((item) => resolveTitleDurationSecondsForFill(item)) .filter((value) => Number.isFinite(value) && value > 0); if (durations.length === 0) { return 1; } const medianDuration = medianPositiveNumber(durations); if (!medianDuration) { return 1; } const baselinePool = durations.filter((value) => value <= medianDuration * 1.35); const baselineDuration = medianPositiveNumber(baselinePool.length > 0 ? baselinePool : durations) || medianDuration; if (!baselineDuration) { return 1; } const ratio = titleDuration / baselineDuration; const roundedSpan = Math.round(ratio); if (!Number.isFinite(roundedSpan) || roundedSpan < 2 || roundedSpan > 4) { return 1; } const minRatio = roundedSpan === 2 ? 1.55 : roundedSpan * 0.72; const maxRatio = roundedSpan * 1.32; if (ratio < minRatio || ratio > maxRatio) { return 1; } return roundedSpan; } function normalizeTrackIdList(values) { const list = Array.isArray(values) ? values : []; const seen = new Set(); const output = []; for (const value of list) { const normalized = normalizeTrackId(value); if (normalized === null) { continue; } const key = String(normalized); if (seen.has(key)) { continue; } seen.add(key); output.push(normalized); } return output; } function isBurnedSubtitleTrack(track) { const flags = Array.isArray(track?.subtitlePreviewFlags) ? track.subtitlePreviewFlags : (Array.isArray(track?.flags) ? track.flags : []); const hasBurnedFlag = flags.some((flag) => String(flag || '').trim().toLowerCase() === 'burned'); const summary = `${track?.subtitlePreviewSummary || ''} ${track?.subtitleActionSummary || ''}`; return Boolean( track?.subtitlePreviewBurnIn || track?.burnIn || hasBurnedFlag || /burned/i.test(summary) ); } function isDuplicateSubtitleTrack(track) { return Boolean(track?.duplicate); } 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' ); 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) { if (!input || typeof input !== 'string') { return []; } const args = []; let current = ''; let quote = null; let escaping = false; for (const ch of input) { if (escaping) { current += ch; escaping = false; continue; } if (ch === '\\') { escaping = true; continue; } if (quote) { if (ch === quote) { quote = null; } else { current += ch; } continue; } if (ch === '"' || ch === "'") { quote = ch; continue; } if (/\s/.test(ch)) { if (current.length > 0) { args.push(current); current = ''; } continue; } current += ch; } if (current.length > 0) { args.push(current); } return args; } const AUDIO_SELECTION_KEYS_WITH_VALUE = new Set(['-a', '--audio', '--audio-lang-list']); const AUDIO_SELECTION_KEYS_FLAG_ONLY = new Set(['--all-audio', '--first-audio']); const SUBTITLE_SELECTION_KEYS_WITH_VALUE = new Set(['-s', '--subtitle', '--subtitle-lang-list']); const SUBTITLE_SELECTION_KEYS_FLAG_ONLY = new Set(['--all-subtitles', '--first-subtitle']); const SUBTITLE_FLAG_KEYS_WITH_VALUE = new Set(['--subtitle-burned', '--subtitle-default', '--subtitle-forced']); const TITLE_SELECTION_KEYS_WITH_VALUE = new Set(['-t', '--title']); function removeSelectionArgs(extraArgs) { const args = Array.isArray(extraArgs) ? extraArgs : []; const filtered = []; for (let i = 0; i < args.length; i += 1) { const token = String(args[i] || ''); const key = token.includes('=') ? token.slice(0, token.indexOf('=')) : token; const isAudioWithValue = AUDIO_SELECTION_KEYS_WITH_VALUE.has(key); const isAudioFlagOnly = AUDIO_SELECTION_KEYS_FLAG_ONLY.has(key); const isSubtitleWithValue = SUBTITLE_SELECTION_KEYS_WITH_VALUE.has(key) || SUBTITLE_FLAG_KEYS_WITH_VALUE.has(key); const isSubtitleFlagOnly = SUBTITLE_SELECTION_KEYS_FLAG_ONLY.has(key); const isTitleWithValue = TITLE_SELECTION_KEYS_WITH_VALUE.has(key); const skip = isAudioWithValue || isAudioFlagOnly || isSubtitleWithValue || isSubtitleFlagOnly || isTitleWithValue; if (!skip) { filtered.push(token); continue; } if ((isAudioWithValue || isSubtitleWithValue || isTitleWithValue) && !token.includes('=')) { const nextToken = String(args[i + 1] || ''); if (nextToken && !nextToken.startsWith('-')) { i += 1; } } } return filtered; } function shellQuote(value) { const raw = String(value ?? ''); if (raw.length === 0) { return "''"; } if (/^[A-Za-z0-9_./:=,+-]+$/.test(raw)) { return raw; } 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, selectedAudioTrackIds, selectedSubtitleTrackIds, selectedSubtitleVariantSelection = {}, selectedSubtitleLanguageOrder = [], commandOutputPath = null, presetOverride = null }) { const inputPath = String(title?.filePath || review?.encodeInputPath || '').trim(); const handBrakeCmd = String( review?.selectors?.handbrakeCommand || review?.selectors?.handBrakeCommand || 'HandBrakeCLI' ).trim() || 'HandBrakeCLI'; const preset = presetOverride !== null ? String(presetOverride.handbrakePreset || '').trim() : String(review?.selectors?.preset || '').trim(); const extraArgs = presetOverride !== null ? String(presetOverride.extraArgs || '').trim() : String(review?.selectors?.extraArgs || '').trim(); const rawMappedTitleId = Number( title?.handBrakeTitleId ?? title?.titleIndex ?? title?.id ?? review?.handBrakeTitleId ); const mappedTitleId = Number.isFinite(rawMappedTitleId) && rawMappedTitleId > 0 ? Math.trunc(rawMappedTitleId) : null; 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)); }); const subtitleBurnTrackId = normalizeTrackIdList( selectedSubtitleTracks.filter((track) => Boolean(track?.subtitlePreviewBurnIn || track?.burnIn)).map((track) => track?.id) )[0] || null; const subtitleDefaultTrackId = normalizeTrackIdList( selectedSubtitleTracks.filter((track) => Boolean(track?.subtitlePreviewDefaultTrack || track?.defaultTrack)).map((track) => track?.id) )[0] || null; const sanitizedOutputPath = sanitizeOutputPathForCommand(commandOutputPath); const baseArgs = [ '-i', inputPath || '', '-o', String(sanitizedOutputPath || '').trim() || '' ]; if (mappedTitleId !== null) { baseArgs.push('-t', String(mappedTitleId)); } if (preset) { baseArgs.push('-Z', preset); } const filteredExtra = removeSelectionArgs(splitArgs(extraArgs)); const overrideArgs = [ '-a', normalizeTrackIdList(selectedAudioTrackIds).join(',') || 'none', '-s', subtitleTrackIds.length > 0 ? subtitleTrackIds.join(',') : 'none' ]; if (subtitleBurnTrackId !== null) { overrideArgs.push(`--subtitle-burned=${subtitleBurnTrackId}`); } if (subtitleDefaultTrackId !== null) { overrideArgs.push(`--subtitle-default=${subtitleDefaultTrackId}`); } if (resolvedSubtitleSelection.subtitleForcedTrackIndexes.length > 0) { overrideArgs.push(`--subtitle-forced=${resolvedSubtitleSelection.subtitleForcedTrackIndexes.join(',')}`); } const finalArgs = [...baseArgs, ...filteredExtra, ...overrideArgs]; return `${handBrakeCmd} ${finalArgs.map((arg) => shellQuote(arg)).join(' ')}`; } function toLang2(value) { const raw = String(value || '').trim().toLowerCase(); if (!raw) { return 'und'; } const map = { en: 'en', eng: 'en', de: 'de', deu: 'de', ger: 'de', tr: 'tr', tur: 'tr', fr: 'fr', fra: 'fr', fre: 'fr', es: 'es', spa: 'es', it: 'it', ita: 'it' }; if (map[raw]) { return map[raw]; } if (raw.length === 2) { return raw; } if (raw.length >= 3) { return raw.slice(0, 2); } return raw; } function simplifyCodec(type, value, hint = null) { const raw = String(value || '').trim(); const hintRaw = String(hint || '').trim(); const lower = raw.toLowerCase(); const merged = `${raw} ${hintRaw}`.toLowerCase(); if (!raw) { return '-'; } if (type === 'subtitle') { if (merged.includes('pgs')) { return 'PGS'; } return raw.toUpperCase(); } if (merged.includes('dts-hd ma') || merged.includes('dts hd ma')) { return 'DTS-HD MA'; } if (merged.includes('dts-hd hra') || merged.includes('dts hd hra')) { return 'DTS-HD HRA'; } if (merged.includes('dts-hd') || merged.includes('dts hd')) { return 'DTS-HD'; } if (merged.includes('dts') || merged.includes('dca')) { return 'DTS'; } if (merged.includes('truehd')) { return 'TRUEHD'; } if (merged.includes('e-ac-3') || merged.includes('eac3') || merged.includes('dd+')) { return 'E-AC-3'; } if (merged.includes('ac-3') || merged.includes('ac3') || merged.includes('dolby digital')) { return 'AC-3'; } const numeric = Number(raw); if (Number.isFinite(numeric)) { if (numeric === 262144) { return 'DTS-HD'; } if (numeric === 131072) { return 'DTS'; } } return raw.toUpperCase(); } function extractAudioVariant(hint) { const raw = String(hint || '').trim(); if (!raw) { return ''; } const paren = raw.match(/\(([^)]+)\)/); if (!paren) { return ''; } const parts = paren[1] .split(',') .map((item) => item.trim()) .filter(Boolean); const extras = parts.filter((item) => { const lower = item.toLowerCase(); if (lower.includes('dts') || lower.includes('ac3') || lower.includes('e-ac3') || lower.includes('eac3')) { return false; } if (/\d+(?:\.\d+)?\s*ch/i.test(item)) { return false; } if (/\d+\s*kbps/i.test(lower)) { return false; } return true; }); return extras.join(', '); } function channelCount(rawValue) { const raw = String(rawValue || '').trim().toLowerCase(); if (!raw) { return null; } if (raw.includes('7.1')) { return 8; } if (raw.includes('5.1')) { return 6; } if (raw.includes('stereo') || raw.includes('2.0') || raw.includes('downmix')) { return 2; } if (raw.includes('mono') || raw.includes('1.0')) { return 1; } const numeric = Number(raw); if (Number.isFinite(numeric) && numeric > 0) { if (Math.abs(numeric - 7.1) < 0.2) { return 8; } if (Math.abs(numeric - 5.1) < 0.2) { return 6; } return Math.trunc(numeric); } const match = raw.match(/(\d+)\s*ch/); if (match) { const value = Number(match[1]); return Number.isFinite(value) && value > 0 ? Math.trunc(value) : null; } return null; } function audioChannelLabel(rawValue) { const raw = String(rawValue || '').trim().toLowerCase(); const count = channelCount(rawValue); if (raw.includes('7.1') || count === 8) { return 'Surround 7.1'; } if (raw.includes('5.1') || count === 6) { return 'Surround 5.1'; } if (raw.includes('stereo') || raw.includes('2.0') || raw.includes('downmix') || count === 2) { return 'Stereo'; } if (count === 1) { return 'Mono'; } return ''; } const DEFAULT_AUDIO_FALLBACK_PREVIEW = 'av_aac'; function mapTrackToCopyCodec(track) { const raw = [ track?.codecToken, track?.format, track?.codecName, track?.description, track?.title ] .map((value) => String(value || '').trim().toLowerCase()) .filter(Boolean) .join(' '); if (!raw) { return null; } if (raw.includes('e-ac-3') || raw.includes('eac3') || raw.includes('dd+')) { return 'eac3'; } if (raw.includes('ac-3') || raw.includes('ac3') || raw.includes('dolby digital')) { return 'ac3'; } if (raw.includes('truehd')) { return 'truehd'; } if (raw.includes('dts-hd') || raw.includes('dtshd')) { return 'dtshd'; } if (raw.includes('dca') || raw.includes('dts')) { return 'dts'; } if (raw.includes('aac')) { return 'aac'; } if (raw.includes('flac')) { return 'flac'; } if (raw.includes('mp3') || raw.includes('mpeg audio')) { return 'mp3'; } if (raw.includes('opus')) { return 'opus'; } if (raw.includes('pcm') || raw.includes('lpcm')) { return 'lpcm'; } return null; } function resolveAudioEncoderPreviewLabel(track, encoderToken, copyMask, fallbackEncoder) { const normalizedToken = String(encoderToken || '').trim().toLowerCase(); if (!normalizedToken || normalizedToken === 'preset-default') { return 'Preset-Default (HandBrake)'; } if (normalizedToken.startsWith('copy')) { const sourceCodec = mapTrackToCopyCodec(track); const explicitCopyCodec = normalizedToken.includes(':') ? normalizedToken.split(':').slice(1).join(':').trim().toLowerCase() : null; const normalizedCopyMask = Array.isArray(copyMask) ? copyMask.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; let canCopy = false; let effectiveCodec = sourceCodec; if (explicitCopyCodec) { canCopy = Boolean(sourceCodec && sourceCodec === explicitCopyCodec); } else if (sourceCodec && normalizedCopyMask.length > 0) { canCopy = normalizedCopyMask.includes(sourceCodec); // DTS-HD MA contains an embedded DTS core. When dtshd is not in the copy // mask but dts is, HandBrake will extract and copy the DTS core layer. if (!canCopy && sourceCodec === 'dtshd' && normalizedCopyMask.includes('dts')) { canCopy = true; effectiveCodec = 'dts'; } } if (canCopy) { return `Copy (${effectiveCodec || track?.format || 'Quelle'})`; } const fallback = String(fallbackEncoder || DEFAULT_AUDIO_FALLBACK_PREVIEW).trim().toLowerCase() || DEFAULT_AUDIO_FALLBACK_PREVIEW; return `Fallback Transcode (${fallback})`; } return `Transcode (${normalizedToken})`; } function parseAudioSelectorFromArgs(extraArgsString, baseSelector) { const base = baseSelector && typeof baseSelector === 'object' ? baseSelector : {}; const args = String(extraArgsString || '').trim(); if (!args) { return base; } // Tokenize: split on whitespace but respect quoted strings const tokens = []; let current = ''; let inQuote = null; for (let i = 0; i < args.length; i++) { const ch = args[i]; if (inQuote) { if (ch === inQuote) { inQuote = null; } else { current += ch; } } else if (ch === '"' || ch === "'") { inQuote = ch; } else if (ch === ' ' || ch === '\t') { if (current) { tokens.push(current); current = ''; } } else { current += ch; } } if (current) { tokens.push(current); } const result = { ...base }; const getNext = (i) => (i + 1 < tokens.length ? tokens[i + 1] : null); for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; // Support both --flag=value and --flag value const eqIdx = token.indexOf('='); const flag = eqIdx !== -1 ? token.slice(0, eqIdx) : token; const inlineVal = eqIdx !== -1 ? token.slice(eqIdx + 1) : null; const getValue = () => { if (inlineVal !== null) return inlineVal; const next = getNext(i); if (next && !next.startsWith('-')) { i++; return next; } return null; }; if (flag === '--aencoder') { const val = getValue(); if (val) { result.encoders = val.split(',').map((s) => s.trim()).filter(Boolean); result.encoderSource = 'args'; } } else if (flag === '--audio-copy-mask') { const val = getValue(); if (val) { result.copyMask = val.split(',').map((s) => s.trim()).filter(Boolean); } } else if (flag === '--audio-fallback') { const val = getValue(); if (val) { result.fallbackEncoder = val.trim(); } } } return result; } function buildAudioActionPreviewSummary(track, selectedIndex, audioSelector) { const selector = audioSelector && typeof audioSelector === 'object' ? audioSelector : {}; const availableEncoders = Array.isArray(selector.encoders) ? selector.encoders : []; let encoderPlan = []; if (selector.encoderSource === 'args' && availableEncoders.length > 0) { const safeIndex = Number.isFinite(selectedIndex) && selectedIndex >= 0 ? selectedIndex : 0; encoderPlan = [availableEncoders[Math.min(safeIndex, availableEncoders.length - 1)]]; } else if (availableEncoders.length > 0) { encoderPlan = [...availableEncoders]; } else { encoderPlan = ['preset-default']; } const labels = encoderPlan .map((token) => resolveAudioEncoderPreviewLabel(track, token, selector.copyMask, selector.fallbackEncoder)) .filter(Boolean); return labels.join(' + ') || 'Übernehmen'; } function TrackList({ title, tracks, 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 (
); }; 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 (

{title}

{!tracks || tracks.length === 0 ? (

Keine Einträge.

) : (
{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 (
{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 })} Untertitel: {actionInfo}
); } 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 (
{`#${track.id} | ${displayLanguage}`}
{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 })} Untertitel: {actionInfo}
); } const checked = allowSelection ? Boolean(languageSelection.full) : Boolean(track?.selectedForEncode); const disabled = !allowSelection || burned; const actionInfo = resolveSubtitleActionInfo(track, checked); return (
{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})` })} Untertitel: {actionInfo}
); })}
)}
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. Confidence-Index (nur für Automatische Übersetzungen): high = explizites Track-Signal erkannt, medium = "forced" im Titel erkannt, low = heuristische Einschätzung.
); } const selectedIds = normalizeTrackIdList(selectedTrackIds); const checkedTrackOrder = orderedTracks .map((track) => normalizeTrackId(track?.id)) .filter((trackId, index) => { if (trackId === null) { return false; } if (allowSelection) { return selectedIds.includes(trackId); } const track = orderedTracks[index]; return Boolean(track?.selectedForEncode); }); return (

{title}

{!tracks || tracks.length === 0 ? (

Keine Einträge.

) : (
{orderedTracks.map((track) => { const trackId = normalizeTrackId(track.id); const checked = allowSelection ? (trackId !== null && selectedIds.includes(trackId)) : Boolean(track.selectedForEncode); const selectedIndex = trackId !== null ? checkedTrackOrder.indexOf(trackId) : -1; const actionInfo = type === 'audio' ? (checked ? (() => { // When encoder comes from user preset extraArgs, always recompute // (server-stored summary reflects a different/default preset) if (audioSelector?.encoderSource === 'args') { return buildAudioActionPreviewSummary(track, selectedIndex, audioSelector); } const base = String(track.encodePreviewSummary || track.encodeActionSummary || '').trim(); const staleUnselectedSummary = /^nicht übernommen$/i.test(base); if (staleUnselectedSummary) { return buildAudioActionPreviewSummary(track, selectedIndex, audioSelector); } return base || buildAudioActionPreviewSummary(track, selectedIndex, audioSelector); })() : 'Nicht übernommen') : 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; let displayText = `#${track.id} | ${displayLanguage} | ${displayCodec}`; if (type === 'audio') { if (displayChannelCount !== null) { displayText += ` | ${displayChannelCount}ch`; } if (displayAudioTitle) { displayText += ` | ${displayAudioTitle}`; } if (audioVariant) { displayText += ` | ${audioVariant}`; } } return (
{actionInfo ? Encode: {actionInfo} : null}
); })}
)}
); } function normalizeTitleId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function normalizeScriptId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function normalizeChainId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function normalizeScriptIdList(values) { const list = Array.isArray(values) ? values : []; const seen = new Set(); const output = []; for (const value of list) { const normalized = normalizeScriptId(value); if (normalized === null) { continue; } const key = String(normalized); if (seen.has(key)) { continue; } seen.add(key); output.push(normalized); } return output; } 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 = [], postEncodeItems = [], allowEncodeItemSelection = false, onAddPreEncodeItem = null, onChangePreEncodeItem = null, onRemovePreEncodeItem = null, onReorderPreEncodeItem = null, onAddPostEncodeItem = null, onChangePostEncodeItem = null, onRemovePostEncodeItem = null, onReorderPostEncodeItem = null, userPresets = [], selectedUserPresetId = null, onUserPresetChange = null, selectedHandBrakePreset = '' }) { if (!review) { return

Keine Mediainfo-Daten vorhanden.

; } const titles = review.titles || []; 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 episodeIdStart = Number(episode?.episodeIdStart ?? episode?.idStart ?? episode?.episodeId ?? episode?.id ?? 0); const episodeIdEnd = Number(episode?.episodeIdEnd ?? episode?.idEnd ?? 0); const episodeNumberStart = Number( episode?.episodeNumberStart ?? episode?.numberStart ?? episode?.episodeNoStart ?? episode?.episodeNumber ?? episode?.number ?? 0 ); const episodeNumberEnd = Number( episode?.episodeNumberEnd ?? episode?.numberEnd ?? episode?.episodeNoEnd ?? 0 ); const episodeSpan = Number(episode?.episodeSpan ?? episode?.episodeCount ?? 0); const episodeNumber = Number(episode?.episodeNumber ?? episode?.number ?? episodeNumberStart ?? 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, episodeIdStart: Number.isFinite(episodeIdStart) && episodeIdStart > 0 ? Math.trunc(episodeIdStart) : null, episodeIdEnd: Number.isFinite(episodeIdEnd) && episodeIdEnd > 0 ? Math.trunc(episodeIdEnd) : null, episodeNumber: Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null, episodeNumberStart: Number.isFinite(episodeNumberStart) && episodeNumberStart > 0 ? Math.trunc(episodeNumberStart) : null, episodeNumberEnd: Number.isFinite(episodeNumberEnd) && episodeNumberEnd > 0 ? Math.trunc(episodeNumberEnd) : null, episodeSpan: Number.isFinite(episodeSpan) && episodeSpan > 0 ? Math.trunc(episodeSpan) : null, seasonNumber: Number.isFinite(seasonNumber) && seasonNumber > 0 ? Math.trunc(seasonNumber) : null, episodeTitle: episodeTitle || null }; }) .filter((episode) => episode.episodeId || episode.episodeNumber !== null); const selectedTitlesForFill = selectedTitleIds.length > 0 ? displayTitles.filter((title) => selectedTitleIdSet.has(String(normalizeTitleId(title?.id)))) : displayTitles.filter((title) => Boolean(title?.selectedForEncode)); const titleAnchorsByEpisodeIndex = (() => { const anchors = new Map(); const rows = Array.isArray(normalizedEpisodeRows) ? normalizedEpisodeRows : []; if (rows.length === 0) { return anchors; } let cursor = 0; for (const title of selectedTitlesForFill) { if (cursor >= rows.length) { break; } const explicitSpan = normalizePositiveInt( title?.episodeSpan ?? title?.episodeCount ?? null ); const estimatedSpan = estimateEpisodeSpanForFill(title, selectedTitlesForFill); const span = Math.max(1, explicitSpan || 1, estimatedSpan || 1); const safeSpan = Math.max(1, Math.min(span, rows.length - cursor)); anchors.set(cursor, { span: safeSpan }); cursor += safeSpan; } return anchors; })(); const fallbackEpisodeFillOptions = (() => { const rows = Array.isArray(normalizedEpisodeRows) ? normalizedEpisodeRows : []; const options = []; let index = 0; while (index < rows.length) { const startRow = rows[index]; const seasonNumber = normalizePositiveInt(startRow?.seasonNumber ?? null); const seasonLabel = seasonNumber !== null ? `S${String(seasonNumber).padStart(2, '0')}` : 'S??'; const rangeStart = normalizePositiveInt( startRow?.episodeNumberStart ?? startRow?.episodeNoStart ?? startRow?.episodeNumber ?? null ); const explicitRangeEnd = normalizePositiveInt( startRow?.episodeNumberEnd ?? startRow?.episodeNoEnd ?? null ); const explicitRangeSpan = normalizePositiveInt( startRow?.episodeSpan ?? startRow?.episodeCount ?? null ); let consumedRows = 1; let rangeEnd = explicitRangeEnd || rangeStart; if ( rangeStart && ( (explicitRangeEnd && explicitRangeEnd > rangeStart) || (explicitRangeSpan && explicitRangeSpan > 1) ) ) { const effectiveSpan = explicitRangeEnd && explicitRangeEnd > rangeStart ? Math.max(1, explicitRangeEnd - rangeStart + 1) : Math.max(1, explicitRangeSpan || 1); rangeEnd = rangeStart + effectiveSpan - 1; consumedRows = 1; } else { const anchor = titleAnchorsByEpisodeIndex.get(index) || null; const anchorSpan = normalizePositiveInt(anchor?.span) || 1; if (anchorSpan > 1) { const endIndex = Math.min(rows.length - 1, index + anchorSpan - 1); const endRow = rows[endIndex]; const endRowStart = normalizePositiveInt( endRow?.episodeNumberStart ?? endRow?.episodeNoStart ?? endRow?.episodeNumber ?? null ); rangeEnd = endRowStart || (rangeStart ? (rangeStart + anchorSpan - 1) : null); consumedRows = anchorSpan; } } if (rangeStart && rangeEnd && rangeEnd < rangeStart) { rangeEnd = rangeStart; } const isMulti = Boolean(rangeStart && rangeEnd && rangeEnd > rangeStart); const episodeRangeToken = buildEpisodeRangeToken(rangeStart, rangeEnd); const partsToken = buildEpisodePartsToken(rangeStart, rangeEnd); const baseTitle = String(startRow?.episodeTitle || '(ohne Titel)').trim() || '(ohne Titel)'; const normalizedTitle = normalizeEpisodeFillTitle(baseTitle, isMulti); const titleLabel = isMulti ? `${normalizedTitle} (Teil ${partsToken})` : normalizedTitle; const episodeLabel = rangeStart ? `E${episodeRangeToken}` : 'E??'; const value = String( startRow?.episodeIdStart || startRow?.episodeId || rangeStart || '' ); if (value) { options.push({ label: `${seasonLabel}${episodeLabel} - ${titleLabel}`, value, startRef: value }); } index += Math.max(1, consumedRows); } return options; })(); const episodeFillOptions = fallbackEpisodeFillOptions; const selectedEpisodeFillValue = (() => { const raw = String(selectedEpisodeFillStart || '').trim(); if (!raw) { return null; } const direct = episodeFillOptions.find((option) => String(option?.value || '').trim() === raw); if (direct) { return direct.value; } const byStartRef = episodeFillOptions.find((option) => String(option?.startRef || '').trim() === raw); if (byStartRef) { return byStartRef.value; } const tokenMatch = raw.match(/^fill:([^:]+):title:\d+$/i); if (tokenMatch && tokenMatch[1]) { const byTokenRef = episodeFillOptions.find((option) => String(option?.startRef || '').trim() === String(tokenMatch[1]).trim()); if (byTokenRef) { return byTokenRef.value; } } return null; })(); 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)'; // User preset resolution const normalizedUserPresets = Array.isArray(userPresets) ? userPresets : []; const selectedUserPreset = selectedUserPresetId ? normalizedUserPresets.find((p) => Number(p.id) === Number(selectedUserPresetId)) || null : null; const hasUserPresets = normalizedUserPresets.length > 0; const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection; const isUserPresetActive = Boolean(selectedUserPreset); const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim(); const isHandBrakePresetActive = !isUserPresetActive && Boolean(trimmedHandBrakePreset); const effectivePresetOverride = isUserPresetActive ? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' } : (isHandBrakePresetActive ? { handbrakePreset: trimmedHandBrakePreset, extraArgs: '' } : null); const effectiveAudioSelector = effectivePresetOverride?.extraArgs ? parseAudioSelectorFromArgs(effectivePresetOverride.extraArgs, review?.selectors?.audio) : (review?.selectors?.audio || null); const presetSourceLabel = isUserPresetActive ? `User-Preset: ${selectedUserPreset?.name || 'Preset'}` : (isHandBrakePresetActive ? 'HandBrake-Preset' : ''); const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : []) .map((item) => ({ id: normalizeScriptId(item?.id), name: String(item?.name || '').trim() })) .filter((item) => item.id !== null && item.name.length > 0); const scriptById = new Map(scriptCatalog.map((item) => [item.id, item])); const chainCatalog = (Array.isArray(availableChains) ? availableChains : []) .map((item) => ({ id: normalizeChainId(item?.id), name: String(item?.name || '').trim() })) .filter((item) => item.id !== null && item.name.length > 0); const chainById = new Map(chainCatalog.map((item) => [item.id, item])); const makeHandleDrop = (items, onReorder) => (event, targetIndex) => { if (!allowEncodeItemSelection || typeof onReorder !== 'function' || items.length < 2) { return; } event.preventDefault(); const fromIndex = Number(event.dataTransfer?.getData('text/plain')); if (!Number.isInteger(fromIndex)) { return; } onReorder(fromIndex, targetIndex); }; 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 (
{!compactTitleSelection && allowUserPresetSelection && (
({ label: `${p.name}${p.mediaType !== 'all' ? ` [${p.mediaType === 'bluray' ? 'Blu-ray' : p.mediaType === 'dvd' ? 'DVD' : 'Sonstiges'}]` : ''}`, value: Number(p.id) })) ]} onChange={(e) => onUserPresetChange(e.value)} placeholder="(Einstellungen-Fallback)" style={{ width: '100%' }} /> {selectedUserPreset && (
{selectedUserPreset.handbrakePreset ? -Z {selectedUserPreset.handbrakePreset} : (kein Preset-Name)} {selectedUserPreset.extraArgs && ( Args: {selectedUserPreset.extraArgs} )} {selectedUserPreset.description && ( {selectedUserPreset.description} )}
)}
)} {!compactTitleSelection ? (
Preset:{' '} {effectivePresetOverride ? (effectivePresetOverride.handbrakePreset || '(kein Preset)') : presetLabel} {effectivePresetOverride && presetSourceLabel && ( ({presetSourceLabel}) )}
Extra Args:{' '} {effectivePresetOverride ? (effectivePresetOverride.extraArgs || '(keine)') : (review.selectors?.extraArgs || '(keine)')} {effectivePresetOverride && isUserPresetActive && !selectedUserPreset?.extraArgs && ( (aus User-Preset) )}
Preset-Profil: {effectivePresetOverride ? (isUserPresetActive ? 'user-preset' : 'handbrake-preset') : (review.selectors?.presetProfileSource || '-')}
MIN_LENGTH_MINUTES: {review.minLengthMinutes}
Encode Input: {encodeInputTitle?.fileName || '-'}
Audio Auswahl: {effectiveAudioSelector?.mode || '-'}
Audio Encoder: {(effectiveAudioSelector?.encoders || []).join(', ') || 'Preset-Default'}
Audio Copy-Mask: {(effectiveAudioSelector?.copyMask || []).join(', ') || '-'}
Audio Fallback: {effectiveAudioSelector?.fallbackEncoder || '-'}
Subtitle Auswahl: {review.selectors?.subtitle?.mode || '-'}
Subtitle Flags: {review.selectors?.subtitle?.burnBehavior === 'first' ? 'burned(first)' : '-'}
) : null} {!compactTitleSelection && review.partial ? ( Zwischenstand: {processedFiles}/{totalFiles} Datei(en) analysiert. ) : null} {!compactTitleSelection && playlistRecommendation ? (
Empfehlung: {playlistRecommendation.playlistFile || '-'} {playlistRecommendation.reviewTitleId ? ` (Titel #${playlistRecommendation.reviewTitleId})` : ''} {playlistRecommendation.reason ? {playlistRecommendation.reason} : null}
) : null} {!compactTitleSelection && Array.isArray(review.notes) && review.notes.length > 0 ? (
{review.notes.map((note, idx) => ( {note} ))}
) : null} {/* Pre-Encode Items (scripts + chains unified) */} {!compactTitleSelection && (allowEncodeItemSelection || preEncodeItems.length > 0) ? (

Pre-Encode Ausführungen (optional)

{scriptCatalog.length === 0 && chainCatalog.length === 0 ? ( Keine Skripte oder Ketten konfiguriert. In den Settings anlegen. ) : null} {preEncodeItems.length === 0 ? ( Keine Pre-Encode Ausführungen ausgewählt. ) : null} {preEncodeItems.map((item, rowIndex) => { const isScript = item.type === 'script'; const canDrag = allowEncodeItemSelection && preEncodeItems.length > 1; const scriptObj = isScript ? (scriptById.get(normalizeScriptId(item.id)) || null) : null; const chainObj = !isScript ? (chainById.get(Number(item.id)) || null) : null; const name = isScript ? (scriptObj?.name || `Skript #${item.id}`) : (chainObj?.name || `Kette #${item.id}`); const usedScriptIds = new Set( preEncodeItems .filter((it, i) => it.type === 'script' && i !== rowIndex) .map((it) => normalizeScriptId(it.id)) .filter((id) => id !== null) .map((id) => String(id)) ); const usedChainIds = new Set( preEncodeItems .filter((it, i) => it.type === 'chain' && i !== rowIndex) .map((it) => normalizeChainId(it.id)) .filter((id) => id !== null) .map((id) => String(id)) ); const scriptOptions = scriptCatalog.map((s) => ({ label: s.name, value: s.id, disabled: usedScriptIds.has(String(s.id)) })); const chainOptions = chainCatalog.map((c) => ({ label: c.name, value: c.id, disabled: usedChainIds.has(String(c.id)) })); return (
{ if (!canDrag) return; event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; }} onDrop={(event) => handlePreDrop(event, rowIndex)} > {allowEncodeItemSelection ? ( <> { if (!canDrag) return; event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', String(rowIndex)); }} /> {isScript ? ( onChangePreEncodeItem?.(rowIndex, 'script', event.value)} className="full-width" /> ) : ( onChangePreEncodeItem?.(rowIndex, 'chain', event.value)} className="full-width" /> )}
); })} {allowEncodeItemSelection ? (
{scriptCatalog.length > preEncodeItems.filter((i) => i.type === 'script').length ? (
) : null} Ausführung vor dem Encoding, strikt nacheinander. Bei Fehler wird der Encode abgebrochen.
) : null} {/* Post-Encode Items (scripts + chains unified) */} {!compactTitleSelection ? (

Post-Encode Ausführungen (optional)

{scriptCatalog.length === 0 && chainCatalog.length === 0 ? ( Keine Skripte oder Ketten konfiguriert. In den Settings anlegen. ) : null} {postEncodeItems.length === 0 ? ( Keine Post-Encode Ausführungen ausgewählt. ) : null} {postEncodeItems.map((item, rowIndex) => { const isScript = item.type === 'script'; const canDrag = allowEncodeItemSelection && postEncodeItems.length > 1; const scriptObj = isScript ? (scriptById.get(normalizeScriptId(item.id)) || null) : null; const chainObj = !isScript ? (chainById.get(Number(item.id)) || null) : null; const name = isScript ? (scriptObj?.name || `Skript #${item.id}`) : (chainObj?.name || `Kette #${item.id}`); const usedScriptIds = new Set( postEncodeItems .filter((it, i) => it.type === 'script' && i !== rowIndex) .map((it) => normalizeScriptId(it.id)) .filter((id) => id !== null) .map((id) => String(id)) ); const usedChainIds = new Set( postEncodeItems .filter((it, i) => it.type === 'chain' && i !== rowIndex) .map((it) => normalizeChainId(it.id)) .filter((id) => id !== null) .map((id) => String(id)) ); const scriptOptions = scriptCatalog.map((s) => ({ label: s.name, value: s.id, disabled: usedScriptIds.has(String(s.id)) })); const chainOptions = chainCatalog.map((c) => ({ label: c.name, value: c.id, disabled: usedChainIds.has(String(c.id)) })); return (
{ if (!canDrag) return; event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; }} onDrop={(event) => handlePostDrop(event, rowIndex)} > {allowEncodeItemSelection ? ( <> { if (!canDrag) return; event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', String(rowIndex)); }} /> {isScript ? ( onChangePostEncodeItem?.(rowIndex, 'script', event.value)} className="full-width" /> ) : ( onChangePostEncodeItem?.(rowIndex, 'chain', event.value)} className="full-width" /> )}
); })} {allowEncodeItemSelection ? (
{scriptCatalog.length > postEncodeItems.filter((i) => i.type === 'script').length ? (
) : null} Ausführung nach erfolgreichem Encode, strikt nacheinander (Drag-and-Drop möglich).
) : null}

Titel

{episodeFillOptions.length > 0 ? (
onEpisodeFillStartChange?.(event?.value || null)} placeholder="Episode auswählen …" disabled={!allowTitleSelection} style={{ width: '100%' }} filter showClear /> Startet bei der gewählten Episode und befüllt alle ausgewählten DVD-Titel der Reihe nach. Multi-Episoden werden als Bereich (z.B. E16-17 | Teil 1+2) angezeigt.
) : null} {showGlobalEpisodeTrackControls ? (

Spurauswahl für alle Episoden

Gilt für alle aktuell gewählten Episoden. Einzelne Episoden können danach weiterhin manuell überschrieben werden.
{globalTrackCompatibility.audio && globalMasterSelectionState ? ( { 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); } }} /> ) : (

Tonspuren (alle Episoden)

Globale Auswahl nicht verfügbar, da die Tonspur-Layouts zwischen Episoden abweichen.
)} {globalTrackCompatibility.subtitle && globalMasterSelectionState ? ( !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); } }} /> ) : (

Subtitles (alle Episoden)

Globale Auswahl nicht verfügbar, da die Untertitel-Layouts zwischen Episoden abweichen.
)}
) : null}
{displayTitles.length === 0 ? (

Keine Titel analysiert.

) : displayTitles.map((title) => { const normalizedTitleId = normalizeTitleId(title.id); const titleChecked = allowTitleSelection ? (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)) .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 ); const allowTrackSelectionForTitle = Boolean( allowTrackSelection && allowTitleSelection && titleChecked ); return (
{titleChecked ? (
{ if (!allowTitleSelection || typeof onEpisodeAssignmentChange !== 'function') { return; } onEpisodeAssignmentChange(normalizedTitleId, { episodeTitle: event?.target?.value || '' }); }} placeholder="Episodentitel" disabled={!allowTitleSelection} />
) : null} {title.playlistFile || title.playlistEvaluationLabel || title.playlistSegmentCommand ? (
Playlist: {title.playlistFile || '-'} {title.playlistRecommended ? ' | empfohlen' : ''} {title.playlistEvaluationLabel ? ( Bewertung: {title.playlistEvaluationLabel} ) : null} {title.playlistSegmentCommand ? ( Analyse-Command: {title.playlistSegmentCommand} ) : null} {Array.isArray(title.playlistSegmentFiles) && title.playlistSegmentFiles.length > 0 ? (
Segment-Dateien anzeigen ({title.playlistSegmentFiles.length})
{title.playlistSegmentFiles.join('\n')}
) : ( Segment-Ausgabe: keine m2ts-Einträge gefunden. )}
) : null} {!compactTitleSelection && useEpisodeTrackAccordion ? (
Tonspuren und Untertitel
{ if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') { return; } onTrackSelectionChange(title.id, 'audio', trackId, checked); }} /> !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); }} />
) : (!compactTitleSelection ? (
{ if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') { return; } onTrackSelectionChange(title.id, 'audio', trackId, checked); }} /> !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); }} />
) : null)} {!compactTitleSelection && titleChecked ? (() => { const resolvedCommandOutputPath = String( commandOutputPathByTitle?.[normalizeTitleId(title?.id)] || commandOutputPathByTitle?.[String(normalizeTitleId(title?.id))] || commandOutputPath || '' ).trim() || null; const commandPreview = buildHandBrakeCommandPreview({ review, title, selectedAudioTrackIds, selectedSubtitleTrackIds, selectedSubtitleVariantSelection, selectedSubtitleLanguageOrder, commandOutputPath: resolvedCommandOutputPath, presetOverride: effectivePresetOverride }); return (
Finaler HandBrakeCLI-Befehl (Preview):
{commandPreview}
); })() : null}
); })}
); }