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