"""Queue management endpoints.""" from __future__ import annotations import uuid from datetime import datetime from typing import List from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select, func, delete, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_user_id, get_db_session from app.models import Episode, Show, QueueItem, Progress, PlaybackEvent router = APIRouter(prefix="/api/queue", tags=["queue"]) # ── Schemas ── class QueueAddRequest(BaseModel): episode_id: str class QueueReorderRequest(BaseModel): episode_ids: List[str] # ── Endpoints ── @router.get("") async def get_queue( user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Get user's queue ordered by sort_order, with episode and show info.""" stmt = ( select(QueueItem, Episode, Show) .join(Episode, Episode.id == QueueItem.episode_id) .join(Show, Show.id == Episode.show_id) .where(QueueItem.user_id == user_id) .order_by(QueueItem.sort_order) ) result = await db.execute(stmt) rows = result.all() return [ { "queue_id": str(qi.id), "sort_order": qi.sort_order, "added_at": qi.added_at.isoformat() if qi.added_at else None, "episode": { "id": str(ep.id), "title": ep.title, "audio_url": ep.audio_url, "duration_seconds": ep.duration_seconds, "artwork_url": ep.artwork_url or show.artwork_url, "published_at": ep.published_at.isoformat() if ep.published_at else None, }, "show": { "id": str(show.id), "title": show.title, "author": show.author, "artwork_url": show.artwork_url, }, } for qi, ep, show in rows ] @router.post("/add", status_code=201) async def add_to_queue( body: QueueAddRequest, user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Add episode to end of queue.""" eid = uuid.UUID(body.episode_id) ep = await db.get(Episode, eid) if not ep or ep.user_id != user_id: raise HTTPException(404, "Episode not found") # Check if already in queue existing = await db.execute( select(QueueItem).where(QueueItem.user_id == user_id, QueueItem.episode_id == eid) ) if existing.scalar_one_or_none(): raise HTTPException(409, "Episode already in queue") # Get max sort_order max_order = await db.scalar( select(func.coalesce(func.max(QueueItem.sort_order), -1)).where(QueueItem.user_id == user_id) ) qi = QueueItem( user_id=user_id, episode_id=eid, sort_order=max_order + 1, ) db.add(qi) await db.commit() return {"status": "added", "sort_order": qi.sort_order} @router.post("/play-next", status_code=201) async def play_next( body: QueueAddRequest, user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Insert episode at position 1 (after currently playing).""" eid = uuid.UUID(body.episode_id) ep = await db.get(Episode, eid) if not ep or ep.user_id != user_id: raise HTTPException(404, "Episode not found") # Remove if already in queue await db.execute( delete(QueueItem).where(QueueItem.user_id == user_id, QueueItem.episode_id == eid) ) # Shift all items at position >= 1 up by 1 stmt = ( select(QueueItem) .where(QueueItem.user_id == user_id, QueueItem.sort_order >= 1) .order_by(QueueItem.sort_order.desc()) ) items = (await db.execute(stmt)).scalars().all() for item in items: item.sort_order += 1 qi = QueueItem(user_id=user_id, episode_id=eid, sort_order=1) db.add(qi) await db.commit() return {"status": "added", "sort_order": 1} @router.post("/play-now", status_code=201) async def play_now( body: QueueAddRequest, user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Insert at position 0 and start playing.""" eid = uuid.UUID(body.episode_id) ep = await db.get(Episode, eid) if not ep or ep.user_id != user_id: raise HTTPException(404, "Episode not found") # Remove if already in queue await db.execute( delete(QueueItem).where(QueueItem.user_id == user_id, QueueItem.episode_id == eid) ) # Shift everything up stmt = ( select(QueueItem) .where(QueueItem.user_id == user_id) .order_by(QueueItem.sort_order.desc()) ) items = (await db.execute(stmt)).scalars().all() for item in items: item.sort_order += 1 qi = QueueItem(user_id=user_id, episode_id=eid, sort_order=0) db.add(qi) # Also create/update progress and log play event prog_stmt = select(Progress).where(Progress.user_id == user_id, Progress.episode_id == eid) prog = (await db.execute(prog_stmt)).scalar_one_or_none() if not prog: prog = Progress( user_id=user_id, episode_id=eid, duration_seconds=ep.duration_seconds, ) db.add(prog) prog.last_played_at = datetime.utcnow() prog.is_completed = False db.add(PlaybackEvent( user_id=user_id, episode_id=eid, event_type="play", position_seconds=prog.position_seconds or 0, playback_speed=prog.playback_speed, )) await db.commit() return {"status": "playing", "sort_order": 0} @router.delete("/{episode_id}", status_code=204) async def remove_from_queue( episode_id: str, user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Remove episode from queue.""" eid = uuid.UUID(episode_id) result = await db.execute( delete(QueueItem).where(QueueItem.user_id == user_id, QueueItem.episode_id == eid) ) if result.rowcount == 0: raise HTTPException(404, "Episode not in queue") await db.commit() @router.post("/reorder") async def reorder_queue( body: QueueReorderRequest, user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Reorder queue by providing episode IDs in desired order.""" for i, eid_str in enumerate(body.episode_ids): eid = uuid.UUID(eid_str) stmt = select(QueueItem).where(QueueItem.user_id == user_id, QueueItem.episode_id == eid) qi = (await db.execute(stmt)).scalar_one_or_none() if qi: qi.sort_order = i await db.commit() return {"status": "reordered", "count": len(body.episode_ids)} @router.delete("") async def clear_queue( user_id: str = Depends(get_user_id), db: AsyncSession = Depends(get_db_session), ): """Clear entire queue.""" await db.execute(delete(QueueItem).where(QueueItem.user_id == user_id)) await db.commit() return {"status": "cleared"}