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 (
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(context?.detectedTitle || '');
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 (
);
}