J'ai implémenté une solution complète pour optimiser ton système audio : 1. Backend - Transcodage & Waveforms Nouveau module de transcodage (transcoder.py): Transcodage automatique en MP3 128kbps via FFmpeg Stockage dans dossier transcoded/ Compression ~70-90% selon format source Waveforms pré-calculées (waveform_generator.py): Génération lors du scan (800 points) Stockage JSON dans dossier waveforms/ Chargement instantané Schema BDD mis à jour (schema.py): filepath : fichier original (download) stream_filepath : MP3 128kbps (streaming) waveform_filepath : JSON pré-calculé Scanner amélioré (scanner.py): Transcode automatiquement chaque fichier Pré-calcule la waveform Stocke les 3 chemins en BDD 2. API - Endpoints Endpoint /api/library/scan (library.py): POST pour lancer un scan Tâche en arrière-plan Statut consultable via GET /api/library/scan/status Streaming optimisé (audio.py): Utilise stream_filepath (MP3 128kbps) en priorité Fallback sur fichier original si absent Waveform chargée depuis JSON pré-calculé 3. Frontend - Interface Bouton Rescan (page.tsx): Dans le header à droite Icône qui tourne pendant le scan Affichage progression en temps réel Reload automatique après scan 4. Base de données Migration appliquée (20251223_003_add_stream_waveform_paths.py): ALTER TABLE audio_tracks ADD COLUMN stream_filepath VARCHAR; ALTER TABLE audio_tracks ADD COLUMN waveform_filepath VARCHAR; CREATE INDEX idx_stream_filepath ON audio_tracks (stream_filepath); 🚀 Utilisation Via l'interface web Clique sur le bouton "Rescan" dans le header Le scan démarre automatiquement Tu vois la progression en temps réel La page se recharge automatiquement à la fin Via CLI (dans le container) docker-compose exec backend python -m src.cli.scanner /music 📊 Avantages ✅ Streaming ultra-rapide : MP3 128kbps = ~70-90% plus léger ✅ Waveform instantanée : Pré-calculée, pas de latence ✅ Download qualité : Fichier original préservé ✅ Rescan facile : Bouton dans l'UI ✅ Prêt pour serveur distant : Optimisé pour la bande passante
173 lines
4.8 KiB
Python
173 lines
4.8 KiB
Python
"""Audio streaming and download endpoints."""
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy.orm import Session
|
|
from uuid import UUID
|
|
from pathlib import Path
|
|
|
|
from ...models.database import get_db
|
|
from ...models import crud
|
|
from ...core.waveform_generator import get_waveform_data
|
|
from ...utils.logging import get_logger
|
|
|
|
router = APIRouter()
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
@router.get("/stream/{track_id}")
|
|
async def stream_audio(
|
|
track_id: UUID,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Stream audio file with range request support.
|
|
|
|
Uses the transcoded MP3 128kbps file for fast streaming if available,
|
|
otherwise falls back to the original file.
|
|
|
|
Args:
|
|
track_id: Track UUID
|
|
request: HTTP request
|
|
db: Database session
|
|
|
|
Returns:
|
|
Audio file for streaming
|
|
|
|
Raises:
|
|
HTTPException: 404 if track not found or file doesn't exist
|
|
"""
|
|
track = crud.get_track_by_id(db, track_id)
|
|
|
|
if not track:
|
|
raise HTTPException(status_code=404, detail="Track not found")
|
|
|
|
# Prefer stream_filepath (transcoded MP3) if available
|
|
if track.stream_filepath and Path(track.stream_filepath).exists():
|
|
file_path = Path(track.stream_filepath)
|
|
media_type = "audio/mpeg"
|
|
logger.debug(f"Streaming transcoded file: {file_path}")
|
|
else:
|
|
# Fallback to original file
|
|
file_path = Path(track.filepath)
|
|
|
|
if not file_path.exists():
|
|
logger.error(f"File not found: {track.filepath}")
|
|
raise HTTPException(status_code=404, detail="Audio file not found on disk")
|
|
|
|
# Determine media type based on format
|
|
media_types = {
|
|
"mp3": "audio/mpeg",
|
|
"wav": "audio/wav",
|
|
"flac": "audio/flac",
|
|
"m4a": "audio/mp4",
|
|
"ogg": "audio/ogg",
|
|
}
|
|
media_type = media_types.get(track.format, "audio/mpeg")
|
|
logger.debug(f"Streaming original file: {file_path}")
|
|
|
|
return FileResponse(
|
|
path=str(file_path),
|
|
media_type=media_type,
|
|
filename=track.filename,
|
|
headers={
|
|
"Accept-Ranges": "bytes",
|
|
"Content-Disposition": f'inline; filename="{track.filename}"',
|
|
},
|
|
)
|
|
|
|
|
|
@router.get("/download/{track_id}")
|
|
async def download_audio(
|
|
track_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Download audio file.
|
|
|
|
Args:
|
|
track_id: Track UUID
|
|
db: Database session
|
|
|
|
Returns:
|
|
Audio file for download
|
|
|
|
Raises:
|
|
HTTPException: 404 if track not found or file doesn't exist
|
|
"""
|
|
track = crud.get_track_by_id(db, track_id)
|
|
|
|
if not track:
|
|
raise HTTPException(status_code=404, detail="Track not found")
|
|
|
|
file_path = Path(track.filepath)
|
|
|
|
if not file_path.exists():
|
|
logger.error(f"File not found: {track.filepath}")
|
|
raise HTTPException(status_code=404, detail="Audio file not found on disk")
|
|
|
|
# Determine media type
|
|
media_types = {
|
|
"mp3": "audio/mpeg",
|
|
"wav": "audio/wav",
|
|
"flac": "audio/flac",
|
|
"m4a": "audio/mp4",
|
|
"ogg": "audio/ogg",
|
|
}
|
|
media_type = media_types.get(track.format, "audio/mpeg")
|
|
|
|
return FileResponse(
|
|
path=str(file_path),
|
|
media_type=media_type,
|
|
filename=track.filename,
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="{track.filename}"',
|
|
},
|
|
)
|
|
|
|
|
|
@router.get("/waveform/{track_id}")
|
|
async def get_waveform(
|
|
track_id: UUID,
|
|
num_peaks: int = 800,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Get waveform peak data for visualization.
|
|
|
|
Uses pre-computed waveform if available, otherwise generates on-the-fly.
|
|
|
|
Args:
|
|
track_id: Track UUID
|
|
num_peaks: Number of peaks to generate
|
|
db: Database session
|
|
|
|
Returns:
|
|
Waveform data with peaks and duration
|
|
|
|
Raises:
|
|
HTTPException: 404 if track not found or file doesn't exist
|
|
"""
|
|
track = crud.get_track_by_id(db, track_id)
|
|
|
|
if not track:
|
|
raise HTTPException(status_code=404, detail="Track not found")
|
|
|
|
file_path = Path(track.filepath)
|
|
|
|
if not file_path.exists():
|
|
logger.error(f"File not found: {track.filepath}")
|
|
raise HTTPException(status_code=404, detail="Audio file not found on disk")
|
|
|
|
try:
|
|
# Use pre-computed waveform if available
|
|
waveform_cache_path = track.waveform_filepath if track.waveform_filepath else None
|
|
|
|
waveform_data = get_waveform_data(
|
|
str(file_path),
|
|
num_peaks=num_peaks,
|
|
waveform_cache_path=waveform_cache_path
|
|
)
|
|
return waveform_data
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate waveform for {track_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to generate waveform")
|