fix(node-player): use AuthImg component for cover art with Bearer auth
SW doesn't reliably intercept <img> requests (no-cors mode). Use a thin AuthImg component that loads images via axios (which has the Bearer token) and displays them as blob URLs. Audio streaming still works via SW. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react'
|
import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react'
|
||||||
import './furumi-player.css'
|
import './furumi-player.css'
|
||||||
import { API_ROOT, searchTracks, recordPlay } from './furumiApi'
|
import { furumiApi, searchTracks, recordPlay } from './furumiApi'
|
||||||
import { store, useAppDispatch, useAppSelector } from './store'
|
import { store, useAppDispatch, useAppSelector } from './store'
|
||||||
import { fetchArtists } from './store/slices/artistsSlice'
|
import { fetchArtists } from './store/slices/artistsSlice'
|
||||||
import { fetchArtistAlbums } from './store/slices/albumsSlice'
|
import { fetchArtistAlbums } from './store/slices/albumsSlice'
|
||||||
@@ -88,13 +88,17 @@ export function FurumiPlayer({ user }: { user: UserProfile }) {
|
|||||||
document.title = `${nowPlayingTrack.title} — Furumi`
|
document.title = `${nowPlayingTrack.title} — Furumi`
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
try {
|
try {
|
||||||
const coverUrl = `${API_ROOT}/tracks/${nowPlayingTrack.slug}/cover`
|
const meta = new window.MediaMetadata({
|
||||||
navigator.mediaSession.metadata = new window.MediaMetadata({
|
|
||||||
title: nowPlayingTrack.title,
|
title: nowPlayingTrack.title,
|
||||||
artist: nowPlayingTrack.artist || '',
|
artist: nowPlayingTrack.artist || '',
|
||||||
album: '',
|
album: '',
|
||||||
artwork: [{ src: coverUrl, sizes: '512x512' }],
|
|
||||||
})
|
})
|
||||||
|
navigator.mediaSession.metadata = meta
|
||||||
|
furumiApi.get(`/tracks/${nowPlayingTrack.slug}/cover`, { responseType: 'blob' })
|
||||||
|
.then((res) => {
|
||||||
|
meta.artwork = [{ src: URL.createObjectURL(res.data), sizes: '512x512' }]
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { furumiApi } from '../furumiApi'
|
||||||
|
|
||||||
|
export function AuthImg({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||||
|
const [blobUrl, setBlobUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!src) return
|
||||||
|
let revoked = false
|
||||||
|
furumiApi.get(src, { responseType: 'blob' })
|
||||||
|
.then((res) => {
|
||||||
|
if (!revoked) setBlobUrl(URL.createObjectURL(res.data))
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
return () => {
|
||||||
|
revoked = true
|
||||||
|
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [src])
|
||||||
|
|
||||||
|
if (!blobUrl) return null
|
||||||
|
return <img src={blobUrl} alt={alt ?? ''} {...props} />
|
||||||
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { API_ROOT } from '../furumiApi'
|
import { API_ROOT } from '../furumiApi'
|
||||||
|
import { AuthImg } from './AuthImg'
|
||||||
import type { QueueItem } from './QueueList'
|
import type { QueueItem } from './QueueList'
|
||||||
|
|
||||||
function Cover({ src }: { src: string }) {
|
function Cover({ slug }: { slug: string }) {
|
||||||
const [errored, setErrored] = useState(false)
|
const [errored, setErrored] = useState(false)
|
||||||
|
const src = `/tracks/${slug}/cover`
|
||||||
useEffect(() => {
|
|
||||||
setErrored(false)
|
|
||||||
}, [src])
|
|
||||||
|
|
||||||
if (errored) return <>🎵</>
|
if (errored) return <>🎵</>
|
||||||
return <img src={src} alt="" onError={() => setErrored(true)} />
|
return <AuthImg src={src} alt="" onError={() => setErrored(true)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NowPlaying({ track }: { track: QueueItem | null }) {
|
export function NowPlaying({ track }: { track: QueueItem | null }) {
|
||||||
@@ -32,12 +30,10 @@ export function NowPlaying({ track }: { track: QueueItem | null }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverUrl = `${API_ROOT}/tracks/${track.slug}/cover`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="np-info">
|
<div className="np-info">
|
||||||
<div className="np-cover" id="npCover">
|
<div className="np-cover" id="npCover">
|
||||||
<Cover src={coverUrl} />
|
<Cover slug={track.slug} />
|
||||||
</div>
|
</div>
|
||||||
<div className="np-text">
|
<div className="np-text">
|
||||||
<div className="np-title" id="npTitle">
|
<div className="np-title" id="npTitle">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { API_ROOT } from '../furumiApi'
|
import { AuthImg } from './AuthImg'
|
||||||
|
|
||||||
export type QueueItem = {
|
export type QueueItem = {
|
||||||
slug: string
|
slug: string
|
||||||
@@ -32,11 +32,10 @@ function fmt(secs: number) {
|
|||||||
return `${m}:${pad(s % 60)}`
|
return `${m}:${pad(s % 60)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function Cover({ src }: { src: string }) {
|
function Cover({ slug }: { slug: string }) {
|
||||||
const [errored, setErrored] = useState(false)
|
const [errored, setErrored] = useState(false)
|
||||||
useEffect(() => { setErrored(false) }, [src])
|
|
||||||
if (errored) return <>🎵</>
|
if (errored) return <>🎵</>
|
||||||
return <img src={src} alt="" onError={() => setErrored(true)} />
|
return <AuthImg src={`/tracks/${slug}/cover`} alt="" onError={() => setErrored(true)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QueueList({
|
export function QueueList({
|
||||||
@@ -74,7 +73,7 @@ export function QueueList({
|
|||||||
if (!t) return null
|
if (!t) return null
|
||||||
|
|
||||||
const isPlaying = origIdx === playingOrigIdx
|
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 dur = t.duration ? fmt(t.duration) : ''
|
||||||
const isDragging = draggingPos === pos
|
const isDragging = draggingPos === pos
|
||||||
const isDragOver = dragOverPos === pos
|
const isDragOver = dragOverPos === pos
|
||||||
@@ -115,7 +114,7 @@ export function QueueList({
|
|||||||
>
|
>
|
||||||
<span className="qi-index">{isPlaying ? '' : pos + 1}</span>
|
<span className="qi-index">{isPlaying ? '' : pos + 1}</span>
|
||||||
<div className="qi-cover">
|
<div className="qi-cover">
|
||||||
{coverSrc ? <Cover src={coverSrc} /> : <>🎵</>}
|
{hasAlbum ? <Cover slug={t.slug} /> : <>🎵</>}
|
||||||
</div>
|
</div>
|
||||||
<div className="qi-info">
|
<div className="qi-info">
|
||||||
<div className="qi-title">{t.title}</div>
|
<div className="qi-title">{t.title}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user