feat(library): surface metadata enrichment status, errors and covers
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled
Docker Build & Publish / Prune old image versions (push) Has been cancelled

The mapper dropped metadata_status and hardcoded availability, so enrichment
state was invisible and a just-uploaded track never appeared to change. Map
metadata_status/metadata_error/has_cover onto Track; add MetadataStatusBadge
(pending spinner / enriched / failed-with-reason / manual) shown in TrackRow,
and serve token-bearing track covers via getTrackCoverUrl.

UploadPage now polls each uploaded track (stops once enrichment settles) so the
resolved title/artist — or a failure reason — appears live. i18n in en + ru.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Senko-san
2026-06-13 13:29:22 +03:00
parent facc215450
commit a37c19fd45
8 changed files with 198 additions and 11 deletions
+57 -6
View File
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { Badge, Button, Callout, ScrollArea, Spinner } from '@olly/modern-sk';
@@ -6,6 +6,8 @@ import {
buildUploadFormData,
useUploadTrackMutation,
} from '../../api/endpoints/upload';
import { useGetTrackQuery } from '../../api/endpoints/library';
import { MetadataStatusBadge } from '../../components/track/MetadataStatusBadge';
/** Pure client-side state — this is a transient upload queue, never server data. */
type ItemStatus = 'queued' | 'uploading' | 'done' | 'duplicate' | 'error';
@@ -273,11 +275,7 @@ function UploadRow({
{item.error}
</div>
)}
{done && (
<div style={{ fontSize: '0.75rem', color: 'var(--color-text-3)' }}>
{t('upload.unknownArtist')}
</div>
)}
{done && item.trackId && <EnrichmentStatus trackId={item.trackId} />}
</div>
<StatusBadge status={item.status} />
@@ -301,6 +299,59 @@ function UploadRow({
);
}
/**
* Polls a just-uploaded track until enrichment settles, then shows the outcome.
* Metadata enrichment runs asynchronously in a worker after the upload response
* returns, so without polling the row would never reflect the resolved title/
* artist or a failure reason. Polling stops (interval → 0) once the status
* leaves `pending`.
*/
function EnrichmentStatus({ trackId }: { trackId: string }) {
const { t } = useTranslation();
const [pollMs, setPollMs] = useState(2500);
const { data } = useGetTrackQuery(trackId, { pollingInterval: pollMs });
useEffect(() => {
if (data && data.metadataStatus !== 'pending') setPollMs(0);
}, [data]);
const status = data?.metadataStatus ?? 'pending';
const resolved =
data && data.metadataStatus === 'enriched'
? `${data.artistName} · ${data.title}`
: t(`metadata.statusHint.${status}`);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
marginTop: '0.25rem',
}}
>
<MetadataStatusBadge
status={status}
error={data?.metadataError}
hideWhenEnriched={false}
/>
<span
style={{
fontSize: '0.75rem',
color: 'var(--color-text-3)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{status === 'failed' && data?.metadataError
? data.metadataError
: resolved}
</span>
</div>
);
}
function StatusBadge({ status }: { status: ItemStatus }) {
const { t } = useTranslation();
if (status === 'uploading') {