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