Add track filtering, WS keepalive, and improve error handling

- Add track filtering by uploader (my tracks / all tracks) with UI tabs
- Add WebSocket ping/pong keepalive (30s interval) to prevent disconnects
- Add auto-reconnect on WebSocket close (3s delay)
- Add request logging middleware with DATABASE_URL output on startup
- Handle missing S3 files gracefully (return 404 instead of 500)
- Add debug logging for audio ended event

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-12 18:10:25 +03:00
parent 3dd10d6dab
commit fdc854256c
7 changed files with 144 additions and 11 deletions

View File

@@ -1,13 +1,23 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI
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 .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():
@@ -65,6 +75,15 @@ async def lifespan(app: FastAPI):
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,

View File

@@ -19,8 +19,16 @@ router = APIRouter(prefix="/api/tracks", tags=["tracks"])
@router.get("", response_model=list[TrackResponse])
async def get_tracks(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Track).order_by(Track.created_at.desc()))
async def get_tracks(
my: bool = False,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
query = select(Track)
if my:
query = query.where(Track.uploaded_by == current_user.id)
query = query.order_by(Track.created_at.desc())
result = await db.execute(query)
return result.scalars().all()
@@ -174,6 +182,8 @@ async def stream_track(track_id: uuid.UUID, request: Request, db: AsyncSession =
# Get file size from S3 (without downloading)
file_size = get_file_size(track.s3_key)
if file_size is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Track file not found in storage")
# Encode filename for non-ASCII characters
encoded_filename = quote(f"{track.title}.mp3")

View File

@@ -58,6 +58,11 @@ async def room_websocket(websocket: WebSocket, room_id: UUID):
data = await websocket.receive_text()
message = json.loads(data)
# Handle ping/pong for keepalive
if message.get("type") == "ping":
await websocket.send_json({"type": "pong"})
continue
async with async_session() as db:
if message["type"] == "player_action":
await handle_player_action(db, room_id, user, message)

View File

@@ -77,11 +77,16 @@ def get_file_content(s3_key: str) -> bytes:
return response["Body"].read()
def get_file_size(s3_key: str) -> int:
"""Get file size from S3 without downloading"""
def get_file_size(s3_key: str) -> int | None:
"""Get file size from S3 without downloading. Returns None if file not found."""
client = get_s3_client()
response = client.head_object(Bucket=settings.s3_bucket_name, Key=s3_key)
return response["ContentLength"]
try:
response = client.head_object(Bucket=settings.s3_bucket_name, Key=s3_key)
return response["ContentLength"]
except client.exceptions.ClientError as e:
if e.response['Error']['Code'] == '404':
return None
raise
def get_file_range(s3_key: str, start: int, end: int):