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:
@@ -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 <audio src> ohne Token-Header-Problematik.
|
||||
"""
|
||||
logger.info(
|
||||
f"Stream request: session={session_id} track={track} "
|
||||
f"range={request.headers.get('range')!r} ua={request.headers.get('user-agent', '?')[:40]!r}"
|
||||
)
|
||||
result = await db.execute(
|
||||
select(PlaybackSession).where(PlaybackSession.id == session_id)
|
||||
)
|
||||
session = result.scalar_one_or_none()
|
||||
if not session:
|
||||
logger.warning(f"Stream: Session nicht gefunden: {session_id}")
|
||||
raise HTTPException(status_code=404, detail="Session nicht gefunden")
|
||||
|
||||
files_result = await db.execute(
|
||||
@@ -253,19 +249,9 @@ async def stream_file(
|
||||
async def stream_head(
|
||||
session_id: str,
|
||||
track: int,
|
||||
token: str | None = Query(None),
|
||||
authorization: str | None = Header(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""HEAD-Request für Audio-Datei (Metadaten ohne Body)."""
|
||||
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):
|
||||
raise HTTPException(status_code=401, detail="Nicht autorisiert")
|
||||
|
||||
"""HEAD-Request für Audio-Datei. Session-ID = Auth."""
|
||||
result = await db.execute(
|
||||
select(PlaybackSession).where(PlaybackSession.id == session_id)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user