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: 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
69 """The last.fm API key"""
70 return self._api_key
71
72 @property
74 """The last.fm API secret"""
75 return self._secret
76
77 @property
79 """Session key for the authenticated session."""
80 return self._session_key
81
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
308
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
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
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
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
469 (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
470 path = path.replace(' ', '+')
471
472
473 if path_elements:
474
475 p = [i for i in path_elements if i]
476 if not path.endswith('/'):
477 path += '/'
478 path += '/'.join(p)
479
480
481 if extra_params and len(extra_params) > 0:
482 extra_query = self._encode_parameters(extra_params)
483
484 if query:
485 query += '&' + extra_query
486 else:
487 query = extra_query
488
489
490 return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
491
493 if request_headers:
494 self._request_headers = request_headers
495 else:
496 self._request_headers = {}
497
499 user_agent = 'Python-urllib/%s (python-lastfm/%s)' % \
500 (self._urllib.__version__, __version__)
501 self.set_user_agent(user_agent)
502
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
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
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
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
539 url = self._build_url(url, extra_params=parameters)
540 if self._debug:
541 print url
542
543 opener = self._get_opener(url)
544
545
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
553 key = url.encode('utf-8')
554
555
556 last_cached = self._cache.GetCachedTime(key)
557
558
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
569 return url_data
570
571 - def _fetch_data(self,
572 params,
573 sign = False,
574 session = False,
575 no_cache = False):
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
627
642
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
670 return md5.new(string).hexdigest()
671 else:
672 from hashlib import md5
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