fix(library): show album cover art in the Albums grid
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 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:
Senko-san
2026-06-14 02:15:50 +03:00
parent 6595417246
commit b966ad8be5
5 changed files with 33 additions and 4 deletions
+15
View File
@@ -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)}`;
}
+4
View File
@@ -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
+2
View File
@@ -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}
+7 -2
View File
@@ -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}