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