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