from fastapi import APIRouter, Depends, HTTPException from starlette.websockets import WebSocket, WebSocketDisconnect from sqlalchemy.orm import Session from typing import Optional from pydantic import BaseModel from ..db.database import get_db from ..schemas.shareplay import SharePlayCueResponse from ..services.shareplay import SharePlayManager class RoomIdRequest(BaseModel): room_id: str class ControlRequest(BaseModel): room_id: str type: str payload: Optional[dict] = None router = APIRouter(prefix="/api/shareplay", tags=["shareplay"]) manager = SharePlayManager() # Track active WebSocket connections per room active_connections: dict[str, list[WebSocket]] = {} @router.post("/create") def create_room(db: Session = Depends(get_db)): return manager.create_room(db, creator="user") @router.post("/join") def join_room(data: RoomIdRequest, db: Session = Depends(get_db)): result = manager.join_room(db, data.room_id) if not result: raise HTTPException(status_code=404, detail="Room not found") return result @router.post("/leave") def leave_room(data: RoomIdRequest, db: Session = Depends(get_db)): success = manager.leave_room(db, data.room_id) if not success: raise HTTPException(status_code=404, detail="Room not found") return {"message": "Left room"} @router.get("/cue") def get_cue(room_id: str, db: Session = Depends(get_db)): items = manager.get_cue(db, room_id) return SharePlayCueResponse(items=items, next_song=None) @router.post("/cue") def add_to_cue(room_id: str, song_id: str, db: Session = Depends(get_db)): success = manager.add_to_cue(db, room_id, song_id) if not success: raise HTTPException(status_code=400, detail="Failed to add to cue") return {"message": "Added to cue"} @router.post("/control") def send_control(data: ControlRequest, db: Session = Depends(get_db)): state = manager.get_state(data.room_id) if not state: raise HTTPException(status_code=404, detail="Room not found") if data.type == "play": manager.update_state(data.room_id, is_playing=True) elif data.type == "pause": manager.update_state(data.room_id, is_playing=False) elif data.type == "seek" and data.payload: manager.update_state(data.room_id, position=data.payload.get("position", 0)) elif data.type == "shuffle": current = manager.get_state(data.room_id) manager.update_state(data.room_id, shuffle=not current.get("shuffle", False)) return {"message": f"Command '{data.type}' sent"} # WebSocket endpoint @router.websocket("/ws/{room_id}") async def websocket_endpoint(websocket: WebSocket, room_id: str): # pragma: no cover await websocket.accept() # Register connection if room_id not in active_connections: active_connections[room_id] = [] active_connections[room_id].append(websocket) # Send initial state state = manager.get_state(room_id) if state: await websocket.send_json({"type": "state", "data": state}) try: while True: data = await websocket.receive_text() import json message = json.loads(data) if message.get("type") == "control": cmd = message.get("payload", {}) state = manager.get_state(room_id) if state and cmd.get("type") == "play": manager.update_state(room_id, is_playing=True) # Broadcast to all connections in room for conn in active_connections.get(room_id, []): try: await conn.send_json({ "type": "playback_update", "data": {"is_playing": True} }) except Exception: pass elif state and cmd.get("type") == "pause": manager.update_state(room_id, is_playing=False) for conn in active_connections.get(room_id, []): try: await conn.send_json({ "type": "playback_update", "data": {"is_playing": False} }) except Exception: pass await websocket.send_json({"type": "ack", "data": message}) elif message.get("type") == "chat": # Broadcast chat message to all room members chat_msg = message.get("payload", {}) for conn in active_connections.get(room_id, []): try: await conn.send_json({ "type": "chat", "data": chat_msg }) except Exception: pass elif message.get("type") == "seek": pos = message.get("payload", {}).get("position", 0) manager.update_state(room_id, position=pos) for conn in active_connections.get(room_id, []): try: await conn.send_json({ "type": "seek_update", "data": {"position": pos} }) except Exception: pass except WebSocketDisconnect: # Clean up disconnected client if room_id in active_connections: try: active_connections[room_id].remove(websocket) except ValueError: pass # Remove room entry if no connections left if not active_connections[room_id]: del active_connections[room_id]