Files
AudiOhm/backend/app/services/music_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

274 lines
7.9 KiB
Python

"""Music service."""
from typing import List, Optional
from uuid import UUID
from sqlalchemy import select, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.track import Track
from app.models.artist import Artist
from app.models.album import Album
from app.services.youtube_service import YouTubeService
class MusicService:
"""Service for music operations."""
def __init__(self, db: AsyncSession):
self.db = db
self.youtube = YouTubeService()
async def search(
self,
query: str,
search_type: str = "all",
limit: int = 20,
offset: int = 0,
) -> dict:
"""
Search for music across database and YouTube.
Args:
query: Search query
search_type: Type of content (track, artist, album, all)
limit: Maximum results
offset: Pagination offset
Returns:
Search results with tracks, artists, albums
"""
results = {
"tracks": [],
"artists": [],
"albums": [],
"total": 0,
"query": query,
}
# Search database first
if search_type in ["track", "all"]:
results["tracks"] = await self._search_tracks(query, limit)
results["total"] += len(results["tracks"])
if search_type in ["artist", "all"]:
results["artists"] = await self._search_artists(query, limit)
results["total"] += len(results["artists"])
if search_type in ["album", "all"]:
results["albums"] = await self._search_albums(query, limit)
results["total"] += len(results["albums"])
# If no local results, search YouTube
if results["total"] == 0:
yt_results = await self.youtube.search(query, max_results=limit)
results["tracks"] = yt_results[:limit]
return results
async def _search_tracks(self, query: str, limit: int) -> List[dict]:
"""Search tracks in database."""
stmt = (
select(Track)
.options(selectinload(Track.artist), selectinload(Track.album))
.where(
or_(
Track.title.ilike(f"%{query}%"),
)
)
.limit(limit)
)
result = await self.db.execute(stmt)
tracks = result.scalars().all()
return [
{
"id": str(track.id),
"title": track.title,
"duration": track.duration,
"image_url": track.image_url,
"artist": track.artist.name if track.artist else None,
"album": track.album.title if track.album else None,
"youtube_id": track.youtube_id,
}
for track in tracks
]
async def _search_artists(self, query: str, limit: int) -> List[dict]:
"""Search artists in database."""
stmt = (
select(Artist)
.where(Artist.name.ilike(f"%{query}%"))
.limit(limit)
)
result = await self.db.execute(stmt)
artists = result.scalars().all()
return [
{
"id": str(artist.id),
"name": artist.name,
"image_url": artist.image_url,
"genres": artist.genres,
"popularity": artist.popularity,
}
for artist in artists
]
async def _search_albums(self, query: str, limit: int) -> List[dict]:
"""Search albums in database."""
stmt = (
select(Album)
.options(selectinload(Album.artist))
.where(Album.title.ilike(f"%{query}%"))
.limit(limit)
)
result = await self.db.execute(stmt)
albums = result.scalars().all()
return [
{
"id": str(album.id),
"title": album.title,
"image_url": album.image_url,
"artist": album.artist.name if album.artist else None,
"total_tracks": album.total_tracks,
"release_date": album.release_date.isoformat() if album.release_date else None,
}
for album in albums
]
async def get_track(self, track_id: UUID) -> Optional[Track]:
"""
Get track by ID.
Args:
track_id: Track UUID
Returns:
Track or None
"""
stmt = (
select(Track)
.options(selectinload(Track.artist), selectinload(Track.album))
.where(Track.id == track_id)
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_stream_url(
self,
track_id: UUID,
quality: str = "high",
) -> Optional[str]:
"""
Get stream URL for a track.
Args:
track_id: Track UUID
quality: Audio quality
Returns:
Stream URL or None
"""
track = await self.get_track(track_id)
if not track or not track.youtube_id:
return None
# Try to get direct stream URL from YouTube
stream_url = await self.youtube.get_stream_url(track.youtube_id)
if stream_url:
return stream_url
# Fallback: download and serve locally
cache_path = await self.youtube.download_audio(track.youtube_id, quality)
if cache_path:
# In production, you'd serve this through a dedicated endpoint
return f"/api/v1/music/tracks/{track_id}/stream?cache=true"
return None
async def create_track_from_youtube(
self,
youtube_id: str,
title: str,
artist_name: Optional[str] = None,
album_name: Optional[str] = None,
) -> Track:
"""
Create a track from YouTube video ID.
Args:
youtube_id: YouTube video ID
title: Track title
artist_name: Optional artist name
album_name: Optional album name
Returns:
Created track
"""
# Get video info from YouTube
video_info = await self.youtube.get_video_info(youtube_id)
if video_info:
title = video_info.get("title", title)
artist_name = artist_name or video_info.get("artist")
duration = video_info.get("duration")
thumbnail = video_info.get("thumbnail")
else:
duration = None
thumbnail = None
# Find or create artist
artist = None
if artist_name:
stmt = select(Artist).where(Artist.name == artist_name)
result = await self.db.execute(stmt)
artist = result.scalar_one_or_none()
if not artist:
artist = Artist(
name=artist_name,
image_url=thumbnail,
)
self.db.add(artist)
await self.db.flush()
# Create track
track = Track(
title=title,
youtube_id=youtube_id,
artist_id=artist.id if artist else None,
duration=duration,
image_url=thumbnail,
)
self.db.add(track)
await self.db.commit()
await self.db.refresh(track)
return track
async def get_recommendations(
self,
track_id: UUID,
limit: int = 10,
) -> List[dict]:
"""
Get recommendations based on a track.
Args:
track_id: Seed track UUID
limit: Number of recommendations
Returns:
List of recommended tracks
"""
track = await self.get_track(track_id)
if not track or not track.youtube_id:
return []
# Get related videos from YouTube
related = await self.youtube.get_related_videos(track.youtube_id, max_results=limit)
return related[:limit]