python-lastfm/src/api.py

317 lines
10 KiB
Python

#!/usr/bin/env python
__author__ = "Abhinav Sarkar <abhinav@abhinavsarkar.net>"
__version__ = "0.2"
__license__ = "GNU Lesser General Public License"
class Api(object):
"""The class representing the last.fm web services API."""
DEFAULT_CACHE_TIMEOUT = 3600 # cache for 1 hour
API_ROOT_URL = "http://ws.audioscrobbler.com/2.0/"
FETCH_INTERVAL = 1
SEARCH_XMLNS = "http://a9.com/-/spec/opensearch/1.1/"
def __init__(self,
apiKey,
input_encoding=None,
request_headers=None,
no_cache = False,
debug = False):
self.__apiKey = apiKey
self._cache = FileCache()
self._urllib = urllib2
self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
self._InitializeRequestHeaders(request_headers)
self._InitializeUserAgent()
self._input_encoding = input_encoding
self._no_cache = no_cache
self._debug = debug
self._lastFetchTime = datetime.now()
def getApiKey(self):
return self.__apiKey
def setCache(self, cache):
'''Override the default cache. Set to None to prevent caching.
Args:
cache: an instance that supports the same API as the audioscrobblerws.FileCache
'''
self._cache = cache
def setUrllib(self, urllib):
'''Override the default urllib implementation.
Args:
urllib: an instance that supports the same API as the urllib2 module
'''
self._urllib = urllib
def setCacheTimeout(self, cache_timeout):
'''Override the default cache timeout.
Args:
cache_timeout: time, in seconds, that responses should be reused.
'''
self._cache_timeout = cache_timeout
def setUserAgent(self, user_agent):
'''Override the default user agent
Args:
user_agent: a string that should be send to the server as the User-agent
'''
self._request_headers['User-Agent'] = user_agent
def _BuildUrl(self, url, path_elements=None, extra_params=None):
# Break url into consituent parts
(scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
path = path.replace(' ', '+')
# Add any additional path elements to the path
if path_elements:
# Filter out the path elements that have a value of None
p = [i for i in path_elements if i]
if not path.endswith('/'):
path += '/'
path += '/'.join(p)
# Add any additional query parameters to the query string
if extra_params and len(extra_params) > 0:
extra_query = self._EncodeParameters(extra_params)
# Add it to the existing query
if query:
query += '&' + extra_query
else:
query = extra_query
# Return the rebuilt URL
return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
def _InitializeRequestHeaders(self, request_headers):
if request_headers:
self._request_headers = request_headers
else:
self._request_headers = {}
def _InitializeUserAgent(self):
user_agent = 'Python-urllib/%s (python-lastfm/%s)' % \
(self._urllib.__version__, __version__)
self.setUserAgent(user_agent)
def _GetOpener(self, url):
opener = self._urllib.build_opener()
opener.addheaders = self._request_headers.items()
return opener
def _Encode(self, s):
if self._input_encoding:
return unicode(s, self._input_encoding).encode('utf-8')
else:
return unicode(s).encode('utf-8')
def _EncodeParameters(self, parameters):
'''Return a string in key=value&key=value form
Values of None are not included in the output string.
Args:
parameters:
A dict of (key, value) tuples, where value is encoded as
specified by self._encoding
Returns:
A URL-encoded string in "key=value&key=value" form
'''
if parameters is None:
return None
else:
return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in parameters.items() if v is not None]))
def getAlbum(self,
artist = None,
album = None,
mbid = None):
if isinstance(artist, Artist):
artist = artist.name
return Album.getInfo(self, artist, album, mbid)
def getArtist(self,
artist = None,
mbid = None):
return Artist.getInfo(self, artist, mbid)
def searchArtist(self,
artist,
limit = None):
return Artist.search(self, artist, limit)
def getEvent(self, event):
return Event.getInfo(self, event)
def getLocation(self, name):
return Location(self, name = name)
def getCountry(self, name):
return Country(self, name = name)
def getGroup(self, name):
return Group(self, name = name)
def fetchPlaylist(self, url):
return Playlist.fetch(self, url)
def getTag(self, name):
return Tag(self, name = name)
def getGlobalTopTags(self):
return Tag.getTopTags(self)
def searchTag(self,
tag,
limit = None):
return Tag.search(self, tag, limit)
def compareTaste(self,
type1, type2,
value1, value2,
limit = None):
return Tasteometer.compare(self, type1, type2, value1, value2, limit)
def getTrack(self, track, artist):
if isinstance(artist, Artist):
artist = artist.name
result = Track.search(self, track, artist)
if len(result.matches) == 0:
raise LastfmInvalidResourceError("'%s' by %s: no such track found" % (track, artist))
return result.matches[0]
def searchTrack(self,
track,
artist = None,
limit = None):
if isinstance(artist, Artist):
artist = artist.name
return Track.search(self, track, artist, limit)
def getUser(self, name):
user = None
try:
user = User(self, name = name)
user.friends
except LastfmError, e:
raise e
return user
def _fetchUrl(self,
url,
parameters = None,
no_cache = False):
'''Fetch a URL, optionally caching for a specified time.
Args:
url: The URL to retrieve
parameters: A dict of key/value pairs that should added to
the query string. [OPTIONAL]
no_cache: If true, overrides the cache on the current request
Returns:
A string containing the body of the response.
'''
# Add key/value parameters to the query string of the url
url = self._BuildUrl(url, extra_params=parameters)
if self._debug:
print url
# Get a url opener that can handle basic auth
opener = self._GetOpener(url)
def readUrlData():
now = datetime.now()
delta = now - self._lastFetchTime
delta = delta.seconds + float(delta.microseconds)/1000000
if delta < Api.FETCH_INTERVAL:
time.sleep(Api.FETCH_INTERVAL - delta)
url_data = opener.open(url).read()
self._lastFetchTime = datetime.now()
return url_data
# Open and return the URL immediately if we're not going to cache
if no_cache or not self._cache or not self._cache_timeout:
try:
url_data = readUrlData()
except urllib2.HTTPError, e:
url_data = e.read()
else:
# Unique keys are a combination of the url and the username
key = url.encode('utf-8')
# See if it has been cached before
last_cached = self._cache.GetCachedTime(key)
# If the cached version is outdated then fetch another and store it
if not last_cached or time.time() >= last_cached + self._cache_timeout:
try:
url_data = readUrlData()
except urllib2.HTTPError, e:
url_data = e.read()
self._cache.Set(key, url_data)
else:
url_data = self._cache.Get(key)
# Always return the latest version
return url_data
def _fetchData(self,
params,
no_cache = False):
params.update({'api_key': self.__apiKey})
xml = self._fetchUrl(Api.API_ROOT_URL, params, no_cache = self._no_cache or no_cache)
#print xml
try:
data = ElementTree.XML(xml)
except SyntaxError, e:
raise LastfmOperationFailedError("Error in parsing XML: %s" % e)
if data.get('status') != "ok":
code = int(data.find("error").get('code'))
message = data.findtext('error')
if code in errorMap.keys():
raise errorMap[code](message, code)
else:
raise LastfmError(message, code)
return data
def __repr__(self):
return "<lastfm.Api: %s>" % self.__apiKey
from datetime import datetime
import sys
import time
import urllib
import urllib2
import urlparse
from album import Album
from artist import Artist
from error import errorMap, LastfmError, LastfmOperationFailedError, LastfmInvalidResourceError
from event import Event
from filecache import FileCache
from geo import Location, Country
from group import Group
from playlist import Playlist
from tag import Tag
from tasteometer import Tasteometer
from track import Track
from user import User
if sys.version.startswith('2.5'):
import xml.etree.cElementTree as ElementTree
else:
try:
import cElementTree as ElementTree
except ImportError:
try:
import ElementTree
except ImportError:
raise LastfmError("Install ElementTree package for using python-lastfm")