mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
fix(queue-info): show artist image for current track (#732)
* fix(queue-info): pin artist image cache key to matching info, not lagging state
`artistInfo` state and `artistId` updated on different cycles, so on
track change the info panel rendered one frame with the previous
track's `largeImageUrl` under the new `heroCacheKey`. CachedImage's
IndexedDB persisted that mismatched blob under the new key, leaving
every subsequent track stuck on the previous artist's image.
Hold artist info + song detail as `{ id, info }` tuples and gate
render on id-match so `src` and `cacheKey` always come from the
same source.
* docs(changelog): queue info artist image fix (PR #732)
This commit is contained in:
committed by
GitHub
parent
efd85ffde3
commit
58d3bcd695
@@ -553,6 +553,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
|
||||
|
||||
* **Lucky Mix** ran many queue edits (`pruneUpcomingToCurrent`, **`playTrack`**, **`enqueue`** batches). Each pushed onto the bounded **`QUEUE_UNDO_MAX`** stack, so the snapshot taken **before** Lucky Mix was usually shifted off — Ctrl+Z only stepped through small edits or could not restore the prior queue. The mix flow now pushes **one** undo snapshot up-front and skips intermediate **`enqueue`** / prune snapshots (**`skipQueueUndo`**) so a single undo restores the queue from immediately **before** Lucky Mix.
|
||||
|
||||
### Queue panel Info — artist image now follows the current track
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#732](https://github.com/Psychotoxical/psysonic/pull/732)**
|
||||
|
||||
* The Info tab rendered one frame on each track switch with the previous track's artist image URL paired with the new track's cache key — **`CachedImage`**'s IndexedDB then persisted that mismatched blob, so every subsequent track stayed stuck on the previous artist's image. Artist info and song detail are now held as **`{ id, info }`** tuples and the image render is gated on id-match, so source and cache key always come from the same track.
|
||||
|
||||
## [1.45.0] - 2026-05-04
|
||||
|
||||
## Added
|
||||
|
||||
@@ -22,6 +22,9 @@ const BIO_CLAMP_LINES = 4;
|
||||
const artistInfoCache = new Map<string, SubsonicArtistInfo | null>();
|
||||
const songDetailCache = new Map<string, SubsonicSong | null>();
|
||||
|
||||
type ArtistInfoEntry = { id: string; info: SubsonicArtistInfo | null };
|
||||
type SongDetailEntry = { id: string; song: SubsonicSong | null };
|
||||
|
||||
function isoToParts(iso: string): { month: string; day: string; weekday: string; time: string } | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
@@ -89,16 +92,21 @@ export default function NowPlayingInfo() {
|
||||
const artistId = currentTrack?.artistId || '';
|
||||
const songId = currentTrack?.id || '';
|
||||
|
||||
const [artistInfo, setArtistInfo] = useState<SubsonicArtistInfo | null>(
|
||||
artistId && subsonicServerId
|
||||
? artistInfoCache.get(queuePanelCacheKey(subsonicServerId, artistId)) ?? null
|
||||
: null,
|
||||
);
|
||||
const [songDetail, setSongDetail] = useState<SubsonicSong | null>(
|
||||
songId && subsonicServerId
|
||||
? songDetailCache.get(queuePanelCacheKey(subsonicServerId, songId)) ?? null
|
||||
: null,
|
||||
);
|
||||
// Tuple { id, info } gates rendering on "info matches the current artistId" so
|
||||
// `heroImage` (from info) and `heroCacheKey` (from artistId) can never be from
|
||||
// different tracks. Otherwise a track switch would render one frame with a
|
||||
// lagging url under the new key, and CachedImage's IndexedDB would persist
|
||||
// the wrong blob under the new key — sticky "previous track" image (#…).
|
||||
const [artistInfoEntry, setArtistInfoEntry] = useState<ArtistInfoEntry | null>(() => {
|
||||
if (!artistId || !subsonicServerId) return null;
|
||||
const cached = artistInfoCache.get(queuePanelCacheKey(subsonicServerId, artistId));
|
||||
return cached === undefined ? null : { id: artistId, info: cached };
|
||||
});
|
||||
const [songDetailEntry, setSongDetailEntry] = useState<SongDetailEntry | null>(() => {
|
||||
if (!songId || !subsonicServerId) return null;
|
||||
const cached = songDetailCache.get(queuePanelCacheKey(subsonicServerId, songId));
|
||||
return cached === undefined ? null : { id: songId, song: cached };
|
||||
});
|
||||
const [tourEvents, setTourEvents] = useState<BandsintownEvent[]>([]);
|
||||
const [tourLoading, setTourLoading] = useState(false);
|
||||
const [bioExpanded, setBioExpanded] = useState(false);
|
||||
@@ -112,27 +120,29 @@ export default function NowPlayingInfo() {
|
||||
|
||||
// Artist bio + image
|
||||
useEffect(() => {
|
||||
if (!subsonicReady || !subsonicServerId || !artistId) { setArtistInfo(null); return; }
|
||||
if (!subsonicReady || !subsonicServerId || !artistId) { setArtistInfoEntry(null); return; }
|
||||
const cacheKey = queuePanelCacheKey(subsonicServerId, artistId);
|
||||
const cached = artistInfoCache.get(cacheKey);
|
||||
if (cached !== undefined) { setArtistInfo(cached); return; }
|
||||
if (cached !== undefined) { setArtistInfoEntry({ id: artistId, info: cached }); return; }
|
||||
setArtistInfoEntry(null);
|
||||
let cancelled = false;
|
||||
getArtistInfo(artistId)
|
||||
.then(info => { if (!cancelled) { artistInfoCache.set(cacheKey, info ?? null); setArtistInfo(info ?? null); } })
|
||||
.catch(() => { if (!cancelled) { artistInfoCache.set(cacheKey, null); setArtistInfo(null); } });
|
||||
.then(info => { if (!cancelled) { artistInfoCache.set(cacheKey, info ?? null); setArtistInfoEntry({ id: artistId, info: info ?? null }); } })
|
||||
.catch(() => { if (!cancelled) { artistInfoCache.set(cacheKey, null); setArtistInfoEntry({ id: artistId, info: null }); } });
|
||||
return () => { cancelled = true; };
|
||||
}, [subsonicReady, subsonicServerId, artistId]);
|
||||
|
||||
// Song detail (for OpenSubsonic contributors[])
|
||||
useEffect(() => {
|
||||
if (!subsonicReady || !subsonicServerId || !songId) { setSongDetail(null); return; }
|
||||
if (!subsonicReady || !subsonicServerId || !songId) { setSongDetailEntry(null); return; }
|
||||
const cacheKey = queuePanelCacheKey(subsonicServerId, songId);
|
||||
const cached = songDetailCache.get(cacheKey);
|
||||
if (cached !== undefined) { setSongDetail(cached); return; }
|
||||
if (cached !== undefined) { setSongDetailEntry({ id: songId, song: cached }); return; }
|
||||
setSongDetailEntry(null);
|
||||
let cancelled = false;
|
||||
getSong(songId)
|
||||
.then(song => { if (!cancelled) { songDetailCache.set(cacheKey, song ?? null); setSongDetail(song ?? null); } })
|
||||
.catch(() => { if (!cancelled) { songDetailCache.set(cacheKey, null); setSongDetail(null); } });
|
||||
.then(song => { if (!cancelled) { songDetailCache.set(cacheKey, song ?? null); setSongDetailEntry({ id: songId, song: song ?? null }); } })
|
||||
.catch(() => { if (!cancelled) { songDetailCache.set(cacheKey, null); setSongDetailEntry({ id: songId, song: null }); } });
|
||||
return () => { cancelled = true; };
|
||||
}, [subsonicReady, subsonicServerId, songId]);
|
||||
|
||||
@@ -147,9 +157,16 @@ export default function NowPlayingInfo() {
|
||||
return () => { cancelled = true; };
|
||||
}, [enableBandsintown, artistName]);
|
||||
|
||||
// Only consume info that belongs to the current track — never render with a
|
||||
// stale entry from the previous track.
|
||||
const matchedArtistInfo =
|
||||
artistInfoEntry && artistInfoEntry.id === artistId ? artistInfoEntry.info : null;
|
||||
const matchedSongDetail =
|
||||
songDetailEntry && songDetailEntry.id === songId ? songDetailEntry.song : null;
|
||||
|
||||
// Detect whether the (clamped) bio actually overflows so we hide the toggle
|
||||
// when it would do nothing.
|
||||
const bio = artistInfo?.biography?.trim() || '';
|
||||
const bio = matchedArtistInfo?.biography?.trim() || '';
|
||||
const bioClean = bio.replace(/<a [^>]*>.*?<\/a>\.?/gi, '').trim();
|
||||
useLayoutEffect(() => {
|
||||
const el = bioRef.current;
|
||||
@@ -158,8 +175,8 @@ export default function NowPlayingInfo() {
|
||||
}, [bioClean]);
|
||||
|
||||
const contributorRows = useMemo(
|
||||
() => buildContributorRows(songDetail, artistName),
|
||||
[songDetail, artistName],
|
||||
() => buildContributorRows(matchedSongDetail, artistName),
|
||||
[matchedSongDetail, artistName],
|
||||
);
|
||||
|
||||
if (!currentTrack) {
|
||||
@@ -170,8 +187,9 @@ export default function NowPlayingInfo() {
|
||||
);
|
||||
}
|
||||
|
||||
const heroImage = artistInfo?.largeImageUrl || artistInfo?.mediumImageUrl || '';
|
||||
const heroCacheKey = artistId ? `artistInfo:${artistId}:hero` : '';
|
||||
const heroImage =
|
||||
matchedArtistInfo?.largeImageUrl || matchedArtistInfo?.mediumImageUrl || '';
|
||||
const heroCacheKey = matchedArtistInfo && artistId ? `artistInfo:${artistId}:hero` : '';
|
||||
|
||||
const visibleTours = showAllTours ? tourEvents : tourEvents.slice(0, TOUR_LIMIT);
|
||||
const hiddenTourCount = Math.max(0, tourEvents.length - visibleTours.length);
|
||||
|
||||
Reference in New Issue
Block a user