First test

This commit is contained in:
2026-03-12 08:00:23 +00:00
parent 190f6fe1a5
commit c13ce5a50b
15 changed files with 1842 additions and 46 deletions

View File

@@ -273,6 +273,25 @@ export const api = {
searchOmdb(q) {
return request(`/pipeline/omdb/search?q=${encodeURIComponent(q)}`);
},
searchMusicBrainz(q) {
return request(`/pipeline/cd/musicbrainz/search?q=${encodeURIComponent(q)}`);
},
async selectCdMetadata(payload) {
const result = await request('/pipeline/cd/select-metadata', {
method: 'POST',
body: JSON.stringify(payload)
});
afterMutationInvalidate(['/history', '/pipeline/queue']);
return result;
},
async startCdRip(jobId, ripConfig) {
const result = await request(`/pipeline/cd/start/${jobId}`, {
method: 'POST',
body: JSON.stringify(ripConfig || {})
});
afterMutationInvalidate(['/history', '/pipeline/queue']);
return result;
},
async selectMetadata(payload) {
const result = await request('/pipeline/select-metadata', {
method: 'POST',

View File

@@ -0,0 +1,282 @@
import { useEffect, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { Checkbox } from 'primereact/checkbox';
function formatDurationMs(ms) {
const totalSec = Math.round((ms || 0) / 1000);
const min = Math.floor(totalSec / 60);
const sec = totalSec % 60;
return `${min}:${String(sec).padStart(2, '0')}`;
}
export default function CdMetadataDialog({
visible,
context,
onHide,
onSubmit,
onSearch,
busy
}) {
const [selected, setSelected] = useState(null);
const [query, setQuery] = useState('');
const [extraResults, setExtraResults] = useState([]);
// Manual metadata inputs
const [manualTitle, setManualTitle] = useState('');
const [manualArtist, setManualArtist] = useState('');
const [manualYear, setManualYear] = useState(null);
// Per-track title editing
const [trackTitles, setTrackTitles] = useState({});
const [selectedTrackPositions, setSelectedTrackPositions] = useState(new Set());
const tocTracks = Array.isArray(context?.tracks) ? context.tracks : [];
useEffect(() => {
if (!visible) {
return;
}
setSelected(null);
setQuery(context?.detectedTitle || '');
setManualTitle(context?.detectedTitle || '');
setManualArtist('');
setManualYear(null);
setExtraResults([]);
const titles = {};
const positions = new Set();
for (const t of tocTracks) {
titles[t.position] = t.title || `Track ${t.position}`;
positions.add(t.position);
}
setTrackTitles(titles);
setSelectedTrackPositions(positions);
}, [visible, context]);
useEffect(() => {
if (!selected) {
return;
}
setManualTitle(selected.title || '');
setManualArtist(selected.artist || '');
setManualYear(selected.year || null);
// Pre-fill track titles from the MusicBrainz result
if (Array.isArray(selected.tracks) && selected.tracks.length > 0) {
const titles = {};
for (const t of selected.tracks) {
if (t.position <= tocTracks.length) {
titles[t.position] = t.title || `Track ${t.position}`;
}
}
// Fill any remaining tracks not in MB result
for (const t of tocTracks) {
if (!titles[t.position]) {
titles[t.position] = t.title || `Track ${t.position}`;
}
}
setTrackTitles(titles);
}
}, [selected]);
const allMbRows = [
...(Array.isArray(context?.mbCandidates) ? context.mbCandidates : []),
...extraResults
].filter(Boolean);
// Deduplicate by mbId
const mbRows = [];
const seen = new Set();
for (const r of allMbRows) {
if (r.mbId && !seen.has(r.mbId)) {
seen.add(r.mbId);
mbRows.push(r);
}
}
const handleSearch = async () => {
if (!query.trim()) {
return;
}
const results = await onSearch(query.trim());
setExtraResults(results || []);
};
const handleToggleTrack = (position) => {
setSelectedTrackPositions((prev) => {
const next = new Set(prev);
if (next.has(position)) {
next.delete(position);
} else {
next.add(position);
}
return next;
});
};
const handleToggleAll = () => {
if (selectedTrackPositions.size === tocTracks.length) {
setSelectedTrackPositions(new Set());
} else {
setSelectedTrackPositions(new Set(tocTracks.map((t) => t.position)));
}
};
const handleSubmit = async () => {
const tracks = tocTracks.map((t) => ({
position: t.position,
title: trackTitles[t.position] || `Track ${t.position}`,
selected: selectedTrackPositions.has(t.position)
}));
const payload = {
jobId: context.jobId,
title: manualTitle.trim() || context?.detectedTitle || 'Audio CD',
artist: manualArtist.trim() || null,
year: manualYear || null,
mbId: selected?.mbId || null,
coverUrl: selected?.coverArtUrl || null,
tracks
};
await onSubmit(payload);
};
const mbTitleBody = (row) => (
<div className="mb-result-row">
{row.coverArtUrl ? (
<img src={row.coverArtUrl} alt={row.title} className="poster-thumb-lg" />
) : (
<div className="poster-thumb-lg poster-fallback">-</div>
)}
<div>
<div><strong>{row.title}</strong></div>
<small>{row.artist}{row.year ? ` | ${row.year}` : ''}</small>
{row.label ? <small> | {row.label}</small> : null}
</div>
</div>
);
const allSelected = tocTracks.length > 0 && selectedTrackPositions.size === tocTracks.length;
const noneSelected = selectedTrackPositions.size === 0;
return (
<Dialog
header="CD-Metadaten auswählen"
visible={visible}
onHide={onHide}
style={{ width: '58rem', maxWidth: '97vw' }}
className="cd-metadata-dialog"
breakpoints={{ '1200px': '92vw', '768px': '96vw', '560px': '98vw' }}
modal
>
{/* MusicBrainz search */}
<div className="search-row">
<InputText
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Album / Interpret suchen"
/>
<Button label="MusicBrainz Suche" icon="pi pi-search" onClick={handleSearch} loading={busy} />
</div>
{mbRows.length > 0 ? (
<div className="table-scroll-wrap table-scroll-medium">
<DataTable
value={mbRows}
selectionMode="single"
selection={selected}
onSelectionChange={(e) => setSelected(e.value)}
dataKey="mbId"
size="small"
scrollable
scrollHeight="16rem"
emptyMessage="Keine Treffer"
>
<Column header="Album" body={mbTitleBody} />
<Column field="year" header="Jahr" style={{ width: '6rem' }} />
<Column field="country" header="Land" style={{ width: '6rem' }} />
</DataTable>
</div>
) : null}
{/* Manual metadata */}
<h4 style={{ marginTop: '1rem', marginBottom: '0.5rem' }}>Metadaten</h4>
<div className="metadata-grid">
<InputText
value={manualTitle}
onChange={(e) => setManualTitle(e.target.value)}
placeholder="Album-Titel"
/>
<InputText
value={manualArtist}
onChange={(e) => setManualArtist(e.target.value)}
placeholder="Interpret / Band"
/>
<InputNumber
value={manualYear}
onValueChange={(e) => setManualYear(e.value)}
placeholder="Jahr"
useGrouping={false}
min={1900}
max={2100}
/>
</div>
{/* Track selection */}
{tocTracks.length > 0 ? (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginTop: '1rem', marginBottom: '0.25rem' }}>
<h4 style={{ margin: 0 }}>Tracks ({tocTracks.length})</h4>
<Button
label={allSelected ? 'Alle abwählen' : 'Alle auswählen'}
size="small"
severity="secondary"
outlined
onClick={handleToggleAll}
/>
</div>
<div className="cd-track-list">
{tocTracks.map((track) => (
<div key={track.position} className="cd-track-row">
<Checkbox
checked={selectedTrackPositions.has(track.position)}
onChange={() => handleToggleTrack(track.position)}
inputId={`track-${track.position}`}
/>
<span className="cd-track-num">{String(track.position).padStart(2, '0')}</span>
<InputText
value={trackTitles[track.position] ?? `Track ${track.position}`}
onChange={(e) => setTrackTitles((prev) => ({ ...prev, [track.position]: e.target.value }))}
className="cd-track-title-input"
placeholder={`Track ${track.position}`}
disabled={!selectedTrackPositions.has(track.position)}
/>
<span className="cd-track-duration">
{track.durationMs ? formatDurationMs(track.durationMs) : '-'}
</span>
</div>
))}
</div>
</>
) : null}
<div className="dialog-actions" style={{ marginTop: '1rem' }}>
<Button label="Abbrechen" severity="secondary" text onClick={onHide} />
<Button
label="Weiter"
icon="pi pi-arrow-right"
onClick={handleSubmit}
loading={busy}
disabled={noneSelected || (!manualTitle.trim() && !context?.detectedTitle)}
/>
</div>
</Dialog>
);
}

View File

@@ -0,0 +1,256 @@
import { useState, useEffect } from 'react';
import { Dropdown } from 'primereact/dropdown';
import { Slider } from 'primereact/slider';
import { Button } from 'primereact/button';
import { ProgressBar } from 'primereact/progressbar';
import { Tag } from 'primereact/tag';
import { CD_FORMATS, CD_FORMAT_SCHEMAS, getDefaultFormatOptions } from '../config/cdFormatSchemas';
function isFieldVisible(field, values) {
if (!field.showWhen) {
return true;
}
return values[field.showWhen.field] === field.showWhen.value;
}
function FormatField({ field, value, onChange }) {
if (field.type === 'slider') {
return (
<div className="cd-format-field">
<label>
{field.label}: <strong>{value}</strong>
</label>
{field.description ? <small>{field.description}</small> : null}
<Slider
value={value}
onChange={(e) => onChange(field.key, e.value)}
min={field.min}
max={field.max}
step={field.step || 1}
/>
</div>
);
}
if (field.type === 'select') {
return (
<div className="cd-format-field">
<label>{field.label}</label>
{field.description ? <small>{field.description}</small> : null}
<Dropdown
value={value}
options={field.options}
optionLabel="label"
optionValue="value"
onChange={(e) => onChange(field.key, e.value)}
/>
</div>
);
}
return null;
}
export default function CdRipConfigPanel({
pipeline,
onStart,
onCancel,
busy
}) {
const context = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : {};
const tracks = Array.isArray(context.tracks) ? context.tracks : [];
const selectedMeta = context.selectedMetadata || {};
const state = String(pipeline?.state || '').trim().toUpperCase();
const isRipping = state === 'CD_RIPPING' || state === 'CD_ENCODING';
const isFinished = state === 'FINISHED';
const [format, setFormat] = useState('flac');
const [formatOptions, setFormatOptions] = useState(() => getDefaultFormatOptions('flac'));
// Track selection: position → boolean
const [selectedTracks, setSelectedTracks] = useState(() => {
const map = {};
for (const t of tracks) {
map[t.position] = true;
}
return map;
});
useEffect(() => {
setFormatOptions(getDefaultFormatOptions(format));
}, [format]);
useEffect(() => {
const map = {};
for (const t of tracks) {
map[t.position] = selectedTracks[t.position] !== false;
}
setSelectedTracks(map);
}, [tracks.length]);
const handleFormatOptionChange = (key, value) => {
setFormatOptions((prev) => ({ ...prev, [key]: value }));
};
const handleToggleTrack = (position) => {
setSelectedTracks((prev) => ({ ...prev, [position]: !prev[position] }));
};
const handleToggleAll = () => {
const allSelected = tracks.every((t) => selectedTracks[t.position] !== false);
const map = {};
for (const t of tracks) {
map[t.position] = !allSelected;
}
setSelectedTracks(map);
};
const handleStart = () => {
const selected = tracks
.filter((t) => selectedTracks[t.position] !== false)
.map((t) => t.position);
if (selected.length === 0) {
return;
}
onStart && onStart({
format,
formatOptions,
selectedTracks: selected
});
};
const schema = CD_FORMAT_SCHEMAS[format] || { fields: [] };
const visibleFields = schema.fields.filter((f) => isFieldVisible(f, formatOptions));
const selectedCount = tracks.filter((t) => selectedTracks[t.position] !== false).length;
const progress = Number(pipeline?.progress ?? 0);
const clampedProgress = Math.max(0, Math.min(100, progress));
const eta = String(pipeline?.eta || '').trim();
const statusText = String(pipeline?.statusText || '').trim();
if (isRipping || isFinished) {
return (
<div className="cd-rip-config-panel">
<div className="cd-rip-status">
<Tag
value={state === 'CD_RIPPING' ? 'Ripping' : state === 'CD_ENCODING' ? 'Encodierung' : 'Fertig'}
severity={isFinished ? 'success' : 'info'}
/>
{statusText ? <small>{statusText}</small> : null}
{!isFinished ? (
<>
<ProgressBar value={clampedProgress} />
<small>{Math.round(clampedProgress)}%{eta ? ` | ETA ${eta}` : ''}</small>
</>
) : null}
</div>
{!isFinished ? (
<Button
label="Abbrechen"
icon="pi pi-times"
severity="danger"
outlined
onClick={() => onCancel && onCancel()}
disabled={busy}
/>
) : null}
</div>
);
}
return (
<div className="cd-rip-config-panel">
<h4 style={{ marginTop: 0, marginBottom: '0.75rem' }}>CD-Rip Konfiguration</h4>
{selectedMeta.title ? (
<div className="cd-meta-summary">
<strong>{selectedMeta.artist ? `${selectedMeta.artist} ` : ''}{selectedMeta.title}</strong>
{selectedMeta.year ? <span> ({selectedMeta.year})</span> : null}
</div>
) : null}
{/* Format selection */}
<div className="cd-format-field">
<label>Ausgabeformat</label>
<Dropdown
value={format}
options={CD_FORMATS}
optionLabel="label"
optionValue="value"
onChange={(e) => setFormat(e.value)}
disabled={busy}
/>
</div>
{/* Format-specific options */}
{visibleFields.map((field) => (
<FormatField
key={field.key}
field={field}
value={formatOptions[field.key] ?? field.default}
onChange={handleFormatOptionChange}
/>
))}
{/* Track selection */}
{tracks.length > 0 ? (
<div className="cd-track-selection">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem' }}>
<strong>Tracks ({selectedCount} / {tracks.length} ausgewählt)</strong>
<Button
label={selectedCount === tracks.length ? 'Alle abwählen' : 'Alle auswählen'}
size="small"
severity="secondary"
outlined
onClick={handleToggleAll}
disabled={busy}
/>
</div>
<div className="cd-track-list">
{tracks.map((track) => {
const isSelected = selectedTracks[track.position] !== false;
const totalSec = Math.round((track.durationMs || track.durationSec * 1000 || 0) / 1000);
const min = Math.floor(totalSec / 60);
const sec = totalSec % 60;
const duration = totalSec > 0 ? `${min}:${String(sec).padStart(2, '0')}` : '-';
return (
<button
key={track.position}
type="button"
className={`cd-track-row selectable${isSelected ? ' selected' : ''}`}
onClick={() => !busy && handleToggleTrack(track.position)}
disabled={busy}
>
<span className="cd-track-num">{String(track.position).padStart(2, '0')}</span>
<span className="cd-track-title">{track.title || `Track ${track.position}`}</span>
<span className="cd-track-duration">{duration}</span>
</button>
);
})}
</div>
</div>
) : null}
{/* Actions */}
<div className="actions-row" style={{ marginTop: '1rem' }}>
<Button
label="Abbrechen"
severity="secondary"
outlined
onClick={() => onCancel && onCancel()}
disabled={busy}
/>
<Button
label="Rip starten"
icon="pi pi-play"
onClick={handleStart}
loading={busy}
disabled={selectedCount === 0}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,126 @@
/**
* CD output format schemas.
* Each format defines the fields shown in CdRipConfigPanel.
*/
export const CD_FORMATS = [
{ label: 'FLAC (verlustlos)', value: 'flac' },
{ label: 'MP3', value: 'mp3' },
{ label: 'Opus', value: 'opus' },
{ label: 'OGG Vorbis', value: 'ogg' },
{ label: 'WAV (unkomprimiert)', value: 'wav' }
];
export const CD_FORMAT_SCHEMAS = {
wav: {
fields: []
},
flac: {
fields: [
{
key: 'flacCompression',
label: 'Kompressionsstufe',
description: '0 = schnell / wenig Kompression, 8 = maximale Kompression',
type: 'slider',
min: 0,
max: 8,
step: 1,
default: 5
}
]
},
mp3: {
fields: [
{
key: 'mp3Mode',
label: 'Modus',
type: 'select',
options: [
{ label: 'CBR (Konstante Bitrate)', value: 'cbr' },
{ label: 'VBR (Variable Bitrate)', value: 'vbr' }
],
default: 'cbr'
},
{
key: 'mp3Bitrate',
label: 'Bitrate (kbps)',
type: 'select',
showWhen: { field: 'mp3Mode', value: 'cbr' },
options: [
{ label: '128 kbps', value: 128 },
{ label: '160 kbps', value: 160 },
{ label: '192 kbps', value: 192 },
{ label: '256 kbps', value: 256 },
{ label: '320 kbps', value: 320 }
],
default: 192
},
{
key: 'mp3Quality',
label: 'VBR Qualität (V0V9)',
description: '0 = beste Qualität, 9 = kleinste Datei',
type: 'slider',
min: 0,
max: 9,
step: 1,
showWhen: { field: 'mp3Mode', value: 'vbr' },
default: 4
}
]
},
opus: {
fields: [
{
key: 'opusBitrate',
label: 'Bitrate (kbps)',
description: 'Empfohlen: 96192 kbps für Musik',
type: 'slider',
min: 32,
max: 512,
step: 8,
default: 160
},
{
key: 'opusComplexity',
label: 'Encoder-Komplexität',
description: '0 = schnell, 10 = beste Qualität',
type: 'slider',
min: 0,
max: 10,
step: 1,
default: 10
}
]
},
ogg: {
fields: [
{
key: 'oggQuality',
label: 'Qualität',
description: '-1 = kleinste Datei, 10 = beste Qualität. Empfohlen: 57.',
type: 'slider',
min: -1,
max: 10,
step: 1,
default: 6
}
]
}
};
export function getDefaultFormatOptions(format) {
const schema = CD_FORMAT_SCHEMAS[format];
if (!schema) {
return {};
}
const defaults = {};
for (const field of schema.fields) {
if (field.default !== undefined) {
defaults[field.key] = field.default;
}
}
return defaults;
}

View File

@@ -9,12 +9,14 @@ import { InputNumber } from 'primereact/inputnumber';
import { api } from '../api/client';
import PipelineStatusCard from '../components/PipelineStatusCard';
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
import CdMetadataDialog from '../components/CdMetadataDialog';
import CdRipConfigPanel from '../components/CdRipConfigPanel';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';
const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'];
const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING'];
const dashboardStatuses = new Set([
'ANALYZING',
'METADATA_SELECTION',
@@ -25,7 +27,12 @@ const dashboardStatuses = new Set([
'RIPPING',
'ENCODING',
'CANCELLED',
'ERROR'
'ERROR',
'CD_METADATA_SELECTION',
'CD_READY_TO_RIP',
'CD_ANALYZING',
'CD_RIPPING',
'CD_ENCODING'
]);
function normalizeJobId(value) {
@@ -362,32 +369,25 @@ function resolveMediaType(job) {
if (['dvd', 'disc', 'dvdvideo', 'dvd-video', 'dvdrom', 'dvd-rom', 'video_ts', 'iso9660'].includes(raw)) {
return 'dvd';
}
if (['cd', 'audio_cd', 'audio cd'].includes(raw)) {
return 'cd';
}
}
return 'other';
}
function mediaIndicatorMeta(job) {
const mediaType = resolveMediaType(job);
return mediaType === 'bluray'
? {
mediaType,
src: blurayIndicatorIcon,
alt: 'Blu-ray',
title: 'Blu-ray'
}
: mediaType === 'dvd'
? {
mediaType,
src: discIndicatorIcon,
alt: 'DVD',
title: 'DVD'
}
: {
mediaType,
src: otherIndicatorIcon,
alt: 'Sonstiges Medium',
title: 'Sonstiges Medium'
};
if (mediaType === 'bluray') {
return { mediaType, src: blurayIndicatorIcon, alt: 'Blu-ray', title: 'Blu-ray' };
}
if (mediaType === 'dvd') {
return { mediaType, src: discIndicatorIcon, alt: 'DVD', title: 'DVD' };
}
if (mediaType === 'cd') {
return { mediaType, src: otherIndicatorIcon, alt: 'Audio CD', title: 'Audio CD' };
}
return { mediaType, src: otherIndicatorIcon, alt: 'Sonstiges Medium', title: 'Sonstiges Medium' };
}
function JobStepChecks({ backupSuccess, encodeSuccess }) {
@@ -547,6 +547,9 @@ export default function DashboardPage({
};
const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
const [metadataDialogContext, setMetadataDialogContext] = useState(null);
const [cdMetadataDialogVisible, setCdMetadataDialogVisible] = useState(false);
const [cdMetadataDialogContext, setCdMetadataDialogContext] = useState(null);
const [cdRipPanelJobId, setCdRipPanelJobId] = useState(null);
const [cancelCleanupDialog, setCancelCleanupDialog] = useState({
visible: false,
jobId: null,
@@ -664,6 +667,24 @@ export default function DashboardPage({
}
}, [pipeline?.state, metadataDialogVisible, metadataDialogContext?.jobId]);
// Auto-open CD metadata dialog when pipeline enters CD_METADATA_SELECTION
useEffect(() => {
const currentState = String(pipeline?.state || '').trim().toUpperCase();
if (currentState === 'CD_METADATA_SELECTION') {
const ctx = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : null;
if (ctx?.jobId && !cdMetadataDialogVisible) {
setCdMetadataDialogContext(ctx);
setCdMetadataDialogVisible(true);
}
}
if (currentState === 'CD_READY_TO_RIP') {
const ctx = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : null;
if (ctx?.jobId) {
setCdRipPanelJobId(ctx.jobId);
}
}
}, [pipeline?.state, pipeline?.context?.jobId]);
useEffect(() => {
setQueueState(normalizeQueue(pipeline?.queue));
}, [pipeline?.queue]);
@@ -1322,6 +1343,47 @@ export default function DashboardPage({
}
};
const handleMusicBrainzSearch = async (query) => {
try {
const response = await api.searchMusicBrainz(query);
return response.results || [];
} catch (error) {
showError(error);
return [];
}
};
const handleCdMetadataSubmit = async (payload) => {
setBusy(true);
try {
await api.selectCdMetadata(payload);
await refreshPipeline();
await loadDashboardJobs();
setCdMetadataDialogVisible(false);
setCdMetadataDialogContext(null);
} catch (error) {
showError(error);
} finally {
setBusy(false);
}
};
const handleCdRipStart = async (jobId, ripConfig) => {
if (!jobId) {
return;
}
setJobBusy(jobId, true);
try {
await api.startCdRip(jobId, ripConfig);
await refreshPipeline();
await loadDashboardJobs();
} catch (error) {
showError(error);
} finally {
setJobBusy(jobId, false);
}
};
const device = lastDiscEvent || pipeline?.context?.device;
const canReanalyze = state === 'ENCODING'
? Boolean(device)
@@ -2034,6 +2096,40 @@ export default function DashboardPage({
disabled={busyJobIds.has(jobId)}
/>
</div>
{(() => {
const jobState = String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase();
const isCdJob = jobState.startsWith('CD_');
if (isCdJob) {
return (
<>
{jobState === 'CD_METADATA_SELECTION' ? (
<Button
label="CD-Metadaten auswählen"
icon="pi pi-list"
onClick={() => {
const ctx = pipelineForJob?.context && typeof pipelineForJob.context === 'object'
? pipelineForJob.context
: pipeline?.context || {};
setCdMetadataDialogContext({ ...ctx, jobId });
setCdMetadataDialogVisible(true);
}}
disabled={busyJobIds.has(jobId)}
/>
) : null}
{(jobState === 'CD_READY_TO_RIP' || jobState === 'CD_RIPPING' || jobState === 'CD_ENCODING') ? (
<CdRipConfigPanel
pipeline={pipelineForJob}
onStart={(ripConfig) => handleCdRipStart(jobId, ripConfig)}
onCancel={() => handleCancel(jobId, jobState)}
busy={busyJobIds.has(jobId)}
/>
) : null}
</>
);
}
return null;
})()}
{!String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase().startsWith('CD_') ? (
<PipelineStatusCard
pipeline={pipelineForJob}
onAnalyze={handleAnalyze}
@@ -2051,6 +2147,7 @@ export default function DashboardPage({
busy={busyJobIds.has(jobId)}
liveJobLog={isCurrentSession ? liveJobLog : ''}
/>
) : null}
</div>
);
}
@@ -2165,6 +2262,18 @@ export default function DashboardPage({
busy={busy}
/>
<CdMetadataDialog
visible={cdMetadataDialogVisible}
context={cdMetadataDialogContext || pipeline?.context || {}}
onHide={() => {
setCdMetadataDialogVisible(false);
setCdMetadataDialogContext(null);
}}
onSubmit={handleCdMetadataSubmit}
onSearch={handleMusicBrainzSearch}
busy={busy}
/>
<Dialog
header={cancelCleanupDialog?.target === 'raw' ? 'Rip abgebrochen' : 'Encode abgebrochen'}
visible={Boolean(cancelCleanupDialog.visible)}

View File

@@ -12,7 +12,12 @@ const STATUS_LABELS = {
POST_ENCODE_SCRIPTS: 'Nachbearbeitung',
FINISHED: 'Fertig',
CANCELLED: 'Abgebrochen',
ERROR: 'Fehler'
ERROR: 'Fehler',
CD_ANALYZING: 'CD-Analyse',
CD_METADATA_SELECTION: 'CD-Metadatenauswahl',
CD_READY_TO_RIP: 'CD bereit zum Rippen',
CD_RIPPING: 'CD rippen',
CD_ENCODING: 'CD encodieren'
};
const PROCESS_STATUS_LABELS = {
@@ -46,6 +51,8 @@ export function getStatusSeverity(status, options = {}) {
if (normalized === 'ERROR') return 'danger';
if (normalized === 'READY_TO_START' || normalized === 'READY_TO_ENCODE') return 'info';
if (normalized === 'WAITING_FOR_USER_DECISION') return 'warning';
if (normalized === 'CD_READY_TO_RIP') return 'info';
if (normalized === 'CD_METADATA_SELECTION') return 'warning';
if (
normalized === 'RIPPING'
|| normalized === 'ENCODING'
@@ -53,6 +60,9 @@ export function getStatusSeverity(status, options = {}) {
|| normalized === 'MEDIAINFO_CHECK'
|| normalized === 'METADATA_SELECTION'
|| normalized === 'POST_ENCODE_SCRIPTS'
|| normalized === 'CD_ANALYZING'
|| normalized === 'CD_RIPPING'
|| normalized === 'CD_ENCODING'
) {
return 'warning';
}