Files
enigFM/backend/app/main.py
mamonov.ep 0fb16f791d Add user ping system and room deletion functionality
Backend changes:
- Fix track deletion foreign key constraint (tracks.py)
  * Clear current_track_id from rooms before deleting track
  * Prevent deletion errors when track is currently playing

- Implement user ping/keepalive system (sync.py, websocket.py, ping_task.py, main.py)
  * Track last pong timestamp for each user
  * Background task sends ping every 30s, disconnects users after 60s timeout
  * Auto-pause playback when room becomes empty
  * Remove disconnected users from room_participants

- Enhance room deletion (rooms.py)
  * Broadcast room_deleted event to all connected users
  * Close all WebSocket connections before deletion
  * Cascade delete participants, queue, and messages

Frontend changes:
- Add ping/pong WebSocket handling (activeRoom.js)
  * Auto-respond to server pings
  * Handle room_deleted event with redirect to home

- Add room deletion UI (RoomView.vue, HomeView.vue, RoomCard.vue)
  * Delete button visible only to room owner
  * Confirmation dialog with warning
  * Delete button on room cards (shows on hover)
  * Redirect to home page after deletion

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

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

120 lines
3.6 KiB
Python

import asyncio
import logging
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import select
from .routers import auth, rooms, tracks, websocket, messages
from .database import async_session
from .models.room import Room
from .services.sync import manager
from .services.ping_task import ping_users_task
from .config import get_settings
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Log config on startup
settings = get_settings()
logger.info(f"DATABASE_URL: {settings.database_url}")
async def periodic_sync():
"""Send sync updates to all rooms every 10 seconds"""
while True:
await asyncio.sleep(10)
# Get all active rooms
for room_id in list(manager.active_connections.keys()):
try:
async with async_session() as db:
result = await db.execute(select(Room).where(Room.id == room_id))
room = result.scalar_one_or_none()
if not room or not room.is_playing:
continue
# Calculate current position
current_position = room.playback_position or 0
if room.playback_started_at:
elapsed = (datetime.utcnow() - room.playback_started_at).total_seconds() * 1000
current_position = int((room.playback_position or 0) + elapsed)
track_url = None
if room.current_track_id:
track_url = f"/api/tracks/{room.current_track_id}/stream"
await manager.broadcast_to_room(
room_id,
{
"type": "sync_state",
"is_playing": room.is_playing,
"position": current_position,
"current_track_id": str(room.current_track_id) if room.current_track_id else None,
"track_url": track_url,
"server_time": datetime.utcnow().isoformat(),
},
)
except Exception:
pass
@asynccontextmanager
async def lifespan(app: FastAPI):
# Start background tasks
sync_task = asyncio.create_task(periodic_sync())
ping_task = asyncio.create_task(ping_users_task())
logger.info("Background tasks started: periodic_sync, ping_users_task")
yield
# Cleanup
sync_task.cancel()
ping_task.cancel()
try:
await sync_task
except asyncio.CancelledError:
pass
try:
await ping_task
except asyncio.CancelledError:
pass
app = FastAPI(title="EnigFM", description="Listen to music together with friends", lifespan=lifespan)
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"Request: {request.method} {request.url.path}")
response = await call_next(request)
logger.info(f"Response: {request.method} {request.url.path} - {response.status_code}")
return response
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Routers
app.include_router(auth.router)
app.include_router(rooms.router)
app.include_router(tracks.router)
app.include_router(messages.router)
app.include_router(websocket.router)
@app.get("/")
async def root():
return {"message": "EnigFM API", "version": "1.0.0"}
@app.get("/health")
async def health():
return {"status": "ok"}