From d93f9720795bb0abd557f81cbd14cc98845632ec Mon Sep 17 00:00:00 2001 From: Audiolib Date: Tue, 26 May 2026 18:09:22 +0200 Subject: [PATCH] Simplify streaming auth + add local cover extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/app/routers/matching.py | 31 ++++++ backend/app/routers/stream.py | 36 +++---- backend/app/services/scanner.py | 99 +++++++++++++++++++ .../src/components/player/AudioPlayer.tsx | 29 +++++- 4 files changed, 166 insertions(+), 29 deletions(-) diff --git a/backend/app/routers/matching.py b/backend/app/routers/matching.py index af725be..cceec42 100644 --- a/backend/app/routers/matching.py +++ b/backend/app/routers/matching.py @@ -119,6 +119,37 @@ async def apply_match( return await _enrich_item_with_files(item, db) +@router.post("/{item_id}/extract-cover") +async def extract_local_cover( + item_id: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Extrahiert ein Cover aus Ordner-Dateien oder eingebettetem Artwork.""" + from ..services.scanner import _save_local_cover + from ..models.media_item import BookFile + import os + + result = await db.execute(select(LibraryItem).where(LibraryItem.id == item_id)) + item = result.scalar_one_or_none() + if not item: + raise HTTPException(status_code=404, detail="Item not found") + + files_result = await db.execute( + select(BookFile).where(BookFile.library_item_id == item_id).order_by(BookFile.track_index) + ) + audio_files = [f.path for f in files_result.scalars().all()] + + cover = _save_local_cover(item.path, audio_files, item.id) + if cover: + item.cover_path = cover + item.updated_at = datetime.utcnow() + await db.commit() + logger.info(f"Lokales Cover gesetzt für {item_id}: {cover}") + return {"success": True, "cover_path": cover} + return {"success": False, "message": "Kein Cover gefunden"} + + @router.delete("/{item_id}/match") async def clear_match( item_id: str, diff --git a/backend/app/routers/stream.py b/backend/app/routers/stream.py index 3003166..f900743 100644 --- a/backend/app/routers/stream.py +++ b/backend/app/routers/stream.py @@ -2,7 +2,7 @@ import os import uuid import logging from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException, Query, Header, Request +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import FileResponse, StreamingResponse, Response from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select @@ -11,7 +11,6 @@ from ..models.user import User from ..models.media_item import LibraryItem, BookFile, Chapter from ..models.session import PlaybackSession from ..models.progress import MediaProgress -from ..services.auth import decode_token logger = logging.getLogger(__name__) router = APIRouter(tags=["stream"]) @@ -175,25 +174,22 @@ async def stream_file( session_id: str, track: int, request: Request, - token: str | None = Query(None), - authorization: str | None = Header(None), db: AsyncSession = Depends(get_db), ): - """Audio-Streaming mit nativen HTTP Range Requests (206 Partial Content).""" - raw = token - if not raw and authorization: - parts = authorization.split(" ", 1) - if len(parts) == 2 and parts[0].lower() == "bearer": - raw = parts[1] - if not raw or not decode_token(raw): - logger.warning(f"Stream 401: session={session_id} track={track}") - raise HTTPException(status_code=401, detail="Nicht autorisiert") - + """Audio-Streaming mit nativen HTTP Range Requests (206 Partial Content). + Session-ID (UUID, 128-bit Entropie) dient als Auth wie bei Audiobookshelf. + Damit funktioniert das mit