1
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
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
70 """
71 The last.fm API key
72 @rtype: L{str}
73 """
74 return self._api_key
75
76 @property
78 """
79 The last.fm API secret
80 @rtype: L{str}
81 """
82 return self._secret
83
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
95 """
96 Session key for the authenticated session
97 @rtype: L{str}
98 """
99 return self._session_key
100
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
340
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
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
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
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
502 (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
503 path = path.replace(' ', '+')
504
505
506 if path_elements:
507
508 p = [i for i in path_elements if i]
509 if not path.endswith('/'):
510 path += '/'
511 path += '/'.join(p)
512
513
514 if extra_params and len(extra_params) > 0:
515 extra_query = self._encode_parameters(extra_params)
516
517 if query:
518 query += '&' + extra_query
519 else:
520 query = extra_query
521
522
523 return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
524
526 if request_headers:
527 self._request_headers = request_headers
528 else:
529 self._request_headers = {}
530
532 user_agent = 'Python-urllib/%s (python-lastfm/%s)' % \
533 (self._urllib.__version__, __version__)
534 self.set_user_agent(user_agent)
535
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
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
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
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
572 url = self._build_url(url, extra_params=parameters)
573 if self._debug:
574 print url
575
576 opener = self._get_opener(url)
577
578
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
586 key = url.encode('utf-8')
587
588
589 last_cached = self._cache.GetCachedTime(key)
590
591
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
602 return url_data
603
604 - def _fetch_data(self,
605 params,
606 sign = False,
607 session = False,
608 no_cache = False):
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
660
675
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
703 return md5.new(string).hexdigest()
704 else:
705 from hashlib import md5
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