fix(library): show album artist correctly in album grids (#1056) (#1057)

* fix(library): show album artist in album grids (#1056)

Prefer album-artist tags over track artist when building album rows from
the local index, and align grid cards with OpenSubsonic displayArtist.

* fix(library): align album artist in FTS, search, and offline paths (#1056)

Apply album-artist preference in FTS album dedupe and live search, fix
offline pin hydration order, and use albumArtistDisplayName in remaining
cheap UI/export/download call sites.

* docs(changelog): album artist grid fix for compilations (PR #1057)

* fix(library): parity guard and live-search album artist helper (#1056)

Align SQL ELSE branch with pick_album_group_artist trimming, add parity
test, and use albumArtistDisplayName in LiveSearch and MobileSearchOverlay.
This commit is contained in:
cucadmuh
2026-06-10 15:54:46 +03:00
committed by GitHub
parent 707a41f615
commit fb5a257735
23 changed files with 217 additions and 46 deletions
+4 -3
View File
@@ -21,7 +21,7 @@ import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
import { useDragDrop } from '../contexts/DragDropContext';
import { isAlbumRecentlyAdded } from '../utils/albumRecency';
import { deriveAlbumArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { albumArtistDisplayName, deriveAlbumArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { coverServerScopeForServerId } from '../cover/serverScope';
import { appendServerQuery } from '../utils/navigation/detailServerScope';
@@ -93,6 +93,7 @@ function AlbumCard({
}, [coverRef, displayCssPx]);
const isNewAlbum = isAlbumRecentlyAdded(album.created);
const artistRefs = useMemo(() => deriveAlbumArtistRefs(album), [album]);
const artistLabel = useMemo(() => albumArtistDisplayName(album), [album]);
const handleClick = (opts?: { shiftKey?: boolean }) => {
if (selectionMode) { onToggleSelect?.(album.id, opts); return; }
@@ -105,7 +106,7 @@ function AlbumCard({
onClick={e => handleClick({ shiftKey: e.shiftKey })}
role="button"
tabIndex={0}
aria-label={`${album.name} von ${album.artist}`}
aria-label={`${album.name} von ${artistLabel}`}
onKeyDown={e => e.key === 'Enter' && handleClick()}
onContextMenu={(e) => {
e.preventDefault();
@@ -213,7 +214,7 @@ function AlbumCard({
<p className="album-card-artist truncate">
<OpenArtistRefInline
refs={artistRefs}
fallbackName={album.artist}
fallbackName={artistLabel}
onGoArtist={id => navigate(`/artist/${id}`)}
as="none"
linkTag="span"
+4 -2
View File
@@ -25,6 +25,7 @@ import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
import { formatHumanHoursMinutes } from '../utils/format/formatHumanDuration';
import AlbumRow from './AlbumRow';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
const ANCHOR_HISTORY_KEY_PREFIX = 'psysonic_because_anchor_history:';
const PICKS_HISTORY_KEY_PREFIX = 'psysonic_because_picks:';
@@ -599,6 +600,7 @@ const BecauseCard = memo(function BecauseCard({ album, anchor, disableArtwork, e
});
const imgSrc = coverImgSrc(coverHandle.src);
const bgResolved = coverHandle.src;
const artistLabel = useMemo(() => albumArtistDisplayName(album), [album]);
const handleOpen = () => navigate(`/album/${album.id}`);
const handleEnqueue = async (e: React.MouseEvent) => {
e.stopPropagation();
@@ -620,7 +622,7 @@ const BecauseCard = memo(function BecauseCard({ album, anchor, disableArtwork, e
className={`because-card${enter ? ' because-card--slot-enter' : ''}`}
onClick={handleOpen}
onKeyDown={e => { if (e.key === 'Enter') handleOpen(); }}
aria-label={`${album.name} ${album.artist}`}
aria-label={`${album.name} ${artistLabel}`}
>
{!disableArtwork && bgResolved && (
<div
@@ -683,7 +685,7 @@ const BecauseCard = memo(function BecauseCard({ album, anchor, disableArtwork, e
{t('home.similarTo', { artist: anchor })}
</div>
<div className="because-card-title">{album.name}</div>
<div className="because-card-artist">{album.artist}</div>
<div className="because-card-artist">{artistLabel}</div>
</div>
{album.releaseTypes && album.releaseTypes[0] ? (
<div className="because-card-pills">
+6 -1
View File
@@ -19,6 +19,7 @@ import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
const INTERVAL_MS = 10000;
const HERO_ALBUM_COUNT = 8;
@@ -266,6 +267,10 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) {
}, [albums.length, startTimer]);
const album = albums[activeIdx] ?? null;
const heroArtistLabel = useMemo(
() => (album ? albumArtistDisplayName(album) : ''),
[album],
);
// Lazily fetch format label for the currently-visible album (cached by id)
const [albumFormats, setAlbumFormats] = useState<Record<string, string>>({});
@@ -335,7 +340,7 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) {
<div className="hero-text">
<span className="hero-eyebrow">{t('hero.eyebrow')}</span>
<h2 className="hero-title">{album.name}</h2>
<p className="hero-artist">{album.artist}</p>
<p className="hero-artist">{heroArtistLabel}</p>
<div className="hero-meta">
{album.year && <span className="badge">{album.year}</span>}
{album.genre && <span className="badge">{album.genre}</span>}
+2 -1
View File
@@ -28,6 +28,7 @@ import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { useTranslation } from 'react-i18next';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from './CachedImage';
import type { SubsonicSong } from '../api/subsonicTypes';
import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage';
@@ -775,7 +776,7 @@ export default function LiveSearch() {
)}
<div>
<div className="search-result-name">{a.name}</div>
<div className="search-result-sub">{a.artist}</div>
<div className="search-result-sub">{albumArtistDisplayName(a)}</div>
</div>
</button>
);
+2 -1
View File
@@ -16,6 +16,7 @@ import { ArtistCoverArtImage } from '../cover/ArtistCoverArtImage';
import { CoverArtImage } from '../cover/CoverArtImage';
import { albumCoverRefForSong } from '../cover/ref';
import { showToast } from '../utils/ui/toast';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs';
import { useShareSearch } from '../hooks/useShareSearch';
import ShareSearchResults from './search/ShareSearchResults';
import {
@@ -383,7 +384,7 @@ export default function MobileSearchOverlay({ onClose }: { onClose: () => void }
)}
<div className="mobile-search-item-info">
<span className="mobile-search-item-title">{a.name}</span>
<span className="mobile-search-item-sub">{a.artist}</span>
<span className="mobile-search-item-sub">{albumArtistDisplayName(a)}</span>
</div>
<ChevronRight size={16} className="mobile-search-item-chevron" />
</button>