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:
+68
-32
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user