prod: UI Optimisée mise en production

- Documentation archivée et réorganisée
- Backend: Ajout tests, migrations, library service, rate limiting
- Frontend: Suppression Flutter, focus sur interface web HTML/JS
- Tailwind CSS ajouté pour le style
- Améliorations UX et corrections bugs

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
root
2026-01-20 09:56:39 +00:00
parent bc03225e47
commit 801e6a050b
263 changed files with 33100 additions and 23058 deletions
+68 -32
View File
@@ -1,10 +1,13 @@
"""Music API routes."""
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException, Query, status, Request
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
from app.api.dependencies import CurrentUser, CurrentUserOptional, DBSession
from app.schemas.music import (
AlbumResponse,
@@ -47,13 +50,15 @@ async def search_music(
# Convert results without strict validation
tracks = []
for t in results.get("tracks", []):
# Use youtube_id as the id for YouTube-only results
track_id = t.get("id") or t.get("youtube_id")
track_data = {
"title": t.get("title", "Unknown"),
"youtube_id": t.get("youtube_id", ""),
"duration": t.get("duration"),
"image_url": t.get("thumbnail"),
"artist_name": t.get("artist", "Unknown Artist"),
"id": None,
"id": track_id,
}
tracks.append(track_data)
@@ -96,44 +101,87 @@ async def get_track(
@router.get("/youtube/{youtube_id}/stream")
@router.head("/youtube/{youtube_id}/stream")
async def stream_youtube_track(
async def stream_youtube_audio(
youtube_id: str,
db: DBSession,
request: Request = None,
):
"""
Stream a track directly from YouTube by youtube_id.
Stream audio from a YouTube video.
This endpoint bypasses the database and streams directly from YouTube.
Downloads the audio as MP3 and streams it to the client.
Supports HTTP Range requests for proper audio playback.
"""
music_service = MusicService(db)
try:
# Get YouTube stream URL
stream_url = await music_service.get_stream_url_by_youtube_id(youtube_id)
# Download audio as MP3
from pathlib import Path
if not stream_url:
audio_path = await music_service.youtube.download_audio(youtube_id)
if not audio_path or not audio_path.exists():
raise HTTPException(
status_code=404,
detail=f"Could not get stream for youtube_id: {youtube_id}"
detail=f"Could not download audio for youtube_id: {youtube_id}"
)
# Get range header from request
# Get file info
file_size = audio_path.stat().st_size
# Handle Range request
range_header = request.headers.get("range") if request else None
# Stream directly from YouTube
from fastapi.responses import StreamingResponse
if range_header:
# Parse Range header (format: "bytes=start-end")
try:
range_match = range_header.replace("bytes=", "").strip()
range_parts = range_match.split("-")
start = int(range_parts[0]) if range_parts[0] else 0
end = int(range_parts[1]) if len(range_parts) > 1 and range_parts[1] else file_size - 1
return await music_service.stream_audio_from_youtube(stream_url, range_header)
# Read the specific range
with open(audio_path, "rb") as f:
f.seek(start)
chunk_size = end - start + 1
data = f.read(chunk_size)
from fastapi.responses import Response
return Response(
content=data,
status_code=206, # Partial Content
media_type="audio/mpeg",
headers={
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(chunk_size),
"Content-Disposition": f"inline; filename={youtube_id}.mp3",
}
)
except Exception as e:
logger.error(f"Error handling range request: {e}")
# Fall through to full file response
# Full file response
from fastapi.responses import FileResponse
return FileResponse(
audio_path,
media_type="audio/mpeg",
filename=f"{youtube_id}.mp3",
headers={
"Accept-Ranges": "bytes",
"Content-Length": str(file_size),
}
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to stream from YouTube: {str(e)}"
detail=f"Failed to stream audio: {str(e)}"
)
@@ -267,29 +315,17 @@ async def get_track_recommendations(
async def get_trending(
db: DBSession,
limit: int = Query(20, ge=1, le=50),
days: int = Query(7, ge=1, le=30, description="Number of days to look back"),
):
"""
Get trending tracks.
Get trending tracks based on play count and recent listens.
Currently returns placeholder data.
In production, this would use actual trending data.
Returns the most played tracks from the database, sorted by popularity.
Combines total play count with recent activity to determine trending tracks.
"""
music_service = MusicService(db)
# Search for popular music on YouTube
results = await music_service.search("music 2024", search_type="track", limit=limit)
# Convert YouTube results to TrackSearchResult with only available fields
tracks = []
for t in results.get("tracks", []):
track_data = {
"title": t.get("title", "Unknown"),
"youtube_id": t.get("youtube_id", ""),
"duration": t.get("duration"),
"image_url": t.get("thumbnail"),
"artist_name": t.get("artist", "Unknown Artist"),
"id": None,
}
tracks.append(track_data)
# Get trending tracks from database
tracks = await music_service.get_trending(limit=limit, days=days)
return tracks