Package lastfm :: Module api

[hide private]
[frames] | no frames]

# Source Code for Module lastfm.api

      1  #!/usr/bin/env python
      2  """The last.fm web service API access functionalities"""
      3
      4  __author__ = "Abhinav Sarkar <abhinav@abhinavsarkar.net>"
      5  __version__ = "0.2"
      6  __license__ = "GNU Lesser General Public License"
      7
      8  from lastfm.base import LastfmBase
      9  from lastfm.decorators import cached_property
     10
     11 -class Api(object):
     12      """The class representing the last.fm web services API."""
     13
     14      DEFAULT_CACHE_TIMEOUT = 3600 # cache for 1 hour
     15      """Default file cache timeout, in seconds"""
     16
     17      API_ROOT_URL = "http://ws.audioscrobbler.com/2.0/"
     18      """URL of the webservice API root"""
     19
     20      FETCH_INTERVAL = 1
     21      """The minimum interval between successive HTTP request, in seconds"""
     22
     23      SEARCH_XMLNS = "http://a9.com/-/spec/opensearch/1.1/"
     24
     25 -    def __init__(self,
     26                   api_key,
     27                   secret = None,
     28                   session_key = None,
     29                   input_encoding=None,
     30                   request_headers=None,
     31                   no_cache = False,
     32                   debug = False):
     33          """
     34          Create an Api object to access the last.fm webservice API. Use this object as a
     35          starting point for accessing all the webservice methods.
     36
     37          @param api_key:            last.fm API key
     38          @type api_key:             L{str}
     39          @param secret:             last.fm API secret (optional, required only for
     40                                     authenticated webservice methods)
     41          @type secret:              L{str}
     42          @param session_key:        session key for the authenticated session (optional,
     43                                     required only for authenticated webservice methods)
     44          @type session_key:         L{str}
     45          @param input_encoding:     encoding of the input data (optional)
     46          @type input_encoding:      L{str}
     47          @param request_headers:    HTTP headers for the requests to last.fm webservices
     48                                     (optional)
     49          @type request_headers:     L{dict}
     50          @param no_cache:           flag to switch off file cache (optional)
     51          @type no_cache:            L{bool}
     52          @param debug:              flag to switch on debugging (optional)
     53          @type debug:               L{bool}
     54          """
     55          self._api_key = api_key
     56          self._secret = secret
     57          self._session_key = session_key
     58          self._cache = FileCache()
     59          self._urllib = urllib2
     60          self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
     61          self._initialize_request_headers(request_headers)
     62          self._initialize_user_agent()
     63          self._input_encoding = input_encoding
     64          self._no_cache = no_cache
     65          self._debug = debug
     66          self._last_fetch_time = datetime.now()
     67
     68      @property
     69 -    def api_key(self):
     70          """
     71          The last.fm API key
     72          @rtype: L{str}
     73          """
     74          return self._api_key
     75
     76      @property
     77 -    def secret(self):
     78          """
     79          The last.fm API secret
     80          @rtype: L{str}
     81          """
     82          return self._secret
     83
     84 -    def set_secret(self, secret):
     85          """
     86          Set the last.fm API secret.
     87
     88          @param secret:    the secret
     89          @type secret:     L{str}
     90          """
     91          self._secret = secret
     92
     93      @property
     94 -    def session_key(self):
     95          """
     96          Session key for the authenticated session
     97          @rtype: L{str}
     98          """
     99          return self._session_key
    100
    101 -    def set_session_key(self, session_key = None):
    102          """
    103          Set the session key for the authenticated session.
    104
    105          @param session_key: the session key for authentication (optional). If not provided then
    106                              a new one is fetched from last.fm
    107          @type session_key: L{str}
    108
    109          @raise lastfm.AuthenticationFailedError: Either session_key should be provided or
    110                                                   API secret must be present.
    111          """
    112          if session_key is not None:
    113              self._session_key = session_key
    114          else:
    115              params = {'method': 'auth.getSession', 'token': self.auth_token}
    116              self._session_key = self._fetch_data(params, sign = True).findtext('session/key')
    117              self._auth_token = None
    118
    119      @cached_property
    120 -    def auth_token(self):
    121          """
    122          The authentication token for the authenticated session.
    123          @rtype: L{str}
    124          """
    125          params = {'method': 'auth.getToken'}
    126          return self._fetch_data(params, sign = True).findtext('token')
    127
    128      @cached_property
    129 -    def auth_url(self):
    130          """
    131          The authentication URL for the authenticated session.
    132          @rtype: L{str}
    133          """
    134          return "http://www.last.fm/api/auth/?api_key=%s&token=%s" % (self.api_key, self.auth_token)
    135
    136 -    def set_cache(self, cache):
    137          """
    138          Override the default cache.  Set to None to prevent caching.
    139
    140          @param cache: an instance that supports the same API as the L{FileCache}
    141          @type cache: L{FileCache}
    142          """
    143          self._cache = cache
    144
    145 -    def set_urllib(self, urllib):
    146          """
    147          Override the default urllib implementation.
    148
    149          @param urllib: an instance that supports the same API as the urllib2 module
    150          @type urllib: urllib2
    151          """
    152          self._urllib = urllib
    153
    154 -    def set_cache_timeout(self, cache_timeout):
    155          """
    156          Override the default cache timeout.
    157
    158          @param cache_timeout: time, in seconds, that responses should be reused
    159          @type cache_timeout: L{int}
    160          """
    161          self._cache_timeout = cache_timeout
    162
    163 -    def set_user_agent(self, user_agent):
    164          """
    165          Override the default user agent.
    166
    167          @param user_agent: a string that should be send to the server as the User-agent
    168          @type user_agent: L{str}
    169          """
    170          self._request_headers['User-Agent'] = user_agent
    171
    172 -    def get_album(self,
    173                   album = None,
    174                   artist = None,
    175                   mbid = None):
    176          """
    177          Get an album object.
    178
    179          @param album:    the album name
    180          @type album:     L{str}
    181          @param artist:   the album artist name
    182          @type artist:    L{str} OR L{Artist}
    183          @param mbid:     MBID of the album
    184          @type mbid:      L{str}
    185
    186          @return:         an Album object corresponding the provided album name
    187          @rtype:          L{Album}
    188
    189          @raise lastfm.InvalidParametersError: Either album and artist parameters or
    190                                                mbid parameter has to be provided.
    191                                                Otherwise exception is raised.
    192
    193          @see:            L{Album.get_info}
    194          """
    195          if isinstance(artist, Artist):
    196              artist = artist.name
    197          return Album.get_info(self, artist, album, mbid)
    198
    199 -    def search_album(self, album, limit = None):
    200          """
    201          Search for an album by name.
    202
    203          @param album:     the album name
    204          @type album:      L{str}
    205          @param limit:     maximum number of results returned (optional)
    206          @type limit:      L{int}
    207
    208          @return:          matches sorted by relevance
    209          @rtype:           L{lazylist} of L{Album}
    210
    211          @see:             L{Album.search}
    212          """
    213          return Album.search(self, search_item = album, limit = limit)
    214
    215 -    def get_artist(self,
    216                    artist = None,
    217                    mbid = None):
    218          """
    219          Get an artist object.
    220
    221          @param artist:    the artist name
    222          @type artist:     L{str}
    223          @param mbid:      MBID of the artist
    224          @type mbid:       L{str}
    225
    226          @return:         an Artist object corresponding the provided artist name
    227          @rtype:          L{Artist}
    228
    229          @raise lastfm.InvalidParametersError: either artist or mbid parameter has
    230                                                to be provided. Otherwise exception is raised.
    231
    232          @see:            L{Artist.get_info}
    233          """
    234          return Artist.get_info(self, artist, mbid)
    235
    236 -    def search_artist(self,
    237                       artist,
    238                       limit = None):
    239          """
    240          Search for an artist by name.
    241
    242          @param artist:    the artist name
    243          @type artist:     L{str}
    244          @param limit:     maximum number of results returned (optional)
    245          @type limit:      L{int}
    246
    247          @return:          matches sorted by relevance
    248          @rtype:           L{lazylist} of L{Artist}
    249
    250          @see:             L{Artist.search}
    251          """
    252          return Artist.search(self, search_item = artist, limit = limit)
    253
    254 -    def get_event(self, event):
    255          """
    256          Get an event object.
    257
    258          @param event:     the event id
    259          @type event:      L{int}
    260
    261          @return:          an event object corresponding to the event id provided
    262          @rtype:           L{Event}
    263
    264          @raise InvalidParametersError: Exception is raised if an invalid event id is supplied.
    265
    266          @see:             L{Event.get_info}
    267          """
    268          return Event.get_info(self, event)
    269
    270 -    def get_location(self, city):
    271          """
    272          Get a location object.
    273
    274          @param city:    the city name
    275          @type city:     L{str}
    276
    277          @return:        a location object corresponding to the city name provided
    278          @rtype:         L{Location}
    279          """
    280          return Location(self, city = city)
    281
    282 -    def get_country(self, name):
    283          """
    284          Get a country object.
    285
    286          @param name:    the country name
    287          @type name:     L{str}
    288
    289          @return:        a country object corresponding to the country name provided
    290          @rtype:         L{Country}
    291          """
    292          return Country(self, name = name)
    293
    294 -    def get_group(self, name):
    295          """
    296          Get a group object.
    297
    298          @param name:    the group name
    299          @type name:     L{str}
    300
    301          @return:        a group object corresponding to the group name provided
    302          @rtype:         L{Group}
    303          """
    304          return Group(self, name = name)
    305
    306 -    def get_playlist(self, url):
    307          """
    308          Get a playlist object.
    309
    310          @param url:    lastfm url of the playlist
    311          @type url:     L{str}
    312
    313          @return:        a playlist object corresponding to the playlist url provided
    314          @rtype:         L{Playlist}
    315
    316          @see:           L{Playlist.fetch}
    317          """
    318          return Playlist.fetch(self, url)
    319
    320 -    def get_tag(self, name):
    321          """
    322          Get a tag object.
    323
    324          @param name:    the tag name
    325          @type name:     L{str}
    326
    327          @return:        a tag object corresponding to the tag name provided
    328          @rtype:         L{Tag}
    329          """
    330          return Tag(self, name = name)
    331
    332 -    def get_global_top_tags(self):
    333          """
    334          Get the top global tags on Last.fm, sorted by popularity (number of times used).
    335
    336          @return:        a list of top global tags
    337          @rtype:         L{list} of L{Tag}
    338          """
    339          return Tag.get_top_tags(self)
    340
    341 -    def search_tag(self,
    342                    tag,
    343                    limit = None):
    344          """
    345          Search for a tag by name.
    346
    347          @param tag:       the tag name
    348          @type tag:        L{str}
    349          @param limit:     maximum number of results returned (optional)
    350          @type limit:      L{int}
    351
    352          @return:          matches sorted by relevance
    353          @rtype:           L{lazylist} of L{Tag}
    354
    355          @see:             L{Tag.search}
    356          """
    357          return Tag.search(self, search_item = tag, limit = limit)
    358
    359 -    def compare_taste(self,
    360                       type1, type2,
    361                       value1, value2,
    362                       limit = None):
    363          """
    364          Get a Tasteometer score from two inputs, along with a list of
    365          shared artists. If the input is a User or a Myspace URL, some
    366          additional information is returned.
    367
    368          @param type1:    'user' OR 'artists' OR 'myspace'
    369          @type type1:     L{str}
    370          @param type2:    'user' OR 'artists' OR 'myspace'
    371          @type type2:     L{str}
    372          @param value1:   Last.fm username OR Comma-separated artist names OR MySpace profile URL
    373          @type value1:    L{str}
    374          @param value2:   Last.fm username OR Comma-separated artist names OR MySpace profile URL
    375          @type value2:    L{str}
    376          @param limit:    maximum number of results returned (optional)
    377          @type limit:     L{int}
    378
    379          @return:         the taste-o-meter score for the inputs
    380          @rtype:          L{Tasteometer}
    381
    382          @see:            L{Tasteometer.compare}
    383          """
    384          return Tasteometer.compare(self, type1, type2, value1, value2, limit)
    385
    386 -    def get_track(self, track, artist = None, mbid = None):
    387          """
    388          Get a track object.
    389
    390          @param track:    the track name
    391          @type track:     L{str}
    392          @param artist:   the track artist
    393          @type artist:    L{str} OR L{Artist}
    394          @param mbid:     MBID of the track
    395          @type mbid:      L{str}
    396
    397          @return:         a track object corresponding to the track name provided
    398          @rtype:          L{Track}
    399
    400          @raise lastfm.InvalidParametersError: either artist or mbid parameter has
    401                                                to be provided. Otherwise exception is raised.
    402
    403          @see:            L{Track.get_info}
    404          """
    405          if isinstance(artist, Artist):
    406              artist = artist.name
    407          return Track.get_info(self, artist, track, mbid)
    408
    409 -    def search_track(self,
    410                      track,
    411                      artist = None,
    412                      limit = None):
    413          """
    414          Search for a track by name.
    415
    416          @param track:     the track name
    417          @type track:      L{str}
    418          @param artist:    the track artist (optional)
    419          @type artist:     L{str} OR L{Artist}
    420          @param limit:     maximum number of results returned (optional)
    421          @type limit:      L{int}
    422
    423          @return:          matches sorted by relevance
    424          @rtype:           L{lazylist} of L{Track}
    425
    426          @see:             L{Track.search}
    427          """
    428          if isinstance(artist, Artist):
    429              artist = artist.name
    430          return Track.search(self, search_item = track, limit = limit, artist = artist)
    431
    432 -    def get_user(self, name):
    433          """
    434          Get an user object.
    435
    436          @param name:    the last.fm user name
    437          @type name:     L{str}
    438
    439          @return:        an user object corresponding to the user name provided
    440          @rtype:         L{User}
    441
    442          @raise InvalidParametersError: Exception is raised if an invalid user name is supplied.
    443
    444          @see:           L{User.get_info}
    445          """
    446          return User.get_info(self, name = name)
    447
    448 -    def get_authenticated_user(self):
    449          """
    450          Get the currently authenticated user.
    451
    452          @return:     The currently authenticated user if the session is authenticated
    453          @rtype:      L{User}
    454
    455          @see:        L{User.get_authenticated_user}
    456          """
    457          if self.session_key is not None:
    458              return User.get_authenticated_user(self)
    459          else:
    460              raise AuthenticationFailedError("session key must be present to call this method")
    461
    462 -    def get_venue(self, venue):
    463          """
    464          Get a venue object.
    465
    466          @param venue:    the venue name
    467          @type venue:     L{str}
    468
    469          @return:         a venue object corresponding to the venue name provided
    470          @rtype:          L{Venue}
    471
    472          @raise InvalidParametersError: Exception is raised if an non-existant venue name is supplied.
    473
    474          @see:            L{search_venue}
    475          """
    476          try:
    477              return self.search_venue(venue)[0]
    478          except IndexError:
    479              raise InvalidParametersError("No such venue exists")
    480
    481 -    def search_venue(self, venue, limit = None, country = None):
    482          """
    483          Search for a venue by name.
    484
    485          @param venue:     the venue name
    486          @type venue:      L{str}
    487          @param country:   filter the results by country. Expressed as an ISO 3166-2 code.
    488                            (optional)
    489          @type country:    L{str}
    490          @param limit:     maximum number of results returned (optional)
    491          @type limit:      L{int}
    492
    493          @return:          matches sorted by relevance
    494          @rtype:           L{lazylist} of L{Venue}
    495
    496          @see:             L{Venue.search}
    497          """
    498          return Venue.search(self, search_item = venue, limit = limit, country = country)
    499
    500 -    def _build_url(self, url, path_elements=None, extra_params=None):
    501          # Break url into consituent parts
    502          (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
    503          path = path.replace(' ', '+')
    504
    505          # Add any additional path elements to the path
    506          if path_elements:
    507              # Filter out the path elements that have a value of None
    508              p = [i for i in path_elements if i]
    509              if not path.endswith('/'):
    510                  path += '/'
    511                  path += '/'.join(p)
    512
    513          # Add any additional query parameters to the query string
    514          if extra_params and len(extra_params) > 0:
    515              extra_query = self._encode_parameters(extra_params)
    516              # Add it to the existing query
    517              if query:
    518                  query += '&' + extra_query
    519              else:
    520                  query = extra_query
    521
    522          # Return the rebuilt URL
    523          return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
    524
    525 -    def _initialize_request_headers(self, request_headers):
    526          if request_headers:
    527              self._request_headers = request_headers
    528          else:
    529              self._request_headers = {}
    530
    531 -    def _initialize_user_agent(self):
    532          user_agent = 'Python-urllib/%s (python-lastfm/%s)' % \
    533                       (self._urllib.__version__, __version__)
    534          self.set_user_agent(user_agent)
    535
    536 -    def _get_opener(self, url):
    537          opener = self._urllib.build_opener()
    538          if self._urllib._opener is not None:
    539              opener = self._urllib.build_opener(*self._urllib._opener.handlers)
    540          opener.addheaders = self._request_headers.items()
    541          return opener
    542
    543 -    def _encode(self, s):
    544          if self._input_encoding:
    545              return unicode(s, self._input_encoding).encode('utf-8')
    546          else:
    547              return unicode(s).encode('utf-8')
    548
    549 -    def _encode_parameters(self, parameters):
    550          if parameters is None:
    551              return None
    552          else:
    553              keys = parameters.keys()
    554              keys.sort()
    555              return urllib.urlencode([(k, self._encode(parameters[k])) for k in keys if parameters[k] is not None])
    556
    557 -    def _read_url_data(self, opener, url, data = None):
    558          now = datetime.now()
    559          delta = now - self._last_fetch_time
    560          delta = delta.seconds + float(delta.microseconds)/1000000
    561          if delta < Api.FETCH_INTERVAL:
    562              time.sleep(Api.FETCH_INTERVAL - delta)
    563          url_data = opener.open(url, data).read()
    564          self._last_fetch_time = datetime.now()
    565          return url_data
    566
    567 -    def _fetch_url(self,
    568                    url,
    569                    parameters = None,
    570                    no_cache = False):
    571          # Add key/value parameters to the query string of the url
    572          url = self._build_url(url, extra_params=parameters)
    573          if self._debug:
    574              print url
    575          # Get a url opener that can handle basic auth
    576          opener = self._get_opener(url)
    577
    578          # Open and return the URL immediately if we're not going to cache
    579          if no_cache or not self._cache or not self._cache_timeout:
    580              try:
    581                  url_data = self._read_url_data(opener, url)
    582              except urllib2.HTTPError, e:
    583                  url_data = e.read()
    584          else:
    585              # Unique keys are a combination of the url and the username
    586              key = url.encode('utf-8')
    587
    588              # See if it has been cached before
    589              last_cached = self._cache.GetCachedTime(key)
    590
    591              # If the cached version is outdated then fetch another and store it
    592              if not last_cached or time.time() >= last_cached + self._cache_timeout:
    593                  try:
    594                      url_data = self._read_url_data(opener, url)
    595                  except urllib2.HTTPError, e:
    596                      url_data = e.read()
    597                  self._cache.Set(key, url_data)
    598              else:
    599                  url_data = self._cache.Get(key)
    600
    601          # Always return the latest version
    602          return url_data
    603
    604 -    def _fetch_data(self,
    605                     params,
    606                     sign = False,
    607                     session = False,
    608                     no_cache = False):
    609          params = params.copy()
    610          params['api_key'] = self.api_key
    611
    612          if session:
    613              if self.session_key is not None:
    614                  params['sk'] = self.session_key
    615              else:
    616                  raise AuthenticationFailedError("session key must be present to call this method")
    617
    618          if sign:
    619              params['api_sig'] = self._get_api_sig(params)
    620
    621          xml = self._fetch_url(Api.API_ROOT_URL, params, no_cache = self._no_cache or no_cache)
    622          return self._check_xml(xml)
    623
    624 -    def _post_url(self,
    625                   url,
    626                   parameters):
    627          url = self._build_url(url)
    628          data = self._encode_parameters(parameters)
    629          if self._debug:
    630              print data
    631          opener = self._get_opener(url)
    632          url_data = self._read_url_data(opener, url, data)
    633          return url_data
    634
    635 -    def _post_data(self, params):
    636          params['api_key'] = self.api_key
    637
    638          if self.session_key is not None:
    639              params['sk'] = self.session_key
    640          else:
    641              raise AuthenticationFailedError("session key must be present to call this method")
    642
    643          params['api_sig'] = self._get_api_sig(params)
    644          xml = self._post_url(Api.API_ROOT_URL, params)
    645          return self._check_xml(xml)
    646
    647 -    def _get_api_sig(self, params):
    648          if self.secret is not None:
    649                  keys = params.keys()[:]
    650                  keys.sort()
    651                  sig = unicode()
    652                  for name in keys:
    653                      if name == 'api_sig': continue
    654                      sig += ("%s%s" % (name, params[name]))
    655                  sig += self.secret
    656                  hashed_sig = md5hash(sig)
    657                  return hashed_sig
    658          else:
    659              raise AuthenticationFailedError("api secret must be present to call this method")
    660
    661 -    def _check_xml(self, xml):
    662          data = None
    663          try:
    664              data = ElementTree.XML(xml)
    665          except SyntaxError, e:
    666              raise OperationFailedError("Error in parsing XML: %s" % e)
    667          if data.get('status') != "ok":
    668              code = int(data.find("error").get('code'))
    669              message = data.findtext('error')
    670              if code in error_map.keys():
    671                  raise error_map[code](message, code)
    672              else:
    673                  raise LastfmError(message, code)
    674          return data
    675
    676 -    def __repr__(self):
    677          return "<lastfm.Api: %s>" % self._api_key
    678
    679  from datetime import datetime
    680  import sys
    681  import time
    682  import urllib
    683  import urllib2
    684  import urlparse
    685
    686  from lastfm.album import Album
    687  from lastfm.artist import Artist
    688  from lastfm.error import error_map, LastfmError, OperationFailedError, AuthenticationFailedError
    689  from lastfm.event import Event
    690  from lastfm.filecache import FileCache
    691  from lastfm.geo import Location, Country
    692  from lastfm.group import Group
    693  from lastfm.playlist import Playlist
    694  from lastfm.tag import Tag
    695  from lastfm.tasteometer import Tasteometer
    696  from lastfm.track import Track
    697  from lastfm.user import User
    698  from lastfm.venue import Venue
    699
    700  if sys.version < '2.6':
    701      import md5
    702 -    def md5hash(string):
    703          return md5.new(string).hexdigest()
    704  else:
    705      from hashlib import md5
    706 -    def md5hash(string):
    707          return md5(string).hexdigest()
    708
    709  if sys.version_info >= (2, 5):
    710      import xml.etree.cElementTree as ElementTree
    711  else:
    712      try:
    713          import cElementTree as ElementTree
    714      except ImportError:
    715          try:
    716              import ElementTree
    717          except ImportError:
    718              raise LastfmError("Install ElementTree package for using python-lastfm")
    719