0.12.0-1 Plugins und diverses

This commit is contained in:
2026-03-19 18:57:05 +00:00
parent 0a9cf6969f
commit 24955a956d
33 changed files with 3978 additions and 479 deletions
+168 -55
View File
@@ -43,6 +43,14 @@ const dashboardStatuses = new Set([
'CD_RIPPING',
'CD_ENCODING'
]);
const hiddenCancelledReviewOrigins = new Set([
'READY_TO_START',
'READY_TO_ENCODE',
'METADATA_SELECTION',
'WAITING_FOR_USER_DECISION',
'CD_METADATA_SELECTION',
'CD_READY_TO_RIP'
]);
function normalizeJobId(value) {
const parsed = Number(value);
@@ -792,6 +800,9 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
const liveContext = liveJobProgress?.context && typeof liveJobProgress.context === 'object'
? liveJobProgress.context
: 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
? {
...computedContext,
@@ -805,11 +816,15 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
: computedContext;
return {
state: liveJobProgress?.state || jobStatus,
state: ignoreStaleLiveProgress ? jobStatus : (liveJobProgress?.state || jobStatus),
activeJobId: jobId,
progress: liveJobProgress != null ? Number(liveJobProgress.progress ?? 0) : 0,
eta: liveJobProgress?.eta || null,
statusText: liveJobProgress?.statusText || job?.error_message || null,
progress: ignoreStaleLiveProgress
? 0
: (liveJobProgress != null ? Number(liveJobProgress.progress ?? 0) : 0),
eta: ignoreStaleLiveProgress ? null : (liveJobProgress?.eta || null),
statusText: ignoreStaleLiveProgress
? (job?.error_message || null)
: (liveJobProgress?.statusText || job?.error_message || null),
context: mergedContext
};
}
@@ -876,6 +891,7 @@ export default function DashboardPage({
const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
const [knownDrives, setKnownDrives] = useState([]);
const [driveRescanBusy, setDriveRescanBusy] = useState(() => new Set());
const [driveAnalyzeBusy, setDriveAnalyzeBusy] = useState(() => new Set());
const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
const [insertWaitSeconds, setInsertWaitSeconds] = useState(30);
@@ -978,18 +994,45 @@ export default function DashboardPage({
if (queueResponse.status === 'fulfilled') {
setQueueState(normalizeQueue(queueResponse.value?.queue));
}
const shouldDisplayOnDashboard = (job) => {
const normalizedStatus = String(job?.status || '').trim().toUpperCase();
if (!dashboardStatuses.has(normalizedStatus)) {
return false;
}
if (normalizedStatus !== 'CANCELLED') {
return true;
}
const cancelledOrigin = String(job?.last_state || '').trim().toUpperCase();
return !hiddenCancelledReviewOrigins.has(cancelledOrigin);
};
const next = allJobs
.filter((job) => dashboardStatuses.has(String(job?.status || '').trim().toUpperCase()))
.filter((job) => shouldDisplayOnDashboard(job))
.sort((a, b) => Number(b?.id || 0) - Number(a?.id || 0));
if (currentPipelineJobId && !next.some((job) => normalizeJobId(job?.id) === currentPipelineJobId)) {
try {
const active = await api.getJob(currentPipelineJobId, { lite: true });
if (active?.job) {
next.unshift(active.job);
const pinnedJobIds = new Set();
if (currentPipelineJobId) {
pinnedJobIds.add(currentPipelineJobId);
}
for (const driveState of Object.values(pipeline?.cdDrives || {})) {
const driveJobId = normalizeJobId(driveState?.jobId);
if (driveJobId) {
pinnedJobIds.add(driveJobId);
}
}
const missingPinnedJobIds = Array.from(pinnedJobIds)
.filter((jobId) => !next.some((job) => normalizeJobId(job?.id) === jobId));
if (missingPinnedJobIds.length > 0) {
const pinnedResults = await Promise.allSettled(
missingPinnedJobIds.map((jobId) => api.getJob(jobId, { lite: true, forceRefresh: true }))
);
for (const result of pinnedResults) {
if (result.status !== 'fulfilled') {
continue;
}
const job = result.value?.job;
if (job && shouldDisplayOnDashboard(job)) {
next.unshift(job);
}
} catch (_error) {
// ignore; dashboard still shows available rows
}
}
@@ -1067,7 +1110,7 @@ export default function DashboardPage({
useEffect(() => {
void loadDashboardJobs();
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, jobsRefreshToken]);
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, pipeline?.cdDrives, jobsRefreshToken]);
useEffect(() => {
api.getDetectedDrives()
@@ -1328,7 +1371,7 @@ export default function DashboardPage({
};
const handleAnalyzeForDrive = async (devicePath) => {
setBusy(true);
setDriveAnalyzeBusy((prev) => new Set([...prev, devicePath]));
try {
await api.analyzeDisc(devicePath);
await refreshPipeline();
@@ -1336,10 +1379,27 @@ export default function DashboardPage({
} catch (error) {
showError(error);
} finally {
setBusy(false);
setDriveAnalyzeBusy((prev) => {
const next = new Set(prev);
next.delete(devicePath);
return next;
});
}
};
const handleAnalyzeAll = async () => {
const drivesToAnalyze = allDrives
.filter((drv) => {
const cdState = drv.cdDrive ? String(drv.cdDrive.state || '').toUpperCase() : null;
if (cdState) return cdState === 'DISC_DETECTED';
if (Boolean(drv.pipelineDevice) && String(drv.pipelineState || '').toUpperCase() === 'DISC_DETECTED') return true;
return Boolean(drv.detectedDisc);
})
.map((drv) => drv.path);
if (drivesToAnalyze.length === 0) return;
await Promise.all(drivesToAnalyze.map((path) => handleAnalyzeForDrive(path)));
};
const handleRescan = async () => {
setBusy(true);
try {
@@ -1351,7 +1411,7 @@ export default function DashboardPage({
severity: count > 0 ? 'success' : 'info',
summary: 'Laufwerke neu gelesen',
detail: count > 0
? `${count} Medium${count > 1 ? 'en' : ''} erkannt.`
? `${count > 1 ? `${count} Medien` : '1 Medium'} erkannt.`
: 'Kein Medium erkannt.',
life: 2800
});
@@ -1771,6 +1831,31 @@ export default function DashboardPage({
}
};
const handleRestartCdReviewFromRaw = async (jobId) => {
const normalizedJobId = normalizeJobId(jobId);
if (!normalizedJobId) {
return;
}
setJobBusy(normalizedJobId, true);
try {
const response = await api.restartCdReviewFromRaw(normalizedJobId);
const result = getQueueActionResult(response);
const replacementJobId = normalizeJobId(result?.jobId) || normalizedJobId;
await refreshPipeline();
await loadDashboardJobs();
if (result.queued) {
showQueuedToast(toastRef, 'CD-Vorprüfung', result);
} else {
setExpandedJobId(replacementJobId);
}
} catch (error) {
showError(error);
} finally {
setJobBusy(normalizedJobId, false);
}
};
const handleQueueDragEnter = (targetEntryId) => {
const targetId = Number(targetEntryId);
const draggedId = Number(draggingQueueEntryId);
@@ -2047,11 +2132,20 @@ export default function DashboardPage({
const base = driveMap.get(device.path) || { path: device.path, model: device.model };
driveMap.set(device.path, { ...base, pipelineDevice: device, pipelineState: state });
}
return Array.from(driveMap.values()).sort((a, b) => (a.path || '').localeCompare(b.path || ''));
}, [knownDrives, pipeline?.cdDrives, device, state]);
// For drives that the detection service found but aren't tracked by cdDrives or the global
// state machine (e.g. a second non-CD disc), show DISC_DETECTED using detectedDiscs.
for (const [drivePath, discDevice] of Object.entries(pipeline?.detectedDiscs || {})) {
const existing = driveMap.get(drivePath);
if (existing && !existing.cdDrive && !existing.pipelineDevice) {
driveMap.set(drivePath, { ...existing, detectedDisc: discDevice });
}
}
return Array.from(driveMap.values())
.filter((drv) => !String(drv.path || '').startsWith('__virtual__'))
.sort((a, b) => (a.path || '').localeCompare(b.path || ''));
}, [knownDrives, pipeline?.cdDrives, pipeline?.detectedDiscs, device, state]);
const isDriveActive = driveActiveStates.includes(state);
const canRescan = !isDriveActive;
const canReanalyze = !isDriveActive && (state === 'ENCODING' ? Boolean(device) : !processingStates.includes(state));
const canOpenMetadataModal = Boolean(defaultMetadataDialogContext?.jobId);
const queueRunningJobs = Array.isArray(queueState?.runningJobs) ? queueState.runningJobs : [];
const queuedJobs = Array.isArray(queueState?.queuedJobs) ? queueState.queuedJobs : [];
@@ -2291,33 +2385,7 @@ export default function DashboardPage({
</Card>
<Card title="Disk-Information">
{/* Global action buttons (non-CD / DVD / Bluray) */}
<div className="actions-row">
<Button
label="Alle neu lesen"
icon="pi pi-refresh"
severity="secondary"
onClick={handleRescan}
loading={busy}
disabled={!canRescan}
/>
<Button
label="Disk neu analysieren"
icon="pi pi-search"
severity="warning"
onClick={handleReanalyze}
loading={busy}
disabled={!canReanalyze}
/>
<Button
label="Metadaten-Modal öffnen"
icon="pi pi-list"
onClick={() => handleOpenMetadataDialog()}
disabled={!canOpenMetadataModal}
/>
</div>
{/* Compact per-drive list */}
{/* Per-drive list */}
{allDrives.length > 0 ? (
<div className="drive-list">
{allDrives.map((drv) => {
@@ -2325,6 +2393,7 @@ export default function DashboardPage({
const cdDrive = drv.cdDrive;
const pipelineDevice = drv.pipelineDevice;
const isRescanBusy = driveRescanBusy.has(drivePath);
const isAnalyzeBusy = driveAnalyzeBusy.has(drivePath);
// Determine display state
let stateLabel = null;
@@ -2354,6 +2423,10 @@ export default function DashboardPage({
: processingStates.includes(s) ? 'warning'
: 'info';
discInfo = pipelineDevice.discLabel || pipelineDevice.label || null;
} else if (drv.detectedDisc) {
stateLabel = 'DISC_DETECTED';
stateSeverity = 'info';
discInfo = drv.detectedDisc.discLabel || drv.detectedDisc.label || null;
} else {
stateLabel = 'LEER';
stateSeverity = 'secondary';
@@ -2362,7 +2435,11 @@ export default function DashboardPage({
const driveJobId = normalizeJobId(cdDrive?.jobId);
const driveCtx = cdDrive?.context && typeof cdDrive.context === 'object' ? cdDrive.context : {};
const cdState = cdDrive ? String(cdDrive.state || '').toUpperCase() : null;
const pipelineState = String(drv.pipelineState || '').toUpperCase();
const isCdDriveLocked = cdState ? activeCdDriveStates.includes(cdState) : false;
const canAnalyzeDrive = cdState
? cdState === 'DISC_DETECTED'
: (Boolean(pipelineDevice) && pipelineState === 'DISC_DETECTED') || Boolean(drv.detectedDisc);
return (
<div key={drivePath} className="drive-list-item">
@@ -2373,7 +2450,7 @@ export default function DashboardPage({
{drv.discArg && <span className="drive-list-disc-arg">{drv.discArg}</span>}
</div>
<div className="drive-list-state">
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} />}
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} className="drive-list-status-tag" />}
</div>
<div className="drive-list-actions">
<Button
@@ -2388,14 +2465,18 @@ export default function DashboardPage({
title="Laufwerk neu lesen"
aria-label="Laufwerk neu lesen"
/>
{cdState === 'DISC_DETECTED' && (
{canAnalyzeDrive && (
<Button
label="Analysieren"
icon="pi pi-search"
text
rounded
size="small"
severity="secondary"
loading={isAnalyzeBusy}
disabled={isAnalyzeBusy}
onClick={() => handleAnalyzeForDrive(drivePath)}
loading={busy}
disabled={busy}
title="Disk analysieren"
aria-label="Disk analysieren"
/>
)}
{cdState === 'CD_METADATA_SELECTION' && (
@@ -2413,13 +2494,16 @@ export default function DashboardPage({
)}
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
<Button
label="Retry"
icon="pi pi-replay"
text
rounded
size="small"
severity="warning"
onClick={() => handleAnalyzeForDrive(drivePath)}
loading={busy}
disabled={busy}
loading={isAnalyzeBusy}
disabled={isAnalyzeBusy}
title="Erneut analysieren"
aria-label="Erneut analysieren"
/>
)}
</div>
@@ -2438,6 +2522,32 @@ export default function DashboardPage({
) : (
<p className="drive-list-empty">Keine Laufwerke erkannt.</p>
)}
{/* Global action buttons — below drive list, side by side */}
<div className="drive-list-global-actions">
<Button
label="Alle neu lesen"
icon="pi pi-refresh"
severity="secondary"
size="small"
onClick={handleRescan}
loading={busy}
disabled={!canRescan}
/>
<Button
label="Alle analysieren"
icon="pi pi-search"
severity="warning"
size="small"
onClick={handleAnalyzeAll}
disabled={allDrives.every((drv) => {
const cs = drv.cdDrive ? String(drv.cdDrive.state || '').toUpperCase() : null;
if (cs) return cs !== 'DISC_DETECTED';
if (Boolean(drv.pipelineDevice) && String(drv.pipelineState || '').toUpperCase() === 'DISC_DETECTED') return false;
return !drv.detectedDisc;
})}
/>
</div>
</Card>
<Card title="Freier Speicher">
@@ -2538,7 +2648,9 @@ export default function DashboardPage({
? pipelineForJob.context.selectedMetadata
: {};
const audiobookChapterCount = Array.isArray(audiobookMeta?.chapters) ? audiobookMeta.chapters.length : 0;
const pluginExecution = resolveJobPluginExecution(job, pipelineForJob?.context);
const pluginExecution = resolveJobPluginExecution(job, pipelineForJob?.context, {
currentStatus: jobState
});
if (isExpanded) {
return (
@@ -2589,6 +2701,7 @@ export default function DashboardPage({
onStart={(ripConfig) => handleCdRipStart(jobId, ripConfig)}
onCancel={() => handleCancel(jobId, jobState)}
onRetry={() => handleRetry(jobId)}
onRestartReview={() => handleRestartCdReviewFromRaw(jobId)}
onOpenMetadata={() => {
const ctx = pipelineForJob?.context && typeof pipelineForJob.context === 'object'
? pipelineForJob.context