|
|
@@ -8,6 +8,9 @@ from contextlib import asynccontextmanager
|
|
|
import json
|
|
|
from datetime import datetime
|
|
|
from typing import Optional
|
|
|
+import httpx
|
|
|
+import os
|
|
|
+from pathlib import Path
|
|
|
|
|
|
from app.database import init_db, get_db
|
|
|
from app.models import Book, ListeningSession, Recommendation, User, AppSettings
|
|
|
@@ -228,6 +231,49 @@ async def logout(response: Response):
|
|
|
# ==================== API Routes ====================
|
|
|
|
|
|
|
|
|
+async def download_cover_image(abs_client, book_id: str) -> Optional[str]:
|
|
|
+ """
|
|
|
+ Download cover image from Audiobookshelf and save locally.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ abs_client: Audiobookshelf client with authentication
|
|
|
+ book_id: Library item ID
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ Local path to saved cover (e.g., /static/covers/book_id.jpg) or None
|
|
|
+ """
|
|
|
+ if not book_id:
|
|
|
+ return None
|
|
|
+
|
|
|
+ # Create covers directory if it doesn't exist
|
|
|
+ covers_dir = Path("app/static/covers")
|
|
|
+ covers_dir.mkdir(parents=True, exist_ok=True)
|
|
|
+
|
|
|
+ # Use Audiobookshelf API cover endpoint
|
|
|
+ cover_url = f"{abs_client.base_url}/api/items/{book_id}/cover"
|
|
|
+
|
|
|
+ async with httpx.AsyncClient() as client:
|
|
|
+ try:
|
|
|
+ response = await client.get(cover_url, headers=abs_client.headers, follow_redirects=True)
|
|
|
+ response.raise_for_status()
|
|
|
+
|
|
|
+ # Determine extension from content-type
|
|
|
+ content_type = response.headers.get("content-type", "image/jpeg")
|
|
|
+ ext = ".webp" if "webp" in content_type else ".jpg"
|
|
|
+ local_filename = f"{book_id}{ext}"
|
|
|
+ local_path = covers_dir / local_filename
|
|
|
+
|
|
|
+ # Save to local file
|
|
|
+ with open(local_path, "wb") as f:
|
|
|
+ f.write(response.content)
|
|
|
+
|
|
|
+ # Return path relative to static directory
|
|
|
+ return f"/static/covers/{local_filename}"
|
|
|
+ except Exception as e:
|
|
|
+ print(f"Failed to download cover for {book_id}: {e}")
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
@app.get("/api/sync")
|
|
|
async def sync_with_audiobookshelf(
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
@@ -270,6 +316,9 @@ async def sync_with_audiobookshelf(
|
|
|
result = await db.execute(select(Book).where(Book.id == book_id))
|
|
|
book = result.scalar_one_or_none()
|
|
|
|
|
|
+ # Download cover image and get local path
|
|
|
+ local_cover_url = await download_cover_image(abs_client, book_id)
|
|
|
+
|
|
|
# Create or update book
|
|
|
if not book:
|
|
|
book = Book(
|
|
|
@@ -281,14 +330,15 @@ async def sync_with_audiobookshelf(
|
|
|
genres=json.dumps(metadata.get("genres", [])),
|
|
|
tags=json.dumps(media.get("tags", [])),
|
|
|
duration=media.get("duration", 0),
|
|
|
- cover_url=media.get("coverPath") # Store relative path
|
|
|
+ cover_url=local_cover_url # Store local path
|
|
|
)
|
|
|
db.add(book)
|
|
|
else:
|
|
|
# Update existing book
|
|
|
book.title = metadata.get("title", book.title)
|
|
|
book.author = metadata.get("authorName", book.author)
|
|
|
- book.cover_url = media.get("coverPath") # Store relative path
|
|
|
+ if local_cover_url: # Only update if download succeeded
|
|
|
+ book.cover_url = local_cover_url
|
|
|
book.updated_at = datetime.now()
|
|
|
|
|
|
# Update or create listening session
|
|
|
@@ -732,39 +782,3 @@ async def delete_user(
|
|
|
async def health_check():
|
|
|
"""Health check endpoint."""
|
|
|
return {"status": "healthy"}
|
|
|
-
|
|
|
-
|
|
|
-@app.get("/api/cover/{book_id}")
|
|
|
-async def get_book_cover(
|
|
|
- book_id: str,
|
|
|
- db: AsyncSession = Depends(get_db),
|
|
|
- user: User = Depends(get_current_user)
|
|
|
-):
|
|
|
- """Proxy book cover images from Audiobookshelf with authentication."""
|
|
|
- import httpx
|
|
|
-
|
|
|
- # Get book from database
|
|
|
- result = await db.execute(select(Book).where(Book.id == book_id))
|
|
|
- book = result.scalar_one_or_none()
|
|
|
-
|
|
|
- if not book or not book.cover_url:
|
|
|
- raise HTTPException(status_code=404, detail="Cover not found")
|
|
|
-
|
|
|
- # Get user's ABS client
|
|
|
- abs_client = get_abs_client(user)
|
|
|
-
|
|
|
- # Fetch cover from Audiobookshelf with auth
|
|
|
- cover_url = f"{abs_client.base_url}{book.cover_url}"
|
|
|
-
|
|
|
- async with httpx.AsyncClient() as client:
|
|
|
- try:
|
|
|
- response = await client.get(cover_url, headers=abs_client.headers)
|
|
|
- response.raise_for_status()
|
|
|
-
|
|
|
- # Return image with appropriate content type
|
|
|
- return Response(
|
|
|
- content=response.content,
|
|
|
- media_type=response.headers.get("content-type", "image/jpeg")
|
|
|
- )
|
|
|
- except httpx.HTTPError:
|
|
|
- raise HTTPException(status_code=404, detail="Cover image not found")
|