Files
Audio-Classifier/backend/src/api/routes/audio.py
Benoit 76d014bda2 Transcodage systématique MP3 128kbps
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
2025-12-23 10:08:16 +01:00

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")