from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from sqlalchemy.orm import selectinload from ..database import get_db from ..models.user import User from ..models.room import Room, RoomParticipant from ..models.track import RoomQueue from ..schemas.room import RoomCreate, RoomResponse, RoomDetailResponse, QueueAdd, QueueAddMultiple, QueueItemResponse from ..schemas.track import TrackResponse from ..schemas.user import UserResponse from ..services.auth import get_current_user from ..services.sync import manager from ..config import get_settings settings = get_settings() router = APIRouter(prefix="/api/rooms", tags=["rooms"]) @router.get("", response_model=list[RoomResponse]) async def get_rooms(db: AsyncSession = Depends(get_db)): result = await db.execute( select(Room, func.count(RoomParticipant.user_id).label("participants_count")) .outerjoin(RoomParticipant) .group_by(Room.id) .order_by(Room.created_at.desc()) ) rooms = [] for room, count in result.all(): room_dict = { "id": room.id, "name": room.name, "owner_id": room.owner_id, "current_track_id": room.current_track_id, "playback_position": room.playback_position, "is_playing": room.is_playing, "created_at": room.created_at, "participants_count": count, } rooms.append(RoomResponse(**room_dict)) return rooms @router.post("", response_model=RoomResponse) async def create_room( room_data: RoomCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): room = Room(name=room_data.name, owner_id=current_user.id) db.add(room) await db.flush() return RoomResponse( id=room.id, name=room.name, owner_id=room.owner_id, current_track_id=room.current_track_id, playback_position=room.playback_position, is_playing=room.is_playing, created_at=room.created_at, participants_count=0, ) @router.get("/{room_id}", response_model=RoomDetailResponse) async def get_room(room_id: UUID, db: AsyncSession = Depends(get_db)): result = await db.execute( select(Room) .options( selectinload(Room.owner), selectinload(Room.current_track), selectinload(Room.participants).selectinload(RoomParticipant.user), ) .where(Room.id == room_id) ) room = result.scalar_one_or_none() if not room: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found") return RoomDetailResponse( id=room.id, name=room.name, owner=UserResponse.model_validate(room.owner), current_track=TrackResponse.model_validate(room.current_track) if room.current_track else None, playback_position=room.playback_position, is_playing=room.is_playing, created_at=room.created_at, participants=[UserResponse.model_validate(p.user) for p in room.participants], ) @router.delete("/{room_id}") async def delete_room( room_id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute(select(Room).where(Room.id == room_id)) room = result.scalar_one_or_none() if not room: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found") if room.owner_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not room owner") # Уведомляем всех подключенных пользователей о удалении комнаты await manager.broadcast_to_room( room_id, {"type": "room_deleted", "message": "Room has been deleted by owner"}, ) # Отключаем всех пользователей от WebSocket if room_id in manager.active_connections: connections = list(manager.active_connections[room_id]) for websocket, user_id in connections: try: await websocket.close(code=1000, reason="Room deleted") except Exception: pass manager.disconnect(websocket, room_id, user_id) # Удаляем комнату (cascade delete удалит participants, queue, messages) await db.delete(room) return {"status": "deleted"} @router.post("/{room_id}/join") async def join_room( room_id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute(select(Room).where(Room.id == room_id)) room = result.scalar_one_or_none() if not room: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found") # Check participant limit result = await db.execute( select(func.count(RoomParticipant.user_id)).where(RoomParticipant.room_id == room_id) ) count = result.scalar() if count >= settings.max_room_participants: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Room is full") # Check if already joined result = await db.execute( select(RoomParticipant).where( RoomParticipant.room_id == room_id, RoomParticipant.user_id == current_user.id, ) ) if result.scalar_one_or_none(): return {"status": "already joined"} participant = RoomParticipant(room_id=room_id, user_id=current_user.id) db.add(participant) # Notify others await manager.broadcast_to_room( room_id, {"type": "user_joined", "user": {"id": str(current_user.id), "username": current_user.username}}, ) return {"status": "joined"} @router.post("/{room_id}/leave") async def leave_room( room_id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(RoomParticipant).where( RoomParticipant.room_id == room_id, RoomParticipant.user_id == current_user.id, ) ) participant = result.scalar_one_or_none() if participant: await db.delete(participant) # Notify others await manager.broadcast_to_room( room_id, {"type": "user_left", "user_id": str(current_user.id)}, ) return {"status": "left"} @router.get("/{room_id}/queue", response_model=list[QueueItemResponse]) async def get_queue(room_id: UUID, db: AsyncSession = Depends(get_db)): result = await db.execute( select(RoomQueue) .options(selectinload(RoomQueue.track)) .where(RoomQueue.room_id == room_id) .order_by(RoomQueue.position) ) queue_items = result.scalars().all() return [ QueueItemResponse( track=TrackResponse.model_validate(item.track), added_by=item.added_by ) for item in queue_items ] @router.post("/{room_id}/queue") async def add_to_queue( room_id: UUID, data: QueueAdd, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): # Check if track already in queue result = await db.execute( select(RoomQueue).where( RoomQueue.room_id == room_id, RoomQueue.track_id == data.track_id, ) ) existing_item = result.scalar_one_or_none() if existing_item: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Track already in queue", ) # Get max position result = await db.execute( select(func.max(RoomQueue.position)).where(RoomQueue.room_id == room_id) ) max_pos = result.scalar() or 0 queue_item = RoomQueue( room_id=room_id, track_id=data.track_id, position=max_pos + 1, added_by=current_user.id, ) db.add(queue_item) await db.flush() # Notify others await manager.broadcast_to_room( room_id, {"type": "queue_updated"}, ) return {"status": "added"} @router.post("/{room_id}/queue/bulk") async def add_multiple_to_queue( room_id: UUID, data: QueueAddMultiple, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Add multiple tracks to queue at once, skipping duplicates.""" if not data.track_ids: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No tracks provided", ) # Get existing tracks in queue result = await db.execute( select(RoomQueue.track_id).where(RoomQueue.room_id == room_id) ) existing_track_ids = set(result.scalars().all()) # Filter out duplicates new_track_ids = [tid for tid in data.track_ids if tid not in existing_track_ids] if not new_track_ids: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="All tracks already in queue", ) # Get max position result = await db.execute( select(func.max(RoomQueue.position)).where(RoomQueue.room_id == room_id) ) max_pos = result.scalar() or 0 # Add all new tracks added_count = 0 for i, track_id in enumerate(new_track_ids): queue_item = RoomQueue( room_id=room_id, track_id=track_id, position=max_pos + i + 1, added_by=current_user.id, ) db.add(queue_item) added_count += 1 await db.flush() # Notify others await manager.broadcast_to_room( room_id, {"type": "queue_updated"}, ) skipped_count = len(data.track_ids) - added_count return { "status": "added", "added": added_count, "skipped": skipped_count, } @router.delete("/{room_id}/queue/{track_id}") async def remove_from_queue( room_id: UUID, track_id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(RoomQueue).where( RoomQueue.room_id == room_id, RoomQueue.track_id == track_id, ) ) queue_item = result.scalar_one_or_none() if not queue_item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Track not in queue" ) # Check if user added this track to queue if queue_item.added_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Can only remove tracks you added" ) await db.delete(queue_item) # Notify others await manager.broadcast_to_room( room_id, {"type": "queue_updated"}, ) return {"status": "removed"}