diff --git a/src/components/player/PersistentPlayer.tsx b/src/components/player/PersistentPlayer.tsx index cc1c34d..ee0f9e5 100644 --- a/src/components/player/PersistentPlayer.tsx +++ b/src/components/player/PersistentPlayer.tsx @@ -15,8 +15,9 @@ import { } from '../../store/slices/player'; import { useAudioPlayer } from '../../hooks/useAudioPlayer'; import { useStreamCached } from '../../hooks/useStreamCached'; +import { useResolvedQueueEntry } from '../../hooks/useResolvedQueueEntry'; import { formatDuration } from '../../lib/format'; -import { getCoverUrl } from '../../api/endpoints/streaming'; +import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming'; export function PersistentPlayer() { const { t } = useTranslation(); @@ -24,7 +25,11 @@ export function PersistentPlayer() { const { seek, playNext, playPrev } = useAudioPlayer(); const player = useAppSelector((s) => s.player); const queue = useAppSelector((s) => s.queue); + const token = useAppSelector((s) => s.auth.accessToken); const currentEntry = queue.entries[queue.currentIndex]; + // Read through to the live Track cache so enrichment updates reach the player, + // not just the play-time snapshot frozen in the queue slice. + const current = useResolvedQueueEntry(currentEntry); // Source indicator: cached → playing locally, otherwise streaming. const cached = useStreamCached(currentEntry?.trackId); @@ -32,8 +37,12 @@ export function PersistentPlayer() { return
{t('player.nothingPlaying')}
; } - const artUrl = getCoverUrl(currentEntry?.albumArtUrl); - const seedLabel = currentEntry?.albumTitle ?? currentEntry?.title ?? ''; + const artUrl = + getCoverUrl(currentEntry?.albumArtUrl) ?? + (token && current?.hasCover + ? getTrackCoverUrl(current.trackId, token, true) + : undefined); + const seedLabel = current?.albumTitle ?? current?.title ?? ''; const onStream = !cached; return ( @@ -45,8 +54,8 @@ export function PersistentPlayer() { >
-
{currentEntry?.title ?? '—'}
-
{currentEntry?.artistName ?? ''}
+
{current?.title ?? '—'}
+
{current?.artistName ?? ''}
s.queue); const isOpen = useAppSelector((s) => s.player.isQueueOpen); - const now = + const nowEntry = queue.currentIndex >= 0 ? queue.entries[queue.currentIndex] : undefined; + const now = useResolvedQueueEntry(nowEntry); const upNext = queue.entries .map((entry, index) => ({ entry, index })) .filter(({ index }) => index > queue.currentIndex); @@ -120,33 +123,12 @@ export function QueuePanel() {
{t('queue.nothingNext')}
) : ( upNext.map(({ entry, index }) => ( -
dispatch(goToIndex(index))} - title={t('queue.doubleClickPlay')} - > - - - - -
-
{entry.title}
-
{entry.artistName}
-
- -
+ entry={entry} + onPlay={() => dispatch(goToIndex(index))} + onRemove={() => dispatch(removeFromQueue(index))} + /> )) )} @@ -162,3 +144,40 @@ export function QueuePanel() { ); } + +/** An "up next" row, resolving its display fields against the live Track cache + * (same read-through as the now-playing entry) so enrichment updates show. */ +function QueueRow({ + entry, + onPlay, + onRemove, +}: { + entry: QueueEntry; + onPlay: () => void; + onRemove: () => void; +}) { + const { t } = useTranslation(); + const resolved = useResolvedQueueEntry(entry); + const albumTitle = resolved?.albumTitle ?? entry.albumTitle; + + return ( +
+ + + + +
+
{resolved?.title ?? entry.title}
+
{resolved?.artistName ?? entry.artistName}
+
+ +
+ ); +} diff --git a/src/hooks/useResolvedQueueEntry.ts b/src/hooks/useResolvedQueueEntry.ts new file mode 100644 index 0000000..16c30af --- /dev/null +++ b/src/hooks/useResolvedQueueEntry.ts @@ -0,0 +1,37 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useGetTrackQuery } from '../api/endpoints/library'; +import type { QueueEntry } from '../store/slices/queue'; + +export interface ResolvedQueueEntry { + trackId: string; + title: string; + artistName: string; + albumTitle: string; + durationMs: number; + hasCover: boolean; +} + +/** + * Merge a queue entry's play-time snapshot with the live `Track` cache. + * + * The queue slice stores denormalized display fields (title/artist/…) captured + * when a track was queued, so they go stale after metadata enrichment updates + * the track. This reads through to the RTKQ `Track` cache — invalidated by the + * same tags that refresh the library — and prefers its fresh values, falling + * back to the snapshot for instant render and offline. Returns undefined when + * there is no current entry. + */ +export function useResolvedQueueEntry( + entry: QueueEntry | undefined, +): ResolvedQueueEntry | undefined { + const { data } = useGetTrackQuery(entry?.trackId ?? skipToken); + if (!entry) return undefined; + return { + trackId: entry.trackId, + title: data?.title ?? entry.title, + artistName: data?.artistName ?? entry.artistName, + albumTitle: data?.albumTitle ?? entry.albumTitle, + durationMs: data?.durationMs ?? entry.durationMs, + hasCover: data?.hasCover ?? false, + }; +}