feat(node-player): use Service Worker for auth, enable streaming playback

Add a Service Worker that intercepts /api/* requests and injects the
Bearer token. This allows <audio> and <img> elements to use direct
URLs instead of downloading entire files as blobs first.

- Audio now streams progressively (no full download before playback)
- Cover art loads via regular <img src> (SW adds auth header)
- Remove blob-based preloadStream, fetchCoverBlob, useCoverUrl hook
- Register SW in main.tsx, token synced via postMessage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ultradesu
2026-04-08 17:30:17 +01:00
parent 6b1aa6b5d5
commit d6dd046fad
9 changed files with 61 additions and 64 deletions
@@ -1,12 +1,15 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { API_ROOT } from '../furumiApi'
import type { QueueItem } from './QueueList'
import { useCoverUrl } from '../hooks/useCoverUrl'
function Cover({ slug }: { slug: string }) {
function Cover({ src }: { src: string }) {
const [errored, setErrored] = useState(false)
const src = useCoverUrl(slug)
if (!src || errored) return <>&#127925;</>
useEffect(() => {
setErrored(false)
}, [src])
if (errored) return <>&#127925;</>
return <img src={src} alt="" onError={() => setErrored(true)} />
}
@@ -29,10 +32,12 @@ 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 slug={track.slug} />
<Cover src={coverUrl} />
</div>
<div className="np-text">
<div className="np-title" id="npTitle">
@@ -45,4 +50,3 @@ export function NowPlaying({ track }: { track: QueueItem | null }) {
</div>
)
}
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { useCoverUrl } from '../hooks/useCoverUrl'
import { API_ROOT } from '../furumiApi'
export type QueueItem = {
slug: string
@@ -32,11 +32,10 @@ function fmt(secs: number) {
return `${m}:${pad(s % 60)}`
}
function Cover({ slug }: { slug: string }) {
function Cover({ src }: { src: string }) {
const [errored, setErrored] = useState(false)
const src = useCoverUrl(slug)
if (!src || errored) return <>&#127925;</>
useEffect(() => { setErrored(false) }, [src])
if (errored) return <>&#127925;</>
return <img src={src} alt="" onError={() => setErrored(true)} />
}
@@ -75,7 +74,7 @@ export function QueueList({
if (!t) return null
const isPlaying = origIdx === playingOrigIdx
const hasAlbum = !!t.album_slug
const coverSrc = t.album_slug ? `${API_ROOT}/tracks/${t.slug}/cover` : ''
const dur = t.duration ? fmt(t.duration) : ''
const isDragging = draggingPos === pos
const isDragOver = dragOverPos === pos
@@ -116,7 +115,7 @@ export function QueueList({
>
<span className="qi-index">{isPlaying ? '' : pos + 1}</span>
<div className="qi-cover">
{hasAlbum ? <Cover slug={t.slug} /> : <>&#127925;</>}
{coverSrc ? <Cover src={coverSrc} /> : <>&#127925;</>}
</div>
<div className="qi-info">
<div className="qi-title">{t.title}</div>