From 2bbf7e823d0ddde3cdfaf2f2c02964ec8bad75ff Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Fri, 7 Nov 2025 13:03:48 +1100 Subject: [PATCH] Fix error --- backend/app/api/music.py | 81 ++++++++++++++++++++++++++++++++++++---- frontend/src/App.tsx | 40 +++++++++++++++++++- 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/backend/app/api/music.py b/backend/app/api/music.py index 763e6d7..8bb0755 100644 --- a/backend/app/api/music.py +++ b/backend/app/api/music.py @@ -1,5 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query -from fastapi.responses import FileResponse +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request +from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, or_ from typing import List, Optional @@ -7,6 +7,7 @@ import os import shutil from pathlib import Path import re +import logging from app.db.session import get_db from app.models.models import Music @@ -15,6 +16,7 @@ from app.services.downloader import music_downloader from app.core.config import settings router = APIRouter() +logger = logging.getLogger(__name__) def normalize_artist_name(artist: str) -> str: @@ -398,14 +400,16 @@ async def scan_music_directory(db: AsyncSession = Depends(get_db)): @router.get("/file/{music_id}") async def serve_music_file( music_id: int, + request: Request, db: AsyncSession = Depends(get_db) ): - """Serve music file - handles both regular and local music directory files""" + """Serve music file with Range request support for audio streaming""" # Get music record result = await db.execute(select(Music).where(Music.id == music_id)) music = result.scalar_one_or_none() if not music: + logger.error(f"Music ID {music_id} not found in database") raise HTTPException(status_code=404, detail="Music not found") # Determine file path @@ -418,16 +422,79 @@ async def serve_music_file( # Check if file exists if not os.path.exists(file_path): - raise HTTPException(status_code=404, detail="File not found on disk") + logger.error(f"File not found on disk: {file_path} (Music ID: {music_id})") + raise HTTPException(status_code=404, detail=f"File not found: {music.file_path}") - # Return file + # Map file extensions to proper MIME types + mime_type_map = { + 'mp3': 'audio/mpeg', + 'mp4': 'audio/mp4', + 'm4a': 'audio/mp4', + 'wav': 'audio/wav', + 'flac': 'audio/flac', + 'ogg': 'audio/ogg', + 'opus': 'audio/opus', + 'webm': 'audio/webm', + 'aac': 'audio/aac', + } + + # Get MIME type based on file extension + file_ext = music.file_format or os.path.splitext(file_path)[1].lstrip('.') + media_type = mime_type_map.get(file_ext.lower(), 'audio/mpeg') + + logger.info(f"Serving music file: {file_path} (ID: {music_id}, MIME: {media_type})") + + # Return file with proper headers for audio streaming return FileResponse( path=file_path, - media_type=f"audio/{music.file_format or 'mpeg'}", - filename=os.path.basename(file_path) + media_type=media_type, + filename=os.path.basename(file_path), + headers={ + 'Accept-Ranges': 'bytes', + 'Cache-Control': 'public, max-age=3600', + } ) +@router.get("/file/{music_id}/check") +async def check_music_file( + music_id: int, + db: AsyncSession = Depends(get_db) +): + """Check if music file exists and is accessible (diagnostic endpoint)""" + # Get music record + result = await db.execute(select(Music).where(Music.id == music_id)) + music = result.scalar_one_or_none() + + if not music: + return { + "exists": False, + "error": "Music record not found in database", + "music_id": music_id + } + + # Determine file path + if music.file_location and os.path.isabs(music.file_location): + file_path = music.file_location + else: + file_path = os.path.join(settings.MUSIC_DIR, music.file_path) + + file_exists = os.path.exists(file_path) + + return { + "exists": file_exists, + "music_id": music_id, + "title": music.title, + "file_path": music.file_path, + "full_path": file_path, + "file_location": music.file_location, + "file_format": music.file_format, + "music_dir": settings.MUSIC_DIR, + "file_size": os.path.getsize(file_path) if file_exists else None, + "is_readable": os.access(file_path, os.R_OK) if file_exists else False, + } + + @router.get("/share/{share_token}", response_model=MusicSchema) async def get_music_by_share_token(share_token: str, db: AsyncSession = Depends(get_db)): """Get music by share token (public endpoint) - checks expiration""" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0c7f2e3..65d23a2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -132,9 +132,45 @@ function App() { // Check if it's a streaming URL const isStreamUrl = currentMusic.file_path.startsWith('/api/stream') || currentMusic.file_path.startsWith('http') // Use dedicated file serving endpoint for all local files - audioRef.current.src = isStreamUrl ? currentMusic.file_path : `/api/music/file/${currentMusic.id}` + const audioSrc = isStreamUrl ? currentMusic.file_path : `/api/music/file/${currentMusic.id}` + + console.log(`Loading audio: ${currentMusic.title} (ID: ${currentMusic.id})`) + console.log(`Audio source: ${audioSrc}`) + + audioRef.current.src = audioSrc + + // Add error handler to help debug loading issues + const handleError = (e: Event) => { + console.error('Audio loading error:', { + musicId: currentMusic.id, + title: currentMusic.title, + src: audioSrc, + error: (e.target as HTMLAudioElement)?.error, + networkState: (e.target as HTMLAudioElement)?.networkState, + readyState: (e.target as HTMLAudioElement)?.readyState, + }) + + // Try fallback to direct /music/ path if API endpoint fails + if (!isStreamUrl && audioRef.current && !audioRef.current.src.includes('/music/')) { + const fallbackSrc = `/music/${currentMusic.file_path}` + console.log(`Trying fallback source: ${fallbackSrc}`) + audioRef.current.src = fallbackSrc + if (isPlaying) { + audioRef.current.play().catch(err => console.error('Fallback play error:', err)) + } + } + } + + audioRef.current.addEventListener('error', handleError) + if (isPlaying) { - audioRef.current.play() + audioRef.current.play().catch(err => { + console.error('Play error:', err) + }) + } + + return () => { + audioRef.current?.removeEventListener('error', handleError) } } }, [currentMusic])