Files
AudiOhm/backend/app/services/youtube_service.py
T
root a89c7894cf Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube
Backend:
- FastAPI avec PostgreSQL et Redis
- Authentification JWT complète
- API REST pour musique, playlists, recherche
- Streaming audio via yt-dlp
- SQLAlchemy 2.0 async

Frontend:
- Flutter avec thème néon cyberpunk
- State management Riverpod
- Layout adaptatif desktop/mobile
- Lecteur audio avec mini-player

Infrastructure:
- Docker Compose (PostgreSQL + Redis)
- Scripts d'installation automatisés
- Scripts de build pour exécutables

Fichiers ajoutés:
- BUILD_CLIENT_*.bat/sh: Scripts de compilation
- BUILD_CLIENT_README.md: Documentation compilation
- CHECK_FLUTTER.sh: Vérificateur d'environnement
- requirements.txt mis à jour pour Python 3.13
- Modèles SQLAlchemy corrigés (metadata -> extra_metadata)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 20:08:36 +00:00

296 lines
8.5 KiB
Python

"""YouTube service using yt-dlp."""
import asyncio
import json
import subprocess
from pathlib import Path
from typing import List, Optional, Dict, Any
from uuid import uuid4
from app.core.config import settings
class YouTubeService:
"""Service for YouTube operations using yt-dlp."""
def __init__(self):
self.ytdlp_path = settings.YTDLP_PATH
self.cache_dir = Path(settings.AUDIO_CACHE_PATH)
self.cache_dir.mkdir(parents=True, exist_ok=True)
async def search(
self,
query: str,
max_results: int = 20,
search_type: str = "videos",
) -> List[Dict[str, Any]]:
"""
Search YouTube for videos.
Args:
query: Search query
max_results: Maximum number of results
search_type: Type of search (videos, playlists, etc.)
Returns:
List of search results
"""
cmd = [
self.ytdlp_path,
"ytsearch" + str(max_results) + ":" + query,
"--dump-json",
"--flat-playlist",
"--skip-download",
"--no-warnings",
]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode() if stderr else "Unknown error"
print(f"yt-dlp search error: {error_msg}")
return []
# Parse JSON output (one line per video)
results = []
for line in stdout.decode().split("\n"):
if not line.strip():
continue
try:
data = json.loads(line)
results.append(self._parse_search_result(data))
except json.JSONDecodeError:
continue
return results
except Exception as e:
print(f"Error searching YouTube: {e}")
return []
def _parse_search_result(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Parse yt-dlp search result."""
return {
"youtube_id": data.get("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', '')}",
}
def _parse_duration(self, duration: Optional[int]) -> Optional[int]:
"""Parse duration in seconds."""
if duration is None:
return None
return int(duration)
async def get_video_info(self, video_id: str) -> Optional[Dict[str, Any]]:
"""
Get detailed information about a video.
Args:
video_id: YouTube video ID
Returns:
Video information or None
"""
url = f"https://www.youtube.com/watch?v={video_id}"
cmd = [
self.ytdlp_path,
url,
"--dump-json",
"--skip-download",
"--no-warnings",
]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return None
data = json.loads(stdout.decode())
return self._parse_video_info(data)
except Exception as e:
print(f"Error getting video info: {e}")
return None
def _parse_video_info(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Parse yt-dlp video info."""
return {
"youtube_id": data.get("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"),
"description": data.get("description"),
"genres": data.get("genres", []),
"upload_date": data.get("upload_date"),
}
async def get_stream_url(self, video_id: str) -> Optional[str]:
"""
Get direct stream URL for a video.
Args:
video_id: YouTube video ID
Returns:
Stream URL or None
"""
url = f"https://www.youtube.com/watch?v={video_id}"
cmd = [
self.ytdlp_path,
url,
"--get-url",
"--format",
"bestaudio[ext=m4a]/bestaudio/best",
"--no-warnings",
]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return None
stream_url = stdout.decode().strip()
return stream_url if stream_url else None
except Exception as e:
print(f"Error getting stream URL: {e}")
return None
async def download_audio(
self,
video_id: str,
quality: str = "high",
) -> Optional[Path]:
"""
Download audio from YouTube and cache it.
Args:
video_id: YouTube video ID
quality: Audio quality (low, medium, high)
Returns:
Path to downloaded file or None
"""
url = f"https://www.youtube.com/watch?v={video_id}"
cache_path = self.cache_dir / f"{video_id}.mp3"
# Check if already cached
if cache_path.exists():
return cache_path
# Determine format based on quality
if quality == "high":
audio_format = "320"
elif quality == "medium":
audio_format = "192"
else:
audio_format = "128"
cmd = [
self.ytdlp_path,
url,
"--extract-audio",
"--audio-format",
"mp3",
"--audio-quality",
audio_format,
"--output",
str(cache_path),
"--no-warnings",
]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode() if stderr else "Unknown error"
print(f"Error downloading audio: {error_msg}")
return None
return cache_path if cache_path.exists() else None
except Exception as e:
print(f"Error downloading audio: {e}")
return None
async def get_related_videos(self, video_id: str, max_results: int = 10) -> List[Dict[str, Any]]:
"""
Get related videos for a video.
Args:
video_id: YouTube video ID
max_results: Maximum number of results
Returns:
List of related videos
"""
url = f"https://www.youtube.com/watch?v={video_id}"
cmd = [
self.ytdlp_path,
url,
"--flat-playlist",
"--playlist-end",
str(max_results),
"--dump-json",
"--no-warnings",
]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return []
results = []
for line in stdout.decode().split("\n"):
if not line.strip():
continue
try:
data = json.loads(line)
if data.get("id") != video_id: # Exclude the video itself
results.append(self._parse_search_result(data))
except json.JSONDecodeError:
continue
return results
except Exception as e:
print(f"Error getting related videos: {e}")
return []