Files
Audiolib/backend/app/routers/me.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

235 lines
7.6 KiB
Python

from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete
from ..dependencies import get_db, get_current_user
from ..models.user import User
from ..models.progress import MediaProgress, Bookmark
from ..models.media_item import LibraryItem
from ..schemas.user import UserOut, UserSettings
from ..routers.auth import _build_user_out
router = APIRouter(prefix="/api/me", tags=["me"])
def _progress_to_out(p: MediaProgress) -> dict:
return {
"id": p.id,
"libraryItemId": p.library_item_id,
"episodeId": p.episode_id,
"duration": p.duration,
"progress": round(p.current_time / p.duration, 4) if p.duration > 0 else 0.0,
"currentTime": p.current_time,
"isFinished": p.is_finished,
"hideFromContinueListening": p.hide_from_continue_listening,
"lastUpdate": int(p.last_update.timestamp() * 1000) if p.last_update else 0,
"startedAt": int(p.started_at.timestamp() * 1000) if p.started_at else 0,
"finishedAt": int(p.finished_at.timestamp() * 1000) if p.finished_at else None,
}
def _bookmark_to_out(b: Bookmark) -> dict:
return {
"libraryItemId": b.library_item_id,
"title": b.title,
"time": b.time_seconds,
"createdAt": int(b.created_at.timestamp() * 1000) if b.created_at else 0,
}
@router.get("")
async def get_me(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
progress_result = await db.execute(
select(MediaProgress).where(MediaProgress.user_id == current_user.id)
)
all_progress = progress_result.scalars().all()
bookmarks_result = await db.execute(
select(Bookmark).where(Bookmark.user_id == current_user.id)
)
all_bookmarks = bookmarks_result.scalars().all()
user_out = _build_user_out(current_user)
user_dict = user_out.model_dump(by_alias=True)
user_dict["mediaProgress"] = [_progress_to_out(p) for p in all_progress]
user_dict["bookmarks"] = [_bookmark_to_out(b) for b in all_bookmarks]
return user_dict
@router.get("/progress")
async def get_all_progress(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(MediaProgress).where(MediaProgress.user_id == current_user.id)
)
return [_progress_to_out(p) for p in result.scalars().all()]
@router.patch("/progress/{library_item_id}")
async def update_progress(
library_item_id: str,
body: dict,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
episode_id = body.get("episodeId")
query = select(MediaProgress).where(
MediaProgress.user_id == current_user.id,
MediaProgress.library_item_id == library_item_id,
)
if episode_id:
query = query.where(MediaProgress.episode_id == episode_id)
result = await db.execute(query)
progress = result.scalar_one_or_none()
if not progress:
progress = MediaProgress(
user_id=current_user.id,
library_item_id=library_item_id,
episode_id=episode_id,
)
db.add(progress)
if "currentTime" in body:
progress.current_time = float(body["currentTime"])
if "duration" in body:
progress.duration = float(body["duration"])
if "isFinished" in body:
progress.is_finished = bool(body["isFinished"])
if progress.is_finished and not progress.finished_at:
progress.finished_at = datetime.utcnow()
if "hideFromContinueListening" in body:
progress.hide_from_continue_listening = bool(body["hideFromContinueListening"])
if not progress.started_at:
progress.started_at = datetime.utcnow()
progress.last_update = datetime.utcnow()
await db.commit()
await db.refresh(progress)
return _progress_to_out(progress)
@router.post("/sync-local-progress")
async def sync_local_progress(
body: dict,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
local_progress_list = body.get("localMediaProgress", [])
updated = []
for lp in local_progress_list:
lib_item_id = lp.get("libraryItemId")
episode_id = lp.get("episodeId")
if not lib_item_id:
continue
query = select(MediaProgress).where(
MediaProgress.user_id == current_user.id,
MediaProgress.library_item_id == lib_item_id,
)
if episode_id:
query = query.where(MediaProgress.episode_id == episode_id)
result = await db.execute(query)
progress = result.scalar_one_or_none()
local_last_update = lp.get("lastUpdate", 0)
server_last_update = int(progress.last_update.timestamp() * 1000) if progress and progress.last_update else 0
# Nur updaten wenn lokaler Stand neuer ist
if not progress or local_last_update > server_last_update:
if not progress:
progress = MediaProgress(
user_id=current_user.id,
library_item_id=lib_item_id,
episode_id=episode_id,
)
db.add(progress)
progress.current_time = float(lp.get("currentTime", 0))
progress.duration = float(lp.get("duration", 0))
progress.is_finished = bool(lp.get("isFinished", False))
progress.last_update = datetime.utcnow()
updated.append(lib_item_id)
await db.commit()
return {"updated": updated}
@router.delete("/progress/{progress_id}")
async def delete_progress(
progress_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(MediaProgress).where(
MediaProgress.id == progress_id,
MediaProgress.user_id == current_user.id,
)
)
progress = result.scalar_one_or_none()
if not progress:
raise HTTPException(status_code=404, detail="Progress not found")
await db.delete(progress)
await db.commit()
return {"success": True}
@router.get("/bookmarks")
async def get_bookmarks(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Bookmark).where(Bookmark.user_id == current_user.id)
)
return [_bookmark_to_out(b) for b in result.scalars().all()]
@router.post("/bookmark/{library_item_id}")
async def create_bookmark(
library_item_id: str,
body: dict,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
bookmark = Bookmark(
user_id=current_user.id,
library_item_id=library_item_id,
time_seconds=float(body.get("time", 0)),
title=body.get("title", "Lesezeichen"),
)
db.add(bookmark)
await db.commit()
await db.refresh(bookmark)
return _bookmark_to_out(bookmark)
@router.delete("/bookmark/{library_item_id}/{time}")
async def delete_bookmark(
library_item_id: str,
time: float,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Bookmark).where(
Bookmark.user_id == current_user.id,
Bookmark.library_item_id == library_item_id,
Bookmark.time_seconds == time,
)
)
bookmark = result.scalar_one_or_none()
if not bookmark:
raise HTTPException(status_code=404, detail="Bookmark not found")
await db.delete(bookmark)
await db.commit()
return {"success": True}