Files
Audiolib/backend/app/routers/libraries.py
Audiolib 14ffee3051 Initial commit: Phase 1 – Projektstruktur, DB-Schema, Core-API
- FastAPI-Backend mit vollständiger ABS v2.x API-Kompatibilität
- SQLAlchemy-Models: User, Library, LibraryItem, BookFile, Chapter,
  Podcast, PodcastEpisode, MediaProgress, Bookmark, PlaybackSession
- Auth: JWT-Login (/login, /logout, /api/authorize)
- Library + Items Endpoints inkl. camelCase ABS-Response-Format
- HLS-Streaming via FFmpeg (POST /api/items/:id/play, Session-Sync)
- Me/Progress Endpoints + Lesezeichen
- User-Management + Server-Settings (Admin)
- Library-Scanner (MP3/WAV Discovery, Hintergrund-Task)
- File Watcher (watchdog, 30s Debounce)
- Matching-Skelett (MusicBrainz, OpenLibrary, Google Books – Phase 5)
- Docker-Setup: backend (Python 3.12+FFmpeg), frontend (React/Vite),
  nginx Reverse-Proxy auf Port 3000
- setup.sh: Installiert Docker auf Debian/Ubuntu, richtet .env ein

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:43:35 +02:00

219 lines
6.7 KiB
Python

import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from ..dependencies import get_db, get_current_user, require_admin
from ..models.user import User
from ..models.library import Library
from ..models.media_item import LibraryItem
from ..models.session import ScanJob
from ..schemas.library import LibraryOut, LibraryFolder, LibrarySettings, LibraryCreate, LibraryUpdate, LibraryItemsResponse
from ..config import get_settings
router = APIRouter(prefix="/api/libraries", tags=["libraries"])
def _library_to_out(lib: Library) -> dict:
settings_data = lib.settings or {}
folders = [
LibraryFolder(
id=f.get("id", str(uuid.uuid4())),
full_path=f.get("fullPath", f.get("full_path", "")),
library_id=lib.id,
added_at=int(lib.created_at.timestamp() * 1000) if lib.created_at else 0,
)
for f in (lib.folders or [])
]
out = LibraryOut(
id=lib.id,
name=lib.name,
folders=folders,
media_type=lib.media_type,
icon=settings_data.get("icon", "database"),
provider=settings_data.get("provider", "google"),
created_at=int(lib.created_at.timestamp() * 1000) if lib.created_at else 0,
last_update=int(lib.updated_at.timestamp() * 1000) if lib.updated_at else 0,
)
return out.model_dump(by_alias=True)
@router.get("")
async def list_libraries(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Library))
libraries = result.scalars().all()
return {"libraries": [_library_to_out(lib) for lib in libraries]}
@router.get("/{library_id}")
async def get_library(
library_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalar_one_or_none()
if not lib:
raise HTTPException(status_code=404, detail="Library not found")
return _library_to_out(lib)
@router.get("/{library_id}/items")
async def get_library_items(
library_id: str,
sort: str = "addedAt",
desc: int = 0,
filter: str | None = None,
search: str | None = None,
page: int = 0,
limit: int = 0,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalar_one_or_none()
if not lib:
raise HTTPException(status_code=404, detail="Library not found")
query = select(LibraryItem).where(LibraryItem.library_id == library_id)
if search:
query = query.where(
LibraryItem.title.ilike(f"%{search}%") |
LibraryItem.author.ilike(f"%{search}%") |
LibraryItem.series.ilike(f"%{search}%")
)
count_result = await db.execute(select(func.count()).select_from(query.subquery()))
total = count_result.scalar()
actual_limit = limit if limit > 0 else 50
query = query.offset(page * actual_limit).limit(actual_limit)
items_result = await db.execute(query)
items = items_result.scalars().all()
from ..routers.items import _item_to_out
return {
"results": [_item_to_out(item) for item in items],
"total": total,
"limit": actual_limit,
"page": page,
}
@router.get("/{library_id}/search")
async def search_library(
library_id: str,
q: str = "",
limit: int = 12,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
query = select(LibraryItem).where(
LibraryItem.library_id == library_id,
LibraryItem.title.ilike(f"%{q}%") |
LibraryItem.author.ilike(f"%{q}%") |
LibraryItem.series.ilike(f"%{q}%")
).limit(limit)
result = await db.execute(query)
items = result.scalars().all()
from ..routers.items import _item_to_out
return {"book": [_item_to_out(item) for item in items]}
@router.post("")
async def create_library(
body: LibraryCreate,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
lib_id = str(uuid.uuid4())
folders = [
{"id": str(uuid.uuid4()), "fullPath": f.get("fullPath", f.get("full_path", ""))}
for f in body.folders
]
lib = Library(
id=lib_id,
name=body.name,
display_name=body.name,
folders=folders,
media_type=body.media_type,
settings={"icon": body.icon, "provider": body.provider},
)
db.add(lib)
await db.commit()
await db.refresh(lib)
return _library_to_out(lib)
@router.patch("/{library_id}")
async def update_library(
library_id: str,
body: LibraryUpdate,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalar_one_or_none()
if not lib:
raise HTTPException(status_code=404, detail="Library not found")
if body.name is not None:
lib.name = body.name
lib.display_name = body.name
if body.folders is not None:
lib.folders = [
{"id": f.get("id", str(uuid.uuid4())), "fullPath": f.get("fullPath", f.get("full_path", ""))}
for f in body.folders
]
if body.settings is not None:
lib.settings = {**(lib.settings or {}), **body.settings}
await db.commit()
await db.refresh(lib)
return _library_to_out(lib)
@router.delete("/{library_id}")
async def delete_library(
library_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalar_one_or_none()
if not lib:
raise HTTPException(status_code=404, detail="Library not found")
await db.delete(lib)
await db.commit()
return {"success": True}
@router.post("/{library_id}/scan")
async def scan_library(
library_id: str,
background_tasks: BackgroundTasks,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalar_one_or_none()
if not lib:
raise HTTPException(status_code=404, detail="Library not found")
job = ScanJob(library_id=library_id, status="queued")
db.add(job)
await db.commit()
await db.refresh(job)
from ..services.scanner import scan_library_task
background_tasks.add_task(scan_library_task, library_id, job.id)
return {"id": job.id, "type": "scan", "libraryId": library_id}