freedify/app/deezer_service.py
2026-01-13 22:26:48 +00:00

160 lines
6.4 KiB
Python

"""
Deezer service for Freedify.
Provides search (tracks, albums, artists) as fallback when Spotify is rate limited.
Deezer API is free and doesn't require authentication for basic searches.
"""
import httpx
from typing import Optional, Dict, List, Any
import logging
logger = logging.getLogger(__name__)
class DeezerService:
"""Service for searching and fetching metadata from Deezer."""
API_BASE = "https://api.deezer.com"
def __init__(self):
self.client = httpx.AsyncClient(timeout=30.0)
async def _api_request(self, endpoint: str, params: dict = None) -> dict:
"""Make API request to Deezer."""
response = await self.client.get(f"{self.API_BASE}{endpoint}", params=params)
response.raise_for_status()
return response.json()
# ========== TRACK METHODS ==========
async def search_tracks(self, query: str, limit: int = 20, offset: int = 0) -> List[Dict[str, Any]]:
"""Search for tracks."""
data = await self._api_request("/search/track", {"q": query, "limit": limit, "index": offset})
return [self._format_track(item) for item in data.get("data", [])]
def _format_track(self, item: dict) -> dict:
"""Format track data for frontend (matching Spotify format)."""
album = item.get("album", {})
artist = item.get("artist", {})
return {
"id": f"dz_{item['id']}",
"type": "track",
"name": item.get("title", ""),
"artists": artist.get("name", ""),
"artist_names": [artist.get("name", "")],
"album": album.get("title", ""),
"album_id": f"dz_{album.get('id', '')}",
"album_art": album.get("cover_xl") or album.get("cover_big") or album.get("cover_medium"),
"duration_ms": item.get("duration", 0) * 1000,
"duration": self._format_duration(item.get("duration", 0) * 1000),
"isrc": item.get("isrc"),
"preview_url": item.get("preview"),
"release_date": album.get("release_date", ""),
"source": "deezer",
}
# ========== ALBUM METHODS ==========
async def search_albums(self, query: str, limit: int = 20, offset: int = 0) -> List[Dict[str, Any]]:
"""Search for albums."""
data = await self._api_request("/search/album", {"q": query, "limit": limit, "index": offset})
return [self._format_album(item) for item in data.get("data", [])]
async def get_album(self, album_id: str) -> Optional[Dict[str, Any]]:
"""Get album with all tracks."""
try:
# Remove dz_ prefix if present
clean_id = album_id.replace("dz_", "")
data = await self._api_request(f"/album/{clean_id}")
album = self._format_album(data)
# Format tracks
tracks = []
for item in data.get("tracks", {}).get("data", []):
track = {
"id": f"dz_{item['id']}",
"type": "track",
"name": item.get("title", ""),
"artists": data.get("artist", {}).get("name", ""),
"artist_names": [data.get("artist", {}).get("name", "")],
"album": data.get("title", ""),
"album_id": f"dz_{clean_id}",
"album_art": album["album_art"],
"duration_ms": item.get("duration", 0) * 1000,
"duration": self._format_duration(item.get("duration", 0) * 1000),
"isrc": item.get("isrc"),
"preview_url": item.get("preview"),
"release_date": data.get("release_date", ""),
"source": "deezer",
}
tracks.append(track)
album["tracks"] = tracks
return album
except Exception as e:
logger.error(f"Error fetching Deezer album {album_id}: {e}")
return None
def _format_album(self, item: dict) -> dict:
"""Format album data for frontend."""
artist = item.get("artist", {})
return {
"id": f"dz_{item['id']}",
"type": "album",
"name": item.get("title", ""),
"artists": artist.get("name", ""),
"album_art": item.get("cover_xl") or item.get("cover_big") or item.get("cover_medium"),
"release_date": item.get("release_date", ""),
"total_tracks": item.get("nb_tracks", 0),
"source": "deezer",
}
# ========== ARTIST METHODS ==========
async def search_artists(self, query: str, limit: int = 20, offset: int = 0) -> List[Dict[str, Any]]:
"""Search for artists."""
data = await self._api_request("/search/artist", {"q": query, "limit": limit, "index": offset})
return [self._format_artist(item) for item in data.get("data", [])]
async def get_artist(self, artist_id: str) -> Optional[Dict[str, Any]]:
"""Get artist info with top tracks."""
try:
clean_id = artist_id.replace("dz_", "")
data = await self._api_request(f"/artist/{clean_id}")
artist = self._format_artist(data)
# Get top tracks
top_tracks = await self._api_request(f"/artist/{clean_id}/top", {"limit": 10})
artist["tracks"] = [self._format_track(t) for t in top_tracks.get("data", [])]
return artist
except Exception as e:
logger.error(f"Error fetching Deezer artist {artist_id}: {e}")
return None
def _format_artist(self, item: dict) -> dict:
"""Format artist data for frontend."""
return {
"id": f"dz_{item['id']}",
"type": "artist",
"name": item.get("name", ""),
"image": item.get("picture_xl") or item.get("picture_big") or item.get("picture_medium"),
"fans": item.get("nb_fan", 0),
"source": "deezer",
}
# ========== UTILITIES ==========
def _format_duration(self, ms: int) -> str:
"""Format duration from ms to MM:SS."""
seconds = ms // 1000
minutes = seconds // 60
secs = seconds % 60
return f"{minutes}:{secs:02d}"
async def close(self):
"""Close the HTTP client."""
await self.client.aclose()
# Singleton instance
deezer_service = DeezerService()