Add global mini-player and improve configuration
- Add global activeRoom store for persistent WebSocket connection - Add MiniPlayer component for playback controls across pages - Add chunked S3 streaming with 64KB chunks and Range support - Add queue item removal button - Move DB credentials to environment variables - Update .env.example with DB configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
from urllib.parse import quote
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Request, Response
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
@@ -11,7 +11,7 @@ from ..models.user import User
|
||||
from ..models.track import Track
|
||||
from ..schemas.track import TrackResponse, TrackWithUrl
|
||||
from ..services.auth import get_current_user
|
||||
from ..services.s3 import upload_file, delete_file, generate_presigned_url, can_upload_file, get_file_content
|
||||
from ..services.s3 import upload_file, delete_file, generate_presigned_url, can_upload_file, get_file_size, stream_file_chunks
|
||||
from ..config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
@@ -172,9 +172,11 @@ async def stream_track(track_id: uuid.UUID, request: Request, db: AsyncSession =
|
||||
if not track:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Track not found")
|
||||
|
||||
# Get full file content
|
||||
content = get_file_content(track.s3_key)
|
||||
file_size = len(content)
|
||||
# Get file size from S3 (without downloading)
|
||||
file_size = get_file_size(track.s3_key)
|
||||
|
||||
# Encode filename for non-ASCII characters
|
||||
encoded_filename = quote(f"{track.title}.mp3")
|
||||
|
||||
# Parse Range header
|
||||
range_header = request.headers.get("range")
|
||||
@@ -192,11 +194,8 @@ async def stream_track(track_id: uuid.UUID, request: Request, db: AsyncSession =
|
||||
end = min(end, file_size - 1)
|
||||
content_length = end - start + 1
|
||||
|
||||
# Encode filename for non-ASCII characters
|
||||
encoded_filename = quote(f"{track.title}.mp3")
|
||||
|
||||
return Response(
|
||||
content=content[start:end + 1],
|
||||
return StreamingResponse(
|
||||
stream_file_chunks(track.s3_key, start, end),
|
||||
status_code=206,
|
||||
media_type="audio/mpeg",
|
||||
headers={
|
||||
@@ -207,12 +206,9 @@ async def stream_track(track_id: uuid.UUID, request: Request, db: AsyncSession =
|
||||
}
|
||||
)
|
||||
|
||||
# Encode filename for non-ASCII characters
|
||||
encoded_filename = quote(f"{track.title}.mp3")
|
||||
|
||||
# No range - return full file
|
||||
return Response(
|
||||
content=content,
|
||||
# No range - stream full file
|
||||
return StreamingResponse(
|
||||
stream_file_chunks(track.s3_key),
|
||||
media_type="audio/mpeg",
|
||||
headers={
|
||||
"Accept-Ranges": "bytes",
|
||||
|
||||
@@ -75,3 +75,40 @@ def get_file_content(s3_key: str) -> bytes:
|
||||
client = get_s3_client()
|
||||
response = client.get_object(Bucket=settings.s3_bucket_name, Key=s3_key)
|
||||
return response["Body"].read()
|
||||
|
||||
|
||||
def get_file_size(s3_key: str) -> int:
|
||||
"""Get file size from S3 without downloading"""
|
||||
client = get_s3_client()
|
||||
response = client.head_object(Bucket=settings.s3_bucket_name, Key=s3_key)
|
||||
return response["ContentLength"]
|
||||
|
||||
|
||||
def get_file_range(s3_key: str, start: int, end: int):
|
||||
"""Get a range of bytes from S3 file"""
|
||||
client = get_s3_client()
|
||||
response = client.get_object(
|
||||
Bucket=settings.s3_bucket_name,
|
||||
Key=s3_key,
|
||||
Range=f"bytes={start}-{end}"
|
||||
)
|
||||
return response["Body"].read()
|
||||
|
||||
|
||||
def stream_file_chunks(s3_key: str, start: int = 0, end: int = None, chunk_size: int = 64 * 1024):
|
||||
"""Stream file from S3 in chunks (default 64KB chunks)"""
|
||||
client = get_s3_client()
|
||||
|
||||
if end is None:
|
||||
range_header = f"bytes={start}-"
|
||||
else:
|
||||
range_header = f"bytes={start}-{end}"
|
||||
|
||||
response = client.get_object(
|
||||
Bucket=settings.s3_bucket_name,
|
||||
Key=s3_key,
|
||||
Range=range_header
|
||||
)
|
||||
|
||||
for chunk in response["Body"].iter_chunks(chunk_size=chunk_size):
|
||||
yield chunk
|
||||
|
||||
Reference in New Issue
Block a user