- Add auth middleware (API key) to protect API routes (#5, #7) - Add WebSocket handlers and cleanup on disconnect (#3, #8) - Add web Dockerfile (#1) - Fix memory upload with streaming chunks (#10) - Move lofi seed to startup, remove per-request seeding (#9) - Document ffmpeg dependency in README and .env.example (#6) - Fill in mobile app with API-connected UI (#4) - Set GENIUS_API_KEY from env var with documentation (#2)
164 lines
5.8 KiB
Python
164 lines
5.8 KiB
Python
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 ..models.shareplay import SharePlayRoom, SharePlayCue as SharePlayCueModel
|
|
from ..schemas.shareplay import SharePlayRoomResponse, SharePlayCommand, 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):
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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]
|