diff --git a/backend/app/main.py b/backend/app/main.py index 54352c9..34c21b0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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, diff --git a/backend/app/routers/tracks.py b/backend/app/routers/tracks.py index 439f193..b872529 100644 --- a/backend/app/routers/tracks.py +++ b/backend/app/routers/tracks.py @@ -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") diff --git a/backend/app/routers/websocket.py b/backend/app/routers/websocket.py index 8ddb486..cc512db 100644 --- a/backend/app/routers/websocket.py +++ b/backend/app/routers/websocket.py @@ -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) diff --git a/backend/app/services/s3.py b/backend/app/services/s3.py index 044fa05..03f76f0 100644 --- a/backend/app/services/s3.py +++ b/backend/app/services/s3.py @@ -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): diff --git a/frontend/src/stores/activeRoom.js b/frontend/src/stores/activeRoom.js index 938c931..f30dc3c 100644 --- a/frontend/src/stores/activeRoom.js +++ b/frontend/src/stores/activeRoom.js @@ -22,6 +22,10 @@ export const useActiveRoomStore = defineStore('activeRoom', () => { let pendingPlay = false let pendingPosition = null + // WebSocket keepalive + let pingInterval = null + let reconnectTimeout = null + const isInRoom = computed(() => roomId.value !== null) function initAudio() { @@ -53,10 +57,15 @@ export const useActiveRoomStore = defineStore('activeRoom', () => { }) audio.addEventListener('ended', () => { + console.log('Track ended event fired') if (onTrackEndedCallback) { onTrackEndedCallback() } }) + + audio.addEventListener('error', (e) => { + console.error('Audio error:', e, audio.error) + }) } function connect(id, name) { @@ -74,10 +83,36 @@ export const useActiveRoomStore = defineStore('activeRoom', () => { ws.value.onopen = () => { connected.value = true send({ type: 'sync_request' }) + + // Start ping interval (every 30 seconds) + if (pingInterval) clearInterval(pingInterval) + pingInterval = setInterval(() => { + if (ws.value && ws.value.readyState === WebSocket.OPEN) { + send({ type: 'ping' }) + } + }, 30000) } ws.value.onclose = () => { connected.value = false + if (pingInterval) { + clearInterval(pingInterval) + pingInterval = null + } + + // Auto-reconnect after 3 seconds if we still have room info + if (roomId.value && !reconnectTimeout) { + reconnectTimeout = setTimeout(() => { + reconnectTimeout = null + if (roomId.value) { + console.log('Reconnecting WebSocket...') + const savedId = roomId.value + const savedName = roomName.value + ws.value = null + connect(savedId, savedName) + } + }, 3000) + } } ws.value.onerror = () => {} @@ -94,13 +129,26 @@ export const useActiveRoomStore = defineStore('activeRoom', () => { } function disconnect() { + // Clear timers + if (pingInterval) { + clearInterval(pingInterval) + pingInterval = null + } + if (reconnectTimeout) { + clearTimeout(reconnectTimeout) + reconnectTimeout = null + } + + // Clear room info first to prevent auto-reconnect + const wasInRoom = roomId.value + roomId.value = null + roomName.value = null + if (ws.value) { ws.value.close() ws.value = null } connected.value = false - roomId.value = null - roomName.value = null chatMessages.value = [] playerStore.reset() if (audio) { diff --git a/frontend/src/stores/tracks.js b/frontend/src/stores/tracks.js index 1303982..d3c5485 100644 --- a/frontend/src/stores/tracks.js +++ b/frontend/src/stores/tracks.js @@ -6,10 +6,10 @@ export const useTracksStore = defineStore('tracks', () => { const tracks = ref([]) const loading = ref(false) - async function fetchTracks() { + async function fetchTracks(myOnly = false) { loading.value = true try { - const response = await api.get('/api/tracks') + const response = await api.get('/api/tracks', { params: { my: myOnly } }) tracks.value = response.data } finally { loading.value = false diff --git a/frontend/src/views/TracksView.vue b/frontend/src/views/TracksView.vue index c9a6cd6..9321829 100644 --- a/frontend/src/views/TracksView.vue +++ b/frontend/src/views/TracksView.vue @@ -5,6 +5,21 @@ +