From a7c79ee2102c06935e52a76bfa4d7c8b412bd5fd Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:54:10 +0200 Subject: [PATCH] fix: show album artist on featured compilation cards (#979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: show album artist on featured compilation cards The 'Also featured on' cards are synthesised from search3 child songs, which only read the flat `albumArtist` field. Compilation children leave that empty — the album-artist credit lives in OpenSubsonic's structured `albumArtists` / `displayAlbumArtist` — so the card fell back to the '—' placeholder. Carry the structured credit (and the display string) onto the synthesised album so it resolves a name (and stays navigable when the server supplies an id). * docs: changelog for featured-compilation artist credit (#979) --- CHANGELOG.md | 6 +++++ src/api/subsonicTypes.ts | 2 ++ src/hooks/useArtistDetailData.test.ts | 32 +++++++++++++++++++++++++++ src/hooks/useArtistDetailData.ts | 7 +++++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ec52cf..25e0df6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -872,6 +872,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Album cards on a **Genre** page split multi-artist credits into individually clickable artist links, matching the rest of the app. * On the **Artists** page the `#` index button now holds only names that start with a number; accented and non-Latin names (Æ Ø Å, Chinese, Japanese, Cyrillic, …) move to a new **Other** section instead of the `#` catch-all. +### Artist detail — credit on "Also featured on" compilations + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#979](https://github.com/Psychotoxical/psysonic/pull/979)** + +* Compilation albums under **Also featured on** show their album artist (e.g. *Various Artists*) again instead of a bare `—`, and the credit links to the artist when the server provides one. + ## [1.46.0] - 2026-05-18 > **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages. diff --git a/src/api/subsonicTypes.ts b/src/api/subsonicTypes.ts index 8edfa049..5e3297bc 100644 --- a/src/api/subsonicTypes.ts +++ b/src/api/subsonicTypes.ts @@ -72,6 +72,8 @@ export interface SubsonicSong { genre?: string; path?: string; albumArtist?: string; + /** OpenSubsonic: single-string album-artist for display (mirrors `albumArtists` joined). */ + displayAlbumArtist?: string; /** ISRC code when available (e.g., Navidrome) */ isrc?: string; /** Times the track has been played, surfaced by Navidrome's Subsonic API. */ diff --git a/src/hooks/useArtistDetailData.test.ts b/src/hooks/useArtistDetailData.test.ts index 85f6c87b..e750db8d 100644 --- a/src/hooks/useArtistDetailData.test.ts +++ b/src/hooks/useArtistDetailData.test.ts @@ -72,6 +72,38 @@ describe('useArtistDetailData — id-gated info', () => { await waitFor(() => expect(result.current.info).toEqual({ largeImageUrl: 'B.jpg' })); }); + it('keeps the album-artist credit on featured compilation albums', async () => { + // "Also featured on" synthesises albums from search3 child songs. A + // compilation has no flat `albumArtist` on the child — the credit lives in + // OpenSubsonic's structured `albumArtists` (and/or `displayAlbumArtist`). + // Dropping it made the card render "—" instead of "Various Artists". + vi.mocked(getArtist).mockResolvedValue({ artist: { id: 'A', name: 'A' }, albums: [] } as any); + vi.mocked(search).mockResolvedValue({ + artists: [], + albums: [], + songs: [ + { + id: 's1', title: 'Track', artistId: 'A', artist: 'A', + album: 'A Compilation', albumId: 'comp1', coverArt: 'c1', duration: 100, + albumArtists: [{ id: 'va', name: 'Various Artists' }], + }, + { + id: 's2', title: 'Other', artistId: 'A', artist: 'A', + album: 'Display Only', albumId: 'comp2', coverArt: 'c2', duration: 90, + displayAlbumArtist: 'Various Artists', + }, + ], + } as any); + + const { result } = renderHook(() => useArtistDetailData('A')); + + await waitFor(() => expect(result.current.featuredAlbums).toHaveLength(2)); + const structured = result.current.featuredAlbums.find(a => a.id === 'comp1'); + const displayOnly = result.current.featuredAlbums.find(a => a.id === 'comp2'); + expect(structured?.artists).toEqual([{ id: 'va', name: 'Various Artists' }]); + expect(displayOnly?.artist).toBe('Various Artists'); + }); + it('ignores a late-arriving resolve for a stale id', async () => { vi.mocked(getArtist).mockImplementation(async (id) => ( { artist: { id, name: id }, albums: [] } as any diff --git a/src/hooks/useArtistDetailData.ts b/src/hooks/useArtistDetailData.ts index 2e1abb9b..6271a403 100644 --- a/src/hooks/useArtistDetailData.ts +++ b/src/hooks/useArtistDetailData.ts @@ -150,8 +150,13 @@ export function useArtistDetailData( albumMap.set(song.albumId, { id: song.albumId, name: song.album, - artist: song.albumArtist ?? '', + // search3 children carry the album-artist credit in OpenSubsonic's + // structured `albumArtists` / `displayAlbumArtist` (e.g. "Various + // Artists" on compilations), not the flat `albumArtist` field — keep + // all of them so the card resolves a name instead of "—". + artist: song.albumArtist ?? song.displayAlbumArtist ?? '', artistId: '', + artists: song.albumArtists, coverArt: song.coverArt, songCount: 1, duration: song.duration,