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