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
11 """The class representing the last.fm web services API."""
12
13 DEFAULT_CACHE_TIMEOUT = 3600
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
69 """
70 The last.fm API key
71 @rtype: L{str}
72 """
73 return self._api_key
74
75 @property
77 """
78 The last.fm API secret
79 @rtype: L{str}
80 """
81 return self._secret
82
83 @property
85 """
86 Session key for the authenticated session
87 @rtype: L{str}
88 """
89 return self._session_key
90
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
323
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
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
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
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
484 (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
485 path = path.replace(' ', '+')
486
487
488 if path_elements:
489
490 p = [i for i in path_elements if i]
491 if not path.endswith('/'):
492 path += '/'
493 path += '/'.join(p)
494
495
496 if extra_params and len(extra_params) > 0:
497 extra_query = self._encode_parameters(extra_params)
498
499 if query:
500 query += '&' + extra_query
501 else:
502 query = extra_query
503
504
505 return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
506
508 if request_headers:
509 self._request_headers = request_headers
510 else:
511 self._request_headers = {}
512
514 user_agent = 'Python-urllib/%s (python-lastfm/%s)' % \
515 (self._urllib.__version__, __version__)
516 self.set_user_agent(user_agent)
517
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
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
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
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
554 url = self._build_url(url, extra_params=parameters)
555 if self._debug:
556 print url
557
558 opener = self._get_opener(url)
559
560
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
568 key = url.encode('utf-8')
569
570
571 last_cached = self._cache.GetCachedTime(key)
572
573
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
584 return url_data
585
586 - def _fetch_data(self,
587 params,
588 sign = False,
589 session = False,
590 no_cache = False):
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
642
657
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
685 return md5.new(string).hexdigest()
686 else:
687 from hashlib import md5
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