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,