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