Files
enigFM/backend/app/routers/rooms.py
mamonov.ep ee8d79d155 Redesign UI with Vuetify and improve configuration
Major changes:
- Full UI redesign with Vuetify 3 (dark theme, modern components)
- Sidebar navigation with gradient logo
- Redesigned player controls with Material Design icons
- New room cards, track lists, and filter UI with chips
- Modern auth pages with centered cards

Configuration improvements:
- Centralized all settings in root .env file
- Removed redundant backend/.env and frontend/.env files
- Increased file upload limit to 100MB (nginx + backend)
- Added build args for Vite environment variables
- Frontend now uses relative paths (better for domain deployment)

UI Components updated:
- App.vue: v-navigation-drawer with sidebar
- MiniPlayer: v-footer with modern controls
- Queue: v-list with styled items
- RoomView: improved filters with clickable chips
- All views: Vuetify cards, buttons, text fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 20:17:52 +03:00

345 lines
10 KiB
Python

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 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"}