Simplify streaming auth + add local cover extraction
Streaming: Drop token-in-URL auth entirely. Session-ID (UUID, 128-bit
entropy) IS the auth — same approach as Audiobookshelf. Eliminates the
entire class of token-related failures and matches how every other
streaming server handles this. Logs every stream request with Range
header and User-Agent for diagnostics.
Player: Visible error banner in UI when audio fails (with HTML5 media
error code translated to German). Stream URL is shown in the banner so
the user can see exactly what failed.
Scanner: Cover extraction from two new sources (in addition to API
matching):
1. Folder-level images (cover.jpg, folder.jpg, front.jpg, etc.)
2. Embedded artwork (ID3 APIC, MP4 covr, FLAC/Vorbis pictures)
Runs on every scan — also fills in covers for items that were already
scanned but never got one from matching.
New endpoint POST /api/items/{id}/extract-cover triggers this manually
for a single item.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,9 +25,7 @@ function findTrackForTime(tracks: Track[], time: number): number {
|
||||
}
|
||||
|
||||
function trackUrl(track: Track): string {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const sep = track.contentUrl.includes('?') ? '&' : '?'
|
||||
return `${track.contentUrl}${sep}token=${encodeURIComponent(token)}`
|
||||
return track.contentUrl
|
||||
}
|
||||
|
||||
export default function AudioPlayer() {
|
||||
@@ -59,6 +57,7 @@ export default function AudioPlayer() {
|
||||
const [showSpeedMenu, setShowSpeedMenu] = useState(false)
|
||||
const [showSleepMenu, setShowSleepMenu] = useState(false)
|
||||
const [muted, setMuted] = useState(false)
|
||||
const [playerError, setPlayerError] = useState<string | null>(null)
|
||||
|
||||
const tracks: Track[] = session?.audioTracks || []
|
||||
|
||||
@@ -82,13 +81,17 @@ export default function AudioPlayer() {
|
||||
|
||||
currentTrackIdx.current = startIdx
|
||||
pendingSeek.current = timeInTrack > 0 ? timeInTrack : null
|
||||
setPlayerError(null)
|
||||
|
||||
audio.preload = 'auto'
|
||||
audio.playbackRate = playbackRate
|
||||
audio.volume = muted ? 0 : volume
|
||||
audio.src = trackUrl(tracks[startIdx])
|
||||
audio.load()
|
||||
audio.play().catch((e) => console.warn('[Player] play() failed:', e))
|
||||
audio.play().catch((e) => {
|
||||
console.warn('[Player] play() failed:', e)
|
||||
setPlayerError(`Wiedergabe blockiert: ${e?.message || e}`)
|
||||
})
|
||||
|
||||
return () => {
|
||||
audio.pause()
|
||||
@@ -198,6 +201,13 @@ export default function AudioPlayer() {
|
||||
const onError = () => {
|
||||
const audio = audioRef.current
|
||||
const err = audio?.error
|
||||
const codeMap: Record<number, string> = {
|
||||
1: 'MEDIA_ERR_ABORTED — Abruf abgebrochen',
|
||||
2: 'MEDIA_ERR_NETWORK — Netzwerkfehler beim Laden',
|
||||
3: 'MEDIA_ERR_DECODE — Format nicht dekodierbar',
|
||||
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED — Quelle nicht unterstützt (404/Format/MIME)',
|
||||
}
|
||||
const msg = err ? `${codeMap[err.code] || `Code ${err.code}`}${err.message ? ': ' + err.message : ''}` : 'Unbekannter Fehler'
|
||||
console.error('[Player] audio error', {
|
||||
code: err?.code,
|
||||
message: err?.message,
|
||||
@@ -205,6 +215,7 @@ export default function AudioPlayer() {
|
||||
networkState: audio?.networkState,
|
||||
readyState: audio?.readyState,
|
||||
})
|
||||
setPlayerError(msg)
|
||||
}
|
||||
|
||||
const fmtTime = (s: number) => {
|
||||
@@ -273,6 +284,16 @@ export default function AudioPlayer() {
|
||||
{author && <p className="text-muted mt-1 truncate" style={{ fontSize: '13px' }}>{author}</p>}
|
||||
</div>
|
||||
|
||||
{playerError && (
|
||||
<div className="mb-4 px-4 py-3 rounded-lg border border-red-500/40 bg-red-500/10 text-red-300" style={{ fontSize: '12px' }}>
|
||||
<p className="font-medium mb-1">Audio-Fehler</p>
|
||||
<p className="opacity-80">{playerError}</p>
|
||||
<p className="opacity-60 mt-2" style={{ fontSize: '10px' }}>
|
||||
Stream-URL: <span className="font-mono break-all">{tracks[currentTrackIdx.current]?.contentUrl}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="relative bg-muted2 rounded-full mb-2" style={{ height: '4px' }}>
|
||||
{chapterMarkers.map((m: any, i: number) => (
|
||||
|
||||
Reference in New Issue
Block a user