import { useEffect, useRef, 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'; function CoverThumb({ url, alt }) { const [failed, setFailed] = useState(false); useEffect(() => { setFailed(false); }, [url]); if (!url || failed) { return
-
; } return ( {alt} setFailed(true)} /> ); } const COVER_PRELOAD_TIMEOUT_MS = 3000; function preloadCoverImage(url) { const src = String(url || '').trim(); if (!src) { return Promise.resolve(); } return new Promise((resolve) => { const image = new Image(); let settled = false; const cleanup = () => { image.onload = null; image.onerror = null; }; const done = () => { if (settled) { return; } settled = true; cleanup(); resolve(); }; const timer = window.setTimeout(done, COVER_PRELOAD_TIMEOUT_MS); image.onload = () => { window.clearTimeout(timer); done(); }; image.onerror = () => { window.clearTimeout(timer); done(); }; image.src = src; }); } export default function CdMetadataDialog({ visible, context, onHide, onSubmit, onSearch, onFetchRelease, busy }) { const [selected, setSelected] = useState(null); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [searchBusy, setSearchBusy] = useState(false); const searchRunRef = useRef(0); // Manual metadata inputs const [manualTitle, setManualTitle] = useState(''); const [manualArtist, setManualArtist] = useState(''); const [manualYear, setManualYear] = useState(null); // Track titles are pre-filled from MusicBrainz and edited in the next step. const [trackTitles, setTrackTitles] = useState({}); const tocTracks = Array.isArray(context?.tracks) ? context.tracks : []; useEffect(() => { if (!visible) { return; } setSelected(null); setQuery(''); setManualTitle(''); setManualArtist(''); setManualYear(null); setResults([]); setSearchBusy(false); const titles = {}; for (const t of tocTracks) { titles[t.position] = t.title || `Track ${t.position}`; } setTrackTitles(titles); }, [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 handleSearch = async () => { const trimmedQuery = query.trim(); if (!trimmedQuery) { return; } setSearchBusy(true); const searchRunId = searchRunRef.current + 1; searchRunRef.current = searchRunId; try { const searchResults = await onSearch(trimmedQuery); const normalizedResults = Array.isArray(searchResults) ? searchResults : []; await Promise.all(normalizedResults.map((item) => preloadCoverImage(item?.coverArtUrl))); if (searchRunRef.current !== searchRunId) { return; } setResults(normalizedResults); setSelected(null); } finally { if (searchRunRef.current === searchRunId) { setSearchBusy(false); } } }; const handleSubmit = async () => { const normalizeTrackText = (value) => String(value || '').replace(/\s+/g, ' ').trim(); let releaseDetails = selected; if (selected?.mbId && (!Array.isArray(selected?.tracks) || selected.tracks.length === 0) && typeof onFetchRelease === 'function') { const fetched = await onFetchRelease(selected.mbId); if (fetched && typeof fetched === 'object') { releaseDetails = fetched; } } const releaseTracks = Array.isArray(releaseDetails?.tracks) ? releaseDetails.tracks : []; const releaseTracksByPosition = new Map(); releaseTracks.forEach((track, index) => { const parsedPosition = Number(track?.position); const normalizedPosition = Number.isFinite(parsedPosition) && parsedPosition > 0 ? Math.trunc(parsedPosition) : index + 1; if (!releaseTracksByPosition.has(normalizedPosition)) { releaseTracksByPosition.set(normalizedPosition, track); } }); const tracks = tocTracks.map((t, index) => { const position = Number(t.position); const byPosition = releaseTracksByPosition.get(position); const byIndex = releaseTracks[index]; return { position, title: normalizeTrackText( byPosition?.title || byIndex?.title || trackTitles[t.position] ) || `Track ${t.position}`, artist: normalizeTrackText( byPosition?.artist || byIndex?.artist || manualArtist.trim() || releaseDetails?.artist ) || null, selected: true }; }); const payload = { jobId: context.jobId, title: manualTitle.trim() || context?.detectedTitle || 'Audio CD', artist: manualArtist.trim() || null, year: manualYear || null, mbId: releaseDetails?.mbId || selected?.mbId || null, coverUrl: releaseDetails?.coverArtUrl || selected?.coverArtUrl || null, tracks }; await onSubmit(payload); }; const mbTitleBody = (row) => (
{row.title}
{row.artist}{row.year ? ` | ${row.year}` : ''} {row.label ? | {row.label} : null}
); return ( {/* MusicBrainz search */}
setQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder="Album / Interpret suchen" />
{results.length > 0 ? (
setSelected(e.value)} dataKey="mbId" size="small" scrollable scrollHeight="16rem" emptyMessage="Keine Treffer" >
) : null} {/* Manual metadata */}

Metadaten

setManualTitle(e.target.value)} placeholder="Album-Titel" /> setManualArtist(e.target.value)} placeholder="Interpret / Band" /> setManualYear(e.value)} placeholder="Jahr" useGrouping={false} min={1900} max={2100} />
{/* Track selection/editing moved to CD-Rip configuration panel */} {tocTracks.length > 0 ? ( {tocTracks.length} Tracks erkannt. Auswahl/Feinschliff (Checkboxen, Interpret, Titel, Länge) erfolgt im nächsten Schritt in der Job-Übersicht. ) : null}
); }