fix(library): show album cover art in the Albums grid
The album cards always fell back to the 💿 placeholder: the mapper dropped the backend's `has_cover` and no album cover URL was ever built. Carry `hasCover` through `RawAlbum`/`Album` and add `getAlbumCoverUrl` (GET /albums/{id}/cover, token in the query like the track/stream URLs). The Library Albums grid and the artist-detail discography now render real covers, same source the album-detail page already used. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,3 +32,18 @@ export function getTrackCoverUrl(
|
|||||||
const base = getApiBaseUrl();
|
const base = getApiBaseUrl();
|
||||||
return `${base}/tracks/${trackId}/cover?token=${encodeURIComponent(token)}`;
|
return `${base}/tracks/${trackId}/cover?token=${encodeURIComponent(token)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cover image URL for an album, served by `GET /albums/{id}/cover`. Same
|
||||||
|
* `?token=` rationale as the track cover. Returns undefined when the album has
|
||||||
|
* no cover (so callers fall back to generated tile art).
|
||||||
|
*/
|
||||||
|
export function getAlbumCoverUrl(
|
||||||
|
albumId: string,
|
||||||
|
token: string,
|
||||||
|
hasCover: boolean,
|
||||||
|
): string | undefined {
|
||||||
|
if (!hasCover) return undefined;
|
||||||
|
const base = getApiBaseUrl();
|
||||||
|
return `${base}/albums/${albumId}/cover?token=${encodeURIComponent(token)}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export interface RawAlbum {
|
|||||||
artist_name: string;
|
artist_name: string;
|
||||||
year: number | null;
|
year: number | null;
|
||||||
track_count: number;
|
track_count: number;
|
||||||
|
has_cover: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +172,10 @@ export const toAlbum = (r: RawAlbum): Album => ({
|
|||||||
title: r.title,
|
title: r.title,
|
||||||
artistId: r.artist_id,
|
artistId: r.artist_id,
|
||||||
artistName: r.artist_name,
|
artistName: r.artist_name,
|
||||||
|
// The album record carries no cover *URL*; `hasCover` says one exists, and the
|
||||||
|
// URL (which needs `?token=`) is built in components via `getAlbumCoverUrl`.
|
||||||
artUrl: undefined,
|
artUrl: undefined,
|
||||||
|
hasCover: r.has_cover,
|
||||||
year: r.year ?? undefined,
|
year: r.year ?? undefined,
|
||||||
trackCount: r.track_count,
|
trackCount: r.track_count,
|
||||||
// AlbumOut has no aggregate duration; computed client-side from tracks when
|
// AlbumOut has no aggregate duration; computed client-side from tracks when
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export interface Album {
|
|||||||
artistId: string;
|
artistId: string;
|
||||||
artistName: string;
|
artistName: string;
|
||||||
artUrl?: string;
|
artUrl?: string;
|
||||||
|
/** Whether the album has cover art served by `GET /albums/{id}/cover`. */
|
||||||
|
hasCover: boolean;
|
||||||
year?: number;
|
year?: number;
|
||||||
trackCount: number;
|
trackCount: number;
|
||||||
totalDurationMs: number;
|
totalDurationMs: number;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from '../../store/selectors/localLibrary';
|
} from '../../store/selectors/localLibrary';
|
||||||
import { setQueue } from '../../store/slices/queue';
|
import { setQueue } from '../../store/slices/queue';
|
||||||
import { formatDuration } from '../../lib/format';
|
import { formatDuration } from '../../lib/format';
|
||||||
import { getCoverUrl } from '../../api/endpoints/streaming';
|
import { getCoverUrl, getAlbumCoverUrl } from '../../api/endpoints/streaming';
|
||||||
import type { Album } from '../../api/types';
|
import type { Album } from '../../api/types';
|
||||||
|
|
||||||
export function ArtistDetailPage() {
|
export function ArtistDetailPage() {
|
||||||
@@ -251,7 +251,10 @@ export function ArtistDetailPage() {
|
|||||||
|
|
||||||
function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) {
|
function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const artUrl = getCoverUrl(album.artUrl);
|
const token = useAppSelector((s) => s.auth.accessToken);
|
||||||
|
const artUrl =
|
||||||
|
getCoverUrl(album.artUrl) ??
|
||||||
|
(token ? getAlbumCoverUrl(album.id, token, album.hasCover) : undefined);
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
} from '../../store/selectors/localLibrary';
|
} from '../../store/selectors/localLibrary';
|
||||||
import { setQueue } from '../../store/slices/queue';
|
import { setQueue } from '../../store/slices/queue';
|
||||||
import type { Track, Album, Artist } from '../../api/types';
|
import type { Track, Album, Artist } from '../../api/types';
|
||||||
import { getCoverUrl } from '../../api/endpoints/streaming';
|
import { getCoverUrl, getAlbumCoverUrl } from '../../api/endpoints/streaming';
|
||||||
import { formatDuration } from '../../lib/format';
|
import { formatDuration } from '../../lib/format';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
@@ -300,7 +300,12 @@ export function LibraryPage() {
|
|||||||
|
|
||||||
function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) {
|
function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const artUrl = getCoverUrl(album.artUrl);
|
const token = useAppSelector((s) => s.auth.accessToken);
|
||||||
|
// The album record has no cover URL; build one from `hasCover` (served by
|
||||||
|
// GET /albums/{id}/cover, token in the query — <img> can't send a header).
|
||||||
|
const artUrl =
|
||||||
|
getCoverUrl(album.artUrl) ??
|
||||||
|
(token ? getAlbumCoverUrl(album.id, token, album.hasCover) : undefined);
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
Reference in New Issue
Block a user