import os import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from ..dependencies import get_db, get_current_user 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.hls import create_hls_session, cleanup_hls_session, get_hls_session_path from ..config import get_settings router = APIRouter(tags=["stream"]) @router.post("/api/items/{item_id}/play") async def start_playback( item_id: str, body: dict | None = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): body = body or {} 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) ) files = files_result.scalars().all() if not files: raise HTTPException(status_code=400, detail="No audio files for this item") chapters_result = await db.execute( select(Chapter).where(Chapter.library_item_id == item_id).order_by(Chapter.chapter_index) ) chapters = chapters_result.scalars().all() # Fortschritt ermitteln progress_result = await db.execute( select(MediaProgress).where( MediaProgress.user_id == current_user.id, MediaProgress.library_item_id == item_id, ) ) progress = progress_result.scalar_one_or_none() current_time = progress.current_time if progress else 0.0 if body.get("startTime") is not None: current_time = float(body["startTime"]) session_id = str(uuid.uuid4()) # HLS-Session asynchron starten audio_paths = [f.path for f in files] hls_dir = await create_hls_session(session_id, audio_paths, start_time=0.0) session = PlaybackSession( id=session_id, user_id=current_user.id, library_item_id=item_id, media_type=item.media_type, current_time=current_time, duration=item.duration_seconds or 0.0, device_id=body.get("deviceId", ""), device_info=body.get("deviceInfo", {}), media_player=body.get("mediaPlayer", ""), hls_session_path=hls_dir, is_active=True, ) db.add(session) await db.commit() settings = get_settings() # URL-Basis relativ — wird durch nginx weitergeleitet hls_url = f"/hls/{session_id}/output.m3u8" audio_tracks = [ { "index": 0, "startOffset": 0.0, "duration": item.duration_seconds or 0.0, "title": "Part 1", "contentUrl": hls_url, "mimeType": "application/x-mpegURL", "metadata": { "filename": "output.m3u8", "ext": ".m3u8", "path": hls_url, "relPath": "output.m3u8", "size": 0, }, } ] chapters_out = [ { "id": c.chapter_index, "start": c.start_seconds, "end": c.end_seconds, "title": c.title, } for c in chapters ] return { "id": session_id, "userId": current_user.id, "libraryId": item.library_id, "libraryItemId": item_id, "episodeId": None, "mediaType": item.media_type, "chapters": chapters_out, "displayTitle": item.title, "displayAuthor": item.author, "coverPath": f"/api/items/{item_id}/cover" if item.cover_path else None, "duration": item.duration_seconds or 0.0, "playMethod": 0, # 0 = HLS Transcode "mediaPlayer": body.get("mediaPlayer", ""), "deviceInfo": body.get("deviceInfo", {}), "serverVersion": "2.4.0", "date": datetime.utcnow().strftime("%Y-%m-%d"), "dayOfWeek": datetime.utcnow().strftime("%A"), "timeListening": 0, "startTime": current_time, "currentTime": current_time, "startedAt": int(datetime.utcnow().timestamp() * 1000), "updatedAt": int(datetime.utcnow().timestamp() * 1000), "audioTracks": audio_tracks, "videoTrack": None, } @router.post("/api/playback-session/{session_id}/sync") async def sync_session( session_id: str, body: dict = {}, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(PlaybackSession).where( PlaybackSession.id == session_id, PlaybackSession.user_id == current_user.id, ) ) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") current_time = float(body.get("currentTime", session.current_time)) duration = float(body.get("duration", session.duration)) time_listening = float(body.get("timeListening", 0)) session.current_time = current_time session.duration = duration session.updated_at = datetime.utcnow() # Fortschritt persistieren progress_result = await db.execute( select(MediaProgress).where( MediaProgress.user_id == current_user.id, MediaProgress.library_item_id == session.library_item_id, MediaProgress.episode_id == session.episode_id, ) ) progress = progress_result.scalar_one_or_none() if not progress: progress = MediaProgress( user_id=current_user.id, library_item_id=session.library_item_id, episode_id=session.episode_id, duration=duration, started_at=datetime.utcnow(), ) db.add(progress) progress.current_time = current_time progress.duration = duration progress.last_update = datetime.utcnow() is_finished = duration > 0 and (current_time / duration) >= 0.99 if is_finished and not progress.is_finished: progress.is_finished = True progress.finished_at = datetime.utcnow() elif not is_finished: progress.is_finished = False await db.commit() return {"id": session_id, "currentTime": current_time} @router.delete("/api/playback-session/{session_id}") async def close_session( session_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(PlaybackSession).where( PlaybackSession.id == session_id, PlaybackSession.user_id == current_user.id, ) ) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") session.is_active = False await db.commit() # HLS-Temp-Dateien bereinigen cleanup_hls_session(session_id) return {"success": True} @router.get("/hls/{session_id}/{filename}") async def serve_hls( session_id: str, filename: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): # Session prüfen result = await db.execute( select(PlaybackSession).where( PlaybackSession.id == session_id, PlaybackSession.user_id == current_user.id, ) ) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") settings = get_settings() file_path = os.path.join(settings.hls_cache_dir, session_id, filename) if not os.path.exists(file_path): raise HTTPException(status_code=404, detail="Segment not found") if filename.endswith(".m3u8"): return FileResponse(file_path, media_type="application/x-mpegURL") elif filename.endswith(".ts"): return FileResponse(file_path, media_type="video/MP2T") else: return FileResponse(file_path)