fix(cover): show album cover for playlist tracks in Now Playing (#1218)

* fix(cover): show album cover for playlist tracks in Now Playing

* docs(changelog): note playlist now-playing cover fix (#1218)
This commit is contained in:
Psychotoxical
2026-07-05 14:44:53 +02:00
committed by GitHub
parent 9557f8e10a
commit 84164570b9
5 changed files with 55 additions and 60 deletions
+27 -8
View File
@@ -64,9 +64,18 @@ describe('albumCoverRef', () => {
});
describe('resolveDistinctDiscCoversForAlbum', () => {
it('detects mf-* fetch id before album page visit', () => {
expect(resolveDistinctDiscCoversForAlbum('al-box', 'mf-d2')).toBe(true);
expect(resolveDistinctDiscCoversForAlbum('al-box', 'al-box')).toBe(false);
it('defaults to album-scoped for an unknown album', () => {
// A single mf-<id> cover is per-song art, not per-disc — must not be guessed
// as distinct (would surface per-track covers in the player/queue).
expect(resolveDistinctDiscCoversForAlbum('al-unknown')).toBe(false);
});
it('respects remembered true for differing disc art', () => {
rememberAlbumDistinctDiscCovers('al-distinct-box', [
{ id: 't1', albumId: 'al-distinct-box', coverArt: 'mf-a', discNumber: 1 },
{ id: 't2', albumId: 'al-distinct-box', coverArt: 'mf-b', discNumber: 2 },
]);
expect(resolveDistinctDiscCoversForAlbum('al-distinct-box')).toBe(true);
});
it('respects remembered false for same art on all discs', () => {
@@ -74,29 +83,39 @@ describe('resolveDistinctDiscCoversForAlbum', () => {
{ id: 't1', albumId: 'al-same', coverArt: 'mf-x', discNumber: 1 },
{ id: 't2', albumId: 'al-same', coverArt: 'mf-x', discNumber: 2 },
]);
expect(resolveDistinctDiscCoversForAlbum('al-same', 'mf-x')).toBe(false);
expect(resolveDistinctDiscCoversForAlbum('al-same')).toBe(false);
});
});
describe('albumCoverRefForSong', () => {
it('keys per-disc without library resolve', () => {
it('keys by album id for an unknown album', () => {
const ref = albumCoverRefForSong({
id: 't2',
albumId: 'al-box',
coverArt: 'mf-d2',
discNumber: 2,
});
expect(ref?.cacheEntityId).toBe('al-box');
});
it('keys per-disc when told explicitly', () => {
const ref = albumCoverRefForSong(
{ id: 't2', albumId: 'al-box', coverArt: 'mf-d2', discNumber: 2 },
true,
);
expect(ref?.cacheEntityId).toBe('mf-d2');
});
});
describe('albumCoverRefForPlayback', () => {
it('keys per-disc from mf coverArt before album page visit', () => {
it('keys by album id from mf coverArt before album page visit', () => {
// Bug fix: a playlist track whose album was never opened must resolve to the
// album cache slot (album cover), not a per-track slot (per-track cover).
const ref = albumCoverRefForPlayback(
{ albumId: 'al-box', coverArt: 'mf-disc2', id: 't2', discNumber: 2 },
{ albumId: 'al-pl', coverArt: 'mf-disc2', id: 't2', discNumber: 2 },
{ kind: 'active' },
);
expect(ref?.cacheEntityId).toBe('mf-disc2');
expect(ref?.cacheEntityId).toBe('al-pl');
expect(ref?.fetchCoverArtId).toBe('mf-disc2');
});
+16 -34
View File
@@ -34,36 +34,22 @@ export function forgetAlbumDistinctDiscCovers(albumId: string): void {
albumDistinctDiscCoversByAlbumId.delete(albumId.trim());
}
export type DistinctDiscCoversHint = Pick<
SubsonicSong,
'discNumber' | 'coverArt' | 'id' | 'albumId'
>;
/**
* Whether per-disc `mf-*` cache slots apply — from album tracklist memory or song hints
* when the album page has not been opened yet.
* Synchronous answer for "does this album use genuine per-disc artwork?", used
* for the initial cover ref before the library index resolves.
*
* Per-disc artwork can only be determined from the full tracklist (see
* {@link albumHasDistinctDiscCovers}). A single track's `mf-<id>` cover or disc
* number is no signal: Navidrome (and other OpenSubsonic servers) give every
* track its own `mf-<id>` coverArt, so guessing per-disc from one track marked
* ordinary per-song albums as distinct and routed playback to a per-track cache
* slot — surfacing per-track art in Now Playing / the queue when a song was
* played from a playlist (the album page seeds the truth, so it looked correct
* there). Trust only the value remembered from a known tracklist; default to
* album-scoped and let the library index correct genuine per-disc albums.
*/
export function resolveDistinctDiscCoversForAlbum(
albumId: string,
fetchCoverArtId?: string | null,
songHint?: DistinctDiscCoversHint,
): boolean {
const album = albumId.trim();
if (!album) return false;
const known = albumDistinctDiscCoversByAlbumId.get(album);
if (known === true) return true;
if (known === false) return false;
if (songHint) {
const cover = songHint.coverArt?.trim();
if ((songHint.discNumber ?? 1) > 1 && Boolean(cover && cover !== album)) return true;
}
const fetch = fetchCoverArtId?.trim();
if (fetch && fetch !== album && fetch.startsWith('mf-')) return true;
return false;
export function resolveDistinctDiscCoversForAlbum(albumId: string): boolean {
return albumDistinctDiscCoversByAlbumId.get(albumId.trim()) === true;
}
function resolveAlbumCoverRefOptions(
@@ -116,7 +102,7 @@ export function albumCoverRefForSong(
const albumId = song.albumId?.trim();
const distinct =
distinctDiscCovers
?? (albumId ? resolveDistinctDiscCoversForAlbum(albumId, song.coverArt, song) : false);
?? (albumId ? resolveDistinctDiscCoversForAlbum(albumId) : false);
const entry = resolveTrackCoverEntry(song, distinct);
return entry ? coverEntryToRef(entry, serverScope) : undefined;
}
@@ -127,11 +113,7 @@ export function albumCoverRefForPlayback(
): CoverArtRef | undefined {
const albumId = track.albumId?.trim();
if (!albumId) return undefined;
const distinctDiscCovers = resolveDistinctDiscCoversForAlbum(
albumId,
track.coverArt,
{ ...track, albumId } as DistinctDiscCoversHint,
);
const distinctDiscCovers = resolveDistinctDiscCoversForAlbum(albumId);
return albumCoverRefForSong(
{ ...track, albumId } as Pick<SubsonicSong, 'albumId' | 'coverArt' | 'id' | 'discNumber'>,
distinctDiscCovers,
+1 -1
View File
@@ -183,7 +183,7 @@ export async function resolveTrackCoverRefFromLibrary(
const albumId = song.albumId?.trim();
const distinct =
distinctDiscCovers
?? (albumId ? resolveDistinctDiscCoversForAlbum(albumId, song.coverArt, song) : false);
?? (albumId ? resolveDistinctDiscCoversForAlbum(albumId) : false);
const trackId = song.id?.trim();
const fromLibrary = trackId
? await libraryResolveCoverEntry(libraryServerIdFromScope(serverScope), 'track', trackId)
+5 -17
View File
@@ -58,8 +58,8 @@ export function useAlbumCoverRef(
const libraryResolve = options?.libraryResolve !== false;
const scopeKey = coverScopeKey(serverScope);
const distinctDiscCovers = useMemo(
() => resolveDistinctDiscCoversForAlbum(albumId ?? '', fallbackCoverArt),
[albumId, fallbackCoverArt],
() => resolveDistinctDiscCoversForAlbum(albumId ?? ''),
[albumId],
);
const syncRef = useMemo(() => {
const id = albumId?.trim();
@@ -146,15 +146,8 @@ export function useTrackCoverRef(
const discNumber = song?.discNumber;
const distinctDiscCovers = useMemo(
() => (albumId?.trim()
? resolveDistinctDiscCoversForAlbum(albumId, coverArt, {
id: songId ?? '',
albumId,
coverArt,
discNumber,
})
: false),
[albumId, coverArt, discNumber, songId],
() => (albumId?.trim() ? resolveDistinctDiscCoversForAlbum(albumId) : false),
[albumId],
);
const syncRef = useMemo(() => {
@@ -267,12 +260,7 @@ export function usePlaybackTrackCoverRef(
const al = albumId?.trim();
if (!tid || !al || !track) return;
let cancelled = false;
const distinctDiscCovers = resolveDistinctDiscCoversForAlbum(al, track.coverArt, {
id: tid,
albumId: al,
coverArt: track.coverArt,
discNumber: track.discNumber,
});
const distinctDiscCovers = resolveDistinctDiscCoversForAlbum(al);
void resolveTrackCoverRefFromLibrary(
{ ...track, id: tid, albumId: al } as Pick<SubsonicSong, 'id' | 'albumId' | 'coverArt' | 'discNumber'>,
scope,