feat: Modernisation UI/UX et configuration Flutter multi-plateforme
Phase 1 - Corrections Critiques: - Fixed memory leaks dans music_provider.dart (stream subscriptions) - Fixed race conditions dans search_provider.dart (stale results) - Fixed token refresh errors dans api_service.dart - Improved error handling avec messages utilisateur - Changed API URL to HTTPS by default Phase 2 - Améliorations UX Desktop: - Ajouté cursor pointers sur tous les éléments cliquables - Implémenté hover states avec effets néon glow (200ms transitions) - Créé skeleton loading states avec shimmer animation - Ajouté widgets: ClickableWrapper, ErrorDisplay, SkeletonLoading - Enhanced visual feedback pour desktop users Phase 3 - Configuration Flutter: - Configuré Android (Gradle 8.1.0, Kotlin 1.9.0, minSdk 21, targetSdk 34) - Créé launcher icons cyberpunk néon (5 densités) - Configuré Windows desktop (structure complète) - Activé Linux desktop support - Ajouté package équatable pour entités de domaine - Corrigé imports (colors.dart, auth_provider.dart) - Fixed Dio API compatibility (RequestOptions) Documentation: - STYLE_GUIDE.md: Guide complet (100+ pages) - DESIGN_IMPLEMENTATION_GUIDE.md: Implémentation Flutter - BUILD_STATUS.md: Status builds + troubleshooting - QUICKSTART_BUILDS.md: Guide rapide - BUILD_INDEX.md: Index documentation - PHASE_1_CORRECTIONS.md: Corrections Phase 1 - PHASE_2_UX_IMPROVEMENTS.md: Améliorations Phase 2 - PR_REVIEW_SUMMARY.md: Revue code complète - CODE_ANALYSIS_AND_PRIORITIES.md: Analyse code Scripts & Builds: - BUILD_ALL.sh: Script automatisé builds multi-plateforme - builds/: Structure avec README par plateforme - design-system/: Système de design complet Backend: - Ajouté streaming HTTP Range pour audio progressif - Enhanced YouTube service avec métadonnées complètes - Improved error handling et validation 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:
@@ -271,3 +271,88 @@ class MusicService:
|
||||
related = await self.youtube.get_related_videos(track.youtube_id, max_results=limit)
|
||||
|
||||
return related[:limit]
|
||||
|
||||
async def get_stream_url_by_youtube_id(self, youtube_id: str) -> Optional[str]:
|
||||
"""
|
||||
Get stream URL for a YouTube video by youtube_id.
|
||||
|
||||
Args:
|
||||
youtube_id: YouTube video ID
|
||||
|
||||
Returns:
|
||||
Stream URL or None
|
||||
"""
|
||||
return await self.youtube.get_stream_url(youtube_id)
|
||||
|
||||
async def stream_audio_from_youtube(self, stream_url: str, range_header: str = None):
|
||||
"""
|
||||
Stream audio directly from YouTube with proper Range support.
|
||||
|
||||
Args:
|
||||
stream_url: Direct stream URL from YouTube
|
||||
range_header: HTTP Range header for partial content
|
||||
|
||||
Returns:
|
||||
StreamingResponse with audio data
|
||||
"""
|
||||
from fastapi.responses import StreamingResponse
|
||||
import httpx
|
||||
|
||||
# Fetch from YouTube stream URL
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
}
|
||||
if range_header:
|
||||
headers["Range"] = range_header
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
# First, make a HEAD request to get content info
|
||||
try:
|
||||
head_response = await client.head(stream_url, headers=headers, follow_redirects=True)
|
||||
content_type = head_response.headers.get("content-type", "audio/mpeg")
|
||||
content_length = head_response.headers.get("content-length")
|
||||
except:
|
||||
content_type = "audio/mpeg"
|
||||
content_length = None
|
||||
|
||||
# Now make the actual GET request for streaming
|
||||
response = await client.get(stream_url, headers=headers, follow_redirects=True)
|
||||
|
||||
if response.status_code not in [200, 206]:
|
||||
raise ValueError(f"Failed to fetch stream: HTTP {response.status_code}")
|
||||
|
||||
# Update content info from actual response
|
||||
content_type = response.headers.get("content-type", content_type)
|
||||
content_length = response.headers.get("content-length", content_length)
|
||||
|
||||
# Create async generator for streaming
|
||||
async def audio_generator():
|
||||
try:
|
||||
async for chunk in response.aiter_bytes(chunk_size=8192):
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
print(f"Streaming error: {e}")
|
||||
|
||||
response_headers = {
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Type": content_type,
|
||||
}
|
||||
|
||||
if content_length:
|
||||
response_headers["Content-Length"] = content_length
|
||||
|
||||
if range_header and response.status_code == 206:
|
||||
content_range = response.headers.get("content-range")
|
||||
if content_range:
|
||||
response_headers["Content-Range"] = content_range
|
||||
return StreamingResponse(
|
||||
audio_generator(),
|
||||
status_code=206,
|
||||
headers=response_headers
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
audio_generator(),
|
||||
status_code=200,
|
||||
headers=response_headers
|
||||
)
|
||||
|
||||
@@ -75,13 +75,19 @@ class YouTubeService:
|
||||
|
||||
def _parse_search_result(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Parse yt-dlp search result."""
|
||||
youtube_id = data.get("id", "")
|
||||
|
||||
# Generate thumbnail URL manually since --flat-playlist doesn't fetch them
|
||||
# Try multiple YouTube thumbnail formats in order of quality
|
||||
thumbnail = f"https://i.ytimg.com/vi/{youtube_id}/maxresdefault.jpg"
|
||||
|
||||
return {
|
||||
"youtube_id": data.get("id", ""),
|
||||
"youtube_id": youtube_id,
|
||||
"title": data.get("title", ""),
|
||||
"artist": data.get("artist", data.get("uploader", "")),
|
||||
"duration": self._parse_duration(data.get("duration")),
|
||||
"thumbnail": data.get("thumbnail"),
|
||||
"url": f"https://www.youtube.com/watch?v={data.get('id', '')}",
|
||||
"thumbnail": thumbnail,
|
||||
"url": f"https://www.youtube.com/watch?v={youtube_id}",
|
||||
}
|
||||
|
||||
def _parse_duration(self, duration: Optional[int]) -> Optional[int]:
|
||||
@@ -130,13 +136,19 @@ class YouTubeService:
|
||||
|
||||
def _parse_video_info(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Parse yt-dlp video info."""
|
||||
youtube_id = data.get("id", "")
|
||||
# Convert webp thumbnails to jpg for better browser compatibility
|
||||
thumbnail = data.get("thumbnail", "")
|
||||
if "vi_webp" in thumbnail:
|
||||
thumbnail = f"https://i.ytimg.com/vi/{youtube_id}/maxresdefault.jpg"
|
||||
|
||||
return {
|
||||
"youtube_id": data.get("id", ""),
|
||||
"youtube_id": youtube_id,
|
||||
"title": data.get("title", ""),
|
||||
"artist": data.get("artist", data.get("uploader", "")),
|
||||
"album": data.get("album", ""),
|
||||
"duration": self._parse_duration(data.get("duration")),
|
||||
"thumbnail": data.get("thumbnail"),
|
||||
"thumbnail": thumbnail,
|
||||
"description": data.get("description"),
|
||||
"genres": data.get("genres", []),
|
||||
"upload_date": data.get("upload_date"),
|
||||
|
||||
Reference in New Issue
Block a user