0.12.0-1 Plugins und diverses
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user