diff --git a/backend/package-lock.json b/backend/package-lock.json index 282905d..81e2da2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 397446b..aadd6a9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index 3c37ca5..d13e58c 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -107,6 +107,16 @@ function nowIso() { return new Date().toISOString(); } +function appendLinesInPlace(target, lines) { + if (!Array.isArray(target) || !Array.isArray(lines) || lines.length === 0) { + return target; + } + for (const line of lines) { + target.push(line); + } + return target; +} + function normalizeLifecycleJobState(value) { return String(value || '').trim().toUpperCase(); } @@ -9637,8 +9647,6 @@ function applyPreviousSelectionDefaultsToReviewPlan(reviewPlan, previousPlan = n const postEncodeScriptIds = normalizeScriptIdList(previousPlan?.postEncodeScriptIds || []); const preEncodeChainIds = normalizeChainIdList(previousPlan?.preEncodeChainIds || []); const postEncodeChainIds = normalizeChainIdList(previousPlan?.postEncodeChainIds || []); - const userPreset = normalizeUserPresetForPlan(previousPlan?.userPreset || null); - nextPlan = { ...nextPlan, preEncodeScriptIds, @@ -9647,7 +9655,7 @@ function applyPreviousSelectionDefaultsToReviewPlan(reviewPlan, previousPlan = n postEncodeScripts: buildScriptDescriptorList(postEncodeScriptIds, previousPlan?.postEncodeScripts || []), preEncodeChainIds, postEncodeChainIds, - userPreset, + userPreset: null, reviewConfirmed: false, reviewConfirmedAt: null, prefilledFromPreviousRun: true, @@ -9659,8 +9667,7 @@ function applyPreviousSelectionDefaultsToReviewPlan(reviewPlan, previousPlan = n || preEncodeScriptIds.length > 0 || postEncodeScriptIds.length > 0 || preEncodeChainIds.length > 0 - || postEncodeChainIds.length > 0 - || Boolean(userPreset); + || postEncodeChainIds.length > 0; return { plan: nextPlan, @@ -9670,7 +9677,7 @@ function applyPreviousSelectionDefaultsToReviewPlan(reviewPlan, previousPlan = n postEncodeScriptCount: postEncodeScriptIds.length, preEncodeChainCount: preEncodeChainIds.length, postEncodeChainCount: postEncodeChainIds.length, - userPresetApplied: Boolean(userPreset) + userPresetApplied: false }; } @@ -18149,7 +18156,7 @@ class PipelineService extends EventEmitter { source: 'HANDBRAKE_SCAN', silent: !this.isPrimaryJob(jobId) }); - lines.push(...pluginScan.scanLines); + appendLinesInPlace(lines, pluginScan.scanLines); runInfo = pluginScan.runInfo || null; sourceArg = pluginScan.sourceArg || null; reviewPluginExecution = this.mergePluginExecutionState( @@ -18514,7 +18521,7 @@ class PipelineService extends EventEmitter { source: 'HANDBRAKE_SCAN_PLAYLIST_MAP', silent: !this.isPrimaryJob(jobId) }); - resolveScanLines.push(...pluginScan.scanLines); + appendLinesInPlace(resolveScanLines, pluginScan.scanLines); reviewPluginExecution = this.mergePluginExecutionState( reviewPluginExecution, pluginScan.pluginExecution @@ -18656,7 +18663,7 @@ class PipelineService extends EventEmitter { source: 'HANDBRAKE_SCAN_PLAYLIST_MAP', silent: !this.isPrimaryJob(jobId) }); - scanLines.push(...pluginScan.scanLines); + appendLinesInPlace(scanLines, pluginScan.scanLines); reviewPluginExecution = this.mergePluginExecutionState( reviewPluginExecution, pluginScan.pluginExecution @@ -19315,8 +19322,15 @@ class PipelineService extends EventEmitter { source: 'HANDBRAKE_SCAN_PLAYLIST_MAP', silent: !this.isPrimaryJob(jobId) }); - resolveScanLines.push(...pluginScan.scanLines); + appendLinesInPlace(resolveScanLines, pluginScan.scanLines); handBrakeResolveRunInfo = pluginScan.runInfo || null; + const resolveScanStatus = String(handBrakeResolveRunInfo?.status || '').trim().toUpperCase(); + if (resolveScanStatus === 'CANCELLED') { + const error = new Error('HandBrake Playlist-Mapping wurde abgebrochen.'); + error.statusCode = 409; + error.runInfo = handBrakeResolveRunInfo; + throw error; + } reviewPluginExecution = this.mergePluginExecutionState( reviewPluginExecution, pluginScan.pluginExecution @@ -19328,6 +19342,16 @@ class PipelineService extends EventEmitter { error.runInfo = handBrakeResolveRunInfo; throw error; } + const knownPlaylists = listAvailableHandBrakePlaylists(resolveScanJson); + if (knownPlaylists.length === 0 && resolveScanStatus && resolveScanStatus !== 'SUCCESS') { + const error = new Error( + `HandBrake Playlist-Mapping unvollständig (${resolveScanStatus}). ` + + 'Scan enthält keine erkennbaren Playlist-IDs.' + ); + error.statusCode = resolveScanStatus === 'CANCELLED' ? 409 : 502; + error.runInfo = handBrakeResolveRunInfo; + throw error; + } resolvedHandBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(resolveScanJson, resolvedPlaylistId, { expectedMakemkvTitleId: selectedTitleForReview, @@ -19336,7 +19360,6 @@ class PipelineService extends EventEmitter { playlistAliases: playlistAliasesForResolve }); if (!resolvedHandBrakeTitleId) { - const knownPlaylists = listAvailableHandBrakePlaylists(resolveScanJson); const error = new Error( `Kein HandBrake-Titel für ${toPlaylistFile(resolvedPlaylistId)} gefunden.` + ` ${knownPlaylists.length > 0 ? `Scan-Playlists: ${knownPlaylists.map((id) => `${id}.mpls`).join(', ')}` : 'Scan enthält keine erkennbaren Playlist-IDs.'}` @@ -22781,6 +22804,10 @@ class PipelineService extends EventEmitter { : ''; const hasExplicitUserPresetSelection = !multipartSettingsLocked && explicitUserPresetId !== null; const hasExplicitHandBrakePresetSelection = !multipartSettingsLocked && Boolean(rawHandBrakePreset); + const explicitPresetClearRequested = !multipartSettingsLocked + && (hasUserPresetIdField || hasHandBrakePresetField) + && explicitUserPresetId === null + && !rawHandBrakePreset; let resolvedUserPreset = null; if (hasExplicitUserPresetSelection) { resolvedUserPreset = await userPresetService.getPresetById(explicitUserPresetId); @@ -22799,7 +22826,7 @@ class PipelineService extends EventEmitter { } : null; } - if (!resolvedUserPreset) { + if (!resolvedUserPreset && !explicitPresetClearRequested) { resolvedUserPreset = normalizeUserPresetForPlan( planForConfirm?.userPreset || encodePlan?.userPreset || null ); @@ -22833,20 +22860,24 @@ class PipelineService extends EventEmitter { const requiresExplicitPreset = readyMediaProfile === 'dvd' || readyMediaProfile === 'bluray' || (readyMediaProfile === 'converter' && confirmedConverterMediaType !== 'audio'); - const planPresetName = String( - planForConfirm?.userPreset?.handbrakePreset - || encodePlan?.userPreset?.handbrakePreset - || planForConfirm?.selectors?.preset - || encodePlan?.selectors?.preset - || '' - ).trim(); - const planExtraArgs = String( - planForConfirm?.userPreset?.extraArgs - || encodePlan?.userPreset?.extraArgs - || planForConfirm?.selectors?.extraArgs - || encodePlan?.selectors?.extraArgs - || '' - ).trim(); + const planPresetName = explicitPresetClearRequested + ? '' + : String( + planForConfirm?.userPreset?.handbrakePreset + || encodePlan?.userPreset?.handbrakePreset + || planForConfirm?.selectors?.preset + || encodePlan?.selectors?.preset + || '' + ).trim(); + const planExtraArgs = explicitPresetClearRequested + ? '' + : String( + planForConfirm?.userPreset?.extraArgs + || encodePlan?.userPreset?.extraArgs + || planForConfirm?.selectors?.extraArgs + || encodePlan?.selectors?.extraArgs + || '' + ).trim(); const effectivePresetName = String( resolvedUserPreset?.handbrakePreset || rawHandBrakePreset @@ -23800,7 +23831,7 @@ class PipelineService extends EventEmitter { source: 'HANDBRAKE_SCAN_DVD', silent: !this.isPrimaryJob(jobId) }); - dvdScanLines.push(...pluginScan.scanLines); + appendLinesInPlace(dvdScanLines, pluginScan.scanLines); dvdScanRunInfo = pluginScan.runInfo || null; reviewPluginExecution = this.mergePluginExecutionState( reviewPluginExecution, @@ -24548,7 +24579,7 @@ class PipelineService extends EventEmitter { source: 'HANDBRAKE_SCAN_DVD_TITLES', silent: !this.isPrimaryJob(jobId) }); - dvdTitleScanLines.push(...pluginScan.scanLines); + appendLinesInPlace(dvdTitleScanLines, pluginScan.scanLines); dvdTitleScanRunInfo = pluginScan.runInfo || null; reviewPluginExecution = this.mergePluginExecutionState( reviewPluginExecution, diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index 88fc2de..adf6bf3 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -308,12 +308,38 @@ function removeSelectionArgs(extraArgs) { 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 isSubtitleSelectionWithValue = SUBTITLE_SELECTION_KEYS_WITH_VALUE.has(key); + const isSubtitleFlagWithValue = SUBTITLE_FLAG_KEYS_WITH_VALUE.has(key); + const isSubtitleWithValue = isSubtitleSelectionWithValue || isSubtitleFlagWithValue; 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 (isSubtitleFlagWithValue) { + const inlineValue = token.includes('=') + ? token.slice(token.indexOf('=') + 1) + : ''; + const hasAttachedValue = token.includes('='); + const nextToken = String(args[i + 1] || ''); + const hasSeparateValue = !hasAttachedValue && nextToken && !nextToken.startsWith('-'); + const candidateValue = String( + hasAttachedValue + ? inlineValue + : (hasSeparateValue ? nextToken : '') + ).trim().toLowerCase(); + + // Keep explicit "none" subtitle flags from extra args. + // This allows users to intentionally clear subtitle burn/default/forced behavior. + if (candidateValue === 'none') { + filtered.push(token); + if (hasSeparateValue) { + filtered.push(nextToken); + i += 1; + } + continue; + } + } + if (!skip) { filtered.push(token); continue; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 78b2cc4..c7c71ae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "dependencies": { "chart.js": "^4.5.1", "primeicons": "^7.0.0", diff --git a/frontend/package.json b/frontend/package.json index a02e1bf..4fd373c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "1.0.0-rc2", + "version": "1.0.0-rc3", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a6b6102..6efd5d6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -449,6 +449,11 @@ function App() { ) ) ); + const isUnknownRunningProgress = Boolean( + normalizedProgressJobId + && incomingIsRunning + && !hasKnownBinding + ); const allowRunningTransitionFromTerminal = Boolean( normalizedProgressJobId && incomingIsRunning @@ -465,7 +470,7 @@ function App() { && ( prevJobProgressIsTerminal || matchingCdDriveIsTerminal - || !hasKnownBinding + || isUnknownRunningProgress || regressesStableJobState ) ) { diff --git a/frontend/src/components/MediaInfoReviewPanel.jsx b/frontend/src/components/MediaInfoReviewPanel.jsx index 2f1612d..9b4a984 100644 --- a/frontend/src/components/MediaInfoReviewPanel.jsx +++ b/frontend/src/components/MediaInfoReviewPanel.jsx @@ -587,12 +587,37 @@ function removeSelectionArgs(extraArgs) { 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 isSubtitleSelectionWithValue = SUBTITLE_SELECTION_KEYS_WITH_VALUE.has(key); + const isSubtitleFlagWithValue = SUBTITLE_FLAG_KEYS_WITH_VALUE.has(key); + const isSubtitleWithValue = isSubtitleSelectionWithValue || isSubtitleFlagWithValue; 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 (isSubtitleFlagWithValue) { + const inlineValue = token.includes('=') + ? token.slice(token.indexOf('=') + 1) + : ''; + const hasAttachedValue = token.includes('='); + const nextToken = String(args[i + 1] || ''); + const hasSeparateValue = !hasAttachedValue && nextToken && !nextToken.startsWith('-'); + const candidateValue = String( + hasAttachedValue + ? inlineValue + : (hasSeparateValue ? nextToken : '') + ).trim().toLowerCase(); + + // Keep explicit "none" subtitle flags in preview, same as backend command builder. + if (candidateValue === 'none') { + filtered.push(token); + if (hasSeparateValue) { + filtered.push(nextToken); + i += 1; + } + continue; + } + } + if (!skip) { filtered.push(token); continue; @@ -709,9 +734,10 @@ function buildHandBrakeCommandPreview({ subtitleLanguageOrder: selectedSubtitleLanguageOrder, fallbackSelectedSubtitleTrackIds: normalizedSelectedSubtitleTrackIds, fallbackSelectedSubtitleTrackIdsOrdered: normalizedSelectedSubtitleTrackIds, - hasExplicitVariantSelection: normalizedSubtitleSelectionMode === 'track_ids' - ? true - : hasExplicitSelectedVariantSelection + // Track-ID mode must use fallbackSelectedSubtitleTrackIds instead of forcing + // variant-explicit mode, otherwise preview can incorrectly collapse to "-s none". + hasExplicitVariantSelection: normalizedSubtitleSelectionMode === 'variants' + && hasExplicitSelectedVariantSelection }); const subtitleTrackIds = resolvedSubtitleSelection.subtitleTrackIds.length > 0 ? resolvedSubtitleSelection.subtitleTrackIds @@ -1894,6 +1920,17 @@ export default function MediaInfoReviewPanel({ const isUserPresetActive = Boolean(selectedUserPreset); const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim(); const isHandBrakePresetActive = !isUserPresetActive && Boolean(trimmedHandBrakePreset); + const settingsPresetFallback = String(review?.selectors?.preset || '').trim(); + const settingsExtraArgsFallback = String(review?.selectors?.extraArgs || '').trim(); + const usesSettingsFallbackOnly = !isUserPresetActive && !isHandBrakePresetActive; + const hasSettingsPresetFallback = Boolean(settingsPresetFallback); + const hasSettingsExtraArgsFallback = Boolean(settingsExtraArgsFallback); + const hasInvalidPresetConfig = usesSettingsFallbackOnly + && !hasSettingsPresetFallback + && !hasSettingsExtraArgsFallback; + const hasSettingsArgsOnlyConfig = usesSettingsFallbackOnly + && !hasSettingsPresetFallback + && hasSettingsExtraArgsFallback; const effectivePresetOverride = isUserPresetActive ? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' } : (isHandBrakePresetActive ? { handbrakePreset: trimmedHandBrakePreset, extraArgs: '' } : null); @@ -1940,7 +1977,7 @@ export default function MediaInfoReviewPanel({ const audioCopyMaskSourceLabel = String(effectiveAudioSelector?.copyMaskSource || '').trim() || 'default'; const audioFallbackSourceLabel = String(effectiveAudioSelector?.fallbackSource || '').trim() || 'default'; const presetDropdownOptions = (() => { - const options = []; + const options = [{ label: 'Kein Preset', value: 'none' }]; if (hasUserPresets) { options.push({ label: 'User-Presets/', value: '__group__userpresets', disabled: true }); for (const preset of normalizedUserPresets) { @@ -1990,7 +2027,7 @@ export default function MediaInfoReviewPanel({ })(); const selectedPresetToken = isUserPresetActive ? `user:${selectedUserPreset.id}` - : (trimmedHandBrakePreset ? `hb:${trimmedHandBrakePreset}` : null); + : (trimmedHandBrakePreset ? `hb:${trimmedHandBrakePreset}` : 'none'); const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : []) .map((item) => ({ @@ -2221,6 +2258,15 @@ export default function MediaInfoReviewPanel({ if (typeof onHandBrakePresetChange === 'function') { onHandBrakePresetChange(presetName); } + return; + } + if (value === 'none') { + if (typeof onUserPresetChange === 'function') { + onUserPresetChange(null); + } + if (typeof onHandBrakePresetChange === 'function') { + onHandBrakePresetChange(''); + } } }} placeholder="Preset auswählen …" @@ -2242,6 +2288,20 @@ export default function MediaInfoReviewPanel({ )} )} + {!compactTitleSelection && !isTitleSelectionMode && hasInvalidPresetConfig ? ( +
Lade Verlauf ...
- ) : historyPoints.length === 0 ? ( + ) : resampledHistoryPoints.length === 0 ? (Noch keine Verlaufsdaten vorhanden.
) : (