mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
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:
+27
-8
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user