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
+}