Queue and UI fixes

This commit is contained in:
2026-03-05 11:04:20 +00:00
parent 23acea4773
commit e3d890c071
103 changed files with 11400 additions and 2010 deletions

View File

@@ -3,6 +3,7 @@ import { Button } from 'primereact/button';
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import { getStatusLabel } from '../utils/statusPresentation';
function JsonView({ title, value }) {
return (
@@ -18,36 +19,43 @@ function resolveMediaType(job) {
return raw === 'bluray' ? 'bluray' : 'disc';
}
function statusBadgeMeta(status) {
function statusBadgeMeta(status, queued = false) {
const normalized = String(status || '').trim().toUpperCase();
const label = getStatusLabel(normalized, { queued });
if (queued) {
return { label, icon: 'pi-list', tone: 'info' };
}
if (normalized === 'FINISHED') {
return { label: normalized, icon: 'pi-check-circle', tone: 'success' };
return { label, icon: 'pi-check-circle', tone: 'success' };
}
if (normalized === 'ERROR') {
return { label: normalized, icon: 'pi-times-circle', tone: 'danger' };
return { label, icon: 'pi-times-circle', tone: 'danger' };
}
if (normalized === 'CANCELLED') {
return { label, icon: 'pi-ban', tone: 'warning' };
}
if (normalized === 'READY_TO_ENCODE' || normalized === 'READY_TO_START') {
return { label: normalized, icon: 'pi-play-circle', tone: 'info' };
return { label, icon: 'pi-play-circle', tone: 'info' };
}
if (normalized === 'WAITING_FOR_USER_DECISION') {
return { label: normalized, icon: 'pi-exclamation-circle', tone: 'warning' };
return { label, icon: 'pi-exclamation-circle', tone: 'warning' };
}
if (normalized === 'METADATA_SELECTION') {
return { label: normalized, icon: 'pi-list', tone: 'warning' };
return { label, icon: 'pi-list', tone: 'warning' };
}
if (normalized === 'ANALYZING') {
return { label: normalized, icon: 'pi-search', tone: 'warning' };
return { label, icon: 'pi-search', tone: 'warning' };
}
if (normalized === 'RIPPING') {
return { label: normalized, icon: 'pi-download', tone: 'warning' };
return { label, icon: 'pi-download', tone: 'warning' };
}
if (normalized === 'MEDIAINFO_CHECK') {
return { label: normalized, icon: 'pi-sliders-h', tone: 'warning' };
return { label, icon: 'pi-sliders-h', tone: 'warning' };
}
if (normalized === 'ENCODING') {
return { label: normalized, icon: 'pi-cog', tone: 'warning' };
return { label, icon: 'pi-cog', tone: 'warning' };
}
return { label: normalized || '-', icon: 'pi-info-circle', tone: 'secondary' };
return { label: label || '-', icon: 'pi-info-circle', tone: 'secondary' };
}
function omdbField(value) {
@@ -82,10 +90,14 @@ export default function JobDetailDialog({
onLoadLog,
logLoadingMode = null,
onAssignOmdb,
onResumeReady,
onRestartEncode,
onRestartReview,
onReencode,
onDeleteFiles,
onDeleteEntry,
onRemoveFromQueue,
isQueued = false,
omdbAssignBusy = false,
actionBusy = false,
reencodeBusy = false,
@@ -95,6 +107,11 @@ export default function JobDetailDialog({
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(job?.status);
const showFinalLog = !running;
const canReencode = !!(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true && mkDone && !running);
const canResumeReady = Boolean(
(String(job?.status || '').trim().toUpperCase() === 'READY_TO_ENCODE' || String(job?.last_state || '').trim().toUpperCase() === 'READY_TO_ENCODE')
&& !running
&& typeof onResumeReady === 'function'
);
const hasConfirmedPlan = Boolean(
job?.encodePlan
&& Array.isArray(job?.encodePlan?.titles)
@@ -103,7 +120,13 @@ export default function JobDetailDialog({
);
const hasRestartInput = Boolean(job?.encode_input_path || job?.raw_path || job?.encodePlan?.encodeInputPath);
const canRestartEncode = Boolean(hasConfirmedPlan && hasRestartInput && !running);
const canRestartReview = Boolean(
(job?.rawStatus?.exists || job?.raw_path)
&& !running
&& typeof onRestartReview === 'function'
);
const canDeleteEntry = !running && typeof onDeleteEntry === 'function';
const queueLocked = Boolean(isQueued && job?.id);
const logCount = Number(job?.log_count || 0);
const logMeta = job?.logMeta && typeof job.logMeta === 'object' ? job.logMeta : null;
const logLoaded = Boolean(logMeta?.loaded) || Boolean(job?.log);
@@ -112,7 +135,7 @@ export default function JobDetailDialog({
const mediaTypeLabel = mediaType === 'bluray' ? 'Blu-ray' : 'Sonstiges Medium';
const mediaTypeIcon = mediaType === 'bluray' ? blurayIndicatorIcon : discIndicatorIcon;
const mediaTypeAlt = mediaType === 'bluray' ? 'Blu-ray' : 'Disc';
const statusMeta = statusBadgeMeta(job?.status);
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
return (
@@ -261,74 +284,112 @@ export default function JobDetailDialog({
<h4>Aktionen</h4>
<div className="actions-row">
<Button
label="OMDb neu zuordnen"
icon="pi pi-search"
severity="secondary"
size="small"
onClick={() => onAssignOmdb?.(job)}
loading={omdbAssignBusy}
disabled={running || typeof onAssignOmdb !== 'function'}
/>
{typeof onRestartEncode === 'function' ? (
{queueLocked ? (
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
label="Aus Queue löschen"
icon="pi pi-times"
severity="danger"
outlined
size="small"
onClick={() => onRestartEncode?.(job)}
onClick={() => onRemoveFromQueue?.(job)}
loading={actionBusy}
disabled={!canRestartEncode}
disabled={typeof onRemoveFromQueue !== 'function'}
/>
) : null}
<Button
label="RAW neu encodieren"
icon="pi pi-cog"
severity="info"
size="small"
onClick={() => onReencode?.(job)}
loading={reencodeBusy}
disabled={!canReencode || typeof onReencode !== 'function'}
/>
<Button
label="RAW löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'raw')}
loading={actionBusy}
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Movie löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Beides löschen"
icon="pi pi-times"
severity="danger"
size="small"
onClick={() => onDeleteFiles?.(job, 'both')}
loading={actionBusy}
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Historieneintrag löschen"
icon="pi pi-trash"
severity="danger"
outlined
size="small"
onClick={() => onDeleteEntry?.(job)}
loading={deleteEntryBusy}
disabled={!canDeleteEntry}
/>
) : (
<>
<Button
label="OMDb neu zuordnen"
icon="pi pi-search"
severity="secondary"
size="small"
onClick={() => onAssignOmdb?.(job)}
loading={omdbAssignBusy}
disabled={running || typeof onAssignOmdb !== 'function'}
/>
{canResumeReady ? (
<Button
label="Im Dashboard öffnen"
icon="pi pi-window-maximize"
severity="info"
outlined
size="small"
onClick={() => onResumeReady?.(job)}
loading={actionBusy}
/>
) : null}
{typeof onRestartEncode === 'function' ? (
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
size="small"
onClick={() => onRestartEncode?.(job)}
loading={actionBusy}
disabled={!canRestartEncode}
/>
) : null}
{typeof onRestartReview === 'function' ? (
<Button
label="Review neu starten"
icon="pi pi-refresh"
severity="info"
outlined
size="small"
onClick={() => onRestartReview?.(job)}
loading={actionBusy}
disabled={!canRestartReview}
/>
) : null}
<Button
label="RAW neu encodieren"
icon="pi pi-cog"
severity="info"
size="small"
onClick={() => onReencode?.(job)}
loading={reencodeBusy}
disabled={!canReencode || typeof onReencode !== 'function'}
/>
<Button
label="RAW löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'raw')}
loading={actionBusy}
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Movie löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Beides löschen"
icon="pi pi-times"
severity="danger"
size="small"
onClick={() => onDeleteFiles?.(job, 'both')}
loading={actionBusy}
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/>
<Button
label="Historieneintrag löschen"
icon="pi pi-trash"
severity="danger"
outlined
size="small"
onClick={() => onDeleteEntry?.(job)}
loading={deleteEntryBusy}
disabled={!canDeleteEntry}
/>
</>
)}
</div>
<h4>Log</h4>

View File

@@ -51,6 +51,20 @@ function normalizeTrackIdList(values) {
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 splitArgs(input) {
if (!input || typeof input !== 'string') {
return [];
@@ -542,8 +556,9 @@ function TrackList({
<div className="media-track-list">
{tracks.map((track) => {
const trackId = normalizeTrackId(track.id);
const burned = type === 'subtitle' ? isBurnedSubtitleTrack(track) : false;
const checked = allowSelection
? (trackId !== null && selectedIds.includes(trackId))
? (trackId !== null && selectedIds.includes(trackId) && !(type === 'subtitle' && burned))
: Boolean(track.selectedForEncode);
const selectedIndex = trackId !== null
? checkedTrackOrder.indexOf(trackId)
@@ -567,26 +582,13 @@ function TrackList({
})()
: 'Nicht übernommen')
: null;
const subtitleFlags = type === 'subtitle' && checked
? (Array.isArray(track.subtitlePreviewFlags)
? track.subtitlePreviewFlags
: (Array.isArray(track.flags) ? track.flags : []))
: [];
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 burned = type === 'subtitle' && checked
? Boolean(
track.subtitlePreviewBurnIn
|| track.burnIn
|| subtitleFlags.includes('burned')
|| /burned/i.test(String(track.subtitlePreviewSummary || track.subtitleActionSummary || ''))
)
: false;
const disabled = !allowSelection || (type === 'subtitle' && burned);
let displayText = `#${track.id} | ${displayLanguage} | ${displayCodec}`;
if (type === 'audio') {
@@ -611,13 +613,13 @@ function TrackList({
type="checkbox"
checked={checked}
onChange={(event) => {
if (!allowSelection || typeof onToggleTrack !== 'function' || trackId === null) {
if (disabled || typeof onToggleTrack !== 'function' || trackId === null) {
return;
}
onToggleTrack(trackId, event.target.checked);
}}
readOnly={!allowSelection}
disabled={!allowSelection}
readOnly={disabled}
disabled={disabled}
/>
<span>{displayText}</span>
</label>
@@ -848,12 +850,18 @@ export default function MediaInfoReviewPanel({
? currentSelectedId === normalizeTitleId(title.id)
: Boolean(title.selectedForEncode);
const titleSelectionEntry = trackSelectionByTitle?.[title.id] || trackSelectionByTitle?.[String(title.id)] || {};
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
const selectableSubtitleTrackIds = subtitleTracks
.filter((track) => !isBurnedSubtitleTrack(track))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null);
const selectableSubtitleTrackIdSet = new Set(selectableSubtitleTrackIds.map((id) => String(id)));
const defaultAudioTrackIds = (Array.isArray(title.audioTracks) ? title.audioTracks : [])
.filter((track) => Boolean(track?.selectedByRule))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null);
const defaultSubtitleTrackIds = (Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [])
.filter((track) => Boolean(track?.selectedByRule))
const defaultSubtitleTrackIds = subtitleTracks
.filter((track) => Boolean(track?.selectedByRule) && !isBurnedSubtitleTrack(track))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null);
const selectedAudioTrackIds = normalizeTrackIdList(
@@ -865,7 +873,7 @@ export default function MediaInfoReviewPanel({
Array.isArray(titleSelectionEntry?.subtitleTrackIds)
? titleSelectionEntry.subtitleTrackIds
: defaultSubtitleTrackIds
);
).filter((id) => selectableSubtitleTrackIdSet.has(String(id)));
const allowTrackSelectionForTitle = Boolean(
allowTrackSelection
&& allowTitleSelection
@@ -934,7 +942,7 @@ export default function MediaInfoReviewPanel({
/>
<TrackList
title={`Subtitles (Titel #${title.id})`}
tracks={title.subtitleTracks || []}
tracks={allowTrackSelectionForTitle ? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track)) : subtitleTracks}
type="subtitle"
allowSelection={allowTrackSelectionForTitle}
selectedTrackIds={selectedSubtitleTrackIds}

View File

@@ -5,21 +5,7 @@ import { ProgressBar } from 'primereact/progressbar';
import { Button } from 'primereact/button';
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
import { api } from '../api/client';
const severityMap = {
IDLE: 'success',
DISC_DETECTED: 'info',
ANALYZING: 'warning',
METADATA_SELECTION: 'warning',
WAITING_FOR_USER_DECISION: 'warning',
READY_TO_START: 'info',
MEDIAINFO_CHECK: 'warning',
READY_TO_ENCODE: 'info',
RIPPING: 'warning',
ENCODING: 'warning',
FINISHED: 'success',
ERROR: 'danger'
};
import { getStatusLabel, getStatusSeverity } from '../utils/statusPresentation';
function normalizeTitleId(value) {
const parsed = Number(value);
@@ -92,6 +78,20 @@ function normalizeScriptIdList(values) {
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 buildDefaultTrackSelection(review) {
const titles = Array.isArray(review?.titles) ? review.titles : [];
const selection = {};
@@ -110,7 +110,7 @@ function buildDefaultTrackSelection(review) {
),
subtitleTrackIds: normalizeTrackIdList(
(Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [])
.filter((track) => Boolean(track?.selectedByRule))
.filter((track) => Boolean(track?.selectedByRule) && !isBurnedSubtitleTrack(track))
.map((track) => track?.id)
)
};
@@ -192,19 +192,25 @@ export default function PipelineStatusCard({
pipeline,
onAnalyze,
onReanalyze,
onOpenMetadata,
onStart,
onRemoveFromQueue,
onRestartEncode,
onRestartReview,
onConfirmReview,
onSelectPlaylist,
onCancel,
onRetry,
isQueued = false,
busy,
liveJobLog = ''
}) {
const state = pipeline?.state || 'IDLE';
const stateLabel = getStatusLabel(state);
const progress = Number(pipeline?.progress || 0);
const running = state === 'ANALYZING' || state === 'RIPPING' || state === 'ENCODING' || state === 'MEDIAINFO_CHECK';
const retryJobId = pipeline?.context?.jobId;
const queueLocked = Boolean(isQueued && retryJobId);
const selectedMetadata = pipeline?.context?.selectedMetadata || null;
const mediaInfoReview = pipeline?.context?.mediaInfoReview || null;
const playlistAnalysis = pipeline?.context?.playlistAnalysis || null;
@@ -298,10 +304,15 @@ export default function PipelineStatusCard({
? Boolean(retryJobId)
: Boolean(retryJobId && encodeInputPath);
const canRestartEncodeFromLastSettings = Boolean(
state === 'ERROR'
(state === 'ERROR' || state === 'CANCELLED')
&& retryJobId
&& pipeline?.context?.canRestartEncodeFromLastSettings
);
const canRestartReviewFromRaw = Boolean(
retryJobId
&& !running
&& (pipeline?.context?.canRestartReviewFromRaw || pipeline?.context?.rawPath)
);
const waitingPlaylistRows = useMemo(() => {
const evaluated = Array.isArray(playlistAnalysis?.evaluatedCandidates)
@@ -401,11 +412,24 @@ export default function PipelineStatusCard({
? defaultTrackSelectionForTitle(mediaInfoReview, encodeTitleId)
: { audioTrackIds: [], subtitleTrackIds: [] };
const effectiveSelection = selectionEntry || fallbackSelection;
const encodeTitle = encodeTitleId
? (Array.isArray(mediaInfoReview?.titles)
? (mediaInfoReview.titles.find((title) => normalizeTitleId(title?.id) === encodeTitleId) || null)
: null)
: null;
const blockedSubtitleTrackIds = new Set(
(Array.isArray(encodeTitle?.subtitleTracks) ? encodeTitle.subtitleTracks : [])
.filter((track) => isBurnedSubtitleTrack(track))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null)
.map((id) => String(id))
);
const selectedTrackSelection = encodeTitleId
? {
[encodeTitleId]: {
audioTrackIds: normalizeTrackIdList(effectiveSelection?.audioTrackIds || []),
subtitleTrackIds: normalizeTrackIdList(effectiveSelection?.subtitleTrackIds || [])
.filter((id) => !blockedSubtitleTrackIds.has(String(id)))
}
}
: null;
@@ -418,9 +442,9 @@ export default function PipelineStatusCard({
};
return (
<Card title="Pipeline Status" subTitle="Live Zustand und Fortschritt">
<Card title="Pipeline-Status" subTitle="Live-Zustand und Fortschritt">
<div className="status-row">
<Tag value={state} severity={severityMap[state] || 'secondary'} />
<Tag value={stateLabel} severity={getStatusSeverity(state)} />
<span>{pipeline?.statusText || 'Bereit'}</span>
</div>
@@ -438,106 +462,142 @@ export default function PipelineStatusCard({
)}
<div className="actions-row">
{(state === 'DISC_DETECTED' || state === 'IDLE') && (
{queueLocked ? (
<Button
label="Analyse starten"
icon="pi pi-search"
onClick={onAnalyze}
loading={busy}
/>
)}
{state === 'READY_TO_START' && retryJobId && (
<Button
label="Job starten"
icon="pi pi-play"
severity="success"
onClick={() => onStart(retryJobId)}
loading={busy}
/>
)}
{playlistDecisionRequiredBeforeStart && retryJobId && (
<Button
label="Playlist übernehmen"
icon="pi pi-check"
severity="warning"
outlined
onClick={() => onSelectPlaylist?.(retryJobId, selectedPlaylistId)}
loading={busy}
disabled={!normalizePlaylistId(selectedPlaylistId)}
/>
)}
{state === 'READY_TO_ENCODE' && retryJobId && (
<Button
label={isPreRipReview ? 'Backup + Encoding starten' : 'Encoding starten'}
icon="pi pi-play"
severity="success"
onClick={async () => {
const requiresAutoConfirm = !reviewConfirmed;
if (!requiresAutoConfirm) {
await onStart(retryJobId);
return;
}
const {
encodeTitleId,
selectedTrackSelection,
selectedPostScriptIds
} = buildSelectedTrackSelectionForCurrentTitle();
await onStart(retryJobId, {
ensureConfirmed: true,
selectedEncodeTitleId: encodeTitleId,
selectedTrackSelection,
selectedPostEncodeScriptIds: selectedPostScriptIds
});
}}
loading={busy}
disabled={!canStartReadyJob || !canConfirmReview}
/>
)}
{running && (
<Button
label="Abbrechen"
icon="pi pi-stop"
label="Aus Queue löschen"
icon="pi pi-times"
severity="danger"
onClick={onCancel}
outlined
onClick={() => onRemoveFromQueue?.(retryJobId)}
loading={busy}
disabled={typeof onRemoveFromQueue !== 'function'}
/>
) : (
<>
{(state === 'DISC_DETECTED' || state === 'IDLE') && (
<Button
label="Analyse starten"
icon="pi pi-search"
onClick={onAnalyze}
loading={busy}
/>
)}
{(state === 'METADATA_SELECTION' || state === 'WAITING_FOR_USER_DECISION') && retryJobId && typeof onOpenMetadata === 'function' ? (
<Button
label="Metadaten öffnen"
icon="pi pi-list"
severity="info"
onClick={() => onOpenMetadata?.(retryJobId)}
loading={busy}
/>
) : null}
{state === 'READY_TO_START' && retryJobId ? (
<Button
label="Job starten"
icon="pi pi-play"
severity="success"
onClick={() => onStart(retryJobId)}
loading={busy}
/>
) : null}
{playlistDecisionRequiredBeforeStart && retryJobId && (
<Button
label="Playlist übernehmen"
icon="pi pi-check"
severity="warning"
outlined
onClick={() => onSelectPlaylist?.(retryJobId, selectedPlaylistId)}
loading={busy}
disabled={!normalizePlaylistId(selectedPlaylistId)}
/>
)}
{state === 'READY_TO_ENCODE' && retryJobId ? (
<Button
label={isPreRipReview ? 'Backup + Encoding starten' : 'Encoding starten'}
icon="pi pi-play"
severity="success"
onClick={async () => {
const requiresAutoConfirm = !reviewConfirmed;
if (!requiresAutoConfirm) {
await onStart(retryJobId);
return;
}
const {
encodeTitleId,
selectedTrackSelection,
selectedPostScriptIds
} = buildSelectedTrackSelectionForCurrentTitle();
await onStart(retryJobId, {
ensureConfirmed: true,
selectedEncodeTitleId: encodeTitleId,
selectedTrackSelection,
selectedPostEncodeScriptIds: selectedPostScriptIds
});
}}
loading={busy}
disabled={!canStartReadyJob || !canConfirmReview}
/>
) : null}
{running && (
<Button
label="Abbrechen"
icon="pi pi-stop"
severity="danger"
onClick={() => onCancel?.(retryJobId, state)}
loading={busy}
/>
)}
{canRestartReviewFromRaw ? (
<Button
label="Review neu starten"
icon="pi pi-refresh"
severity="info"
outlined
onClick={() => onRestartReview?.(retryJobId)}
loading={busy}
disabled={!retryJobId}
/>
) : null}
{canRestartEncodeFromLastSettings ? (
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
onClick={() => onRestartEncode?.(retryJobId)}
loading={busy}
disabled={!retryJobId}
/>
) : null}
{(state === 'ERROR' || state === 'CANCELLED') && retryJobId && (
<Button
label="Retry Rippen"
icon="pi pi-refresh"
severity="warning"
onClick={() => onRetry(retryJobId)}
loading={busy}
/>
)}
{(state === 'ERROR' || state === 'CANCELLED') ? (
<Button
label="Disk-Analyse neu starten"
icon="pi pi-search"
severity="secondary"
onClick={onReanalyze || onAnalyze}
loading={busy}
/>
) : null}
</>
)}
{canRestartEncodeFromLastSettings ? (
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
onClick={() => onRestartEncode?.(retryJobId)}
loading={busy}
disabled={!retryJobId}
/>
) : null}
{state === 'ERROR' && retryJobId && (
<Button
label="Retry Rippen"
icon="pi pi-refresh"
severity="warning"
onClick={() => onRetry(retryJobId)}
loading={busy}
/>
)}
{state === 'ERROR' ? (
<Button
label="Disk-Analyse neu starten"
icon="pi pi-search"
severity="secondary"
onClick={onReanalyze || onAnalyze}
loading={busy}
/>
) : null}
</div>
{running ? (
@@ -547,7 +607,7 @@ export default function PipelineStatusCard({
</div>
) : null}
{playlistDecisionRequiredBeforeStart ? (
{playlistDecisionRequiredBeforeStart && !queueLocked ? (
<div className="playlist-decision-block">
<h3>Playlist-Auswahl erforderlich</h3>
<small>
@@ -561,6 +621,7 @@ export default function PipelineStatusCard({
<input
type="checkbox"
checked={normalizePlaylistId(selectedPlaylistId) === row.playlistId}
disabled={queueLocked}
onChange={() => {
const next = normalizePlaylistId(selectedPlaylistId) === row.playlistId ? null : row.playlistId;
setSelectedPlaylistId(next);
@@ -629,7 +690,7 @@ export default function PipelineStatusCard({
<strong>IMDb:</strong> {selectedMetadata.imdbId || '-'}
</div>
<div>
<strong>Status:</strong> {state}
<strong>Status:</strong> {stateLabel}
</div>
</div>
</div>
@@ -638,7 +699,7 @@ export default function PipelineStatusCard({
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
<div className="mediainfo-review-block">
<h3>Titel-/Spurprüfung</h3>
{state === 'READY_TO_ENCODE' && !reviewConfirmed ? (
{state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked ? (
<small>
{isPreRipReview
? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.'
@@ -651,9 +712,9 @@ export default function PipelineStatusCard({
presetDisplayValue={presetDisplayValue}
commandOutputPath={commandOutputPath}
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
allowTitleSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed}
allowTitleSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
onSelectEncodeTitle={(titleId) => setSelectedEncodeTitleId(normalizeTitleId(titleId))}
allowTrackSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed}
allowTrackSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
trackSelectionByTitle={trackSelectionByTitle}
onTrackSelectionChange={(titleId, trackType, trackId, checked) => {
const normalizedTitleId = normalizeTitleId(titleId);
@@ -684,7 +745,7 @@ export default function PipelineStatusCard({
}}
availablePostScripts={scriptCatalog}
selectedPostEncodeScriptIds={selectedPostEncodeScriptIds}
allowPostScriptSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed}
allowPostScriptSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
onAddPostEncodeScript={() => {
setSelectedPostEncodeScriptIds((prev) => {
const normalizedCurrent = normalizeScriptIdList(prev);