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, 181 album, 182 limit = None):
183 """ 184 Search for an album by name. 185 186 @param album: the album name 187 @type album: L{str} 188 @param limit: maximum number of results returned (optional) 189 @type limit: L{int} 190 191 @return: matches sorted by relevance 192 @rtype: L{lazylist} of L{Album} 193 194 @see: L{Album.search} 195 """ 196 return Album.search(self, search_item = album, limit = limit)
197
198 - def get_artist(self, 199 artist = None, 200 mbid = None):
201 """ 202 Get an artist object. 203 204 @param artist: the artist name 205 @type artist: L{str} 206 @param mbid: MBID of the artist 207 @type mbid: L{str} 208 209 @return: an Artist object corresponding the provided artist name 210 @rtype: L{Artist} 211 212 @raise lastfm.InvalidParametersError: either artist or mbid parameter has 213 to be provided. Otherwise exception is raised. 214 215 @see: L{Artist.get_info} 216 """ 217 return Artist.get_info(self, artist, mbid)
218
219 - def search_artist(self, 220 artist, 221 limit = None):
222 """ 223 Search for an artist by name. 224 225 @param artist: the artist name 226 @type artist: L{str} 227 @param limit: maximum number of results returned (optional) 228 @type limit: L{int} 229 230 @return: matches sorted by relevance 231 @rtype: L{lazylist} of L{Artist} 232 233 @see: L{Artist.search} 234 """ 235 return Artist.search(self, search_item = artist, limit = limit)
236
237 - def get_event(self, event):
238 """ 239 Get an event object. 240 241 @param event: the event id 242 @type event: L{int} 243 244 @return: an event object corresponding to the event id provided 245 @rtype: L{Event} 246 247 @raise InvalidParametersError: Exception is raised if an invalid event id is supplied. 248 249 @see: L{Event.get_info} 250 """ 251 return Event.get_info(self, event)
252
253 - def get_location(self, city):
254 """ 255 Get a location object. 256 257 @param city: the city name 258 @type city: L{str} 259 260 @return: a location object corresponding to the city name provided 261 @rtype: L{Location} 262 """ 263 return Location(self, city = city)
264
265 - def get_country(self, name):
266 """ 267 Get a country object. 268 269 @param name: the country name 270 @type name: L{str} 271 272 @return: a country object corresponding to the country name provided 273 @rtype: L{Country} 274 """ 275 return Country(self, name = name)
276
277 - def get_group(self, name):
278 """ 279 Get a group object. 280 281 @param name: the group name 282 @type name: L{str} 283 284 @return: a group object corresponding to the group name provided 285 @rtype: L{Group} 286 """ 287 return Group(self, name = name)
288
289 - def get_playlist(self, url):
290 """ 291 Get a playlist object. 292 293 @param url: lastfm url of the playlist 294 @type url: L{str} 295 296 @return: a playlist object corresponding to the playlist url provided 297 @rtype: L{Playlist} 298 299 @see: L{Playlist.fetch} 300 """ 301 return Playlist.fetch(self, url)
302
303 - def get_tag(self, name):
304 """ 305 Get a tag object. 306 307 @param name: the tag name 308 @type name: L{str} 309 310 @return: a tag object corresponding to the tag name provided 311 @rtype: L{Tag} 312 """ 313 return Tag(self, name = name)
314
315 - def get_global_top_tags(self):
316 """ 317 Get the top global tags on Last.fm, sorted by popularity (number of times used). 318 319 @return: a list of top global tags 320 @rtype: L{list} of L{Tag} 321 """ 322 return Tag.get_top_tags(self)
323
324 - def search_tag(self, 325 tag, 326 limit = None):
327 """ 328 Search for a tag by name. 329 330 @param tag: the tag name 331 @type tag: L{str} 332 @param limit: maximum number of results returned (optional) 333 @type limit: L{int} 334 335 @return: matches sorted by relevance 336 @rtype: L{lazylist} of L{Tag} 337 338 @see: L{Tag.search} 339 """ 340 return Tag.search(self, search_item = tag, limit = limit)
341
342 - def compare_taste(self, 343 type1, type2, 344 value1, value2, 345 limit = None):
346 """ 347 Get a Tasteometer score from two inputs, along with a list of 348 shared artists. If the input is a User or a Myspace URL, some 349 additional information is returned. 350 351 @param type1: 'user' OR 'artists' OR 'myspace' 352 @type type1: L{str} 353 @param type2: 'user' OR 'artists' OR 'myspace' 354 @type type2: L{str} 355 @param value1: Last.fm username OR Comma-separated artist names OR MySpace profile URL 356 @type value1: L{str} 357 @param value2: Last.fm username OR Comma-separated artist names OR MySpace profile URL 358 @type value2: L{str} 359 @param limit: maximum number of results returned (optional) 360 @type limit: L{int} 361 362 @return: the taste-o-meter score for the inputs 363 @rtype: L{Tasteometer} 364 365 @see: L{Tasteometer.compare} 366 """ 367 return Tasteometer.compare(self, type1, type2, value1, value2, limit)
368
369 - def get_track(self, track, artist = None, mbid = None):
370 """ 371 Get a track object. 372 373 @param track: the track name 374 @type track: L{str} 375 @param artist: the track artist 376 @type artist: L{str} OR L{Artist} 377 @param mbid: MBID of the track 378 @type mbid: L{str} 379 380 @return: a track object corresponding to the track name provided 381 @rtype: L{Track} 382 383 @raise lastfm.InvalidParametersError: either artist or mbid parameter has 384 to be provided. Otherwise exception is raised. 385 386 @see: L{Track.get_info} 387 """ 388 if isinstance(artist, Artist): 389 artist = artist.name 390 return Track.get_info(self, artist, track, mbid)
391
392 - def search_track(self, 393 track, 394 artist = None, 395 limit = None):
396 """ 397 Search for a track by name. 398 399 @param track: the track name 400 @type track: L{str} 401 @param artist: the track artist (optional) 402 @type artist: L{str} OR L{Artist} 403 @param limit: maximum number of results returned (optional) 404 @type limit: L{int} 405 406 @return: matches sorted by relevance 407 @rtype: L{lazylist} of L{Track} 408 409 @see: L{Track.search} 410 """ 411 if isinstance(artist, Artist): 412 artist = artist.name 413 return Track.search(self, search_item = track, limit = limit, artist = artist)
414
415 - def get_user(self, name):
416 """ 417 Get an user object. 418 419 @param name: the last.fm user name 420 @type name: L{str} 421 422 @return: an user object corresponding to the user name provided 423 @rtype: L{User} 424 425 @raise InvalidParametersError: Exception is raised if an invalid user name is supplied. 426 """ 427 user = User(self, name = name) 428 user.friends 429 return user
430
431 - def get_authenticated_user(self):
432 """ 433 Get the currently authenticated user. 434 435 @return: The currently authenticated user if the session is authenticated 436 @rtype: L{User} 437 438 @see: L{User.get_authenticated_user} 439 """ 440 if self.session_key is not None: 441 return User.get_authenticated_user(self) 442 return None
443
444 - def get_venue(self, venue):
445 """ 446 Get a venue object. 447 448 @param venue: the venue name 449 @type venue: L{str} 450 451 @return: a venue object corresponding to the venue name provided 452 @rtype: L{Venue} 453 454 @raise InvalidParametersError: Exception is raised if an non-existant venue name is supplied. 455 456 @see: L{search_venue} 457 """ 458 try: 459 return self.search_venue(venue)[0] 460 except IndexError: 461 raise InvalidParametersError("No such venue exists")
462
463 - def search_venue(self, venue, limit = None, country = None):
464 """ 465 Search for a venue by name. 466 467 @param venue: the venue name 468 @type venue: L{str} 469 @param country: filter the results by country. Expressed as an ISO 3166-2 code. 470 (optional) 471 @type country: L{str} 472 @param limit: maximum number of results returned (optional) 473 @type limit: L{int} 474 475 @return: matches sorted by relevance 476 @rtype: L{lazylist} of L{Venue} 477 478 @see: L{Venue.search} 479 """ 480 return Venue.search(self, search_item = venue, limit = limit, country = country)
481
482 - def _build_url(self, url, path_elements=None, extra_params=None):
483 # Break url into consituent parts 484 (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) 485 path = path.replace(' ', '+') 486 487 # Add any additional path elements to the path 488 if path_elements: 489 # Filter out the path elements that have a value of None 490 p = [i for i in path_elements if i] 491 if not path.endswith('/'): 492 path += '/' 493 path += '/'.join(p) 494 495 # Add any additional query parameters to the query string 496 if extra_params and len(extra_params) > 0: 497 extra_query = self._encode_parameters(extra_params) 498 # Add it to the existing query 499 if query: 500 query += '&' + extra_query 501 else: 502 query = extra_query 503 504 # Return the rebuilt URL 505 return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
506
507 - def _initialize_request_headers(self, request_headers):
508 if request_headers: 509 self._request_headers = request_headers 510 else: 511 self._request_headers = {}
512
513 - def _initialize_user_agent(self):
514 user_agent = 'Python-urllib/%s (python-lastfm/%s)' % \ 515 (self._urllib.__version__, __version__) 516 self.set_user_agent(user_agent)
517
518 - def _get_opener(self, url):
519 opener = self._urllib.build_opener() 520 if self._urllib._opener is not None: 521 opener = self._urllib.build_opener(*self._urllib._opener.handlers) 522 opener.addheaders = self._request_headers.items() 523 return opener
524
525 - def _encode(self, s):
526 if self._input_encoding: 527 return unicode(s, self._input_encoding).encode('utf-8') 528 else: 529 return unicode(s).encode('utf-8')
530
531 - def _encode_parameters(self, parameters):
532 if parameters is None: 533 return None 534 else: 535 keys = parameters.keys() 536 keys.sort() 537 return urllib.urlencode([(k, self._encode(parameters[k])) for k in keys if parameters[k] is not None])
538
539 - def _read_url_data(self, opener, url, data = None):
540 now = datetime.now() 541 delta = now - self._last_fetch_time 542 delta = delta.seconds + float(delta.microseconds)/1000000 543 if delta < Api.FETCH_INTERVAL: 544 time.sleep(Api.FETCH_INTERVAL - delta) 545 url_data = opener.open(url, data).read() 546 self._last_fetch_time = datetime.now() 547 return url_data
548
549 - def _fetch_url(self, 550 url, 551 parameters = None, 552 no_cache = False):
553 # Add key/value parameters to the query string of the url 554 url = self._build_url(url, extra_params=parameters) 555 if self._debug: 556 print url 557 # Get a url opener that can handle basic auth 558 opener = self._get_opener(url) 559 560 # Open and return the URL immediately if we're not going to cache 561 if no_cache or not self._cache or not self._cache_timeout: 562 try: 563 url_data = self._read_url_data(opener, url) 564 except urllib2.HTTPError, e: 565 url_data = e.read() 566 else: 567 # Unique keys are a combination of the url and the username 568 key = url.encode('utf-8') 569 570 # See if it has been cached before 571 last_cached = self._cache.GetCachedTime(key) 572 573 # If the cached version is outdated then fetch another and store it 574 if not last_cached or time.time() >= last_cached + self._cache_timeout: 575 try: 576 url_data = self._read_url_data(opener, url) 577 except urllib2.HTTPError, e: 578 url_data = e.read() 579 self._cache.Set(key, url_data) 580 else: 581 url_data = self._cache.Get(key) 582 583 # Always return the latest version 584 return url_data
585
586 - def _fetch_data(self, 587 params, 588 sign = False, 589 session = False, 590 no_cache = False):
591 params = params.copy() 592 params['api_key'] = self.api_key 593 594 if session: 595 if self.session_key is not None: 596 params['sk'] = self.session_key 597 else: 598 raise AuthenticationFailedError("session key must be present to call this method") 599 600 if sign: 601 params['api_sig'] = self._get_api_sig(params) 602 603 xml = self._fetch_url(Api.API_ROOT_URL, params, no_cache = self._no_cache or no_cache) 604 return self._check_xml(xml)
605
606 - def _post_url(self, 607 url, 608 parameters):
609 url = self._build_url(url) 610 data = self._encode_parameters(parameters) 611 if self._debug: 612 print data 613 opener = self._get_opener(url) 614 url_data = self._read_url_data(opener, url, data) 615 return url_data
616
617 - def _post_data(self, params):
618 params['api_key'] = self.api_key 619 620 if self.session_key is not None: 621 params['sk'] = self.session_key 622 else: 623 raise AuthenticationFailedError("session key must be present to call this method") 624 625 params['api_sig'] = self._get_api_sig(params) 626 xml = self._post_url(Api.API_ROOT_URL, params) 627 return self._check_xml(xml)
628
629 - def _get_api_sig(self, params):
630 if self.secret is not None: 631 keys = params.keys()[:] 632 keys.sort() 633 sig = unicode() 634 for name in keys: 635 if name == 'api_sig': continue 636 sig += ("%s%s" % (name, params[name])) 637 sig += self.secret 638 hashed_sig = md5hash(sig) 639 return hashed_sig 640 else: 641 raise AuthenticationFailedError("api secret must be present to call this method")
642
643 - def _check_xml(self, xml):
644 data = None 645 try: 646 data = ElementTree.XML(xml) 647 except SyntaxError, e: 648 raise OperationFailedError("Error in parsing XML: %s" % e) 649 if data.get('status') != "ok": 650 code = int(data.find("error").get('code')) 651 message = data.findtext('error') 652 if code in error_map.keys(): 653 raise error_map[code](message, code) 654 else: 655 raise LastfmError(message, code) 656 return data
657
658 - def __repr__(self):
659 return "<lastfm.Api: %s>" % self._api_key
660 661 from datetime import datetime 662 import sys 663 import time 664 import urllib 665 import urllib2 666 import urlparse 667 668 from lastfm.album import Album 669 from lastfm.artist import Artist 670 from lastfm.error import error_map, LastfmError, OperationFailedError, AuthenticationFailedError 671 from lastfm.event import Event 672 from lastfm.filecache import FileCache 673 from lastfm.geo import Location, Country 674 from lastfm.group import Group 675 from lastfm.playlist import Playlist 676 from lastfm.tag import Tag 677 from lastfm.tasteometer import Tasteometer 678 from lastfm.track import Track 679 from lastfm.user import User 680 from lastfm.venue import Venue 681 682 if sys.version < '2.6': 683 import md5
684 - def md5hash(string):
685 return md5.new(string).hexdigest()
686 else: 687 from hashlib import md5
688 - def md5hash(string):
689 return md5(string).hexdigest()
690 691 if sys.version_info >= (2, 5): 692 import xml.etree.cElementTree as ElementTree 693 else: 694 try: 695 import cElementTree as ElementTree 696 except ImportError: 697 try: 698 import ElementTree 699 except ImportError: 700 raise LastfmError("Install ElementTree package for using python-lastfm") 701