mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(album-card): per-artist click on multi-artist albums (#767)
* refactor(album): rename SubsonicAlbum.albumArtists to artists per OpenSubsonic spec
The OpenSubsonic AlbumID3 object exposes structured album-artist credits as
`artists` (not `albumArtists`); psysonic's internal type used the wrong name,
so `album.albumArtists` was always undefined on the Subsonic responses
Navidrome returns. `deriveAlbumHeaderArtistRefs` therefore never hit its
album-level branch — only the song-level fallback was carrying the multi-
artist split on the album-detail header.
- Rename the field on `SubsonicAlbum` and add the spec-defined `displayArtist`
string alongside it.
- Extract `deriveAlbumArtistRefs(album)` for the common case where only the
album object is available (cards, rails); `deriveAlbumHeaderArtistRefs`
now uses it after the song-level fallback. The song-level `albumArtists`
field is unchanged (the spec does name it that way on child songs).
- Tests cover both helpers and the legacy `artist` + `artistId` fallback.
* fix(album-card): per-artist click on multi-artist albums
The artist subtitle under an album card rendered the full `album.artist`
string ("Melvins • Napalm Death") as a single link that always navigated to
`album.artistId`. On the artist-detail page that id is the page's own
artist — so the click resolved to the URL the user was already on and the
router silently no-op'd, with no visible effect.
Render through the existing `OpenArtistRefInline` component instead, fed
from the structured `artists` array via `deriveAlbumArtistRefs`. Each
artist becomes its own ·-separated link; the single-artist fallback is the
same legacy `artist` + `artistId` pair as before, so the behaviour on
servers that don't expose the structured field is unchanged.
* docs(changelog): note PR #767 under Fixed in v1.46.0
This commit is contained in:
committed by
GitHub
parent
48a69754d6
commit
6e0f076f43
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { deriveAlbumHeaderArtistRefs } from './deriveAlbumHeaderArtistRefs';
|
||||
import { deriveAlbumArtistRefs, deriveAlbumHeaderArtistRefs } from './deriveAlbumHeaderArtistRefs';
|
||||
import type { SubsonicAlbum } from '../../api/subsonicTypes';
|
||||
import { makeSubsonicSong } from '@/test/helpers/factories';
|
||||
|
||||
@@ -12,16 +12,35 @@ const baseAlbum = (): SubsonicAlbum => ({
|
||||
duration: 100,
|
||||
});
|
||||
|
||||
describe('deriveAlbumHeaderArtistRefs', () => {
|
||||
it('prefers album-level albumArtists when present', () => {
|
||||
describe('deriveAlbumArtistRefs', () => {
|
||||
it('prefers the OpenSubsonic `artists` array when present', () => {
|
||||
const album: SubsonicAlbum = {
|
||||
...baseAlbum(),
|
||||
albumArtists: [{ id: 'a1', name: 'One' }, { id: 'a2', name: 'Two' }],
|
||||
artists: [{ id: 'a1', name: 'One' }, { id: 'a2', name: 'Two' }],
|
||||
};
|
||||
expect(deriveAlbumHeaderArtistRefs(album, [])).toEqual(album.albumArtists);
|
||||
expect(deriveAlbumArtistRefs(album)).toEqual(album.artists);
|
||||
});
|
||||
|
||||
it('falls back to the first song with albumArtists', () => {
|
||||
it('uses legacy artist + artistId when no structured refs', () => {
|
||||
expect(deriveAlbumArtistRefs(baseAlbum())).toEqual([{ id: 'ar-first', name: 'Joined A / B' }]);
|
||||
});
|
||||
|
||||
it('omits id when artistId is blank', () => {
|
||||
expect(deriveAlbumArtistRefs({ ...baseAlbum(), artistId: ' ', artist: 'Solo' }))
|
||||
.toEqual([{ name: 'Solo' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveAlbumHeaderArtistRefs', () => {
|
||||
it('prefers the album-level `artists` array when present', () => {
|
||||
const album: SubsonicAlbum = {
|
||||
...baseAlbum(),
|
||||
artists: [{ id: 'a1', name: 'One' }, { id: 'a2', name: 'Two' }],
|
||||
};
|
||||
expect(deriveAlbumHeaderArtistRefs(album, [])).toEqual(album.artists);
|
||||
});
|
||||
|
||||
it('falls back to the first song with `albumArtists`', () => {
|
||||
const album = baseAlbum();
|
||||
const songs = [
|
||||
makeSubsonicSong({
|
||||
|
||||
@@ -4,21 +4,31 @@ function nonEmpty(refs: SubsonicOpenArtistRef[] | undefined): refs is SubsonicOp
|
||||
return !!refs && refs.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured album-artist credits without the album-detail Song fallback.
|
||||
* Used wherever only the album object is available (cards, rails). Prefers the
|
||||
* OpenSubsonic `artists` array; falls back to legacy `artist` + `artistId`.
|
||||
*/
|
||||
export function deriveAlbumArtistRefs(album: SubsonicAlbum): SubsonicOpenArtistRef[] {
|
||||
if (nonEmpty(album.artists)) return album.artists;
|
||||
const name = album.artist?.trim() || '—';
|
||||
const id = album.artistId?.trim();
|
||||
return id ? [{ id, name }] : [{ name }];
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenSubsonic album credits for the album-detail header.
|
||||
* Prefer `albumArtists` on the album payload, then on any child song (Navidrome
|
||||
* often attaches the structured list only on songs); fall back to legacy
|
||||
* `artist` + `artistId` strings.
|
||||
* Prefer the album's `artists` array, then any child song's `albumArtists`
|
||||
* (some servers only attach the structured list at song level); fall back to
|
||||
* the legacy `artist` + `artistId` strings.
|
||||
*/
|
||||
export function deriveAlbumHeaderArtistRefs(
|
||||
album: SubsonicAlbum,
|
||||
songs: SubsonicSong[],
|
||||
): SubsonicOpenArtistRef[] {
|
||||
if (nonEmpty(album.albumArtists)) return album.albumArtists;
|
||||
if (nonEmpty(album.artists)) return album.artists;
|
||||
for (const s of songs) {
|
||||
if (nonEmpty(s.albumArtists)) return s.albumArtists;
|
||||
}
|
||||
const name = album.artist?.trim() || '—';
|
||||
const id = album.artistId?.trim();
|
||||
return id ? [{ id, name }] : [{ name }];
|
||||
return deriveAlbumArtistRefs(album);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user