From a9a8ee81b81cb60697aff95e0d04af8c16a03451 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 8 Apr 2026 15:39:14 +0100 Subject: [PATCH] fix(node-player): load cover art via axios with Bearer token Cover images were loaded via which doesn't include the Authorization header, resulting in 401 from the Rust API. Now covers are fetched through axios as blobs and displayed via object URLs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../client/src/FurumiPlayer.tsx | 11 ++++---- .../client/src/components/NowPlaying.tsx | 17 ++++------- .../client/src/components/QueueList.tsx | 16 +++++------ furumi-node-player/client/src/furumiApi.ts | 6 ++++ .../client/src/hooks/useCoverUrl.ts | 28 +++++++++++++++++++ 5 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 furumi-node-player/client/src/hooks/useCoverUrl.ts diff --git a/furumi-node-player/client/src/FurumiPlayer.tsx b/furumi-node-player/client/src/FurumiPlayer.tsx index b17985c..64d3410 100644 --- a/furumi-node-player/client/src/FurumiPlayer.tsx +++ b/furumi-node-player/client/src/FurumiPlayer.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react' import './furumi-player.css' -import { API_ROOT, searchTracks, preloadStream } from './furumiApi' +import { searchTracks, preloadStream, fetchCoverBlob } from './furumiApi' import { store, useAppDispatch, useAppSelector } from './store' import { fetchArtists } from './store/slices/artistsSlice' import { fetchArtistAlbums } from './store/slices/albumsSlice' @@ -80,15 +80,16 @@ export function FurumiPlayer() { return } document.title = `${nowPlayingTrack.title} — Furumi` - const coverUrl = `${API_ROOT}/tracks/${nowPlayingTrack.slug}/cover` if ('mediaSession' in navigator) { try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - navigator.mediaSession.metadata = new window.MediaMetadata({ + const meta = new window.MediaMetadata({ title: nowPlayingTrack.title, artist: nowPlayingTrack.artist || '', album: '', - artwork: [{ src: coverUrl, sizes: '512x512' }], + }) + navigator.mediaSession.metadata = meta + fetchCoverBlob(nowPlayingTrack.slug).then((url) => { + if (url) meta.artwork = [{ src: url, sizes: '512x512' }] }) } catch { // ignore diff --git a/furumi-node-player/client/src/components/NowPlaying.tsx b/furumi-node-player/client/src/components/NowPlaying.tsx index c3275bc..25e4793 100644 --- a/furumi-node-player/client/src/components/NowPlaying.tsx +++ b/furumi-node-player/client/src/components/NowPlaying.tsx @@ -1,15 +1,12 @@ -import { useEffect, useState } from 'react' -import { API_ROOT } from '../furumiApi' +import { useState } from 'react' import type { QueueItem } from './QueueList' +import { useCoverUrl } from '../hooks/useCoverUrl' -function Cover({ src }: { src: string }) { +function Cover({ slug }: { slug: string }) { const [errored, setErrored] = useState(false) + const src = useCoverUrl(slug) - useEffect(() => { - setErrored(false) - }, [src]) - - if (errored) return <>🎵 + if (!src || errored) return <>🎵 return setErrored(true)} /> } @@ -32,12 +29,10 @@ export function NowPlaying({ track }: { track: QueueItem | null }) { ) } - const coverUrl = `${API_ROOT}/tracks/${track.slug}/cover` - return (
- +
diff --git a/furumi-node-player/client/src/components/QueueList.tsx b/furumi-node-player/client/src/components/QueueList.tsx index c371f09..fe53bdf 100644 --- a/furumi-node-player/client/src/components/QueueList.tsx +++ b/furumi-node-player/client/src/components/QueueList.tsx @@ -1,5 +1,5 @@ -import { useEffect, useRef, useState } from 'react' -import { API_ROOT } from '../furumiApi' +import { useRef, useState } from 'react' +import { useCoverUrl } from '../hooks/useCoverUrl' export type QueueItem = { slug: string @@ -32,13 +32,11 @@ function fmt(secs: number) { return `${m}:${pad(s % 60)}` } -function Cover({ src }: { src: string }) { +function Cover({ slug }: { slug: string }) { const [errored, setErrored] = useState(false) - useEffect(() => { - setErrored(false) - }, [src]) + const src = useCoverUrl(slug) - if (errored) return <>🎵 + if (!src || errored) return <>🎵 return setErrored(true)} /> } @@ -77,7 +75,7 @@ export function QueueList({ if (!t) return null const isPlaying = origIdx === playingOrigIdx - const coverSrc = t.album_slug ? `${API_ROOT}/tracks/${t.slug}/cover` : '' + const hasAlbum = !!t.album_slug const dur = t.duration ? fmt(t.duration) : '' const isDragging = draggingPos === pos const isDragOver = dragOverPos === pos @@ -118,7 +116,7 @@ export function QueueList({ > {isPlaying ? '' : pos + 1}
- {coverSrc ? : <>🎵} + {hasAlbum ? : <>🎵}
{t.title}
diff --git a/furumi-node-player/client/src/furumiApi.ts b/furumi-node-player/client/src/furumiApi.ts index 176166a..fa492fb 100644 --- a/furumi-node-player/client/src/furumiApi.ts +++ b/furumi-node-player/client/src/furumiApi.ts @@ -52,3 +52,9 @@ export async function preloadStream(trackSlug: string) { return await furumiApi.get(`/stream/${trackSlug}`, { responseType: 'blob' }).catch(() => null) } +export async function fetchCoverBlob(trackSlug: string): Promise { + const res = await furumiApi.get(`/tracks/${trackSlug}/cover`, { responseType: 'blob' }).catch(() => null) + if (!res?.data) return null + return URL.createObjectURL(res.data) +} + diff --git a/furumi-node-player/client/src/hooks/useCoverUrl.ts b/furumi-node-player/client/src/hooks/useCoverUrl.ts new file mode 100644 index 0000000..1b53be0 --- /dev/null +++ b/furumi-node-player/client/src/hooks/useCoverUrl.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react' +import { fetchCoverBlob } from '../furumiApi' + +export function useCoverUrl(trackSlug: string | undefined): string | null { + const [url, setUrl] = useState(null) + + useEffect(() => { + if (!trackSlug) { + setUrl(null) + return + } + + let revoke: string | null = null + + fetchCoverBlob(trackSlug).then((blobUrl) => { + if (blobUrl) { + revoke = blobUrl + setUrl(blobUrl) + } + }) + + return () => { + if (revoke) URL.revokeObjectURL(revoke) + } + }, [trackSlug]) + + return url +}