Improve mini-player and add periodic sync
- Redesign mini-player: progress bar on top, centered controls - Add vertical volume slider with popup on hover - Add volume percentage display - Add custom speaker SVG icons - Add periodic sync every 10 seconds for playback synchronization - Broadcast user_joined when connecting via WebSocket - Disable nginx proxy buffering for streaming - Allow extra env variables in pydantic settings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ class Settings(BaseSettings):
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
@lru_cache()
|
||||
|
||||
@@ -1,8 +1,69 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI
|
||||
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
|
||||
|
||||
app = FastAPI(title="EnigFM", description="Listen to music together with friends")
|
||||
|
||||
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 sync task
|
||||
sync_task = asyncio.create_task(periodic_sync())
|
||||
yield
|
||||
# Cleanup
|
||||
sync_task.cancel()
|
||||
try:
|
||||
await sync_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
app = FastAPI(title="EnigFM", description="Listen to music together with friends", lifespan=lifespan)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
|
||||
@@ -203,6 +203,8 @@ async def stream_track(track_id: uuid.UUID, request: Request, db: AsyncSession =
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Length": str(content_length),
|
||||
"Content-Disposition": f"inline; filename*=UTF-8''{encoded_filename}",
|
||||
"X-Accel-Buffering": "no",
|
||||
"Cache-Control": "no-cache",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -214,5 +216,7 @@ async def stream_track(track_id: uuid.UUID, request: Request, db: AsyncSession =
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Length": str(file_size),
|
||||
"Content-Disposition": f"inline; filename*=UTF-8''{encoded_filename}",
|
||||
"X-Accel-Buffering": "no",
|
||||
"Cache-Control": "no-cache",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -46,6 +46,13 @@ async def room_websocket(websocket: WebSocket, room_id: UUID):
|
||||
|
||||
await manager.connect(websocket, room_id, user.id)
|
||||
|
||||
# Notify others that user joined
|
||||
await manager.broadcast_to_room(
|
||||
room_id,
|
||||
{"type": "user_joined", "user": {"id": str(user.id), "username": user.username}},
|
||||
exclude_user=user.id
|
||||
)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
@@ -110,5 +110,11 @@ def stream_file_chunks(s3_key: str, start: int = 0, end: int = None, chunk_size:
|
||||
Range=range_header
|
||||
)
|
||||
|
||||
for chunk in response["Body"].iter_chunks(chunk_size=chunk_size):
|
||||
# Use raw stream read instead of iter_chunks for true streaming
|
||||
body = response["Body"]
|
||||
while True:
|
||||
chunk = body.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
body.close()
|
||||
|
||||
Reference in New Issue
Block a user