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
+15
View File
@@ -17,3 +17,18 @@ export function getCoverUrl(artUrl: string | undefined): string | undefined {
const base = getApiBaseUrl();
return `${base}${artUrl}`;
}
/**
* Cover image URL for a track, served by `GET /tracks/{id}/cover`. Like the
* audio stream, an `<img>` can't send an `Authorization` header, so the access
* token rides as `?token=`. Returns undefined when the track has no cover.
*/
export function getTrackCoverUrl(
trackId: string,
token: string,
hasCover: boolean,
): string | undefined {
if (!hasCover) return undefined;
const base = getApiBaseUrl();
return `${base}/tracks/${trackId}/cover?token=${encodeURIComponent(token)}`;
}
+24 -2
View File
@@ -14,12 +14,27 @@
import type {
Album,
Artist,
MetadataStatus,
PaginatedResponse,
Playlist,
Track,
User,
} from './types';
const METADATA_STATUSES: readonly MetadataStatus[] = [
'pending',
'enriched',
'failed',
'manual',
];
/** Map the backend's free-form status string onto the UI union, defaulting any
* unknown value to `pending` (a safe "not yet identified" state). */
const toMetadataStatus = (raw: string): MetadataStatus =>
(METADATA_STATUSES as readonly string[]).includes(raw)
? (raw as MetadataStatus)
: 'pending';
// ---- raw wire shapes (snake_case, exactly as the backend emits) ----
export interface RawPaged<T> {
@@ -49,6 +64,9 @@ export interface RawTrack {
file_format: string;
file_size: number;
metadata_status: string;
metadata_error: string | null;
enriched_at: string | null;
has_cover: boolean;
source: string;
created_at: string;
}
@@ -98,13 +116,17 @@ export const toTrack = (r: RawTrack): Track => ({
artistName: r.artist_name,
albumId: r.album_id ?? '',
albumTitle: r.album_title ?? '',
// Cover endpoints aren't wired on the backend yet — leave art undefined so the
// UI renders generated tile art instead of a broken image.
// `has_cover` says a cover exists; the actual URL (which needs a `?token=`) is
// built in the component from the track id — see `getTrackCoverUrl`. Keep
// `albumArtUrl` undefined so callers fall back to generated tile art.
albumArtUrl: undefined,
hasCover: r.has_cover,
durationMs: (r.duration_seconds ?? 0) * 1000,
// The lean TrackOut carries no availability/like state: a track returned by
// the library is on the server, and per-track like state comes from /likes.
availability: 'server',
metadataStatus: toMetadataStatus(r.metadata_status),
metadataError: r.metadata_error ?? undefined,
liked: false,
format: r.file_format,
fileSize: r.file_size,
+12
View File
@@ -1,5 +1,13 @@
export type TrackAvailability = 'server' | 'downloading' | 'error' | 'missing';
/**
* Metadata-enrichment state, distinct from file `availability`. `pending` = the
* worker hasn't finished (or hasn't started); `enriched` = identity found;
* `failed` = no match / a worker error (see `metadataError`); `manual` = user-
* edited and never auto-overwritten.
*/
export type MetadataStatus = 'pending' | 'enriched' | 'failed' | 'manual';
export interface Track {
id: string;
title: string;
@@ -8,12 +16,16 @@ export interface Track {
albumId: string;
albumTitle: string;
albumArtUrl?: string;
hasCover: boolean;
durationMs: number;
trackNumber?: number;
discNumber?: number;
year?: number;
genre?: string;
availability: TrackAvailability;
metadataStatus: MetadataStatus;
/** Human-readable reason the last enrichment run set `failed`; else undefined. */
metadataError?: string;
fileSize?: number;
format?: string;
bitrate?: number;