1.0.0-rc3 rc3
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "1.0.0-rc2",
|
||||
"version": "1.0.0-rc3",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,14 +22860,18 @@ class PipelineService extends EventEmitter {
|
||||
const requiresExplicitPreset = readyMediaProfile === 'dvd'
|
||||
|| readyMediaProfile === 'bluray'
|
||||
|| (readyMediaProfile === 'converter' && confirmedConverterMediaType !== 'audio');
|
||||
const planPresetName = String(
|
||||
const planPresetName = explicitPresetClearRequested
|
||||
? ''
|
||||
: String(
|
||||
planForConfirm?.userPreset?.handbrakePreset
|
||||
|| encodePlan?.userPreset?.handbrakePreset
|
||||
|| planForConfirm?.selectors?.preset
|
||||
|| encodePlan?.selectors?.preset
|
||||
|| ''
|
||||
).trim();
|
||||
const planExtraArgs = String(
|
||||
const planExtraArgs = explicitPresetClearRequested
|
||||
? ''
|
||||
: String(
|
||||
planForConfirm?.userPreset?.extraArgs
|
||||
|| encodePlan?.userPreset?.extraArgs
|
||||
|| planForConfirm?.selectors?.extraArgs
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "1.0.0-rc2",
|
||||
"version": "1.0.0-rc3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!compactTitleSelection && !isTitleSelectionMode && hasInvalidPresetConfig ? (
|
||||
<div className="media-review-notes">
|
||||
<small>
|
||||
Warnung: Weder HandBrake-Preset noch Extra-Args sind in den Settings gesetzt. Der CLI-Command kann so nicht valide gebaut werden.
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
{!compactTitleSelection && !isTitleSelectionMode && hasSettingsArgsOnlyConfig ? (
|
||||
<div className="media-review-notes">
|
||||
<small>
|
||||
Hinweis: Es wird kein Preset verwendet. Nur CLI-Parameter aus den Settings werden genutzt - bitte auf Korrektheit prüfen.
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
{!compactTitleSelection && !isTitleSelectionMode ? (
|
||||
<div className="media-review-meta">
|
||||
<div>
|
||||
|
||||
@@ -2342,8 +2342,11 @@ export default function PipelineStatusCard({
|
||||
const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle;
|
||||
const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
||||
const settingsPresetFallback = String(mediaInfoReview?.selectors?.preset || '').trim();
|
||||
const settingsExtraArgsFallback = String(mediaInfoReview?.selectors?.extraArgs || '').trim();
|
||||
const hasSelectedPreset = Boolean(selectedUserPresetId) || Boolean(trimmedHandBrakePreset);
|
||||
const hasEffectiveSelectedPreset = hasSelectedPreset || Boolean(settingsPresetFallback);
|
||||
const hasEffectiveSelectedPreset = hasSelectedPreset
|
||||
|| Boolean(settingsPresetFallback)
|
||||
|| Boolean(settingsExtraArgsFallback);
|
||||
const converterMediaType = String(mediaInfoReview?.converterMediaType || 'video').trim().toLowerCase() || 'video';
|
||||
const showConverterConfig = jobMediaProfile === 'converter' && converterMediaType !== 'audio';
|
||||
const requiresPresetSelection = showConverterConfig || jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray';
|
||||
@@ -3856,12 +3859,12 @@ export default function PipelineStatusCard({
|
||||
selectedPostEncodeChainIds: selectedPostChainIds,
|
||||
selectedPreEncodeChainIds: selectedPreChainIds
|
||||
};
|
||||
if (selectedUserPresetId !== null && selectedUserPresetId !== undefined) {
|
||||
startPayload.selectedUserPresetId = selectedUserPresetId;
|
||||
}
|
||||
if (String(selectedHandBrakePreset || '').trim()) {
|
||||
startPayload.selectedUserPresetId = (
|
||||
selectedUserPresetId !== null && selectedUserPresetId !== undefined
|
||||
? selectedUserPresetId
|
||||
: null
|
||||
);
|
||||
startPayload.selectedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
||||
}
|
||||
await onStart(retryJobId, startPayload);
|
||||
}}
|
||||
loading={busy}
|
||||
|
||||
@@ -97,6 +97,10 @@ const HISTORY_WINDOWS = [
|
||||
{ label: '4d', hours: 96 }
|
||||
];
|
||||
const HISTORY_STACK_BREAKPOINT = 1700;
|
||||
const HISTORY_RESAMPLE_TARGET_POINTS = 720;
|
||||
const HISTORY_INTERPOLATION_MIN_GAP_MS = 5 * 60 * 1000;
|
||||
const HISTORY_INTERPOLATION_MAX_GAP_MS = 45 * 60 * 1000;
|
||||
const HISTORY_INTERPOLATION_GAP_FACTOR = 6;
|
||||
|
||||
const CPU_SERIES_COLOR = '#c43d2f';
|
||||
const GPU_SERIES_COLOR = '#c9961a';
|
||||
@@ -113,6 +117,15 @@ const historyChartOptions = {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: (items) => {
|
||||
const first = Array.isArray(items) ? items[0] : null;
|
||||
const rawLabel = first?.label ?? first?.raw?.x ?? first?.parsed?.x ?? '';
|
||||
return formatTickLabel(rawLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
@@ -120,7 +133,17 @@ const historyChartOptions = {
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 8,
|
||||
color: '#6a4d38'
|
||||
color: '#6a4d38',
|
||||
callback: function (value, index, ticks) {
|
||||
const resolvedLabel = typeof this?.getLabelForValue === 'function'
|
||||
? this.getLabelForValue(value)
|
||||
: (
|
||||
Array.isArray(ticks) && ticks[index]
|
||||
? (ticks[index].label ?? ticks[index].value ?? value)
|
||||
: value
|
||||
);
|
||||
return formatTickLabel(resolvedLabel);
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(111,57,34,0.08)'
|
||||
@@ -152,6 +175,182 @@ function formatTickLabel(timestampIso) {
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatDayMarkerLabel(timestampIso) {
|
||||
const parsed = Date.parse(String(timestampIso || ''));
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return '';
|
||||
}
|
||||
const date = new Date(parsed);
|
||||
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.`;
|
||||
}
|
||||
|
||||
function toDayKeyFromTimestamp(timestampIso) {
|
||||
const parsed = Date.parse(String(timestampIso || ''));
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(parsed);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function parseTimestampMs(value) {
|
||||
const parsed = Date.parse(String(value || ''));
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function normalizeHistoryPoints(points = []) {
|
||||
return (Array.isArray(points) ? points : [])
|
||||
.map((point) => {
|
||||
const timestampMs = parseTimestampMs(point?.capturedAt);
|
||||
if (!Number.isFinite(timestampMs)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
capturedAt: new Date(timestampMs).toISOString(),
|
||||
timestampMs,
|
||||
cpuUsagePercent: toNumberOrNull(point?.cpuUsagePercent),
|
||||
ramUsagePercent: toNumberOrNull(point?.ramUsagePercent),
|
||||
gpuUsagePercent: toNumberOrNull(point?.gpuUsagePercent),
|
||||
cpuTemperatureC: toNumberOrNull(point?.cpuTemperatureC),
|
||||
gpuTemperatureC: toNumberOrNull(point?.gpuTemperatureC)
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => left.timestampMs - right.timestampMs);
|
||||
}
|
||||
|
||||
function interpolateMetric(prevPoint, nextPoint, metricKey, targetMs, maxGapMs) {
|
||||
if (!prevPoint || !nextPoint) {
|
||||
return null;
|
||||
}
|
||||
if (prevPoint.timestampMs === nextPoint.timestampMs) {
|
||||
return toNumberOrNull(prevPoint[metricKey]);
|
||||
}
|
||||
const gapMs = nextPoint.timestampMs - prevPoint.timestampMs;
|
||||
if (!Number.isFinite(gapMs) || gapMs <= 0 || gapMs > maxGapMs) {
|
||||
return null;
|
||||
}
|
||||
const prevValue = toNumberOrNull(prevPoint[metricKey]);
|
||||
const nextValue = toNumberOrNull(nextPoint[metricKey]);
|
||||
if (!Number.isFinite(prevValue) || !Number.isFinite(nextValue)) {
|
||||
return null;
|
||||
}
|
||||
const ratio = (targetMs - prevPoint.timestampMs) / gapMs;
|
||||
if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
|
||||
return null;
|
||||
}
|
||||
return Number((prevValue + ((nextValue - prevValue) * ratio)).toFixed(2));
|
||||
}
|
||||
|
||||
function buildResampledHistoryPoints(points = [], historyHours = 24) {
|
||||
const normalized = normalizeHistoryPoints(points);
|
||||
if (normalized.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const historyWindowMs = Math.max(1, Number(historyHours || 24)) * 60 * 60 * 1000;
|
||||
const stepMsRaw = Math.round(historyWindowMs / HISTORY_RESAMPLE_TARGET_POINTS);
|
||||
const stepMs = Math.max(60 * 1000, stepMsRaw);
|
||||
const maxInterpolationGapMs = Math.max(
|
||||
HISTORY_INTERPOLATION_MIN_GAP_MS,
|
||||
Math.min(HISTORY_INTERPOLATION_MAX_GAP_MS, stepMs * HISTORY_INTERPOLATION_GAP_FACTOR)
|
||||
);
|
||||
|
||||
const latestSourceMs = normalized[normalized.length - 1].timestampMs;
|
||||
const windowEndMs = Math.max(Date.now(), latestSourceMs);
|
||||
const windowStartMs = windowEndMs - historyWindowMs;
|
||||
const gridStartMs = Math.floor(windowStartMs / stepMs) * stepMs;
|
||||
const gridEndMs = Math.ceil(windowEndMs / stepMs) * stepMs;
|
||||
const source = normalized.filter((point) => point.timestampMs >= (gridStartMs - maxInterpolationGapMs));
|
||||
|
||||
const keys = [
|
||||
'cpuUsagePercent',
|
||||
'ramUsagePercent',
|
||||
'gpuUsagePercent',
|
||||
'cpuTemperatureC',
|
||||
'gpuTemperatureC'
|
||||
];
|
||||
|
||||
let sourceIndex = 0;
|
||||
const output = [];
|
||||
for (let ts = gridStartMs; ts <= gridEndMs; ts += stepMs) {
|
||||
while (sourceIndex < source.length && source[sourceIndex].timestampMs < ts) {
|
||||
sourceIndex += 1;
|
||||
}
|
||||
const nextPoint = sourceIndex < source.length ? source[sourceIndex] : null;
|
||||
const prevPoint = sourceIndex > 0 ? source[sourceIndex - 1] : null;
|
||||
|
||||
const row = {
|
||||
capturedAt: new Date(ts).toISOString(),
|
||||
timestampMs: ts
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
const nextValue = nextPoint && nextPoint.timestampMs === ts
|
||||
? toNumberOrNull(nextPoint[key])
|
||||
: null;
|
||||
const prevValue = prevPoint && prevPoint.timestampMs === ts
|
||||
? toNumberOrNull(prevPoint[key])
|
||||
: null;
|
||||
row[key] = Number.isFinite(nextValue)
|
||||
? nextValue
|
||||
: (Number.isFinite(prevValue)
|
||||
? prevValue
|
||||
: interpolateMetric(prevPoint, nextPoint, key, ts, maxInterpolationGapMs));
|
||||
}
|
||||
output.push(row);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
const historyDayBoundaryPlugin = {
|
||||
id: 'historyDayBoundary',
|
||||
afterDatasetsDraw(chart, _args, options) {
|
||||
if (options === false) {
|
||||
return;
|
||||
}
|
||||
const xScale = chart?.scales?.x;
|
||||
const area = chart?.chartArea;
|
||||
const labels = Array.isArray(chart?.data?.labels) ? chart.data.labels : [];
|
||||
if (!xScale || !area || labels.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = chart.ctx;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(111,57,34,0.30)';
|
||||
ctx.fillStyle = 'rgba(111,57,34,0.75)';
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
for (let index = 1; index < labels.length; index += 1) {
|
||||
const prevDayKey = toDayKeyFromTimestamp(labels[index - 1]);
|
||||
const nextDayKey = toDayKeyFromTimestamp(labels[index]);
|
||||
if (!prevDayKey || !nextDayKey || prevDayKey === nextDayKey) {
|
||||
continue;
|
||||
}
|
||||
const xPrev = xScale.getPixelForValue(index - 1);
|
||||
const xNext = xScale.getPixelForValue(index);
|
||||
const x = (xPrev + xNext) / 2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, area.top);
|
||||
ctx.lineTo(x, area.bottom);
|
||||
ctx.stroke();
|
||||
|
||||
const markerLabel = formatDayMarkerLabel(labels[index]);
|
||||
if (markerLabel) {
|
||||
ctx.fillText(markerLabel, x + 4, area.top + 4);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
function buildSeriesToggleButtonStyle(color, active) {
|
||||
if (active) {
|
||||
return {
|
||||
@@ -258,6 +457,10 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
const ramTitle = String(memory?.name || memory?.vendor || memory?.model || '').trim() || 'Arbeitsspeicher';
|
||||
const gpuTitle = String(primaryGpu?.name || gpu?.name || gpu?.vendor || '').trim() || 'GPU';
|
||||
const historyPoints = Array.isArray(historyState.points) ? historyState.points : [];
|
||||
const resampledHistoryPoints = useMemo(
|
||||
() => buildResampledHistoryPoints(historyPoints, historyHours),
|
||||
[historyPoints, historyHours]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monitoringState.enabled) {
|
||||
@@ -339,11 +542,11 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
}, []);
|
||||
|
||||
const historyUsageChartData = useMemo(() => ({
|
||||
labels: historyPoints.map((point) => formatTickLabel(point?.capturedAt)),
|
||||
labels: resampledHistoryPoints.map((point) => String(point?.capturedAt || '').trim()),
|
||||
datasets: [
|
||||
{
|
||||
label: 'CPU',
|
||||
data: historyPoints.map((point) => toNumberOrNull(point?.cpuUsagePercent)),
|
||||
data: resampledHistoryPoints.map((point) => toNumberOrNull(point?.cpuUsagePercent)),
|
||||
borderColor: CPU_SERIES_COLOR,
|
||||
backgroundColor: CPU_SERIES_COLOR,
|
||||
borderWidth: 2,
|
||||
@@ -353,7 +556,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
},
|
||||
{
|
||||
label: 'RAM',
|
||||
data: historyPoints.map((point) => toNumberOrNull(point?.ramUsagePercent)),
|
||||
data: resampledHistoryPoints.map((point) => toNumberOrNull(point?.ramUsagePercent)),
|
||||
borderColor: RAM_SERIES_COLOR,
|
||||
backgroundColor: RAM_SERIES_COLOR,
|
||||
borderWidth: 2,
|
||||
@@ -363,7 +566,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
},
|
||||
{
|
||||
label: 'GPU',
|
||||
data: historyPoints.map((point) => toNumberOrNull(point?.gpuUsagePercent)),
|
||||
data: resampledHistoryPoints.map((point) => toNumberOrNull(point?.gpuUsagePercent)),
|
||||
borderColor: GPU_SERIES_COLOR,
|
||||
backgroundColor: GPU_SERIES_COLOR,
|
||||
borderWidth: 2,
|
||||
@@ -372,14 +575,14 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
tension: 0.22
|
||||
}
|
||||
]
|
||||
}), [historyPoints, usageSeriesVisible]);
|
||||
}), [resampledHistoryPoints, usageSeriesVisible]);
|
||||
|
||||
const historyTempChartData = useMemo(() => ({
|
||||
labels: historyPoints.map((point) => formatTickLabel(point?.capturedAt)),
|
||||
labels: resampledHistoryPoints.map((point) => String(point?.capturedAt || '').trim()),
|
||||
datasets: [
|
||||
{
|
||||
label: 'CPU',
|
||||
data: historyPoints.map((point) => toNumberOrNull(point?.cpuTemperatureC)),
|
||||
data: resampledHistoryPoints.map((point) => toNumberOrNull(point?.cpuTemperatureC)),
|
||||
borderColor: CPU_SERIES_COLOR,
|
||||
backgroundColor: CPU_SERIES_COLOR,
|
||||
borderWidth: 2,
|
||||
@@ -389,7 +592,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
},
|
||||
{
|
||||
label: 'GPU',
|
||||
data: historyPoints.map((point) => toNumberOrNull(point?.gpuTemperatureC)),
|
||||
data: resampledHistoryPoints.map((point) => toNumberOrNull(point?.gpuTemperatureC)),
|
||||
borderColor: GPU_SERIES_COLOR,
|
||||
backgroundColor: GPU_SERIES_COLOR,
|
||||
borderWidth: 2,
|
||||
@@ -398,7 +601,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
tension: 0.22
|
||||
}
|
||||
]
|
||||
}), [historyPoints, tempSeriesVisible]);
|
||||
}), [resampledHistoryPoints, tempSeriesVisible]);
|
||||
|
||||
return (
|
||||
<div className="hardware-detail-page">
|
||||
@@ -463,7 +666,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
{historyState.error ? <small className="error-text">{historyState.error}</small> : null}
|
||||
{historyState.loading && historyPoints.length === 0 ? (
|
||||
<p>Lade Verlauf ...</p>
|
||||
) : historyPoints.length === 0 ? (
|
||||
) : resampledHistoryPoints.length === 0 ? (
|
||||
<p>Noch keine Verlaufsdaten vorhanden.</p>
|
||||
) : (
|
||||
<div className="hardware-history-grid">
|
||||
@@ -502,6 +705,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
type="line"
|
||||
data={historyUsageChartData}
|
||||
options={historyChartOptions}
|
||||
plugins={[historyDayBoundaryPlugin]}
|
||||
/>
|
||||
</div>
|
||||
<ChartLegendRow
|
||||
@@ -539,6 +743,7 @@ export default function HardwarePage({ hardwareMonitoring }) {
|
||||
type="line"
|
||||
data={historyTempChartData}
|
||||
options={historyChartOptions}
|
||||
plugins={[historyDayBoundaryPlugin]}
|
||||
/>
|
||||
</div>
|
||||
<ChartLegendRow
|
||||
|
||||
@@ -1476,22 +1476,27 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
||||
: null;
|
||||
const liveState = String(liveJobProgress?.state || '').trim().toUpperCase();
|
||||
const isTerminalJobStatus = jobStatus === 'FINISHED' || jobStatus === 'ERROR' || jobStatus === 'CANCELLED';
|
||||
const ignoreStaleLiveProgress = isTerminalJobStatus && processingStates.includes(liveState);
|
||||
const mergedContext = liveContext
|
||||
const jobStatusIsRunning = processingStates.includes(jobStatus);
|
||||
const ignoreStaleLiveProgress = (
|
||||
(isTerminalJobStatus && processingStates.includes(liveState))
|
||||
|| (!jobStatusIsRunning && jobStatus !== 'IDLE' && processingStates.includes(liveState))
|
||||
);
|
||||
const effectiveLiveContext = ignoreStaleLiveProgress ? null : liveContext;
|
||||
const mergedContext = effectiveLiveContext
|
||||
? {
|
||||
...computedContext,
|
||||
...liveContext,
|
||||
...effectiveLiveContext,
|
||||
// Prefer the DB plan when it has been confirmed but the live (jobProgress) plan hasn't —
|
||||
// this happens when confirmEncodeReview is called with skipPipelineStateUpdate=true
|
||||
// (always the case for queued jobs), leaving jobProgress with the pre-confirmation plan.
|
||||
mediaInfoReview: (computedContext.mediaInfoReview?.reviewConfirmedAt && !liveContext.mediaInfoReview?.reviewConfirmedAt)
|
||||
mediaInfoReview: (computedContext.mediaInfoReview?.reviewConfirmedAt && !effectiveLiveContext.mediaInfoReview?.reviewConfirmedAt)
|
||||
? computedContext.mediaInfoReview
|
||||
: (liveContext.mediaInfoReview || computedContext.mediaInfoReview),
|
||||
tracks: (Array.isArray(liveContext.tracks) && liveContext.tracks.length > 0)
|
||||
? liveContext.tracks
|
||||
: (effectiveLiveContext.mediaInfoReview || computedContext.mediaInfoReview),
|
||||
tracks: (Array.isArray(effectiveLiveContext.tracks) && effectiveLiveContext.tracks.length > 0)
|
||||
? effectiveLiveContext.tracks
|
||||
: computedContext.tracks,
|
||||
selectedMetadata: computedContext.selectedMetadata || liveContext.selectedMetadata,
|
||||
cdRipConfig: liveContext.cdRipConfig || computedContext.cdRipConfig
|
||||
selectedMetadata: computedContext.selectedMetadata || effectiveLiveContext.selectedMetadata,
|
||||
cdRipConfig: effectiveLiveContext.cdRipConfig || computedContext.cdRipConfig
|
||||
}
|
||||
: computedContext;
|
||||
// The card always represents `job.id`; never let stale live context override it.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "1.0.0-rc2",
|
||||
"version": "1.0.0-rc3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "1.0.0-rc2",
|
||||
"version": "1.0.0-rc3",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "1.0.0-rc2",
|
||||
"version": "1.0.0-rc3",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
Reference in New Issue
Block a user