Upload
This commit is contained in:
@@ -20,6 +20,7 @@ export default function JobDetailDialog({
|
||||
onLoadLog,
|
||||
logLoadingMode = null,
|
||||
onAssignOmdb,
|
||||
onRestartEncode,
|
||||
onReencode,
|
||||
onDeleteFiles,
|
||||
onDeleteEntry,
|
||||
@@ -32,7 +33,15 @@ 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 canDeleteEntry = !running;
|
||||
const hasConfirmedPlan = Boolean(
|
||||
job?.encodePlan
|
||||
&& Array.isArray(job?.encodePlan?.titles)
|
||||
&& job?.encodePlan?.titles.length > 0
|
||||
&& Number(job?.encode_review_confirmed || 0) === 1
|
||||
);
|
||||
const hasRestartInput = Boolean(job?.encode_input_path || job?.raw_path || job?.encodePlan?.encodeInputPath);
|
||||
const canRestartEncode = Boolean(hasConfirmedPlan && hasRestartInput && !running);
|
||||
const canDeleteEntry = !running && typeof onDeleteEntry === 'function';
|
||||
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);
|
||||
@@ -106,6 +115,14 @@ export default function JobDetailDialog({
|
||||
<div>
|
||||
<strong>Movie-Dir leer:</strong> {job.movieDirStatus?.isEmpty === null ? '-' : job.movieDirStatus?.isEmpty ? 'ja' : 'nein'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Backup erfolgreich:</strong>{' '}
|
||||
{job?.backupSuccess ? <span className="job-step-inline-ok"><i className="pi pi-check-circle" aria-hidden="true" /><span>ja</span></span> : 'nein'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Encode erfolgreich:</strong>{' '}
|
||||
{job?.encodeSuccess ? <span className="job-step-inline-ok"><i className="pi pi-check-circle" aria-hidden="true" /><span>ja</span></span> : 'nein'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Fehler:</strong> {job.error_message || '-'}
|
||||
</div>
|
||||
@@ -136,8 +153,19 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onAssignOmdb?.(job)}
|
||||
loading={omdbAssignBusy}
|
||||
disabled={running}
|
||||
disabled={running || typeof onAssignOmdb !== 'function'}
|
||||
/>
|
||||
{typeof onRestartEncode === 'function' ? (
|
||||
<Button
|
||||
label="Encode neu starten"
|
||||
icon="pi pi-play"
|
||||
severity="success"
|
||||
size="small"
|
||||
onClick={() => onRestartEncode?.(job)}
|
||||
loading={actionBusy}
|
||||
disabled={!canRestartEncode}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
label="RAW neu encodieren"
|
||||
icon="pi pi-cog"
|
||||
@@ -145,7 +173,7 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onReencode?.(job)}
|
||||
loading={reencodeBusy}
|
||||
disabled={!canReencode}
|
||||
disabled={!canReencode || typeof onReencode !== 'function'}
|
||||
/>
|
||||
<Button
|
||||
label="RAW löschen"
|
||||
@@ -155,7 +183,7 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(job, 'raw')}
|
||||
loading={actionBusy}
|
||||
disabled={!job.rawStatus?.exists}
|
||||
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
|
||||
/>
|
||||
<Button
|
||||
label="Movie löschen"
|
||||
@@ -165,7 +193,7 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(job, 'movie')}
|
||||
loading={actionBusy}
|
||||
disabled={!job.outputStatus?.exists}
|
||||
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
|
||||
/>
|
||||
<Button
|
||||
label="Beides löschen"
|
||||
@@ -174,7 +202,7 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(job, 'both')}
|
||||
loading={actionBusy}
|
||||
disabled={!job.rawStatus?.exists && !job.outputStatus?.exists}
|
||||
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
|
||||
/>
|
||||
<Button
|
||||
label="Historieneintrag löschen"
|
||||
|
||||
@@ -312,6 +312,28 @@ export default function PipelineStatusCard({
|
||||
() => buildOutputPathPreview(settingsMap, selectedMetadata, retryJobId),
|
||||
[settingsMap, selectedMetadata, retryJobId]
|
||||
);
|
||||
const buildSelectedTrackSelectionForCurrentTitle = () => {
|
||||
const encodeTitleId = normalizeTitleId(selectedEncodeTitleId);
|
||||
const selectionEntry = encodeTitleId
|
||||
? (trackSelectionByTitle?.[encodeTitleId] || trackSelectionByTitle?.[String(encodeTitleId)] || null)
|
||||
: null;
|
||||
const fallbackSelection = encodeTitleId
|
||||
? defaultTrackSelectionForTitle(mediaInfoReview, encodeTitleId)
|
||||
: { audioTrackIds: [], subtitleTrackIds: [] };
|
||||
const effectiveSelection = selectionEntry || fallbackSelection;
|
||||
const selectedTrackSelection = encodeTitleId
|
||||
? {
|
||||
[encodeTitleId]: {
|
||||
audioTrackIds: normalizeTrackIdList(effectiveSelection?.audioTrackIds || []),
|
||||
subtitleTrackIds: normalizeTrackIdList(effectiveSelection?.subtitleTrackIds || [])
|
||||
}
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
encodeTitleId,
|
||||
selectedTrackSelection
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Pipeline Status" subTitle="Live Zustand und Fortschritt">
|
||||
@@ -353,37 +375,6 @@ export default function PipelineStatusCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'READY_TO_ENCODE' && retryJobId && (
|
||||
<Button
|
||||
label="Auswahl bestätigen"
|
||||
icon="pi pi-check"
|
||||
severity="warning"
|
||||
outlined
|
||||
onClick={() => {
|
||||
const encodeTitleId = normalizeTitleId(selectedEncodeTitleId);
|
||||
const selectionEntry = encodeTitleId
|
||||
? (trackSelectionByTitle?.[encodeTitleId] || trackSelectionByTitle?.[String(encodeTitleId)] || null)
|
||||
: null;
|
||||
const fallbackSelection = encodeTitleId
|
||||
? defaultTrackSelectionForTitle(mediaInfoReview, encodeTitleId)
|
||||
: { audioTrackIds: [], subtitleTrackIds: [] };
|
||||
const effectiveSelection = selectionEntry || fallbackSelection;
|
||||
const selectedTrackSelection = encodeTitleId
|
||||
? {
|
||||
[encodeTitleId]: {
|
||||
audioTrackIds: normalizeTrackIdList(effectiveSelection?.audioTrackIds || []),
|
||||
subtitleTrackIds: normalizeTrackIdList(effectiveSelection?.subtitleTrackIds || [])
|
||||
}
|
||||
}
|
||||
: null;
|
||||
|
||||
onConfirmReview(retryJobId, encodeTitleId, selectedTrackSelection);
|
||||
}}
|
||||
loading={busy}
|
||||
disabled={reviewConfirmed || !canConfirmReview}
|
||||
/>
|
||||
)}
|
||||
|
||||
{playlistDecisionRequiredBeforeStart && retryJobId && (
|
||||
<Button
|
||||
label="Playlist übernehmen"
|
||||
@@ -398,12 +389,25 @@ export default function PipelineStatusCard({
|
||||
|
||||
{state === 'READY_TO_ENCODE' && retryJobId && (
|
||||
<Button
|
||||
label={isPreRipReview ? 'Backup + Encode starten' : 'Encode starten'}
|
||||
label={isPreRipReview ? 'Backup + Encoding starten' : 'Encoding starten'}
|
||||
icon="pi pi-play"
|
||||
severity="success"
|
||||
onClick={() => onStart(retryJobId)}
|
||||
onClick={async () => {
|
||||
const requiresAutoConfirm = !reviewConfirmed;
|
||||
if (!requiresAutoConfirm) {
|
||||
await onStart(retryJobId);
|
||||
return;
|
||||
}
|
||||
|
||||
const { encodeTitleId, selectedTrackSelection } = buildSelectedTrackSelectionForCurrentTitle();
|
||||
await onStart(retryJobId, {
|
||||
ensureConfirmed: true,
|
||||
selectedEncodeTitleId: encodeTitleId,
|
||||
selectedTrackSelection
|
||||
});
|
||||
}}
|
||||
loading={busy}
|
||||
disabled={!canStartReadyJob || !reviewConfirmed}
|
||||
disabled={!canStartReadyJob || !canConfirmReview}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -550,8 +554,8 @@ export default function PipelineStatusCard({
|
||||
{state === 'READY_TO_ENCODE' && !reviewConfirmed ? (
|
||||
<small>
|
||||
{isPreRipReview
|
||||
? 'Backup/Rip + Encode ist gesperrt, bis die Spurauswahl bestätigt wurde.'
|
||||
: 'Encode ist gesperrt, bis die Titel-/Spurauswahl bestätigt wurde.'}
|
||||
? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.'
|
||||
: 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Encoding starten" wird automatisch bestätigt und gestartet.'}
|
||||
{reviewPlaylistDecisionRequired ? ' Bitte den korrekten Titel per Checkbox auswählen.' : ''}
|
||||
</small>
|
||||
) : null}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { api } from '../api/client';
|
||||
import PipelineStatusCard from '../components/PipelineStatusCard';
|
||||
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
|
||||
@@ -73,6 +74,29 @@ function mediaIndicatorMeta(job) {
|
||||
};
|
||||
}
|
||||
|
||||
function JobStepChecks({ backupSuccess, encodeSuccess }) {
|
||||
const hasAny = Boolean(backupSuccess || encodeSuccess);
|
||||
if (!hasAny) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="job-step-checks">
|
||||
{backupSuccess ? (
|
||||
<span className="job-step-inline-ok" title="Backup/Rip erfolgreich">
|
||||
<i className="pi pi-check-circle" aria-hidden="true" />
|
||||
<span>Backup</span>
|
||||
</span>
|
||||
) : null}
|
||||
{encodeSuccess ? (
|
||||
<span className="job-step-inline-ok" title="Encode erfolgreich">
|
||||
<i className="pi pi-check-circle" aria-hidden="true" />
|
||||
<span>Encode</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
||||
const jobId = normalizeJobId(job?.id);
|
||||
if (
|
||||
@@ -98,12 +122,18 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
||||
const jobStatus = String(job?.status || job?.last_state || 'IDLE').trim().toUpperCase() || 'IDLE';
|
||||
const lastState = String(job?.last_state || '').trim().toUpperCase();
|
||||
const errorText = String(job?.error_message || '').trim().toUpperCase();
|
||||
const hasOutputPath = Boolean(String(job?.output_path || '').trim());
|
||||
const hasEncodePlan = Boolean(encodePlan && Array.isArray(encodePlan?.titles) && encodePlan.titles.length > 0);
|
||||
const looksLikeCancelledEncode = jobStatus === 'ERROR' && (
|
||||
(errorText.includes('ABGEBROCHEN') || errorText.includes('CANCELLED'))
|
||||
&& (hasOutputPath || Boolean(job?.encode_input_path) || Boolean(job?.handbrakeInfo))
|
||||
);
|
||||
const looksLikeEncodingError = jobStatus === 'ERROR' && (
|
||||
errorText.includes('ENCODING')
|
||||
|| errorText.includes('HANDBRAKE')
|
||||
|| lastState === 'ENCODING'
|
||||
|| Boolean(job?.handbrakeInfo)
|
||||
|| looksLikeCancelledEncode
|
||||
);
|
||||
const canRestartEncodeFromLastSettings = Boolean(
|
||||
hasEncodePlan
|
||||
@@ -151,6 +181,12 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
||||
export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline }) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
|
||||
const [cancelCleanupDialog, setCancelCleanupDialog] = useState({
|
||||
visible: false,
|
||||
jobId: null,
|
||||
outputPath: null
|
||||
});
|
||||
const [cancelCleanupBusy, setCancelCleanupBusy] = useState(false);
|
||||
const [liveJobLog, setLiveJobLog] = useState('');
|
||||
const [jobsLoading, setJobsLoading] = useState(false);
|
||||
const [dashboardJobs, setDashboardJobs] = useState([]);
|
||||
@@ -326,11 +362,22 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
const cancelledState = state;
|
||||
const cancelledJobId = currentPipelineJobId;
|
||||
const cancelledJob = dashboardJobs.find((item) => normalizeJobId(item?.id) === cancelledJobId) || null;
|
||||
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.cancelPipeline();
|
||||
await refreshPipeline();
|
||||
await loadDashboardJobs();
|
||||
if (cancelledState === 'ENCODING' && cancelledJobId) {
|
||||
setCancelCleanupDialog({
|
||||
visible: true,
|
||||
jobId: cancelledJobId,
|
||||
outputPath: cancelledJob?.output_path || null
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
@@ -338,13 +385,52 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartJob = async (jobId) => {
|
||||
const handleDeleteCancelledOutput = async () => {
|
||||
const jobId = normalizeJobId(cancelCleanupDialog?.jobId);
|
||||
if (!jobId) {
|
||||
setCancelCleanupDialog({ visible: false, jobId: null, outputPath: null });
|
||||
return;
|
||||
}
|
||||
|
||||
setCancelCleanupBusy(true);
|
||||
try {
|
||||
const response = await api.deleteJobFiles(jobId, 'movie');
|
||||
const summary = response?.summary || {};
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Movie gelöscht',
|
||||
detail: `Entfernt: ${summary.movie?.filesDeleted ?? 0} Datei(en), ${summary.movie?.dirsRemoved ?? 0} Ordner.`,
|
||||
life: 4000
|
||||
});
|
||||
await loadDashboardJobs();
|
||||
await refreshPipeline();
|
||||
setCancelCleanupDialog({ visible: false, jobId: null, outputPath: null });
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setCancelCleanupBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartJob = async (jobId, options = null) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (!normalizedJobId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startOptions = options && typeof options === 'object' ? options : {};
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.startJob(jobId);
|
||||
if (startOptions.ensureConfirmed) {
|
||||
await api.confirmEncodeReview(normalizedJobId, {
|
||||
selectedEncodeTitleId: startOptions.selectedEncodeTitleId ?? null,
|
||||
selectedTrackSelection: startOptions.selectedTrackSelection ?? null
|
||||
});
|
||||
}
|
||||
await api.startJob(normalizedJobId);
|
||||
await refreshPipeline();
|
||||
await loadDashboardJobs();
|
||||
setExpandedJobId(normalizeJobId(jobId));
|
||||
setExpandedJobId(normalizedJobId);
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
@@ -401,6 +487,18 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
};
|
||||
|
||||
const handleRestartEncodeWithLastSettings = async (jobId) => {
|
||||
const job = dashboardJobs.find((item) => normalizeJobId(item?.id) === normalizeJobId(jobId)) || null;
|
||||
const title = job?.title || job?.detected_title || `Job #${jobId}`;
|
||||
if (job?.encodeSuccess) {
|
||||
const confirmed = window.confirm(
|
||||
`Encode für "${title}" ist bereits erfolgreich abgeschlossen. Wirklich erneut encodieren?\n` +
|
||||
'Es wird eine neue Datei mit Kollisionsprüfung angelegt.'
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.restartEncodeWithLastSettings(jobId);
|
||||
@@ -493,6 +591,7 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
{String(job?.status || '').trim().toUpperCase() === 'READY_TO_ENCODE'
|
||||
? <Tag value={reviewConfirmed ? 'Review bestätigt' : 'Review offen'} severity={reviewConfirmed ? 'success' : 'warning'} />
|
||||
: null}
|
||||
<JobStepChecks backupSuccess={Boolean(job?.backupSuccess)} encodeSuccess={Boolean(job?.encodeSuccess)} />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@@ -560,6 +659,7 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
{String(job?.status || '').trim().toUpperCase() === 'READY_TO_ENCODE'
|
||||
? <Tag value={reviewConfirmed ? 'Bestätigt' : 'Unbestätigt'} severity={reviewConfirmed ? 'success' : 'warning'} />
|
||||
: null}
|
||||
<JobStepChecks backupSuccess={Boolean(job?.backupSuccess)} encodeSuccess={Boolean(job?.encodeSuccess)} />
|
||||
</div>
|
||||
<i className="pi pi-angle-down" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -624,6 +724,37 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
onSearch={handleOmdbSearch}
|
||||
busy={busy}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
header="Encode abgebrochen"
|
||||
visible={Boolean(cancelCleanupDialog.visible)}
|
||||
onHide={() => setCancelCleanupDialog({ visible: false, jobId: null, outputPath: null })}
|
||||
style={{ width: '32rem', maxWidth: '96vw' }}
|
||||
modal
|
||||
>
|
||||
<p>
|
||||
Soll die bisher erzeugte Movie-Datei inklusive Job-Ordner im Ausgabeverzeichnis gelöscht werden?
|
||||
</p>
|
||||
{cancelCleanupDialog?.outputPath ? (
|
||||
<small className="muted-inline">Output-Pfad: {cancelCleanupDialog.outputPath}</small>
|
||||
) : null}
|
||||
<div className="dialog-actions">
|
||||
<Button
|
||||
label="Behalten"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => setCancelCleanupDialog({ visible: false, jobId: null, outputPath: null })}
|
||||
disabled={cancelCleanupBusy}
|
||||
/>
|
||||
<Button
|
||||
label="Movie löschen"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
onClick={handleDeleteCancelledOutput}
|
||||
loading={cancelCleanupBusy}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ export default function HistoryPage() {
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [logLoadingMode, setLogLoadingMode] = useState(null);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [reencodeBusyJobId, setReencodeBusyJobId] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toastRef = useRef(null);
|
||||
|
||||
@@ -113,9 +115,109 @@ export default function HistoryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const refreshDetailIfOpen = async (jobId) => {
|
||||
if (!detailVisible || Number(selectedJob?.id || 0) !== Number(jobId || 0)) {
|
||||
return;
|
||||
}
|
||||
const response = await api.getJob(jobId, { includeLogs: false });
|
||||
setSelectedJob(response.job);
|
||||
};
|
||||
|
||||
const handleDeleteFiles = async (row, target) => {
|
||||
const label = target === 'raw' ? 'RAW-Dateien' : target === 'movie' ? 'Movie-Datei(en)' : 'RAW + Movie';
|
||||
const title = row.title || row.detected_title || `Job #${row.id}`;
|
||||
const confirmed = window.confirm(`${label} für "${title}" wirklich löschen?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionBusy(true);
|
||||
try {
|
||||
const response = await api.deleteJobFiles(row.id, target);
|
||||
const summary = response.summary || {};
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Dateien gelöscht',
|
||||
detail: `RAW: ${summary.raw?.filesDeleted ?? 0}, MOVIE: ${summary.movie?.filesDeleted ?? 0}`,
|
||||
life: 3500
|
||||
});
|
||||
await load();
|
||||
await refreshDetailIfOpen(row.id);
|
||||
} catch (error) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Löschen fehlgeschlagen', detail: error.message, life: 4500 });
|
||||
} finally {
|
||||
setActionBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReencode = async (row) => {
|
||||
const title = row.title || row.detected_title || `Job #${row.id}`;
|
||||
const confirmed = window.confirm(`RAW neu encodieren für "${title}" starten?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReencodeBusyJobId(row.id);
|
||||
try {
|
||||
await api.reencodeJob(row.id);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Re-Encode gestartet',
|
||||
detail: 'Job wurde in die Mediainfo-Prüfung gesetzt.',
|
||||
life: 3500
|
||||
});
|
||||
await load();
|
||||
await refreshDetailIfOpen(row.id);
|
||||
} catch (error) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Re-Encode fehlgeschlagen', detail: error.message, life: 4500 });
|
||||
} finally {
|
||||
setReencodeBusyJobId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartEncode = async (row) => {
|
||||
const title = row.title || row.detected_title || `Job #${row.id}`;
|
||||
if (row?.encodeSuccess) {
|
||||
const confirmed = window.confirm(
|
||||
`Encode für "${title}" ist bereits erfolgreich abgeschlossen. Wirklich erneut encodieren?\n` +
|
||||
'Es wird eine neue Datei mit Kollisionsprüfung angelegt.'
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setActionBusy(true);
|
||||
try {
|
||||
await api.restartEncodeWithLastSettings(row.id);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Encode-Neustart gestartet',
|
||||
detail: 'Letzte bestätigte Einstellungen werden verwendet.',
|
||||
life: 3500
|
||||
});
|
||||
await load();
|
||||
await refreshDetailIfOpen(row.id);
|
||||
} catch (error) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Encode-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
|
||||
} finally {
|
||||
setActionBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const statusBody = (row) => <Tag value={row.status} />;
|
||||
const mkBody = (row) => row.makemkvInfo ? `${row.makemkvInfo.status || '-'} ${typeof row.makemkvInfo.lastProgress === 'number' ? `${row.makemkvInfo.lastProgress.toFixed(1)}%` : ''}` : '-';
|
||||
const hbBody = (row) => row.handbrakeInfo ? `${row.handbrakeInfo.status || '-'} ${typeof row.handbrakeInfo.lastProgress === 'number' ? `${row.handbrakeInfo.lastProgress.toFixed(1)}%` : ''}` : '-';
|
||||
const mkBody = (row) => (
|
||||
<span className="job-step-cell">
|
||||
{row?.backupSuccess ? <i className="pi pi-check-circle job-step-ok-icon" aria-label="Backup erfolgreich" title="Backup erfolgreich" /> : null}
|
||||
<span>{row.makemkvInfo ? `${row.makemkvInfo.status || '-'} ${typeof row.makemkvInfo.lastProgress === 'number' ? `${row.makemkvInfo.lastProgress.toFixed(1)}%` : ''}` : '-'}</span>
|
||||
</span>
|
||||
);
|
||||
const hbBody = (row) => (
|
||||
<span className="job-step-cell">
|
||||
{row?.encodeSuccess ? <i className="pi pi-check-circle job-step-ok-icon" aria-label="Encode erfolgreich" title="Encode erfolgreich" /> : null}
|
||||
<span>{row.handbrakeInfo ? `${row.handbrakeInfo.status || '-'} ${typeof row.handbrakeInfo.lastProgress === 'number' ? `${row.handbrakeInfo.lastProgress.toFixed(1)}%` : ''}` : '-'}</span>
|
||||
</span>
|
||||
);
|
||||
const mediaBody = (row) => {
|
||||
const mediaType = resolveMediaType(row);
|
||||
const src = mediaType === 'bluray' ? blurayIndicatorIcon : discIndicatorIcon;
|
||||
@@ -186,6 +288,11 @@ export default function HistoryPage() {
|
||||
detailLoading={detailLoading}
|
||||
onLoadLog={handleLoadLog}
|
||||
logLoadingMode={logLoadingMode}
|
||||
onRestartEncode={handleRestartEncode}
|
||||
onReencode={handleReencode}
|
||||
onDeleteFiles={handleDeleteFiles}
|
||||
actionBusy={actionBusy}
|
||||
reencodeBusy={reencodeBusyJobId === selectedJob?.id}
|
||||
onHide={() => {
|
||||
setDetailVisible(false);
|
||||
setDetailLoading(false);
|
||||
|
||||
@@ -298,6 +298,40 @@ body {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.job-step-checks {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.job-step-inline-ok {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
color: #1c8a3a;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.job-step-inline-ok .pi {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.job-step-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.job-step-ok-icon {
|
||||
color: #1c8a3a;
|
||||
}
|
||||
|
||||
.muted-inline {
|
||||
color: var(--rip-muted);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dashboard-job-poster-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user