295 lines
10 KiB
Python
295 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
|