soundwave/backend/audio/fanart_client.py

294 lines
10 KiB
Python

"""Fanart.tv API client for fetching artist and album artwork"""
import requests
import logging
from typing import Optional, Dict, Any, List
from django.conf import settings
logger = logging.getLogger(__name__)
class FanartClient:
"""Client for Fanart.tv API"""
# Register for API key at: https://fanart.tv/get-an-api-key/
API_KEY = getattr(settings, 'FANART_API_KEY', '')
BASE_URL = 'http://webservice.fanart.tv/v3'
def __init__(self, api_key: str = None):
self.api_key = api_key or self.API_KEY
if not self.api_key:
logger.warning("Fanart.tv API key not configured")
def get_artist_images(self, musicbrainz_id: str) -> Optional[Dict[str, Any]]:
"""
Get artist images by MusicBrainz ID
Args:
musicbrainz_id: MusicBrainz artist ID
Returns:
Dictionary with artist images organized by type
"""
if not self.api_key:
return None
try:
url = f"{self.BASE_URL}/music/{musicbrainz_id}"
params = {'api_key': self.api_key}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# Organize images by type
images = {
'backgrounds': [],
'thumbnails': [],
'logos': [],
'logos_hd': [],
'banners': [],
'album_covers': []
}
# Artist backgrounds
if 'artistbackground' in data:
for img in data['artistbackground']:
images['backgrounds'].append({
'id': img['id'],
'url': img['url'],
'likes': img.get('likes', '0')
})
# Artist thumbnails
if 'artistthumb' in data:
for img in data['artistthumb']:
images['thumbnails'].append({
'id': img['id'],
'url': img['url'],
'likes': img.get('likes', '0')
})
# Music logos
if 'musiclogo' in data:
for img in data['musiclogo']:
images['logos'].append({
'id': img['id'],
'url': img['url'],
'likes': img.get('likes', '0')
})
# HD Music logos
if 'hdmusiclogo' in data:
for img in data['hdmusiclogo']:
images['logos_hd'].append({
'id': img['id'],
'url': img['url'],
'likes': img.get('likes', '0')
})
# Music banners
if 'musicbanner' in data:
for img in data['musicbanner']:
images['banners'].append({
'id': img['id'],
'url': img['url'],
'likes': img.get('likes', '0')
})
# Album covers
if 'albums' in data:
for album_id, album_data in data['albums'].items():
if 'albumcover' in album_data:
for img in album_data['albumcover']:
images['album_covers'].append({
'id': img['id'],
'url': img['url'],
'album_id': album_id,
'likes': img.get('likes', '0')
})
# Sort by likes (descending)
for category in images:
images[category].sort(key=lambda x: int(x['likes']), reverse=True)
return images
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
logger.warning(f"Fanart.tv artist not found: {musicbrainz_id}")
else:
logger.error(f"HTTP error fetching artist from Fanart.tv: {e}")
return None
except Exception as e:
logger.error(f"Error fetching artist from Fanart.tv: {e}")
return None
def get_album_images(self, musicbrainz_release_id: str) -> Optional[Dict[str, Any]]:
"""
Get album images by MusicBrainz release ID
Args:
musicbrainz_release_id: MusicBrainz release ID
Returns:
Dictionary with album images
"""
if not self.api_key:
return None
try:
url = f"{self.BASE_URL}/music/albums/{musicbrainz_release_id}"
params = {'api_key': self.api_key}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
images = {
'covers': [],
'discs': []
}
# Album covers
if 'albums' in data:
for album_id, album_data in data['albums'].items():
if 'albumcover' in album_data:
for img in album_data['albumcover']:
images['covers'].append({
'id': img['id'],
'url': img['url'],
'likes': img.get('likes', '0')
})
# CD art
if 'cdart' in album_data:
for img in album_data['cdart']:
images['discs'].append({
'id': img['id'],
'url': img['url'],
'disc': img.get('disc', '1'),
'likes': img.get('likes', '0')
})
# Sort by likes
images['covers'].sort(key=lambda x: int(x['likes']), reverse=True)
images['discs'].sort(key=lambda x: int(x['likes']), reverse=True)
return images
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
logger.warning(f"Fanart.tv album not found: {musicbrainz_release_id}")
else:
logger.error(f"HTTP error fetching album from Fanart.tv: {e}")
return None
except Exception as e:
logger.error(f"Error fetching album from Fanart.tv: {e}")
return None
def get_best_artist_image(self, musicbrainz_id: str, image_type: str = 'thumbnail') -> Optional[str]:
"""
Get best (most liked) artist image of specific type
Args:
musicbrainz_id: MusicBrainz artist ID
image_type: Type of image ('thumbnail', 'background', 'logo', 'logo_hd', 'banner')
Returns:
URL of the best image or None
"""
images = self.get_artist_images(musicbrainz_id)
if not images:
return None
# Map to correct key
type_map = {
'thumbnail': 'thumbnails',
'background': 'backgrounds',
'logo': 'logos',
'logo_hd': 'logos_hd',
'banner': 'banners'
}
key = type_map.get(image_type, image_type)
if key in images and images[key]:
return images[key][0]['url'] # First item is most liked
return None
def get_best_album_cover(self, musicbrainz_release_id: str) -> Optional[str]:
"""
Get best (most liked) album cover
Args:
musicbrainz_release_id: MusicBrainz release ID
Returns:
URL of the best cover or None
"""
images = self.get_album_images(musicbrainz_release_id)
if not images or not images.get('covers'):
return None
return images['covers'][0]['url'] # First item is most liked
def download_image(self, url: str, output_path: str) -> bool:
"""
Download image from URL
Args:
url: Image URL
output_path: Local path to save image
Returns:
True if successful, False otherwise
"""
try:
response = requests.get(url, timeout=30, stream=True)
response.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
logger.info(f"Downloaded image to {output_path}")
return True
except Exception as e:
logger.error(f"Error downloading image from {url}: {e}")
return False
def search_by_artist_name(self, artist_name: str) -> Optional[str]:
"""
Search for MusicBrainz ID by artist name
Note: Fanart.tv doesn't have a search endpoint, so this uses MusicBrainz API
Args:
artist_name: Artist name to search for
Returns:
MusicBrainz artist ID or None
"""
try:
# Use MusicBrainz API for search
url = 'https://musicbrainz.org/ws/2/artist/'
params = {
'query': f'artist:{artist_name}',
'fmt': 'json',
'limit': 1
}
headers = {
'User-Agent': 'SoundWave/1.0 (https://github.com/tubearchivist/tubearchivist)'
}
response = requests.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if data.get('artists') and len(data['artists']) > 0:
return data['artists'][0]['id']
except Exception as e:
logger.error(f"Error searching for artist on MusicBrainz: {e}")
return None