diff --git a/renderer/src/components/CardRow.tsx b/renderer/src/components/CardRow.tsx index 9438e9b..c6ab13b 100644 --- a/renderer/src/components/CardRow.tsx +++ b/renderer/src/components/CardRow.tsx @@ -17,7 +17,12 @@ function ArtistMediaCard({ const rid = (item.resource?.id ?? (typeof item.id === 'object' ? item.id : undefined)) as | SonosItemId | undefined; - const { data: artistImg } = useArtistImage(rid?.objectId, rid?.serviceId, rid?.accountId); + const { data: artistImg } = useArtistImage( + rid?.objectId, + rid?.serviceId, + rid?.accountId, + item.resource?.defaults as string | undefined, + ); return ( void; onResolveArtist: (name: string) => void; }) { - const parts = artist.split(/,\s*/).map((s) => s.trim()).filter(Boolean); + const parts = splitArtists(artist); if (parts.length === 0) return null; const canUseDirect = parts.length === 1 && !!artistId && !!serviceId && !!accountId; return ( diff --git a/renderer/src/components/album/AlbumPanel.tsx b/renderer/src/components/album/AlbumPanel.tsx index edd0296..2903612 100644 --- a/renderer/src/components/album/AlbumPanel.tsx +++ b/renderer/src/components/album/AlbumPanel.tsx @@ -8,7 +8,7 @@ import { useDominantColor } from '../../hooks/useDominantColor'; import { useGeniusAlbumYear } from '../../hooks/useGeniusAlbumYear'; import { artistQueryOptions } from '../../hooks/useArtistBrowse'; import { useResolveAndOpen } from '../../hooks/useResolveAndOpen'; -import { resolveAlbumParams, isPlaylist, isProgram, getItemArt } from '../../lib/itemHelpers'; +import { resolveAlbumParams, isPlaylist, isProgram, getItemArt, splitArtists } from '../../lib/itemHelpers'; import { createDragGhost } from '../../lib/dragHelpers'; import { AlbumTrackRow } from './AlbumTrackRow'; import type { SonosItem, SonosItemId } from '../../types/sonos'; @@ -111,7 +111,7 @@ export function AlbumPanel({ onAddToQueue }: Props) { // Multi-artist subtitles ("Sonny Stitt, Kenny Garrett") arrive as a single string. // Split on commas so each artist gets its own link; only the single-artist case // can use the cached artistItem for direct navigation. - const artistParts = artist.split(/,\s*/).map((s) => s.trim()).filter(Boolean); + const artistParts = splitArtists(artist); return (
diff --git a/renderer/src/hooks/useArtistBrowse.ts b/renderer/src/hooks/useArtistBrowse.ts index ab4e765..0a2acae 100644 --- a/renderer/src/hooks/useArtistBrowse.ts +++ b/renderer/src/hooks/useArtistBrowse.ts @@ -123,11 +123,12 @@ export function useArtistImage( artistId: string | undefined, serviceId: string | undefined, accountId: string | undefined, + defaults?: string, ) { return useQuery({ queryKey: ['artist-image', artistId] as const, queryFn: async (): Promise => { - const r = await api.browse.artist(artistId!, { serviceId, accountId, muse2: true }); + const r = await api.browse.artist(artistId!, { serviceId, accountId, defaults, muse2: true }); if (r.error || !r.data) return null; return (r.data as ArtistResponse).images?.tile1x1 ?? null; }, diff --git a/renderer/src/hooks/useResolveAndOpen.ts b/renderer/src/hooks/useResolveAndOpen.ts index dece4b0..2a8c79d 100644 --- a/renderer/src/hooks/useResolveAndOpen.ts +++ b/renderer/src/hooks/useResolveAndOpen.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; import { api } from '../lib/sonosApi'; -import { getName, isAlbum, isArtist, parseServiceSearch } from '../lib/itemHelpers'; +import { getName, isAlbum, isArtist, parseServiceSearch, splitArtists } from '../lib/itemHelpers'; import type { ServiceSearch } from '../types/ServiceSearch'; import type { SonosArtist, SonosItem } from '../types/sonos'; import { useOpenItem } from './useOpenItem'; @@ -42,15 +42,18 @@ export function useResolveAndOpen() { const candidates = items.filter(filter); const lowerName = query.toLowerCase(); - const lowerArtist = opts?.artist?.toLowerCase(); + // The hint may be a joined multi-artist string ("Sonny Stitt, Kenny Garrett"); + // candidates list artists individually, so match against each split name. + const hintArtists = opts?.artist ? splitArtists(opts.artist.toLowerCase()) : []; const nameMatches = candidates.filter((c) => getName(c).toLowerCase() === lowerName); // For albums with an artist hint, require the artist to match — otherwise we'd // happily land on a same-named album/single by a different artist. let match: SonosItem | undefined; - if (target === 'album' && lowerArtist) { - match = nameMatches.find((c) => itemArtistNames(c).includes(lowerArtist)) - ?? candidates.find((c) => itemArtistNames(c).includes(lowerArtist)); + if (target === 'album' && hintArtists.length > 0) { + const matchesHint = (c: SonosItem) => + itemArtistNames(c).some((n) => hintArtists.includes(n)); + match = nameMatches.find(matchesHint) ?? candidates.find(matchesHint); } else if (target === 'artist' && lowerName) { match = nameMatches[0] ?? candidates[0]; } else { diff --git a/renderer/src/lib/itemHelpers.ts b/renderer/src/lib/itemHelpers.ts index dee0671..38fb61b 100644 --- a/renderer/src/lib/itemHelpers.ts +++ b/renderer/src/lib/itemHelpers.ts @@ -106,6 +106,11 @@ export function getArtist(item: QueueItem): string { return resolveArtistName(item?.track?.artist ?? item?.artist ?? item?.primaryArtist ?? ''); } +// Sonos serves multi-artist credits as a single joined string ("Sonny Stitt, Kenny Garrett"). +export function splitArtists(artist: string): string[] { + return artist.split(/,\s*/).map((s) => s.trim()).filter(Boolean); +} + export function getAlbum(item: QueueItem): string { const raw = item?.track?.album ?? item?.album ?? ''; if (!raw) return ''; diff --git a/server/src/shared/aggregate.ts b/server/src/shared/aggregate.ts index 1618eee..aee95c3 100644 --- a/server/src/shared/aggregate.ts +++ b/server/src/shared/aggregate.ts @@ -101,8 +101,8 @@ function artistKeysFor(e: RawEvent): string[] { // a single entry, don't try to split. if (/\d/.test(raw)) return [raw]; - const parts = raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean); - return parts.length > 0 ? parts : [raw]; + // parts is only empty when raw is nothing but separators — not a real artist. + return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean); } function bumpQueuer(