- 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>
235 lines
7.6 KiB
Python
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}
|