fix(node-player): load cover art via axios with Bearer token
Publish Metadata Agent Image (dev) / build-and-push-image (push) Successful in 1m22s
Publish Node Player Image (dev) / build-and-push-image (push) Failing after 29s
Publish Web Player Image (dev) / build-and-push-image (push) Has been cancelled

Cover images were loaded via <img src> 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) <noreply@anthropic.com>
This commit is contained in:
Ultradesu
2026-04-08 15:39:14 +01:00
parent 1df10fb0b7
commit a9a8ee81b8
5 changed files with 53 additions and 25 deletions
@@ -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
@@ -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 <>&#127925;</>
if (!src || errored) return <>&#127925;</>
return <img src={src} alt="" onError={() => setErrored(true)} />
}
@@ -32,12 +29,10 @@ export function NowPlaying({ track }: { track: QueueItem | null }) {
)
}
const coverUrl = `${API_ROOT}/tracks/${track.slug}/cover`
return (
<div className="np-info">
<div className="np-cover" id="npCover">
<Cover src={coverUrl} />
<Cover slug={track.slug} />
</div>
<div className="np-text">
<div className="np-title" id="npTitle">
@@ -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 <>&#127925;</>
if (!src || errored) return <>&#127925;</>
return <img src={src} alt="" onError={() => 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({
>
<span className="qi-index">{isPlaying ? '' : pos + 1}</span>
<div className="qi-cover">
{coverSrc ? <Cover src={coverSrc} /> : <>&#127925;</>}
{hasAlbum ? <Cover slug={t.slug} /> : <>&#127925;</>}
</div>
<div className="qi-info">
<div className="qi-title">{t.title}</div>
@@ -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<string | null> {
const res = await furumiApi.get(`/tracks/${trackSlug}/cover`, { responseType: 'blob' }).catch(() => null)
if (!res?.data) return null
return URL.createObjectURL(res.data)
}
@@ -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<string | null>(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
}