296 lines
9.6 KiB
Python
296 lines
9.6 KiB
Python
"""Last.fm API client for fetching music metadata and artwork"""
|
|
import pylast
|
|
import requests
|
|
import logging
|
|
from typing import Optional, Dict, Any, List
|
|
from django.conf import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LastFMClient:
|
|
"""Client for Last.fm API"""
|
|
|
|
# Register for API keys at: https://www.last.fm/api/account/create
|
|
API_KEY = getattr(settings, 'LASTFM_API_KEY', '')
|
|
API_SECRET = getattr(settings, 'LASTFM_API_SECRET', '')
|
|
|
|
def __init__(self, api_key: str = None, api_secret: str = None):
|
|
self.api_key = api_key or self.API_KEY
|
|
self.api_secret = api_secret or self.API_SECRET
|
|
|
|
if self.api_key and self.api_secret:
|
|
self.network = pylast.LastFMNetwork(
|
|
api_key=self.api_key,
|
|
api_secret=self.api_secret
|
|
)
|
|
else:
|
|
self.network = None
|
|
logger.warning("Last.fm API credentials not configured")
|
|
|
|
def search_track(self, artist: str, title: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Search for track information
|
|
|
|
Args:
|
|
artist: Artist name
|
|
title: Track title
|
|
|
|
Returns:
|
|
Dictionary with track information
|
|
"""
|
|
if not self.network:
|
|
return None
|
|
|
|
try:
|
|
track = self.network.get_track(artist, title)
|
|
|
|
# Get track info
|
|
info = {
|
|
'title': track.get_title(),
|
|
'artist': track.get_artist().get_name(),
|
|
'url': track.get_url(),
|
|
'duration': track.get_duration() / 1000 if track.get_duration() else 0, # Convert ms to seconds
|
|
'listeners': track.get_listener_count() or 0,
|
|
'playcount': track.get_playcount() or 0,
|
|
'tags': [tag.item.get_name() for tag in track.get_top_tags(limit=10)],
|
|
}
|
|
|
|
# Try to get album info
|
|
try:
|
|
album = track.get_album()
|
|
if album:
|
|
info['album'] = album.get_title()
|
|
info['album_url'] = album.get_url()
|
|
info['album_cover'] = album.get_cover_image()
|
|
except:
|
|
pass
|
|
|
|
# Try to get MusicBrainz ID
|
|
try:
|
|
mbid = track.get_mbid()
|
|
if mbid:
|
|
info['mbid'] = mbid
|
|
except:
|
|
pass
|
|
|
|
# Get cover images
|
|
try:
|
|
images = self._get_track_images(artist, title)
|
|
if images:
|
|
info['images'] = images
|
|
except:
|
|
pass
|
|
|
|
return info
|
|
|
|
except pylast.WSError as e:
|
|
logger.warning(f"Last.fm track not found: {artist} - {title}: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error fetching track from Last.fm: {e}")
|
|
return None
|
|
|
|
def get_artist_info(self, artist_name: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Get artist information
|
|
|
|
Args:
|
|
artist_name: Artist name
|
|
|
|
Returns:
|
|
Dictionary with artist information
|
|
"""
|
|
if not self.network:
|
|
return None
|
|
|
|
try:
|
|
artist = self.network.get_artist(artist_name)
|
|
|
|
info = {
|
|
'name': artist.get_name(),
|
|
'url': artist.get_url(),
|
|
'listeners': artist.get_listener_count() or 0,
|
|
'playcount': artist.get_playcount() or 0,
|
|
'bio': artist.get_bio_content(),
|
|
'bio_summary': artist.get_bio_summary(),
|
|
'tags': [tag.item.get_name() for tag in artist.get_top_tags(limit=10)],
|
|
}
|
|
|
|
# Try to get MusicBrainz ID
|
|
try:
|
|
mbid = artist.get_mbid()
|
|
if mbid:
|
|
info['mbid'] = mbid
|
|
except:
|
|
pass
|
|
|
|
# Get similar artists
|
|
try:
|
|
similar = artist.get_similar(limit=10)
|
|
info['similar_artists'] = [
|
|
{
|
|
'name': s.item.get_name(),
|
|
'url': s.item.get_url(),
|
|
'match': s.match
|
|
}
|
|
for s in similar
|
|
]
|
|
except:
|
|
info['similar_artists'] = []
|
|
|
|
# Get images
|
|
try:
|
|
images = self._get_artist_images(artist_name)
|
|
if images:
|
|
info['images'] = images
|
|
except:
|
|
pass
|
|
|
|
return info
|
|
|
|
except pylast.WSError as e:
|
|
logger.warning(f"Last.fm artist not found: {artist_name}: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error fetching artist from Last.fm: {e}")
|
|
return None
|
|
|
|
def get_album_info(self, artist: str, album: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Get album information
|
|
|
|
Args:
|
|
artist: Artist name
|
|
album: Album name
|
|
|
|
Returns:
|
|
Dictionary with album information
|
|
"""
|
|
if not self.network:
|
|
return None
|
|
|
|
try:
|
|
album_obj = self.network.get_album(artist, album)
|
|
|
|
info = {
|
|
'title': album_obj.get_title(),
|
|
'artist': album_obj.get_artist().get_name(),
|
|
'url': album_obj.get_url(),
|
|
'playcount': album_obj.get_playcount() or 0,
|
|
'listeners': album_obj.get_listener_count() or 0,
|
|
'tags': [tag.item.get_name() for tag in album_obj.get_top_tags(limit=10)],
|
|
}
|
|
|
|
# Try to get MusicBrainz ID
|
|
try:
|
|
mbid = album_obj.get_mbid()
|
|
if mbid:
|
|
info['mbid'] = mbid
|
|
except:
|
|
pass
|
|
|
|
# Get cover images
|
|
try:
|
|
cover = album_obj.get_cover_image()
|
|
if cover:
|
|
info['cover_url'] = cover
|
|
info['images'] = self._get_album_images_sizes(cover)
|
|
except:
|
|
pass
|
|
|
|
return info
|
|
|
|
except pylast.WSError as e:
|
|
logger.warning(f"Last.fm album not found: {artist} - {album}: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error fetching album from Last.fm: {e}")
|
|
return None
|
|
|
|
def _get_track_images(self, artist: str, title: str) -> List[Dict[str, str]]:
|
|
"""Get track/album images in different sizes"""
|
|
try:
|
|
track = self.network.get_track(artist, title)
|
|
album = track.get_album()
|
|
if album:
|
|
cover_url = album.get_cover_image()
|
|
if cover_url:
|
|
return self._get_album_images_sizes(cover_url)
|
|
except:
|
|
pass
|
|
return []
|
|
|
|
def _get_artist_images(self, artist_name: str) -> List[Dict[str, str]]:
|
|
"""Get artist images in different sizes"""
|
|
if not self.api_key:
|
|
return []
|
|
|
|
try:
|
|
# Use direct API call for more control
|
|
url = 'http://ws.audioscrobbler.com/2.0/'
|
|
params = {
|
|
'method': 'artist.getinfo',
|
|
'artist': artist_name,
|
|
'api_key': self.api_key,
|
|
'format': 'json'
|
|
}
|
|
|
|
response = requests.get(url, params=params, timeout=10)
|
|
data = response.json()
|
|
|
|
if 'artist' in data and 'image' in data['artist']:
|
|
images = []
|
|
for img in data['artist']['image']:
|
|
if img['#text']:
|
|
images.append({
|
|
'size': img['size'],
|
|
'url': img['#text']
|
|
})
|
|
return images
|
|
except Exception as e:
|
|
logger.error(f"Error fetching artist images: {e}")
|
|
|
|
return []
|
|
|
|
def _get_album_images_sizes(self, cover_url: str) -> List[Dict[str, str]]:
|
|
"""Convert single cover URL to different sizes"""
|
|
# Last.fm image URLs follow a pattern
|
|
images = []
|
|
sizes = ['small', 'medium', 'large', 'extralarge', 'mega']
|
|
|
|
for size in sizes:
|
|
# Replace size in URL
|
|
url = cover_url.replace('/300x300/', f'/{size}/')
|
|
images.append({
|
|
'size': size,
|
|
'url': url
|
|
})
|
|
|
|
return images
|
|
|
|
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
|