comparison toolboxes/_python_lib/pylast/pylast.py @ 0:e9a9cd732c1e tip

first hg version after svn
author wolffd
date Tue, 10 Feb 2015 15:05:51 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:e9a9cd732c1e
1 # -*- coding: utf-8 -*-
2 #
3 # pylast - A Python interface to Last.fm (and other API compatible social networks)
4 #
5 # Copyright 2008-2010 Amr Hassan
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 # http://code.google.com/p/pylast/
20
21 __version__ = '0.5'
22 __author__ = 'Amr Hassan'
23 __copyright__ = "Copyright (C) 2008-2010 Amr Hassan"
24 __license__ = "apache2"
25 __email__ = 'amr.hassan@gmail.com'
26
27 import hashlib
28 from xml.dom import minidom
29 import xml.dom
30 import time
31 import shelve
32 import tempfile
33 import sys
34 import collections
35 import warnings
36
37 def _deprecation_warning(message):
38 warnings.warn(message, DeprecationWarning)
39
40 if sys.version_info[0] == 3:
41 from http.client import HTTPConnection
42 import html.entities as htmlentitydefs
43 from urllib.parse import splithost as url_split_host
44 from urllib.parse import quote_plus as url_quote_plus
45
46 unichr = chr
47
48 elif sys.version_info[0] == 2:
49 from httplib import HTTPConnection
50 import htmlentitydefs
51 from urllib import splithost as url_split_host
52 from urllib import quote_plus as url_quote_plus
53
54 STATUS_INVALID_SERVICE = 2
55 STATUS_INVALID_METHOD = 3
56 STATUS_AUTH_FAILED = 4
57 STATUS_INVALID_FORMAT = 5
58 STATUS_INVALID_PARAMS = 6
59 STATUS_INVALID_RESOURCE = 7
60 STATUS_TOKEN_ERROR = 8
61 STATUS_INVALID_SK = 9
62 STATUS_INVALID_API_KEY = 10
63 STATUS_OFFLINE = 11
64 STATUS_SUBSCRIBERS_ONLY = 12
65 STATUS_INVALID_SIGNATURE = 13
66 STATUS_TOKEN_UNAUTHORIZED = 14
67 STATUS_TOKEN_EXPIRED = 15
68
69 EVENT_ATTENDING = '0'
70 EVENT_MAYBE_ATTENDING = '1'
71 EVENT_NOT_ATTENDING = '2'
72
73 PERIOD_OVERALL = 'overall'
74 PERIOD_7DAYS = "7day"
75 PERIOD_3MONTHS = '3month'
76 PERIOD_6MONTHS = '6month'
77 PERIOD_12MONTHS = '12month'
78
79 DOMAIN_ENGLISH = 0
80 DOMAIN_GERMAN = 1
81 DOMAIN_SPANISH = 2
82 DOMAIN_FRENCH = 3
83 DOMAIN_ITALIAN = 4
84 DOMAIN_POLISH = 5
85 DOMAIN_PORTUGUESE = 6
86 DOMAIN_SWEDISH = 7
87 DOMAIN_TURKISH = 8
88 DOMAIN_RUSSIAN = 9
89 DOMAIN_JAPANESE = 10
90 DOMAIN_CHINESE = 11
91
92 COVER_SMALL = 0
93 COVER_MEDIUM = 1
94 COVER_LARGE = 2
95 COVER_EXTRA_LARGE = 3
96 COVER_MEGA = 4
97
98 IMAGES_ORDER_POPULARITY = "popularity"
99 IMAGES_ORDER_DATE = "dateadded"
100
101
102 USER_MALE = 'Male'
103 USER_FEMALE = 'Female'
104
105 SCROBBLE_SOURCE_USER = "P"
106 SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R"
107 SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E"
108 SCROBBLE_SOURCE_LASTFM = "L"
109 SCROBBLE_SOURCE_UNKNOWN = "U"
110
111 SCROBBLE_MODE_PLAYED = ""
112 SCROBBLE_MODE_LOVED = "L"
113 SCROBBLE_MODE_BANNED = "B"
114 SCROBBLE_MODE_SKIPPED = "S"
115
116 class _Network(object):
117 """
118 A music social network website that is Last.fm or one exposing a Last.fm compatible API
119 """
120
121 def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash,
122 domain_names, urls):
123 """
124 name: the name of the network
125 homepage: the homepage url
126 ws_server: the url of the webservices server
127 api_key: a provided API_KEY
128 api_secret: a provided API_SECRET
129 session_key: a generated session_key or None
130 submission_server: the url of the server to which tracks are submitted (scrobbled)
131 username: a username of a valid user
132 password_hash: the output of pylast.md5(password) where password is the user's password
133 domain_names: a dict mapping each DOMAIN_* value to a string domain name
134 urls: a dict mapping types to urls
135
136 if username and password_hash were provided and not session_key, session_key will be
137 generated automatically when needed.
138
139 Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
140
141 You should use a preconfigured network object through a get_*_network(...) method instead of creating an object
142 of this class, unless you know what you're doing.
143 """
144
145 self.name = name
146 self.homepage = homepage
147 self.ws_server = ws_server
148 self.api_key = api_key
149 self.api_secret = api_secret
150 self.session_key = session_key
151 self.submission_server = submission_server
152 self.username = username
153 self.password_hash = password_hash
154 self.domain_names = domain_names
155 self.urls = urls
156
157 self.cache_backend = None
158 self.proxy_enabled = False
159 self.proxy = None
160 self.last_call_time = 0
161
162 #generate a session_key if necessary
163 if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash):
164 sk_gen = SessionKeyGenerator(self)
165 self.session_key = sk_gen.get_session_key(self.username, self.password_hash)
166
167 """def __repr__(self):
168 attributes = ("name", "homepage", "ws_server", "api_key", "api_secret", "session_key", "submission_server",
169 "username", "password_hash", "domain_names", "urls")
170
171 text = "pylast._Network(%s)"
172 args = []
173 for attr in attributes:
174 args.append("=".join((attr, repr(getattr(self, attr)))))
175
176 return text % ", ".join(args)
177 """
178
179 def __str__(self):
180 return "The %s Network" %self.name
181
182 def get_artist(self, artist_name):
183 """
184 Return an Artist object
185 """
186
187 return Artist(artist_name, self)
188
189 def get_track(self, artist, title):
190 """
191 Return a Track object
192 """
193
194 return Track(artist, title, self)
195
196 def get_album(self, artist, title):
197 """
198 Return an Album object
199 """
200
201 return Album(artist, title, self)
202
203 def get_authenticated_user(self):
204 """
205 Returns the authenticated user
206 """
207
208 return AuthenticatedUser(self)
209
210 def get_country(self, country_name):
211 """
212 Returns a country object
213 """
214
215 return Country(country_name, self)
216
217 def get_group(self, name):
218 """
219 Returns a Group object
220 """
221
222 return Group(name, self)
223
224 def get_user(self, username):
225 """
226 Returns a user object
227 """
228
229 return User(username, self)
230
231 def get_tag(self, name):
232 """
233 Returns a tag object
234 """
235
236 return Tag(name, self)
237
238 def get_scrobbler(self, client_id, client_version):
239 """
240 Returns a Scrobbler object used for submitting tracks to the server
241
242 Quote from http://www.last.fm/api/submissions:
243 ========
244 Client identifiers are used to provide a centrally managed database of
245 the client versions, allowing clients to be banned if they are found to
246 be behaving undesirably. The client ID is associated with a version
247 number on the server, however these are only incremented if a client is
248 banned and do not have to reflect the version of the actual client application.
249
250 During development, clients which have not been allocated an identifier should
251 use the identifier tst, with a version number of 1.0. Do not distribute code or
252 client implementations which use this test identifier. Do not use the identifiers
253 used by other clients.
254 =========
255
256 To obtain a new client identifier please contact:
257 * Last.fm: submissions@last.fm
258 * # TODO: list others
259
260 ...and provide us with the name of your client and its homepage address.
261 """
262
263 _deprecation_warning("Use _Network.scrobble(...), _Network.scrobble_many(...), and Netowrk.update_now_playing(...) instead")
264
265 return Scrobbler(self, client_id, client_version)
266
267 def _get_language_domain(self, domain_language):
268 """
269 Returns the mapped domain name of the network to a DOMAIN_* value
270 """
271
272 if domain_language in self.domain_names:
273 return self.domain_names[domain_language]
274
275 def _get_url(self, domain, type):
276 return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type])
277
278 def _get_ws_auth(self):
279 """
280 Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple.
281 """
282 return (self.api_key, self.api_secret, self.session_key)
283
284 def _delay_call(self):
285 """
286 Makes sure that web service calls are at least a second apart
287 """
288
289 # delay time in seconds
290 DELAY_TIME = 1.0
291 now = time.time()
292
293 if (now - self.last_call_time) < DELAY_TIME:
294 time.sleep(1)
295
296 self.last_call_time = now
297
298 def create_new_playlist(self, title, description):
299 """
300 Creates a playlist for the authenticated user and returns it
301 title: The title of the new playlist.
302 description: The description of the new playlist.
303 """
304
305 params = {}
306 params['title'] = title
307 params['description'] = description
308
309 doc = _Request(self, 'playlist.create', params).execute(False)
310
311 e_id = doc.getElementsByTagName("id")[0].firstChild.data
312 user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
313
314 return Playlist(user, e_id, self)
315
316 def get_top_tags(self, limit=None):
317 """Returns a sequence of the most used tags as a sequence of TopItem objects."""
318
319 doc = _Request(self, "tag.getTopTags").execute(True)
320 seq = []
321 for node in doc.getElementsByTagName("tag"):
322 tag = Tag(_extract(node, "name"), self)
323 weight = _number(_extract(node, "count"))
324
325 seq.append(TopItem(tag, weight))
326
327 if limit:
328 seq = seq[:limit]
329
330 return seq
331
332 def enable_proxy(self, host, port):
333 """Enable a default web proxy"""
334
335 self.proxy = [host, _number(port)]
336 self.proxy_enabled = True
337
338 def disable_proxy(self):
339 """Disable using the web proxy"""
340
341 self.proxy_enabled = False
342
343 def is_proxy_enabled(self):
344 """Returns True if a web proxy is enabled."""
345
346 return self.proxy_enabled
347
348 def _get_proxy(self):
349 """Returns proxy details."""
350
351 return self.proxy
352
353 def enable_caching(self, file_path = None):
354 """Enables caching request-wide for all cachable calls.
355 In choosing the backend used for caching, it will try _SqliteCacheBackend first if
356 the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects.
357
358 * file_path: A file path for the backend storage file. If
359 None set, a temp file would probably be created, according the backend.
360 """
361
362 if not file_path:
363 file_path = tempfile.mktemp(prefix="pylast_tmp_")
364
365 self.cache_backend = _ShelfCacheBackend(file_path)
366
367 def disable_caching(self):
368 """Disables all caching features."""
369
370 self.cache_backend = None
371
372 def is_caching_enabled(self):
373 """Returns True if caching is enabled."""
374
375 return not (self.cache_backend == None)
376
377 def _get_cache_backend(self):
378
379 return self.cache_backend
380
381 def search_for_album(self, album_name):
382 """Searches for an album by its name. Returns a AlbumSearch object.
383 Use get_next_page() to retreive sequences of results."""
384
385 return AlbumSearch(album_name, self)
386
387 def search_for_artist(self, artist_name):
388 """Searches of an artist by its name. Returns a ArtistSearch object.
389 Use get_next_page() to retreive sequences of results."""
390
391 return ArtistSearch(artist_name, self)
392
393 def search_for_tag(self, tag_name):
394 """Searches of a tag by its name. Returns a TagSearch object.
395 Use get_next_page() to retreive sequences of results."""
396
397 return TagSearch(tag_name, self)
398
399 def search_for_track(self, artist_name, track_name):
400 """Searches of a track by its name and its artist. Set artist to an empty string if not available.
401 Returns a TrackSearch object.
402 Use get_next_page() to retreive sequences of results."""
403
404 return TrackSearch(artist_name, track_name, self)
405
406 def search_for_venue(self, venue_name, country_name):
407 """Searches of a venue by its name and its country. Set country_name to an empty string if not available.
408 Returns a VenueSearch object.
409 Use get_next_page() to retreive sequences of results."""
410
411 return VenueSearch(venue_name, country_name, self)
412
413 def get_track_by_mbid(self, mbid):
414 """Looks up a track by its MusicBrainz ID"""
415
416 params = {"mbid": mbid}
417
418 doc = _Request(self, "track.getInfo", params).execute(True)
419
420 return Track(_extract(doc, "name", 1), _extract(doc, "name"), self)
421
422 def get_artist_by_mbid(self, mbid):
423 """Loooks up an artist by its MusicBrainz ID"""
424
425 params = {"mbid": mbid}
426
427 doc = _Request(self, "artist.getInfo", params).execute(True)
428
429 return Artist(_extract(doc, "name"), self)
430
431 def get_album_by_mbid(self, mbid):
432 """Looks up an album by its MusicBrainz ID"""
433
434 params = {"mbid": mbid}
435
436 doc = _Request(self, "album.getInfo", params).execute(True)
437
438 return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
439
440 def update_now_playing(self, artist, title, album = None, album_artist = None,
441 duration = None, track_number = None, mbid = None, context = None):
442 """
443 Used to notify Last.fm that a user has started listening to a track.
444
445 Parameters:
446 artist (Required) : The artist name
447 title (Required) : The track title
448 album (Optional) : The album name.
449 album_artist (Optional) : The album artist - if this differs from the track artist.
450 duration (Optional) : The length of the track in seconds.
451 track_number (Optional) : The track number of the track on the album.
452 mbid (Optional) : The MusicBrainz Track ID.
453 context (Optional) : Sub-client version (not public, only enabled for certain API keys)
454 """
455
456 params = {"track": title, "artist": artist}
457
458 if album: params["album"] = album
459 if album_artist: params["albumArtist"] = album_artist
460 if context: params["context"] = context
461 if track_number: params["trackNumber"] = track_number
462 if mbid: params["mbid"] = mbid
463 if duration: params["duration"] = duration
464
465 _Request(self, "track.updateNowPlaying", params).execute()
466
467 def scrobble(self, artist, title, timestamp, album = None, album_artist = None, track_number = None,
468 duration = None, stream_id = None, context = None, mbid = None):
469
470 """Used to add a track-play to a user's profile.
471
472 Parameters:
473 artist (Required) : The artist name.
474 title (Required) : The track name.
475 timestamp (Required) : The time the track started playing, in UNIX timestamp format (integer number of seconds since 00:00:00, January 1st 1970 UTC). This must be in the UTC time zone.
476 album (Optional) : The album name.
477 album_artist (Optional) : The album artist - if this differs from the track artist.
478 context (Optional) : Sub-client version (not public, only enabled for certain API keys)
479 stream_id (Optional) : The stream id for this track received from the radio.getPlaylist service.
480 track_number (Optional) : The track number of the track on the album.
481 mbid (Optional) : The MusicBrainz Track ID.
482 duration (Optional) : The length of the track in seconds.
483 """
484
485 return self.scrobble_many(({"artist": artist, "title": title, "timestamp": timestamp, "album": album, "album_artist": album_artist,
486 "track_number": track_number, "duration": duration, "stream_id": stream_id, "context": context, "mbid": mbid},))
487
488 def scrobble_many(self, tracks):
489 """
490 Used to scrobble a batch of tracks at once. The parameter tracks is a sequence of dicts per
491 track containing the keyword arguments as if passed to the scrobble() method.
492 """
493
494 tracks_to_scrobble = tracks[:50]
495 if len(tracks) > 50:
496 remaining_tracks = tracks[50:]
497 else:
498 remaining_tracks = None
499
500 params = {}
501 for i in range(len(tracks_to_scrobble)):
502
503 params["artist[%d]" % i] = tracks_to_scrobble[i]["artist"]
504 params["track[%d]" % i] = tracks_to_scrobble[i]["title"]
505
506 additional_args = ("timestamp", "album", "album_artist", "context", "stream_id", "track_number", "mbid", "duration")
507 args_map_to = {"album_artist": "albumArtist", "track_number": "trackNumber", "stream_id": "streamID"} # so friggin lazy
508
509 for arg in additional_args:
510
511 if arg in tracks_to_scrobble[i] and tracks_to_scrobble[i][arg]:
512 if arg in args_map_to:
513 maps_to = args_map_to[arg]
514 else:
515 maps_to = arg
516
517 params["%s[%d]" %(maps_to, i)] = tracks_to_scrobble[i][arg]
518
519
520 _Request(self, "track.scrobble", params).execute()
521
522 if remaining_tracks:
523 self.scrobble_many(remaining_tracks)
524
525 class LastFMNetwork(_Network):
526
527 """A Last.fm network object
528
529 api_key: a provided API_KEY
530 api_secret: a provided API_SECRET
531 session_key: a generated session_key or None
532 username: a username of a valid user
533 password_hash: the output of pylast.md5(password) where password is the user's password
534
535 if username and password_hash were provided and not session_key, session_key will be
536 generated automatically when needed.
537
538 Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
539
540 Most read-only webservices only require an api_key and an api_secret, see about obtaining them from:
541 http://www.last.fm/api/account
542 """
543
544 def __init__(self, api_key="", api_secret="", session_key="", username="", password_hash=""):
545 _Network.__init__(self,
546 name = "Last.fm",
547 homepage = "http://last.fm",
548 ws_server = ("ws.audioscrobbler.com", "/2.0/"),
549 api_key = api_key,
550 api_secret = api_secret,
551 session_key = session_key,
552 submission_server = "http://post.audioscrobbler.com:80/",
553 username = username,
554 password_hash = password_hash,
555 domain_names = {
556 DOMAIN_ENGLISH: 'www.last.fm',
557 DOMAIN_GERMAN: 'www.lastfm.de',
558 DOMAIN_SPANISH: 'www.lastfm.es',
559 DOMAIN_FRENCH: 'www.lastfm.fr',
560 DOMAIN_ITALIAN: 'www.lastfm.it',
561 DOMAIN_POLISH: 'www.lastfm.pl',
562 DOMAIN_PORTUGUESE: 'www.lastfm.com.br',
563 DOMAIN_SWEDISH: 'www.lastfm.se',
564 DOMAIN_TURKISH: 'www.lastfm.com.tr',
565 DOMAIN_RUSSIAN: 'www.lastfm.ru',
566 DOMAIN_JAPANESE: 'www.lastfm.jp',
567 DOMAIN_CHINESE: 'cn.last.fm',
568 },
569 urls = {
570 "album": "music/%(artist)s/%(album)s",
571 "artist": "music/%(artist)s",
572 "event": "event/%(id)s",
573 "country": "place/%(country_name)s",
574 "playlist": "user/%(user)s/library/playlists/%(appendix)s",
575 "tag": "tag/%(name)s",
576 "track": "music/%(artist)s/_/%(title)s",
577 "group": "group/%(name)s",
578 "user": "user/%(name)s",
579 }
580 )
581
582 def __repr__(self):
583 return "pylast.LastFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key,
584 "'%s'" %self.username, "'%s'" %self.password_hash)))
585
586 def __str__(self):
587 return "LastFM Network"
588
589 def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
590 """
591 Returns a preconfigured _Network object for Last.fm
592
593 api_key: a provided API_KEY
594 api_secret: a provided API_SECRET
595 session_key: a generated session_key or None
596 username: a username of a valid user
597 password_hash: the output of pylast.md5(password) where password is the user's password
598
599 if username and password_hash were provided and not session_key, session_key will be
600 generated automatically when needed.
601
602 Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
603
604 Most read-only webservices only require an api_key and an api_secret, see about obtaining them from:
605 http://www.last.fm/api/account
606 """
607
608 _deprecation_warning("Create a LastFMNetwork object instead")
609
610 return LastFMNetwork(api_key, api_secret, session_key, username, password_hash)
611
612 class LibreFMNetwork(_Network):
613 """
614 A preconfigured _Network object for Libre.fm
615
616 api_key: a provided API_KEY
617 api_secret: a provided API_SECRET
618 session_key: a generated session_key or None
619 username: a username of a valid user
620 password_hash: the output of pylast.md5(password) where password is the user's password
621
622 if username and password_hash were provided and not session_key, session_key will be
623 generated automatically when needed.
624 """
625
626 def __init__(self, api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
627
628 _Network.__init__(self,
629 name = "Libre.fm",
630 homepage = "http://alpha.dev.libre.fm",
631 ws_server = ("alpha.dev.libre.fm", "/2.0/"),
632 api_key = api_key,
633 api_secret = api_secret,
634 session_key = session_key,
635 submission_server = "http://turtle.libre.fm:80/",
636 username = username,
637 password_hash = password_hash,
638 domain_names = {
639 DOMAIN_ENGLISH: "alpha.dev.libre.fm",
640 DOMAIN_GERMAN: "alpha.dev.libre.fm",
641 DOMAIN_SPANISH: "alpha.dev.libre.fm",
642 DOMAIN_FRENCH: "alpha.dev.libre.fm",
643 DOMAIN_ITALIAN: "alpha.dev.libre.fm",
644 DOMAIN_POLISH: "alpha.dev.libre.fm",
645 DOMAIN_PORTUGUESE: "alpha.dev.libre.fm",
646 DOMAIN_SWEDISH: "alpha.dev.libre.fm",
647 DOMAIN_TURKISH: "alpha.dev.libre.fm",
648 DOMAIN_RUSSIAN: "alpha.dev.libre.fm",
649 DOMAIN_JAPANESE: "alpha.dev.libre.fm",
650 DOMAIN_CHINESE: "alpha.dev.libre.fm",
651 },
652 urls = {
653 "album": "artist/%(artist)s/album/%(album)s",
654 "artist": "artist/%(artist)s",
655 "event": "event/%(id)s",
656 "country": "place/%(country_name)s",
657 "playlist": "user/%(user)s/library/playlists/%(appendix)s",
658 "tag": "tag/%(name)s",
659 "track": "music/%(artist)s/_/%(title)s",
660 "group": "group/%(name)s",
661 "user": "user/%(name)s",
662 }
663 )
664
665 def __repr__(self):
666 return "pylast.LibreFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key,
667 "'%s'" %self.username, "'%s'" %self.password_hash)))
668
669 def __str__(self):
670 return "Libre.fm Network"
671
672 def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
673 """
674 Returns a preconfigured _Network object for Libre.fm
675
676 api_key: a provided API_KEY
677 api_secret: a provided API_SECRET
678 session_key: a generated session_key or None
679 username: a username of a valid user
680 password_hash: the output of pylast.md5(password) where password is the user's password
681
682 if username and password_hash were provided and not session_key, session_key will be
683 generated automatically when needed.
684 """
685
686 _deprecation_warning("DeprecationWarning: Create a LibreFMNetwork object instead")
687
688 return LibreFMNetwork(api_key, api_secret, session_key, username, password_hash)
689
690 class _ShelfCacheBackend(object):
691 """Used as a backend for caching cacheable requests."""
692 def __init__(self, file_path = None):
693 self.shelf = shelve.open(file_path)
694
695 def get_xml(self, key):
696 return self.shelf[key]
697
698 def set_xml(self, key, xml_string):
699 self.shelf[key] = xml_string
700
701 def has_key(self, key):
702 return key in self.shelf.keys()
703
704 class _Request(object):
705 """Representing an abstract web service operation."""
706
707 def __init__(self, network, method_name, params = {}):
708
709 self.network = network
710 self.params = {}
711
712 for key in params:
713 self.params[key] = _unicode(params[key])
714
715 (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth()
716
717 self.params["api_key"] = self.api_key
718 self.params["method"] = method_name
719
720 if network.is_caching_enabled():
721 self.cache = network._get_cache_backend()
722
723 if self.session_key:
724 self.params["sk"] = self.session_key
725 self.sign_it()
726
727 def sign_it(self):
728 """Sign this request."""
729
730 if not "api_sig" in self.params.keys():
731 self.params['api_sig'] = self._get_signature()
732
733 def _get_signature(self):
734 """Returns a 32-character hexadecimal md5 hash of the signature string."""
735
736 keys = list(self.params.keys())
737
738 keys.sort()
739
740 string = ""
741
742 for name in keys:
743 string += name
744 string += self.params[name]
745
746 string += self.api_secret
747
748 return md5(string)
749
750 def _get_cache_key(self):
751 """The cache key is a string of concatenated sorted names and values."""
752
753 keys = list(self.params.keys())
754 keys.sort()
755
756 cache_key = str()
757
758 for key in keys:
759 if key != "api_sig" and key != "api_key" and key != "sk":
760 cache_key += key + _string(self.params[key])
761
762 return hashlib.sha1(cache_key).hexdigest()
763
764 def _get_cached_response(self):
765 """Returns a file object of the cached response."""
766
767 if not self._is_cached():
768 response = self._download_response()
769 self.cache.set_xml(self._get_cache_key(), response)
770
771 return self.cache.get_xml(self._get_cache_key())
772
773 def _is_cached(self):
774 """Returns True if the request is already in cache."""
775
776 return self.cache.has_key(self._get_cache_key())
777
778 def _download_response(self):
779 """Returns a response body string from the server."""
780
781 # Delay the call if necessary
782 #self.network._delay_call() # enable it if you want.
783
784 data = []
785 for name in self.params.keys():
786 data.append('='.join((name, url_quote_plus(_string(self.params[name])))))
787 data = '&'.join(data)
788
789 headers = {
790 "Content-type": "application/x-www-form-urlencoded",
791 'Accept-Charset': 'utf-8',
792 'User-Agent': "pylast" + '/' + __version__
793 }
794
795 (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
796
797 if self.network.is_proxy_enabled():
798 conn = HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
799
800 try:
801 conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
802 body=data, headers=headers)
803 except Exception as e:
804 raise NetworkError(self.network, e)
805
806 else:
807 conn = HTTPConnection(host=HOST_NAME)
808
809 try:
810 conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers)
811 except Exception as e:
812 raise NetworkError(self.network, e)
813
814 try:
815 response_text = _unicode(conn.getresponse().read())
816 except Exception as e:
817 raise MalformedResponseError(self.network, e)
818
819 self._check_response_for_errors(response_text)
820 return response_text
821
822 def execute(self, cacheable = False):
823 """Returns the XML DOM response of the POST Request from the server"""
824
825 if self.network.is_caching_enabled() and cacheable:
826 response = self._get_cached_response()
827 else:
828 response = self._download_response()
829
830 return minidom.parseString(_string(response))
831
832 def _check_response_for_errors(self, response):
833 """Checks the response for errors and raises one if any exists."""
834
835 try:
836 doc = minidom.parseString(_string(response))
837 except Exception as e:
838 raise MalformedResponseError(self.network, e)
839
840 e = doc.getElementsByTagName('lfm')[0]
841
842 if e.getAttribute('status') != "ok":
843 e = doc.getElementsByTagName('error')[0]
844 status = e.getAttribute('code')
845 details = e.firstChild.data.strip()
846 raise WSError(self.network, status, details)
847
848 class SessionKeyGenerator(object):
849 """Methods of generating a session key:
850 1) Web Authentication:
851 a. network = get_*_network(API_KEY, API_SECRET)
852 b. sg = SessionKeyGenerator(network)
853 c. url = sg.get_web_auth_url()
854 d. Ask the user to open the url and authorize you, and wait for it.
855 e. session_key = sg.get_web_auth_session_key(url)
856 2) Username and Password Authentication:
857 a. network = get_*_network(API_KEY, API_SECRET)
858 b. username = raw_input("Please enter your username: ")
859 c. password_hash = pylast.md5(raw_input("Please enter your password: ")
860 d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash)
861
862 A session key's lifetime is infinie, unless the user provokes the rights of the given API Key.
863
864 If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a
865 SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this
866 manually, unless you want to.
867 """
868
869 def __init__(self, network):
870 self.network = network
871 self.web_auth_tokens = {}
872
873 def _get_web_auth_token(self):
874 """Retrieves a token from the network for web authentication.
875 The token then has to be authorized from getAuthURL before creating session.
876 """
877
878 request = _Request(self.network, 'auth.getToken')
879
880 # default action is that a request is signed only when
881 # a session key is provided.
882 request.sign_it()
883
884 doc = request.execute()
885
886 e = doc.getElementsByTagName('token')[0]
887 return e.firstChild.data
888
889 def get_web_auth_url(self):
890 """The user must open this page, and you first, then call get_web_auth_session_key(url) after that."""
891
892 token = self._get_web_auth_token()
893
894 url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \
895 {"homepage": self.network.homepage, "api": self.network.api_key, "token": token}
896
897 self.web_auth_tokens[url] = token
898
899 return url
900
901 def get_web_auth_session_key(self, url):
902 """Retrieves the session key of a web authorization process by its url."""
903
904 if url in self.web_auth_tokens.keys():
905 token = self.web_auth_tokens[url]
906 else:
907 token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed.
908
909 request = _Request(self.network, 'auth.getSession', {'token': token})
910
911 # default action is that a request is signed only when
912 # a session key is provided.
913 request.sign_it()
914
915 doc = request.execute()
916
917 return doc.getElementsByTagName('key')[0].firstChild.data
918
919 def get_session_key(self, username, password_hash):
920 """Retrieve a session key with a username and a md5 hash of the user's password."""
921
922 params = {"username": username, "authToken": md5(username + password_hash)}
923 request = _Request(self.network, "auth.getMobileSession", params)
924
925 # default action is that a request is signed only when
926 # a session key is provided.
927 request.sign_it()
928
929 doc = request.execute()
930
931 return _extract(doc, "key")
932
933 TopItem = collections.namedtuple("TopItem", ["item", "weight"])
934 SimilarItem = collections.namedtuple("SimilarItem", ["item", "match"])
935 LibraryItem = collections.namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
936 PlayedTrack = collections.namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
937 LovedTrack = collections.namedtuple("LovedTrack", ["track", "date", "timestamp"])
938 ImageSizes = collections.namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
939 Image = collections.namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
940 Shout = collections.namedtuple("Shout", ["body", "author", "date"])
941
942 def _string_output(funct):
943 def r(*args):
944 return _string(funct(*args))
945
946 return r
947
948 def _pad_list(given_list, desired_length, padding = None):
949 """
950 Pads a list to be of the desired_length.
951 """
952
953 while len(given_list) < desired_length:
954 given_list.append(padding)
955
956 return given_list
957
958 class _BaseObject(object):
959 """An abstract webservices object."""
960
961 network = None
962
963 def __init__(self, network):
964 self.network = network
965
966 def _request(self, method_name, cacheable = False, params = None):
967 if not params:
968 params = self._get_params()
969
970 return _Request(self.network, method_name, params).execute(cacheable)
971
972 def _get_params(self):
973 """Returns the most common set of parameters between all objects."""
974
975 return {}
976
977 def __hash__(self):
978 return hash(self.network) + \
979 hash(str(type(self)) + "".join(list(self._get_params().keys()) + list(self._get_params().values())).lower())
980
981 class _Taggable(object):
982 """Common functions for classes with tags."""
983
984 def __init__(self, ws_prefix):
985 self.ws_prefix = ws_prefix
986
987 def add_tags(self, tags):
988 """Adds one or several tags.
989 * tags: A sequence of tag names or Tag objects.
990 """
991
992 for tag in tags:
993 self.add_tag(tag)
994
995 def add_tag(self, tag):
996 """Adds one tag.
997 * tag: a tag name or a Tag object.
998 """
999
1000 if isinstance(tag, Tag):
1001 tag = tag.get_name()
1002
1003 params = self._get_params()
1004 params['tags'] = tag
1005
1006 self._request(self.ws_prefix + '.addTags', False, params)
1007
1008 def remove_tag(self, tag):
1009 """Remove a user's tag from this object."""
1010
1011 if isinstance(tag, Tag):
1012 tag = tag.get_name()
1013
1014 params = self._get_params()
1015 params['tag'] = tag
1016
1017 self._request(self.ws_prefix + '.removeTag', False, params)
1018
1019 def get_tags(self):
1020 """Returns a list of the tags set by the user to this object."""
1021
1022 # Uncacheable because it can be dynamically changed by the user.
1023 params = self._get_params()
1024
1025 doc = self._request(self.ws_prefix + '.getTags', False, params)
1026 tag_names = _extract_all(doc, 'name')
1027 tags = []
1028 for tag in tag_names:
1029 tags.append(Tag(tag, self.network))
1030
1031 return tags
1032
1033 def remove_tags(self, tags):
1034 """Removes one or several tags from this object.
1035 * tags: a sequence of tag names or Tag objects.
1036 """
1037
1038 for tag in tags:
1039 self.remove_tag(tag)
1040
1041 def clear_tags(self):
1042 """Clears all the user-set tags. """
1043
1044 self.remove_tags(*(self.get_tags()))
1045
1046 def set_tags(self, tags):
1047 """Sets this object's tags to only those tags.
1048 * tags: a sequence of tag names or Tag objects.
1049 """
1050
1051 c_old_tags = []
1052 old_tags = []
1053 c_new_tags = []
1054 new_tags = []
1055
1056 to_remove = []
1057 to_add = []
1058
1059 tags_on_server = self.get_tags()
1060
1061 for tag in tags_on_server:
1062 c_old_tags.append(tag.get_name().lower())
1063 old_tags.append(tag.get_name())
1064
1065 for tag in tags:
1066 c_new_tags.append(tag.lower())
1067 new_tags.append(tag)
1068
1069 for i in range(0, len(old_tags)):
1070 if not c_old_tags[i] in c_new_tags:
1071 to_remove.append(old_tags[i])
1072
1073 for i in range(0, len(new_tags)):
1074 if not c_new_tags[i] in c_old_tags:
1075 to_add.append(new_tags[i])
1076
1077 self.remove_tags(to_remove)
1078 self.add_tags(to_add)
1079
1080 def get_top_tags(self, limit=None):
1081 """Returns a list of the most frequently used Tags on this object."""
1082
1083 doc = self._request(self.ws_prefix + '.getTopTags', True)
1084
1085 elements = doc.getElementsByTagName('tag')
1086 seq = []
1087
1088 for element in elements:
1089 tag_name = _extract(element, 'name')
1090 tagcount = _extract(element, 'count')
1091
1092 seq.append(TopItem(Tag(tag_name, self.network), tagcount))
1093
1094 if limit:
1095 seq = seq[:limit]
1096
1097 return seq
1098
1099 class WSError(Exception):
1100 """Exception related to the Network web service"""
1101
1102 def __init__(self, network, status, details):
1103 self.status = status
1104 self.details = details
1105 self.network = network
1106
1107 @_string_output
1108 def __str__(self):
1109 return self.details
1110
1111 def get_id(self):
1112 """Returns the exception ID, from one of the following:
1113 STATUS_INVALID_SERVICE = 2
1114 STATUS_INVALID_METHOD = 3
1115 STATUS_AUTH_FAILED = 4
1116 STATUS_INVALID_FORMAT = 5
1117 STATUS_INVALID_PARAMS = 6
1118 STATUS_INVALID_RESOURCE = 7
1119 STATUS_TOKEN_ERROR = 8
1120 STATUS_INVALID_SK = 9
1121 STATUS_INVALID_API_KEY = 10
1122 STATUS_OFFLINE = 11
1123 STATUS_SUBSCRIBERS_ONLY = 12
1124 STATUS_TOKEN_UNAUTHORIZED = 14
1125 STATUS_TOKEN_EXPIRED = 15
1126 """
1127
1128 return self.status
1129
1130 class MalformedResponseError(Exception):
1131 """Exception conveying a malformed response from Last.fm."""
1132
1133 def __init__(self, network, underlying_error):
1134 self.network = network
1135 self.underlying_error = underlying_error
1136
1137 def __str__(self):
1138 return "Malformed response from Last.fm. Underlying error: %s" %str(self.underlying_error)
1139
1140 class NetworkError(Exception):
1141 """Exception conveying a problem in sending a request to Last.fm"""
1142
1143 def __init__(self, network, underlying_error):
1144 self.network = network
1145 self.underlying_error = underlying_error
1146
1147 def __str__(self):
1148 return "NetworkError: %s" %str(self.underlying_error)
1149
1150 class Album(_BaseObject, _Taggable):
1151 """An album."""
1152
1153 title = None
1154 artist = None
1155
1156 def __init__(self, artist, title, network):
1157 """
1158 Create an album instance.
1159 # Parameters:
1160 * artist: An artist name or an Artist object.
1161 * title: The album title.
1162 """
1163
1164 _BaseObject.__init__(self, network)
1165 _Taggable.__init__(self, 'album')
1166
1167 if isinstance(artist, Artist):
1168 self.artist = artist
1169 else:
1170 self.artist = Artist(artist, self.network)
1171
1172 self.title = title
1173
1174 def __repr__(self):
1175 return "pylast.Album(%s, %s, %s)" %(repr(self.artist.name), repr(self.title), repr(self.network))
1176
1177 @_string_output
1178 def __str__(self):
1179 return _unicode("%s - %s") %(self.get_artist().get_name(), self.get_title())
1180
1181 def __eq__(self, other):
1182 return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
1183
1184 def __ne__(self, other):
1185 return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
1186
1187 def _get_params(self):
1188 return {'artist': self.get_artist().get_name(), 'album': self.get_title(), }
1189
1190 def get_artist(self):
1191 """Returns the associated Artist object."""
1192
1193 return self.artist
1194
1195 def get_title(self):
1196 """Returns the album title."""
1197
1198 return self.title
1199
1200 def get_name(self):
1201 """Returns the album title (alias to Album.get_title)."""
1202
1203 return self.get_title()
1204
1205 def get_release_date(self):
1206 """Retruns the release date of the album."""
1207
1208 return _extract(self._request("album.getInfo", cacheable = True), "releasedate")
1209
1210 def get_cover_image(self, size = COVER_EXTRA_LARGE):
1211 """
1212 Returns a uri to the cover image
1213 size can be one of:
1214 COVER_EXTRA_LARGE
1215 COVER_LARGE
1216 COVER_MEDIUM
1217 COVER_SMALL
1218 """
1219
1220 return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size]
1221
1222 def get_id(self):
1223 """Returns the ID"""
1224
1225 return _extract(self._request("album.getInfo", cacheable = True), "id")
1226
1227 def get_playcount(self):
1228 """Returns the number of plays on the network"""
1229
1230 return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
1231
1232 def get_listener_count(self):
1233 """Returns the number of liteners on the network"""
1234
1235 return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners"))
1236
1237 def get_top_tags(self, limit=None):
1238 """Returns a list of the most-applied tags to this album."""
1239
1240 doc = self._request("album.getInfo", True)
1241 e = doc.getElementsByTagName("toptags")[0]
1242
1243 seq = []
1244 for name in _extract_all(e, "name"):
1245 seq.append(Tag(name, self.network))
1246
1247 if limit:
1248 seq = seq[:limit]
1249
1250 return seq
1251
1252 def get_tracks(self):
1253 """Returns the list of Tracks on this album."""
1254
1255 uri = 'lastfm://playlist/album/%s' %self.get_id()
1256
1257 return XSPF(uri, self.network).get_tracks()
1258
1259 def get_mbid(self):
1260 """Returns the MusicBrainz id of the album."""
1261
1262 return _extract(self._request("album.getInfo", cacheable = True), "mbid")
1263
1264 def get_url(self, domain_name = DOMAIN_ENGLISH):
1265 """Returns the url of the album page on the network.
1266 # Parameters:
1267 * domain_name str: The network's language domain. Possible values:
1268 o DOMAIN_ENGLISH
1269 o DOMAIN_GERMAN
1270 o DOMAIN_SPANISH
1271 o DOMAIN_FRENCH
1272 o DOMAIN_ITALIAN
1273 o DOMAIN_POLISH
1274 o DOMAIN_PORTUGUESE
1275 o DOMAIN_SWEDISH
1276 o DOMAIN_TURKISH
1277 o DOMAIN_RUSSIAN
1278 o DOMAIN_JAPANESE
1279 o DOMAIN_CHINESE
1280 """
1281
1282 artist = _url_safe(self.get_artist().get_name())
1283 album = _url_safe(self.get_title())
1284
1285 return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album}
1286
1287 def get_wiki_published_date(self):
1288 """Returns the date of publishing this version of the wiki."""
1289
1290 doc = self._request("album.getInfo", True)
1291
1292 if len(doc.getElementsByTagName("wiki")) == 0:
1293 return
1294
1295 node = doc.getElementsByTagName("wiki")[0]
1296
1297 return _extract(node, "published")
1298
1299 def get_wiki_summary(self):
1300 """Returns the summary of the wiki."""
1301
1302 doc = self._request("album.getInfo", True)
1303
1304 if len(doc.getElementsByTagName("wiki")) == 0:
1305 return
1306
1307 node = doc.getElementsByTagName("wiki")[0]
1308
1309 return _extract(node, "summary")
1310
1311 def get_wiki_content(self):
1312 """Returns the content of the wiki."""
1313
1314 doc = self._request("album.getInfo", True)
1315
1316 if len(doc.getElementsByTagName("wiki")) == 0:
1317 return
1318
1319 node = doc.getElementsByTagName("wiki")[0]
1320
1321 return _extract(node, "content")
1322
1323 class Artist(_BaseObject, _Taggable):
1324 """An artist."""
1325
1326 name = None
1327
1328 def __init__(self, name, network):
1329 """Create an artist object.
1330 # Parameters:
1331 * name str: The artist's name.
1332 """
1333
1334 _BaseObject.__init__(self, network)
1335 _Taggable.__init__(self, 'artist')
1336
1337 self.name = name
1338
1339 def __repr__(self):
1340 return "pylast.Artist(%s, %s)" %(repr(self.get_name()), repr(self.network))
1341
1342 @_string_output
1343 def __str__(self):
1344 return self.get_name()
1345
1346 def __eq__(self, other):
1347 return self.get_name().lower() == other.get_name().lower()
1348
1349 def __ne__(self, other):
1350 return self.get_name().lower() != other.get_name().lower()
1351
1352 def _get_params(self):
1353 return {'artist': self.get_name()}
1354
1355 def get_name(self, properly_capitalized=False):
1356 """Returns the name of the artist.
1357 If properly_capitalized was asserted then the name would be downloaded
1358 overwriting the given one."""
1359
1360 if properly_capitalized:
1361 self.name = _extract(self._request("artist.getInfo", True), "name")
1362
1363 return self.name
1364
1365 def get_cover_image(self, size = COVER_MEGA):
1366 """
1367 Returns a uri to the cover image
1368 size can be one of:
1369 COVER_MEGA
1370 COVER_EXTRA_LARGE
1371 COVER_LARGE
1372 COVER_MEDIUM
1373 COVER_SMALL
1374 """
1375
1376 return _extract_all(self._request("artist.getInfo", True), "image")[size]
1377
1378 def get_playcount(self):
1379 """Returns the number of plays on the network."""
1380
1381 return _number(_extract(self._request("artist.getInfo", True), "playcount"))
1382
1383 def get_mbid(self):
1384 """Returns the MusicBrainz ID of this artist."""
1385
1386 doc = self._request("artist.getInfo", True)
1387
1388 return _extract(doc, "mbid")
1389
1390 def get_listener_count(self):
1391 """Returns the number of liteners on the network."""
1392
1393 if hasattr(self, "listener_count"):
1394 return self.listener_count
1395 else:
1396 self.listener_count = _number(_extract(self._request("artist.getInfo", True), "listeners"))
1397 return self.listener_count
1398
1399 def is_streamable(self):
1400 """Returns True if the artist is streamable."""
1401
1402 return bool(_number(_extract(self._request("artist.getInfo", True), "streamable")))
1403
1404 def get_bio_published_date(self):
1405 """Returns the date on which the artist's biography was published."""
1406
1407 return _extract(self._request("artist.getInfo", True), "published")
1408
1409 def get_bio_summary(self):
1410 """Returns the summary of the artist's biography."""
1411
1412 return _extract(self._request("artist.getInfo", True), "summary")
1413
1414 def get_bio_content(self):
1415 """Returns the content of the artist's biography."""
1416
1417 return _extract(self._request("artist.getInfo", True), "content")
1418
1419 def get_upcoming_events(self):
1420 """Returns a list of the upcoming Events for this artist."""
1421
1422 doc = self._request('artist.getEvents', True)
1423
1424 ids = _extract_all(doc, 'id')
1425
1426 events = []
1427 for e_id in ids:
1428 events.append(Event(e_id, self.network))
1429
1430 return events
1431
1432 def get_similar(self, limit = None):
1433 """Returns the similar artists on the network."""
1434
1435 params = self._get_params()
1436 if limit:
1437 params['limit'] = limit
1438
1439 doc = self._request('artist.getSimilar', True, params)
1440
1441 names = _extract_all(doc, "name")
1442 matches = _extract_all(doc, "match")
1443
1444 artists = []
1445 for i in range(0, len(names)):
1446 artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i])))
1447
1448 return artists
1449
1450 def compare_with_artist(self, artist, shared_artists_limit = None):
1451 """Compare this artist with another Last.fm artist.
1452 Seems like the tasteometer only returns stats on subsets of artist sets...
1453 Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...))
1454 artist: A artist object or a artistname string/unicode object.
1455 """
1456
1457 if isinstance(artist, Artist):
1458 artist = artist.get_name()
1459
1460 params = self._get_params()
1461 if shared_artists_limit:
1462 params['limit'] = shared_artists_limit
1463 params['type1'] = 'artists'
1464 params['type2'] = 'artists'
1465 params['value1'] = self.get_name()
1466 params['value2'] = artist
1467
1468 doc = self._request('tasteometer.compare', False, params)
1469
1470 score = _extract(doc, 'score')
1471
1472 artists = doc.getElementsByTagName('artists')[0]
1473 shared_artists_names = _extract_all(artists, 'name')
1474
1475 shared_artists_seq = []
1476
1477 for name in shared_artists_names:
1478 shared_artists_seq.append(Artist(name, self.network))
1479
1480 return (score, shared_artists_seq)
1481
1482 def get_top_albums(self):
1483 """Retuns a list of the top albums."""
1484
1485 doc = self._request('artist.getTopAlbums', True)
1486
1487 seq = []
1488
1489 for node in doc.getElementsByTagName("album"):
1490 name = _extract(node, "name")
1491 artist = _extract(node, "name", 1)
1492 playcount = _extract(node, "playcount")
1493
1494 seq.append(TopItem(Album(artist, name, self.network), playcount))
1495
1496 return seq
1497
1498 def get_top_tracks(self):
1499 """Returns a list of the most played Tracks by this artist."""
1500
1501 doc = self._request("artist.getTopTracks", True)
1502
1503 seq = []
1504 for track in doc.getElementsByTagName('track'):
1505
1506 title = _extract(track, "name")
1507 artist = _extract(track, "name", 1)
1508 playcount = _number(_extract(track, "playcount"))
1509
1510 seq.append( TopItem(Track(artist, title, self.network), playcount) )
1511
1512 return seq
1513
1514 def get_top_fans(self, limit = None):
1515 """Returns a list of the Users who played this artist the most.
1516 # Parameters:
1517 * limit int: Max elements.
1518 """
1519
1520 doc = self._request('artist.getTopFans', True)
1521
1522 seq = []
1523
1524 elements = doc.getElementsByTagName('user')
1525
1526 for element in elements:
1527 if limit and len(seq) >= limit:
1528 break
1529
1530 name = _extract(element, 'name')
1531 weight = _number(_extract(element, 'weight'))
1532
1533 seq.append(TopItem(User(name, self.network), weight))
1534
1535 return seq
1536
1537 def share(self, users, message = None):
1538 """Shares this artist (sends out recommendations).
1539 # Parameters:
1540 * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
1541 * message str: A message to include in the recommendation message.
1542 """
1543
1544 #last.fm currently accepts a max of 10 recipient at a time
1545 while(len(users) > 10):
1546 section = users[0:9]
1547 users = users[9:]
1548 self.share(section, message)
1549
1550 nusers = []
1551 for user in users:
1552 if isinstance(user, User):
1553 nusers.append(user.get_name())
1554 else:
1555 nusers.append(user)
1556
1557 params = self._get_params()
1558 recipients = ','.join(nusers)
1559 params['recipient'] = recipients
1560 if message:
1561 params['message'] = message
1562
1563 self._request('artist.share', False, params)
1564
1565 def get_url(self, domain_name = DOMAIN_ENGLISH):
1566 """Returns the url of the artist page on the network.
1567 # Parameters:
1568 * domain_name: The network's language domain. Possible values:
1569 o DOMAIN_ENGLISH
1570 o DOMAIN_GERMAN
1571 o DOMAIN_SPANISH
1572 o DOMAIN_FRENCH
1573 o DOMAIN_ITALIAN
1574 o DOMAIN_POLISH
1575 o DOMAIN_PORTUGUESE
1576 o DOMAIN_SWEDISH
1577 o DOMAIN_TURKISH
1578 o DOMAIN_RUSSIAN
1579 o DOMAIN_JAPANESE
1580 o DOMAIN_CHINESE
1581 """
1582
1583 artist = _url_safe(self.get_name())
1584
1585 return self.network._get_url(domain_name, "artist") %{'artist': artist}
1586
1587 def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None):
1588 """
1589 Returns a sequence of Image objects
1590 if limit is None it will return all
1591 order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE.
1592
1593 If limit==None, it will try to pull all the available data.
1594 """
1595
1596 images = []
1597
1598 params = self._get_params()
1599 params["order"] = order
1600 nodes = _collect_nodes(limit, self, "artist.getImages", True, params)
1601 for e in nodes:
1602 if _extract(e, "name"):
1603 user = User(_extract(e, "name"), self.network)
1604 else:
1605 user = None
1606
1607 images.append(Image(
1608 _extract(e, "title"),
1609 _extract(e, "url"),
1610 _extract(e, "dateadded"),
1611 _extract(e, "format"),
1612 user,
1613 ImageSizes(*_extract_all(e, "size")),
1614 (_extract(e, "thumbsup"), _extract(e, "thumbsdown"))
1615 )
1616 )
1617 return images
1618
1619 def get_shouts(self, limit=50):
1620 """
1621 Returns a sequqence of Shout objects
1622 """
1623
1624 shouts = []
1625 for node in _collect_nodes(limit, self, "artist.getShouts", False):
1626 shouts.append(Shout(
1627 _extract(node, "body"),
1628 User(_extract(node, "author"), self.network),
1629 _extract(node, "date")
1630 )
1631 )
1632 return shouts
1633
1634 def shout(self, message):
1635 """
1636 Post a shout
1637 """
1638
1639 params = self._get_params()
1640 params["message"] = message
1641
1642 self._request("artist.Shout", False, params)
1643
1644
1645 class Event(_BaseObject):
1646 """An event."""
1647
1648 id = None
1649
1650 def __init__(self, event_id, network):
1651 _BaseObject.__init__(self, network)
1652
1653 self.id = event_id
1654
1655 def __repr__(self):
1656 return "pylast.Event(%s, %s)" %(repr(self.id), repr(self.network))
1657
1658 @_string_output
1659 def __str__(self):
1660 return "Event #" + self.get_id()
1661
1662 def __eq__(self, other):
1663 return self.get_id() == other.get_id()
1664
1665 def __ne__(self, other):
1666 return self.get_id() != other.get_id()
1667
1668 def _get_params(self):
1669 return {'event': self.get_id()}
1670
1671 def attend(self, attending_status):
1672 """Sets the attending status.
1673 * attending_status: The attending status. Possible values:
1674 o EVENT_ATTENDING
1675 o EVENT_MAYBE_ATTENDING
1676 o EVENT_NOT_ATTENDING
1677 """
1678
1679 params = self._get_params()
1680 params['status'] = attending_status
1681
1682 self._request('event.attend', False, params)
1683
1684 def get_attendees(self):
1685 """
1686 Get a list of attendees for an event
1687 """
1688
1689 doc = self._request("event.getAttendees", False)
1690
1691 users = []
1692 for name in _extract_all(doc, "name"):
1693 users.append(User(name, self.network))
1694
1695 return users
1696
1697 def get_id(self):
1698 """Returns the id of the event on the network. """
1699
1700 return self.id
1701
1702 def get_title(self):
1703 """Returns the title of the event. """
1704
1705 doc = self._request("event.getInfo", True)
1706
1707 return _extract(doc, "title")
1708
1709 def get_headliner(self):
1710 """Returns the headliner of the event. """
1711
1712 doc = self._request("event.getInfo", True)
1713
1714 return Artist(_extract(doc, "headliner"), self.network)
1715
1716 def get_artists(self):
1717 """Returns a list of the participating Artists. """
1718
1719 doc = self._request("event.getInfo", True)
1720 names = _extract_all(doc, "artist")
1721
1722 artists = []
1723 for name in names:
1724 artists.append(Artist(name, self.network))
1725
1726 return artists
1727
1728 def get_venue(self):
1729 """Returns the venue where the event is held."""
1730
1731 doc = self._request("event.getInfo", True)
1732
1733 v = doc.getElementsByTagName("venue")[0]
1734 venue_id = _number(_extract(v, "id"))
1735
1736 return Venue(venue_id, self.network)
1737
1738 def get_start_date(self):
1739 """Returns the date when the event starts."""
1740
1741 doc = self._request("event.getInfo", True)
1742
1743 return _extract(doc, "startDate")
1744
1745 def get_description(self):
1746 """Returns the description of the event. """
1747
1748 doc = self._request("event.getInfo", True)
1749
1750 return _extract(doc, "description")
1751
1752 def get_cover_image(self, size = COVER_MEGA):
1753 """
1754 Returns a uri to the cover image
1755 size can be one of:
1756 COVER_MEGA
1757 COVER_EXTRA_LARGE
1758 COVER_LARGE
1759 COVER_MEDIUM
1760 COVER_SMALL
1761 """
1762
1763 doc = self._request("event.getInfo", True)
1764
1765 return _extract_all(doc, "image")[size]
1766
1767 def get_attendance_count(self):
1768 """Returns the number of attending people. """
1769
1770 doc = self._request("event.getInfo", True)
1771
1772 return _number(_extract(doc, "attendance"))
1773
1774 def get_review_count(self):
1775 """Returns the number of available reviews for this event. """
1776
1777 doc = self._request("event.getInfo", True)
1778
1779 return _number(_extract(doc, "reviews"))
1780
1781 def get_url(self, domain_name = DOMAIN_ENGLISH):
1782 """Returns the url of the event page on the network.
1783 * domain_name: The network's language domain. Possible values:
1784 o DOMAIN_ENGLISH
1785 o DOMAIN_GERMAN
1786 o DOMAIN_SPANISH
1787 o DOMAIN_FRENCH
1788 o DOMAIN_ITALIAN
1789 o DOMAIN_POLISH
1790 o DOMAIN_PORTUGUESE
1791 o DOMAIN_SWEDISH
1792 o DOMAIN_TURKISH
1793 o DOMAIN_RUSSIAN
1794 o DOMAIN_JAPANESE
1795 o DOMAIN_CHINESE
1796 """
1797
1798 return self.network._get_url(domain_name, "event") %{'id': self.get_id()}
1799
1800 def share(self, users, message = None):
1801 """Shares this event (sends out recommendations).
1802 * users: A list that can contain usernames, emails, User objects, or all of them.
1803 * message: A message to include in the recommendation message.
1804 """
1805
1806 #last.fm currently accepts a max of 10 recipient at a time
1807 while(len(users) > 10):
1808 section = users[0:9]
1809 users = users[9:]
1810 self.share(section, message)
1811
1812 nusers = []
1813 for user in users:
1814 if isinstance(user, User):
1815 nusers.append(user.get_name())
1816 else:
1817 nusers.append(user)
1818
1819 params = self._get_params()
1820 recipients = ','.join(nusers)
1821 params['recipient'] = recipients
1822 if message:
1823 params['message'] = message
1824
1825 self._request('event.share', False, params)
1826
1827 def get_shouts(self, limit=50):
1828 """
1829 Returns a sequqence of Shout objects
1830 """
1831
1832 shouts = []
1833 for node in _collect_nodes(limit, self, "event.getShouts", False):
1834 shouts.append(Shout(
1835 _extract(node, "body"),
1836 User(_extract(node, "author"), self.network),
1837 _extract(node, "date")
1838 )
1839 )
1840 return shouts
1841
1842 def shout(self, message):
1843 """
1844 Post a shout
1845 """
1846
1847 params = self._get_params()
1848 params["message"] = message
1849
1850 self._request("event.Shout", False, params)
1851
1852 class Country(_BaseObject):
1853 """A country at Last.fm."""
1854
1855 name = None
1856
1857 def __init__(self, name, network):
1858 _BaseObject.__init__(self, network)
1859
1860 self.name = name
1861
1862 def __repr__(self):
1863 return "pylast.Country(%s, %s)" %(repr(self.name), repr(self.network))
1864
1865 @_string_output
1866 def __str__(self):
1867 return self.get_name()
1868
1869 def __eq__(self, other):
1870 return self.get_name().lower() == other.get_name().lower()
1871
1872 def __ne__(self, other):
1873 return self.get_name() != other.get_name()
1874
1875 def _get_params(self):
1876 return {'country': self.get_name()}
1877
1878 def _get_name_from_code(self, alpha2code):
1879 # TODO: Have this function lookup the alpha-2 code and return the country name.
1880
1881 return alpha2code
1882
1883 def get_name(self):
1884 """Returns the country name. """
1885
1886 return self.name
1887
1888 def get_top_artists(self):
1889 """Returns a sequence of the most played artists."""
1890
1891 doc = self._request('geo.getTopArtists', True)
1892
1893 seq = []
1894 for node in doc.getElementsByTagName("artist"):
1895 name = _extract(node, 'name')
1896 playcount = _extract(node, "playcount")
1897
1898 seq.append(TopItem(Artist(name, self.network), playcount))
1899
1900 return seq
1901
1902 def get_top_tracks(self):
1903 """Returns a sequence of the most played tracks"""
1904
1905 doc = self._request("geo.getTopTracks", True)
1906
1907 seq = []
1908
1909 for n in doc.getElementsByTagName('track'):
1910
1911 title = _extract(n, 'name')
1912 artist = _extract(n, 'name', 1)
1913 playcount = _number(_extract(n, "playcount"))
1914
1915 seq.append( TopItem(Track(artist, title, self.network), playcount))
1916
1917 return seq
1918
1919 def get_url(self, domain_name = DOMAIN_ENGLISH):
1920 """Returns the url of the event page on the network.
1921 * domain_name: The network's language domain. Possible values:
1922 o DOMAIN_ENGLISH
1923 o DOMAIN_GERMAN
1924 o DOMAIN_SPANISH
1925 o DOMAIN_FRENCH
1926 o DOMAIN_ITALIAN
1927 o DOMAIN_POLISH
1928 o DOMAIN_PORTUGUESE
1929 o DOMAIN_SWEDISH
1930 o DOMAIN_TURKISH
1931 o DOMAIN_RUSSIAN
1932 o DOMAIN_JAPANESE
1933 o DOMAIN_CHINESE
1934 """
1935
1936 country_name = _url_safe(self.get_name())
1937
1938 return self.network._get_url(domain_name, "country") %{'country_name': country_name}
1939
1940
1941 class Library(_BaseObject):
1942 """A user's Last.fm library."""
1943
1944 user = None
1945
1946 def __init__(self, user, network):
1947 _BaseObject.__init__(self, network)
1948
1949 if isinstance(user, User):
1950 self.user = user
1951 else:
1952 self.user = User(user, self.network)
1953
1954 self._albums_index = 0
1955 self._artists_index = 0
1956 self._tracks_index = 0
1957
1958 def __repr__(self):
1959 return "pylast.Library(%s, %s)" %(repr(self.user), repr(self.network))
1960
1961 @_string_output
1962 def __str__(self):
1963 return repr(self.get_user()) + "'s Library"
1964
1965 def _get_params(self):
1966 return {'user': self.user.get_name()}
1967
1968 def get_user(self):
1969 """Returns the user who owns this library."""
1970
1971 return self.user
1972
1973 def add_album(self, album):
1974 """Add an album to this library."""
1975
1976 params = self._get_params()
1977 params["artist"] = album.get_artist.get_name()
1978 params["album"] = album.get_name()
1979
1980 self._request("library.addAlbum", False, params)
1981
1982 def add_artist(self, artist):
1983 """Add an artist to this library."""
1984
1985 params = self._get_params()
1986 params["artist"] = artist.get_name()
1987
1988 self._request("library.addArtist", False, params)
1989
1990 def add_track(self, track):
1991 """Add a track to this library."""
1992
1993 params = self._get_params()
1994 params["track"] = track.get_title()
1995
1996 self._request("library.addTrack", False, params)
1997
1998 def get_albums(self, artist=None, limit=50):
1999 """
2000 Returns a sequence of Album objects
2001 If no artist is specified, it will return all, sorted by playcount descendingly.
2002 If limit==None it will return all (may take a while)
2003 """
2004
2005 params = self._get_params()
2006 if artist:
2007 params["artist"] = artist
2008
2009 seq = []
2010 for node in _collect_nodes(limit, self, "library.getAlbums", True, params):
2011 name = _extract(node, "name")
2012 artist = _extract(node, "name", 1)
2013 playcount = _number(_extract(node, "playcount"))
2014 tagcount = _number(_extract(node, "tagcount"))
2015
2016 seq.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount))
2017
2018 return seq
2019
2020 def get_artists(self, limit=50):
2021 """
2022 Returns a sequence of Album objects
2023 if limit==None it will return all (may take a while)
2024 """
2025
2026 seq = []
2027 for node in _collect_nodes(limit, self, "library.getArtists", True):
2028 name = _extract(node, "name")
2029
2030 playcount = _number(_extract(node, "playcount"))
2031 tagcount = _number(_extract(node, "tagcount"))
2032
2033 seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount))
2034
2035 return seq
2036
2037 def get_tracks(self, artist=None, album=None, limit=50):
2038 """
2039 Returns a sequence of Album objects
2040 If limit==None it will return all (may take a while)
2041 """
2042
2043 params = self._get_params()
2044 if artist:
2045 params["artist"] = artist
2046 if album:
2047 params["album"] = album
2048
2049 seq = []
2050 for node in _collect_nodes(limit, self, "library.getTracks", True, params):
2051 name = _extract(node, "name")
2052 artist = _extract(node, "name", 1)
2053 playcount = _number(_extract(node, "playcount"))
2054 tagcount = _number(_extract(node, "tagcount"))
2055
2056 seq.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount))
2057
2058 return seq
2059
2060
2061 class Playlist(_BaseObject):
2062 """A Last.fm user playlist."""
2063
2064 id = None
2065 user = None
2066
2067 def __init__(self, user, id, network):
2068 _BaseObject.__init__(self, network)
2069
2070 if isinstance(user, User):
2071 self.user = user
2072 else:
2073 self.user = User(user, self.network)
2074
2075 self.id = id
2076
2077 @_string_output
2078 def __str__(self):
2079 return repr(self.user) + "'s playlist # " + repr(self.id)
2080
2081 def _get_info_node(self):
2082 """Returns the node from user.getPlaylists where this playlist's info is."""
2083
2084 doc = self._request("user.getPlaylists", True)
2085
2086 for node in doc.getElementsByTagName("playlist"):
2087 if _extract(node, "id") == str(self.get_id()):
2088 return node
2089
2090 def _get_params(self):
2091 return {'user': self.user.get_name(), 'playlistID': self.get_id()}
2092
2093 def get_id(self):
2094 """Returns the playlist id."""
2095
2096 return self.id
2097
2098 def get_user(self):
2099 """Returns the owner user of this playlist."""
2100
2101 return self.user
2102
2103 def get_tracks(self):
2104 """Returns a list of the tracks on this user playlist."""
2105
2106 uri = _unicode('lastfm://playlist/%s') %self.get_id()
2107
2108 return XSPF(uri, self.network).get_tracks()
2109
2110 def add_track(self, track):
2111 """Adds a Track to this Playlist."""
2112
2113 params = self._get_params()
2114 params['artist'] = track.get_artist().get_name()
2115 params['track'] = track.get_title()
2116
2117 self._request('playlist.addTrack', False, params)
2118
2119 def get_title(self):
2120 """Returns the title of this playlist."""
2121
2122 return _extract(self._get_info_node(), "title")
2123
2124 def get_creation_date(self):
2125 """Returns the creation date of this playlist."""
2126
2127 return _extract(self._get_info_node(), "date")
2128
2129 def get_size(self):
2130 """Returns the number of tracks in this playlist."""
2131
2132 return _number(_extract(self._get_info_node(), "size"))
2133
2134 def get_description(self):
2135 """Returns the description of this playlist."""
2136
2137 return _extract(self._get_info_node(), "description")
2138
2139 def get_duration(self):
2140 """Returns the duration of this playlist in milliseconds."""
2141
2142 return _number(_extract(self._get_info_node(), "duration"))
2143
2144 def is_streamable(self):
2145 """Returns True if the playlist is streamable.
2146 For a playlist to be streamable, it needs at least 45 tracks by 15 different artists."""
2147
2148 if _extract(self._get_info_node(), "streamable") == '1':
2149 return True
2150 else:
2151 return False
2152
2153 def has_track(self, track):
2154 """Checks to see if track is already in the playlist.
2155 * track: Any Track object.
2156 """
2157
2158 return track in self.get_tracks()
2159
2160 def get_cover_image(self, size = COVER_EXTRA_LARGE):
2161 """
2162 Returns a uri to the cover image
2163 size can be one of:
2164 COVER_MEGA
2165 COVER_EXTRA_LARGE
2166 COVER_LARGE
2167 COVER_MEDIUM
2168 COVER_SMALL
2169 """
2170
2171 return _extract(self._get_info_node(), "image")[size]
2172
2173 def get_url(self, domain_name = DOMAIN_ENGLISH):
2174 """Returns the url of the playlist on the network.
2175 * domain_name: The network's language domain. Possible values:
2176 o DOMAIN_ENGLISH
2177 o DOMAIN_GERMAN
2178 o DOMAIN_SPANISH
2179 o DOMAIN_FRENCH
2180 o DOMAIN_ITALIAN
2181 o DOMAIN_POLISH
2182 o DOMAIN_PORTUGUESE
2183 o DOMAIN_SWEDISH
2184 o DOMAIN_TURKISH
2185 o DOMAIN_RUSSIAN
2186 o DOMAIN_JAPANESE
2187 o DOMAIN_CHINESE
2188 """
2189
2190 english_url = _extract(self._get_info_node(), "url")
2191 appendix = english_url[english_url.rfind("/") + 1:]
2192
2193 return self.network._get_url(domain_name, "playlist") %{'appendix': appendix, "user": self.get_user().get_name()}
2194
2195
2196 class Tag(_BaseObject):
2197 """A Last.fm object tag."""
2198
2199 # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it)
2200
2201 name = None
2202
2203 def __init__(self, name, network):
2204 _BaseObject.__init__(self, network)
2205
2206 self.name = name
2207
2208 def __repr__(self):
2209 return "pylast.Tag(%s, %s)" %(repr(self.name), repr(self.network))
2210
2211 @_string_output
2212 def __str__(self):
2213 return self.get_name()
2214
2215 def __eq__(self, other):
2216 return self.get_name().lower() == other.get_name().lower()
2217
2218 def __ne__(self, other):
2219 return self.get_name().lower() != other.get_name().lower()
2220
2221 def _get_params(self):
2222 return {'tag': self.get_name()}
2223
2224 def get_name(self, properly_capitalized=False):
2225 """Returns the name of the tag. """
2226
2227 if properly_capitalized:
2228 self.name = _extract(self._request("tag.getInfo", True), "name")
2229
2230 return self.name
2231
2232 def get_similar(self):
2233 """Returns the tags similar to this one, ordered by similarity. """
2234
2235 doc = self._request('tag.getSimilar', True)
2236
2237 seq = []
2238 names = _extract_all(doc, 'name')
2239 for name in names:
2240 seq.append(Tag(name, self.network))
2241
2242 return seq
2243
2244 def get_top_albums(self):
2245 """Retuns a list of the top albums."""
2246
2247 doc = self._request('tag.getTopAlbums', True)
2248
2249 seq = []
2250
2251 for node in doc.getElementsByTagName("album"):
2252 name = _extract(node, "name")
2253 artist = _extract(node, "name", 1)
2254 playcount = _extract(node, "playcount")
2255
2256 seq.append(TopItem(Album(artist, name, self.network), playcount))
2257
2258 return seq
2259
2260 def get_top_tracks(self):
2261 """Returns a list of the most played Tracks by this artist."""
2262
2263 doc = self._request("tag.getTopTracks", True)
2264
2265 seq = []
2266 for track in doc.getElementsByTagName('track'):
2267
2268 title = _extract(track, "name")
2269 artist = _extract(track, "name", 1)
2270 playcount = _number(_extract(track, "playcount"))
2271
2272 seq.append( TopItem(Track(artist, title, self.network), playcount) )
2273
2274 return seq
2275
2276 def get_top_artists(self):
2277 """Returns a sequence of the most played artists."""
2278
2279 doc = self._request('tag.getTopArtists', True)
2280
2281 seq = []
2282 for node in doc.getElementsByTagName("artist"):
2283 name = _extract(node, 'name')
2284 playcount = _extract(node, "playcount")
2285
2286 seq.append(TopItem(Artist(name, self.network), playcount))
2287
2288 return seq
2289
2290 def get_weekly_chart_dates(self):
2291 """Returns a list of From and To tuples for the available charts."""
2292
2293 doc = self._request("tag.getWeeklyChartList", True)
2294
2295 seq = []
2296 for node in doc.getElementsByTagName("chart"):
2297 seq.append( (node.getAttribute("from"), node.getAttribute("to")) )
2298
2299 return seq
2300
2301 def get_weekly_artist_charts(self, from_date = None, to_date = None):
2302 """Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
2303
2304 params = self._get_params()
2305 if from_date and to_date:
2306 params["from"] = from_date
2307 params["to"] = to_date
2308
2309 doc = self._request("tag.getWeeklyArtistChart", True, params)
2310
2311 seq = []
2312 for node in doc.getElementsByTagName("artist"):
2313 item = Artist(_extract(node, "name"), self.network)
2314 weight = _number(_extract(node, "weight"))
2315 seq.append(TopItem(item, weight))
2316
2317 return seq
2318
2319 def get_url(self, domain_name = DOMAIN_ENGLISH):
2320 """Returns the url of the tag page on the network.
2321 * domain_name: The network's language domain. Possible values:
2322 o DOMAIN_ENGLISH
2323 o DOMAIN_GERMAN
2324 o DOMAIN_SPANISH
2325 o DOMAIN_FRENCH
2326 o DOMAIN_ITALIAN
2327 o DOMAIN_POLISH
2328 o DOMAIN_PORTUGUESE
2329 o DOMAIN_SWEDISH
2330 o DOMAIN_TURKISH
2331 o DOMAIN_RUSSIAN
2332 o DOMAIN_JAPANESE
2333 o DOMAIN_CHINESE
2334 """
2335
2336 name = _url_safe(self.get_name())
2337
2338 return self.network._get_url(domain_name, "tag") %{'name': name}
2339
2340 class Track(_BaseObject, _Taggable):
2341 """A Last.fm track."""
2342
2343 artist = None
2344 title = None
2345
2346 def __init__(self, artist, title, network):
2347 _BaseObject.__init__(self, network)
2348 _Taggable.__init__(self, 'track')
2349
2350 if isinstance(artist, Artist):
2351 self.artist = artist
2352 else:
2353 self.artist = Artist(artist, self.network)
2354
2355 self.title = title
2356
2357 def __repr__(self):
2358 return "pylast.Track(%s, %s, %s)" %(repr(self.artist.name), repr(self.title), repr(self.network))
2359
2360 @_string_output
2361 def __str__(self):
2362 return self.get_artist().get_name() + ' - ' + self.get_title()
2363
2364 def __eq__(self, other):
2365 return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
2366
2367 def __ne__(self, other):
2368 return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
2369
2370 def _get_params(self):
2371 return {'artist': self.get_artist().get_name(), 'track': self.get_title()}
2372
2373 def get_artist(self):
2374 """Returns the associated Artist object."""
2375
2376 return self.artist
2377
2378 def get_title(self, properly_capitalized=False):
2379 """Returns the track title."""
2380
2381 if properly_capitalized:
2382 self.title = _extract(self._request("track.getInfo", True), "name")
2383
2384 return self.title
2385
2386 def get_name(self, properly_capitalized=False):
2387 """Returns the track title (alias to Track.get_title)."""
2388
2389 return self.get_title(properly_capitalized)
2390
2391 def get_id(self):
2392 """Returns the track id on the network."""
2393
2394 doc = self._request("track.getInfo", True)
2395
2396 return _extract(doc, "id")
2397
2398 def get_duration(self):
2399 """Returns the track duration."""
2400
2401 doc = self._request("track.getInfo", True)
2402
2403 return _number(_extract(doc, "duration"))
2404
2405 def get_mbid(self):
2406 """Returns the MusicBrainz ID of this track."""
2407
2408 doc = self._request("track.getInfo", True)
2409
2410 return _extract(doc, "mbid")
2411
2412 def get_listener_count(self):
2413 """Returns the listener count."""
2414
2415 if hasattr(self, "listener_count"):
2416 return self.listener_count
2417 else:
2418 doc = self._request("track.getInfo", True)
2419 self.listener_count = _number(_extract(doc, "listeners"))
2420 return self.listener_count
2421
2422 def get_playcount(self):
2423 """Returns the play count."""
2424
2425 doc = self._request("track.getInfo", True)
2426 return _number(_extract(doc, "playcount"))
2427
2428 def is_streamable(self):
2429 """Returns True if the track is available at Last.fm."""
2430
2431 doc = self._request("track.getInfo", True)
2432 return _extract(doc, "streamable") == "1"
2433
2434 def is_fulltrack_available(self):
2435 """Returns True if the fulltrack is available for streaming."""
2436
2437 doc = self._request("track.getInfo", True)
2438 return doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1"
2439
2440 def get_album(self):
2441 """Returns the album object of this track."""
2442
2443 doc = self._request("track.getInfo", True)
2444
2445 albums = doc.getElementsByTagName("album")
2446
2447 if len(albums) == 0:
2448 return
2449
2450 node = doc.getElementsByTagName("album")[0]
2451 return Album(_extract(node, "artist"), _extract(node, "title"), self.network)
2452
2453 def get_wiki_published_date(self):
2454 """Returns the date of publishing this version of the wiki."""
2455
2456 doc = self._request("track.getInfo", True)
2457
2458 if len(doc.getElementsByTagName("wiki")) == 0:
2459 return
2460
2461 node = doc.getElementsByTagName("wiki")[0]
2462
2463 return _extract(node, "published")
2464
2465 def get_wiki_summary(self):
2466 """Returns the summary of the wiki."""
2467
2468 doc = self._request("track.getInfo", True)
2469
2470 if len(doc.getElementsByTagName("wiki")) == 0:
2471 return
2472
2473 node = doc.getElementsByTagName("wiki")[0]
2474
2475 return _extract(node, "summary")
2476
2477 def get_wiki_content(self):
2478 """Returns the content of the wiki."""
2479
2480 doc = self._request("track.getInfo", True)
2481
2482 if len(doc.getElementsByTagName("wiki")) == 0:
2483 return
2484
2485 node = doc.getElementsByTagName("wiki")[0]
2486
2487 return _extract(node, "content")
2488
2489 def love(self):
2490 """Adds the track to the user's loved tracks. """
2491
2492 self._request('track.love')
2493
2494 def ban(self):
2495 """Ban this track from ever playing on the radio. """
2496
2497 self._request('track.ban')
2498
2499 def get_similar(self):
2500 """Returns similar tracks for this track on the network, based on listening data. """
2501
2502 doc = self._request('track.getSimilar', True)
2503
2504 seq = []
2505 for node in doc.getElementsByTagName("track"):
2506 title = _extract(node, 'name')
2507 artist = _extract(node, 'name', 1)
2508 match = _number(_extract(node, "match"))
2509
2510 seq.append(SimilarItem(Track(artist, title, self.network), match))
2511
2512 return seq
2513
2514 def get_top_fans(self, limit = None):
2515 """Returns a list of the Users who played this track."""
2516
2517 doc = self._request('track.getTopFans', True)
2518
2519 seq = []
2520
2521 elements = doc.getElementsByTagName('user')
2522
2523 for element in elements:
2524 if limit and len(seq) >= limit:
2525 break
2526
2527 name = _extract(element, 'name')
2528 weight = _number(_extract(element, 'weight'))
2529
2530 seq.append(TopItem(User(name, self.network), weight))
2531
2532 return seq
2533
2534 def share(self, users, message = None):
2535 """Shares this track (sends out recommendations).
2536 * users: A list that can contain usernames, emails, User objects, or all of them.
2537 * message: A message to include in the recommendation message.
2538 """
2539
2540 #last.fm currently accepts a max of 10 recipient at a time
2541 while(len(users) > 10):
2542 section = users[0:9]
2543 users = users[9:]
2544 self.share(section, message)
2545
2546 nusers = []
2547 for user in users:
2548 if isinstance(user, User):
2549 nusers.append(user.get_name())
2550 else:
2551 nusers.append(user)
2552
2553 params = self._get_params()
2554 recipients = ','.join(nusers)
2555 params['recipient'] = recipients
2556 if message:
2557 params['message'] = message
2558
2559 self._request('track.share', False, params)
2560
2561 def get_url(self, domain_name = DOMAIN_ENGLISH):
2562 """Returns the url of the track page on the network.
2563 * domain_name: The network's language domain. Possible values:
2564 o DOMAIN_ENGLISH
2565 o DOMAIN_GERMAN
2566 o DOMAIN_SPANISH
2567 o DOMAIN_FRENCH
2568 o DOMAIN_ITALIAN
2569 o DOMAIN_POLISH
2570 o DOMAIN_PORTUGUESE
2571 o DOMAIN_SWEDISH
2572 o DOMAIN_TURKISH
2573 o DOMAIN_RUSSIAN
2574 o DOMAIN_JAPANESE
2575 o DOMAIN_CHINESE
2576 """
2577
2578 artist = _url_safe(self.get_artist().get_name())
2579 title = _url_safe(self.get_title())
2580
2581 return self.network._get_url(domain_name, "track") %{'domain': self.network._get_language_domain(domain_name), 'artist': artist, 'title': title}
2582
2583 def get_shouts(self, limit=50):
2584 """
2585 Returns a sequqence of Shout objects
2586 """
2587
2588 shouts = []
2589 for node in _collect_nodes(limit, self, "track.getShouts", False):
2590 shouts.append(Shout(
2591 _extract(node, "body"),
2592 User(_extract(node, "author"), self.network),
2593 _extract(node, "date")
2594 )
2595 )
2596 return shouts
2597
2598 class Group(_BaseObject):
2599 """A Last.fm group."""
2600
2601 name = None
2602
2603 def __init__(self, group_name, network):
2604 _BaseObject.__init__(self, network)
2605
2606 self.name = group_name
2607
2608 def __repr__(self):
2609 return "pylast.Group(%s, %s)" %(repr(self.name), repr(self.network))
2610
2611 @_string_output
2612 def __str__(self):
2613 return self.get_name()
2614
2615 def __eq__(self, other):
2616 return self.get_name().lower() == other.get_name().lower()
2617
2618 def __ne__(self, other):
2619 return self.get_name() != other.get_name()
2620
2621 def _get_params(self):
2622 return {'group': self.get_name()}
2623
2624 def get_name(self):
2625 """Returns the group name. """
2626 return self.name
2627
2628 def get_weekly_chart_dates(self):
2629 """Returns a list of From and To tuples for the available charts."""
2630
2631 doc = self._request("group.getWeeklyChartList", True)
2632
2633 seq = []
2634 for node in doc.getElementsByTagName("chart"):
2635 seq.append( (node.getAttribute("from"), node.getAttribute("to")) )
2636
2637 return seq
2638
2639 def get_weekly_artist_charts(self, from_date = None, to_date = None):
2640 """Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
2641
2642 params = self._get_params()
2643 if from_date and to_date:
2644 params["from"] = from_date
2645 params["to"] = to_date
2646
2647 doc = self._request("group.getWeeklyArtistChart", True, params)
2648
2649 seq = []
2650 for node in doc.getElementsByTagName("artist"):
2651 item = Artist(_extract(node, "name"), self.network)
2652 weight = _number(_extract(node, "playcount"))
2653 seq.append(TopItem(item, weight))
2654
2655 return seq
2656
2657 def get_weekly_album_charts(self, from_date = None, to_date = None):
2658 """Returns the weekly album charts for the week starting from the from_date value to the to_date value."""
2659
2660 params = self._get_params()
2661 if from_date and to_date:
2662 params["from"] = from_date
2663 params["to"] = to_date
2664
2665 doc = self._request("group.getWeeklyAlbumChart", True, params)
2666
2667 seq = []
2668 for node in doc.getElementsByTagName("album"):
2669 item = Album(_extract(node, "artist"), _extract(node, "name"), self.network)
2670 weight = _number(_extract(node, "playcount"))
2671 seq.append(TopItem(item, weight))
2672
2673 return seq
2674
2675 def get_weekly_track_charts(self, from_date = None, to_date = None):
2676 """Returns the weekly track charts for the week starting from the from_date value to the to_date value."""
2677
2678 params = self._get_params()
2679 if from_date and to_date:
2680 params["from"] = from_date
2681 params["to"] = to_date
2682
2683 doc = self._request("group.getWeeklyTrackChart", True, params)
2684
2685 seq = []
2686 for node in doc.getElementsByTagName("track"):
2687 item = Track(_extract(node, "artist"), _extract(node, "name"), self.network)
2688 weight = _number(_extract(node, "playcount"))
2689 seq.append(TopItem(item, weight))
2690
2691 return seq
2692
2693 def get_url(self, domain_name = DOMAIN_ENGLISH):
2694 """Returns the url of the group page on the network.
2695 * domain_name: The network's language domain. Possible values:
2696 o DOMAIN_ENGLISH
2697 o DOMAIN_GERMAN
2698 o DOMAIN_SPANISH
2699 o DOMAIN_FRENCH
2700 o DOMAIN_ITALIAN
2701 o DOMAIN_POLISH
2702 o DOMAIN_PORTUGUESE
2703 o DOMAIN_SWEDISH
2704 o DOMAIN_TURKISH
2705 o DOMAIN_RUSSIAN
2706 o DOMAIN_JAPANESE
2707 o DOMAIN_CHINESE
2708 """
2709
2710 name = _url_safe(self.get_name())
2711
2712 return self.network._get_url(domain_name, "group") %{'name': name}
2713
2714 def get_members(self, limit=50):
2715 """
2716 Returns a sequence of User objects
2717 if limit==None it will return all
2718 """
2719
2720 nodes = _collect_nodes(limit, self, "group.getMembers", False)
2721
2722 users = []
2723
2724 for node in nodes:
2725 users.append(User(_extract(node, "name"), self.network))
2726
2727 return users
2728
2729 class XSPF(_BaseObject):
2730 "A Last.fm XSPF playlist."""
2731
2732 uri = None
2733
2734 def __init__(self, uri, network):
2735 _BaseObject.__init__(self, network)
2736
2737 self.uri = uri
2738
2739 def _get_params(self):
2740 return {'playlistURL': self.get_uri()}
2741
2742 @_string_output
2743 def __str__(self):
2744 return self.get_uri()
2745
2746 def __eq__(self, other):
2747 return self.get_uri() == other.get_uri()
2748
2749 def __ne__(self, other):
2750 return self.get_uri() != other.get_uri()
2751
2752 def get_uri(self):
2753 """Returns the Last.fm playlist URI. """
2754
2755 return self.uri
2756
2757 def get_tracks(self):
2758 """Returns the tracks on this playlist."""
2759
2760 doc = self._request('playlist.fetch', True)
2761
2762 seq = []
2763 for n in doc.getElementsByTagName('track'):
2764 title = _extract(n, 'title')
2765 artist = _extract(n, 'creator')
2766
2767 seq.append(Track(artist, title, self.network))
2768
2769 return seq
2770
2771 class User(_BaseObject):
2772 """A Last.fm user."""
2773
2774 name = None
2775
2776 def __init__(self, user_name, network):
2777 _BaseObject.__init__(self, network)
2778
2779 self.name = user_name
2780
2781 self._past_events_index = 0
2782 self._recommended_events_index = 0
2783 self._recommended_artists_index = 0
2784
2785 def __repr__(self):
2786 return "pylast.User(%s, %s)" %(repr(self.name), repr(self.network))
2787
2788 @_string_output
2789 def __str__(self):
2790 return self.get_name()
2791
2792 def __eq__(self, another):
2793 return self.get_name() == another.get_name()
2794
2795 def __ne__(self, another):
2796 return self.get_name() != another.get_name()
2797
2798 def _get_params(self):
2799 return {"user": self.get_name()}
2800
2801 def get_name(self, properly_capitalized=False):
2802 """Returns the nuser name."""
2803
2804 if properly_capitalized:
2805 self.name = _extract(self._request("user.getInfo", True), "name")
2806
2807 return self.name
2808
2809 def get_upcoming_events(self):
2810 """Returns all the upcoming events for this user. """
2811
2812 doc = self._request('user.getEvents', True)
2813
2814 ids = _extract_all(doc, 'id')
2815 events = []
2816
2817 for e_id in ids:
2818 events.append(Event(e_id, self.network))
2819
2820 return events
2821
2822 def get_friends(self, limit = 50):
2823 """Returns a list of the user's friends. """
2824
2825 seq = []
2826 for node in _collect_nodes(limit, self, "user.getFriends", False):
2827 seq.append(User(_extract(node, "name"), self.network))
2828
2829 return seq
2830
2831 def get_loved_tracks(self, limit=50):
2832 """Returns this user's loved track as a sequence of LovedTrack objects
2833 in reverse order of their timestamp, all the way back to the first track.
2834
2835 If limit==None, it will try to pull all the available data.
2836
2837 This method uses caching. Enable caching only if you're pulling a
2838 large amount of data.
2839
2840 Use extract_items() with the return of this function to
2841 get only a sequence of Track objects with no playback dates. """
2842
2843 params = self._get_params()
2844 if limit:
2845 params['limit'] = limit
2846
2847 seq = []
2848 for track in _collect_nodes(limit, self, "user.getLovedTracks", True, params):
2849
2850 title = _extract(track, "name")
2851 artist = _extract(track, "name", 1)
2852 date = _extract(track, "date")
2853 timestamp = track.getElementsByTagName("date")[0].getAttribute("uts")
2854
2855 seq.append(LovedTrack(Track(artist, title, self.network), date, timestamp))
2856
2857 return seq
2858
2859 def get_neighbours(self, limit = 50):
2860 """Returns a list of the user's friends."""
2861
2862 params = self._get_params()
2863 if limit:
2864 params['limit'] = limit
2865
2866 doc = self._request('user.getNeighbours', True, params)
2867
2868 seq = []
2869 names = _extract_all(doc, 'name')
2870
2871 for name in names:
2872 seq.append(User(name, self.network))
2873
2874 return seq
2875
2876 def get_past_events(self, limit=50):
2877 """
2878 Returns a sequence of Event objects
2879 if limit==None it will return all
2880 """
2881
2882 seq = []
2883 for n in _collect_nodes(limit, self, "user.getPastEvents", False):
2884 seq.append(Event(_extract(n, "id"), self.network))
2885
2886 return seq
2887
2888 def get_playlists(self):
2889 """Returns a list of Playlists that this user owns."""
2890
2891 doc = self._request("user.getPlaylists", True)
2892
2893 playlists = []
2894 for playlist_id in _extract_all(doc, "id"):
2895 playlists.append(Playlist(self.get_name(), playlist_id, self.network))
2896
2897 return playlists
2898
2899 def get_now_playing(self):
2900 """Returns the currently playing track, or None if nothing is playing. """
2901
2902 params = self._get_params()
2903 params['limit'] = '1'
2904
2905 doc = self._request('user.getRecentTracks', False, params)
2906
2907 e = doc.getElementsByTagName('track')[0]
2908
2909 if not e.hasAttribute('nowplaying'):
2910 return None
2911
2912 artist = _extract(e, 'artist')
2913 title = _extract(e, 'name')
2914
2915 return Track(artist, title, self.network)
2916
2917
2918 def get_recent_tracks(self, limit = 10):
2919 """Returns this user's played track as a sequence of PlayedTrack objects
2920 in reverse order of their playtime, all the way back to the first track.
2921
2922 If limit==None, it will try to pull all the available data.
2923
2924 This method uses caching. Enable caching only if you're pulling a
2925 large amount of data.
2926
2927 Use extract_items() with the return of this function to
2928 get only a sequence of Track objects with no playback dates. """
2929
2930 params = self._get_params()
2931 if limit:
2932 params['limit'] = limit
2933
2934 seq = []
2935 for track in _collect_nodes(limit, self, "user.getRecentTracks", True, params):
2936
2937 if track.hasAttribute('nowplaying'):
2938 continue #to prevent the now playing track from sneaking in here
2939
2940 title = _extract(track, "name")
2941 artist = _extract(track, "artist")
2942 date = _extract(track, "date")
2943 timestamp = track.getElementsByTagName("date")[0].getAttribute("uts")
2944
2945 seq.append(PlayedTrack(Track(artist, title, self.network), date, timestamp))
2946
2947 return seq
2948
2949 def get_id(self):
2950 """Returns the user id."""
2951
2952 doc = self._request("user.getInfo", True)
2953
2954 return _extract(doc, "id")
2955
2956 def get_language(self):
2957 """Returns the language code of the language used by the user."""
2958
2959 doc = self._request("user.getInfo", True)
2960
2961 return _extract(doc, "lang")
2962
2963 def get_country(self):
2964 """Returns the name of the country of the user."""
2965
2966 doc = self._request("user.getInfo", True)
2967
2968 return Country(_extract(doc, "country"), self.network)
2969
2970 def get_age(self):
2971 """Returns the user's age."""
2972
2973 doc = self._request("user.getInfo", True)
2974
2975 return _number(_extract(doc, "age"))
2976
2977 def get_gender(self):
2978 """Returns the user's gender. Either USER_MALE or USER_FEMALE."""
2979
2980 doc = self._request("user.getInfo", True)
2981
2982 value = _extract(doc, "gender")
2983
2984 if value == 'm':
2985 return USER_MALE
2986 elif value == 'f':
2987 return USER_FEMALE
2988
2989 return None
2990
2991 def is_subscriber(self):
2992 """Returns whether the user is a subscriber or not. True or False."""
2993
2994 doc = self._request("user.getInfo", True)
2995
2996 return _extract(doc, "subscriber") == "1"
2997
2998 def get_playcount(self):
2999 """Returns the user's playcount so far."""
3000
3001 doc = self._request("user.getInfo", True)
3002
3003 return _number(_extract(doc, "playcount"))
3004
3005 def get_top_albums(self, period = PERIOD_OVERALL):
3006 """Returns the top albums played by a user.
3007 * period: The period of time. Possible values:
3008 o PERIOD_OVERALL
3009 o PERIOD_7DAYS
3010 o PERIOD_3MONTHS
3011 o PERIOD_6MONTHS
3012 o PERIOD_12MONTHS
3013 """
3014
3015 params = self._get_params()
3016 params['period'] = period
3017
3018 doc = self._request('user.getTopAlbums', True, params)
3019
3020 seq = []
3021 for album in doc.getElementsByTagName('album'):
3022 name = _extract(album, 'name')
3023 artist = _extract(album, 'name', 1)
3024 playcount = _extract(album, "playcount")
3025
3026 seq.append(TopItem(Album(artist, name, self.network), playcount))
3027
3028 return seq
3029
3030 def get_top_artists(self, period = PERIOD_OVERALL):
3031 """Returns the top artists played by a user.
3032 * period: The period of time. Possible values:
3033 o PERIOD_OVERALL
3034 o PERIOD_7DAYS
3035 o PERIOD_3MONTHS
3036 o PERIOD_6MONTHS
3037 o PERIOD_12MONTHS
3038 """
3039
3040 params = self._get_params()
3041 params['period'] = period
3042
3043 doc = self._request('user.getTopArtists', True, params)
3044
3045 seq = []
3046 for node in doc.getElementsByTagName('artist'):
3047 name = _extract(node, 'name')
3048 playcount = _extract(node, "playcount")
3049
3050 seq.append(TopItem(Artist(name, self.network), playcount))
3051
3052 return seq
3053
3054 def get_top_tags(self, limit=None):
3055 """Returns a sequence of the top tags used by this user with their counts as TopItem objects.
3056 * limit: The limit of how many tags to return.
3057 """
3058
3059 doc = self._request("user.getTopTags", True)
3060
3061 seq = []
3062 for node in doc.getElementsByTagName("tag"):
3063 seq.append(TopItem(Tag(_extract(node, "name"), self.network), _extract(node, "count")))
3064
3065 if limit:
3066 seq = seq[:limit]
3067
3068 return seq
3069
3070 def get_top_tracks(self, period = PERIOD_OVERALL):
3071 """Returns the top tracks played by a user.
3072 * period: The period of time. Possible values:
3073 o PERIOD_OVERALL
3074 o PERIOD_7DAYS
3075 o PERIOD_3MONTHS
3076 o PERIOD_6MONTHS
3077 o PERIOD_12MONTHS
3078 """
3079
3080 params = self._get_params()
3081 params['period'] = period
3082
3083 doc = self._request('user.getTopTracks', True, params)
3084
3085 seq = []
3086 for track in doc.getElementsByTagName('track'):
3087 name = _extract(track, 'name')
3088 artist = _extract(track, 'name', 1)
3089 playcount = _extract(track, "playcount")
3090
3091 seq.append(TopItem(Track(artist, name, self.network), playcount))
3092
3093 return seq
3094
3095 def get_weekly_chart_dates(self):
3096 """Returns a list of From and To tuples for the available charts."""
3097
3098 doc = self._request("user.getWeeklyChartList", True)
3099
3100 seq = []
3101 for node in doc.getElementsByTagName("chart"):
3102 seq.append( (node.getAttribute("from"), node.getAttribute("to")) )
3103
3104 return seq
3105
3106 def get_weekly_artist_charts(self, from_date = None, to_date = None):
3107 """Returns the weekly artist charts for the week starting from the from_date value to the to_date value."""
3108
3109 params = self._get_params()
3110 if from_date and to_date:
3111 params["from"] = from_date
3112 params["to"] = to_date
3113
3114 doc = self._request("user.getWeeklyArtistChart", True, params)
3115
3116 seq = []
3117 for node in doc.getElementsByTagName("artist"):
3118 item = Artist(_extract(node, "name"), self.network)
3119 weight = _number(_extract(node, "playcount"))
3120 seq.append(TopItem(item, weight))
3121
3122 return seq
3123
3124 def get_weekly_album_charts(self, from_date = None, to_date = None):
3125 """Returns the weekly album charts for the week starting from the from_date value to the to_date value."""
3126
3127 params = self._get_params()
3128 if from_date and to_date:
3129 params["from"] = from_date
3130 params["to"] = to_date
3131
3132 doc = self._request("user.getWeeklyAlbumChart", True, params)
3133
3134 seq = []
3135 for node in doc.getElementsByTagName("album"):
3136 item = Album(_extract(node, "artist"), _extract(node, "name"), self.network)
3137 weight = _number(_extract(node, "playcount"))
3138 seq.append(TopItem(item, weight))
3139
3140 return seq
3141
3142 def get_weekly_track_charts(self, from_date = None, to_date = None):
3143 """Returns the weekly track charts for the week starting from the from_date value to the to_date value."""
3144
3145 params = self._get_params()
3146 if from_date and to_date:
3147 params["from"] = from_date
3148 params["to"] = to_date
3149
3150 doc = self._request("user.getWeeklyTrackChart", True, params)
3151
3152 seq = []
3153 for node in doc.getElementsByTagName("track"):
3154 item = Track(_extract(node, "artist"), _extract(node, "name"), self.network)
3155 weight = _number(_extract(node, "playcount"))
3156 seq.append(TopItem(item, weight))
3157
3158 return seq
3159
3160 def compare_with_user(self, user, shared_artists_limit = None):
3161 """Compare this user with another Last.fm user.
3162 Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...))
3163 user: A User object or a username string/unicode object.
3164 """
3165
3166 if isinstance(user, User):
3167 user = user.get_name()
3168
3169 params = self._get_params()
3170 if shared_artists_limit:
3171 params['limit'] = shared_artists_limit
3172 params['type1'] = 'user'
3173 params['type2'] = 'user'
3174 params['value1'] = self.get_name()
3175 params['value2'] = user
3176
3177 doc = self._request('tasteometer.compare', False, params)
3178
3179 score = _extract(doc, 'score')
3180
3181 artists = doc.getElementsByTagName('artists')[0]
3182 shared_artists_names = _extract_all(artists, 'name')
3183
3184 shared_artists_seq = []
3185
3186 for name in shared_artists_names:
3187 shared_artists_seq.append(Artist(name, self.network))
3188
3189 return (score, shared_artists_seq)
3190
3191 def get_image(self):
3192 """Returns the user's avatar."""
3193
3194 doc = self._request("user.getInfo", True)
3195
3196 return _extract(doc, "image")
3197
3198 def get_url(self, domain_name = DOMAIN_ENGLISH):
3199 """Returns the url of the user page on the network.
3200 * domain_name: The network's language domain. Possible values:
3201 o DOMAIN_ENGLISH
3202 o DOMAIN_GERMAN
3203 o DOMAIN_SPANISH
3204 o DOMAIN_FRENCH
3205 o DOMAIN_ITALIAN
3206 o DOMAIN_POLISH
3207 o DOMAIN_PORTUGUESE
3208 o DOMAIN_SWEDISH
3209 o DOMAIN_TURKISH
3210 o DOMAIN_RUSSIAN
3211 o DOMAIN_JAPANESE
3212 o DOMAIN_CHINESE
3213 """
3214
3215 name = _url_safe(self.get_name())
3216
3217 return self.network._get_url(domain_name, "user") %{'name': name}
3218
3219 def get_library(self):
3220 """Returns the associated Library object. """
3221
3222 return Library(self, self.network)
3223
3224 def get_shouts(self, limit=50):
3225 """
3226 Returns a sequqence of Shout objects
3227 """
3228
3229 shouts = []
3230 for node in _collect_nodes(limit, self, "user.getShouts", False):
3231 shouts.append(Shout(
3232 _extract(node, "body"),
3233 User(_extract(node, "author"), self.network),
3234 _extract(node, "date")
3235 )
3236 )
3237 return shouts
3238
3239 def shout(self, message):
3240 """
3241 Post a shout
3242 """
3243
3244 params = self._get_params()
3245 params["message"] = message
3246
3247 self._request("user.Shout", False, params)
3248
3249 class AuthenticatedUser(User):
3250 def __init__(self, network):
3251 User.__init__(self, "", network);
3252
3253 def _get_params(self):
3254 return {"user": self.get_name()}
3255
3256 def get_name(self):
3257 """Returns the name of the authenticated user."""
3258
3259 doc = self._request("user.getInfo", True, {"user": ""}) # hack
3260
3261 self.name = _extract(doc, "name")
3262 return self.name
3263
3264 def get_recommended_events(self, limit=50):
3265 """
3266 Returns a sequence of Event objects
3267 if limit==None it will return all
3268 """
3269
3270 seq = []
3271 for node in _collect_nodes(limit, self, "user.getRecommendedEvents", False):
3272 seq.append(Event(_extract(node, "id"), self.network))
3273
3274 return seq
3275
3276 def get_recommended_artists(self, limit=50):
3277 """
3278 Returns a sequence of Event objects
3279 if limit==None it will return all
3280 """
3281
3282 seq = []
3283 for node in _collect_nodes(limit, self, "user.getRecommendedArtists", False):
3284 seq.append(Artist(_extract(node, "name"), self.network))
3285
3286 return seq
3287
3288 class _Search(_BaseObject):
3289 """An abstract class. Use one of its derivatives."""
3290
3291 def __init__(self, ws_prefix, search_terms, network):
3292 _BaseObject.__init__(self, network)
3293
3294 self._ws_prefix = ws_prefix
3295 self.search_terms = search_terms
3296
3297 self._last_page_index = 0
3298
3299 def _get_params(self):
3300 params = {}
3301
3302 for key in self.search_terms.keys():
3303 params[key] = self.search_terms[key]
3304
3305 return params
3306
3307 def get_total_result_count(self):
3308 """Returns the total count of all the results."""
3309
3310 doc = self._request(self._ws_prefix + ".search", True)
3311
3312 return _extract(doc, "opensearch:totalResults")
3313
3314 def _retreive_page(self, page_index):
3315 """Returns the node of matches to be processed"""
3316
3317 params = self._get_params()
3318 params["page"] = str(page_index)
3319 doc = self._request(self._ws_prefix + ".search", True, params)
3320
3321 return doc.getElementsByTagName(self._ws_prefix + "matches")[0]
3322
3323 def _retrieve_next_page(self):
3324 self._last_page_index += 1
3325 return self._retreive_page(self._last_page_index)
3326
3327 class AlbumSearch(_Search):
3328 """Search for an album by name."""
3329
3330 def __init__(self, album_name, network):
3331
3332 _Search.__init__(self, "album", {"album": album_name}, network)
3333
3334 def get_next_page(self):
3335 """Returns the next page of results as a sequence of Album objects."""
3336
3337 master_node = self._retrieve_next_page()
3338
3339 seq = []
3340 for node in master_node.getElementsByTagName("album"):
3341 seq.append(Album(_extract(node, "artist"), _extract(node, "name"), self.network))
3342
3343 return seq
3344
3345 class ArtistSearch(_Search):
3346 """Search for an artist by artist name."""
3347
3348 def __init__(self, artist_name, network):
3349 _Search.__init__(self, "artist", {"artist": artist_name}, network)
3350
3351 def get_next_page(self):
3352 """Returns the next page of results as a sequence of Artist objects."""
3353
3354 master_node = self._retrieve_next_page()
3355
3356 seq = []
3357 for node in master_node.getElementsByTagName("artist"):
3358 artist = Artist(_extract(node, "name"), self.network)
3359 artist.listener_count = _number(_extract(node, "listeners"))
3360 seq.append(artist)
3361
3362 return seq
3363
3364 class TagSearch(_Search):
3365 """Search for a tag by tag name."""
3366
3367 def __init__(self, tag_name, network):
3368
3369 _Search.__init__(self, "tag", {"tag": tag_name}, network)
3370
3371 def get_next_page(self):
3372 """Returns the next page of results as a sequence of Tag objects."""
3373
3374 master_node = self._retrieve_next_page()
3375
3376 seq = []
3377 for node in master_node.getElementsByTagName("tag"):
3378 tag = Tag(_extract(node, "name"), self.network)
3379 tag.tag_count = _number(_extract(node, "count"))
3380 seq.append(tag)
3381
3382 return seq
3383
3384 class TrackSearch(_Search):
3385 """Search for a track by track title. If you don't wanna narrow the results down
3386 by specifying the artist name, set it to empty string."""
3387
3388 def __init__(self, artist_name, track_title, network):
3389
3390 _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, network)
3391
3392 def get_next_page(self):
3393 """Returns the next page of results as a sequence of Track objects."""
3394
3395 master_node = self._retrieve_next_page()
3396
3397 seq = []
3398 for node in master_node.getElementsByTagName("track"):
3399 track = Track(_extract(node, "artist"), _extract(node, "name"), self.network)
3400 track.listener_count = _number(_extract(node, "listeners"))
3401 seq.append(track)
3402
3403 return seq
3404
3405 class VenueSearch(_Search):
3406 """Search for a venue by its name. If you don't wanna narrow the results down
3407 by specifying a country, set it to empty string."""
3408
3409 def __init__(self, venue_name, country_name, network):
3410
3411 _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, network)
3412
3413 def get_next_page(self):
3414 """Returns the next page of results as a sequence of Track objects."""
3415
3416 master_node = self._retrieve_next_page()
3417
3418 seq = []
3419 for node in master_node.getElementsByTagName("venue"):
3420 seq.append(Venue(_extract(node, "id"), self.network))
3421
3422 return seq
3423
3424 class Venue(_BaseObject):
3425 """A venue where events are held."""
3426
3427 # TODO: waiting for a venue.getInfo web service to use.
3428
3429 id = None
3430
3431 def __init__(self, id, network):
3432 _BaseObject.__init__(self, network)
3433
3434 self.id = _number(id)
3435
3436 def __repr__(self):
3437 return "pylast.Venue(%s, %s)" %(repr(self.id), repr(self.network))
3438
3439 @_string_output
3440 def __str__(self):
3441 return "Venue #" + str(self.id)
3442
3443 def __eq__(self, other):
3444 return self.get_id() == other.get_id()
3445
3446 def _get_params(self):
3447 return {"venue": self.get_id()}
3448
3449 def get_id(self):
3450 """Returns the id of the venue."""
3451
3452 return self.id
3453
3454 def get_upcoming_events(self):
3455 """Returns the upcoming events in this venue."""
3456
3457 doc = self._request("venue.getEvents", True)
3458
3459 seq = []
3460 for node in doc.getElementsByTagName("event"):
3461 seq.append(Event(_extract(node, "id"), self.network))
3462
3463 return seq
3464
3465 def get_past_events(self):
3466 """Returns the past events held in this venue."""
3467
3468 doc = self._request("venue.getEvents", True)
3469
3470 seq = []
3471 for node in doc.getElementsByTagName("event"):
3472 seq.append(Event(_extract(node, "id"), self.network))
3473
3474 return seq
3475
3476 def md5(text):
3477 """Returns the md5 hash of a string."""
3478
3479 h = hashlib.md5()
3480 h.update(_unicode(text).encode("utf-8"))
3481
3482 return h.hexdigest()
3483
3484 def _unicode(text):
3485 if sys.version_info[0] == 3:
3486 if type(text) in (bytes, bytearray):
3487 return str(text, "utf-8")
3488 elif type(text) == str:
3489 return text
3490 else:
3491 return str(text)
3492
3493 elif sys.version_info[0] ==2:
3494 if type(text) in (str,):
3495 return unicode(text, "utf-8")
3496 elif type(text) == unicode:
3497 return text
3498 else:
3499 return unicode(text)
3500
3501 def _string(text):
3502 """For Python2 routines that can only process str type."""
3503
3504 if sys.version_info[0] == 3:
3505 if type(text) != str:
3506 return str(text)
3507 else:
3508 return text
3509
3510 elif sys.version_info[0] == 2:
3511 if type(text) == str:
3512 return text
3513
3514 if type(text) == int:
3515 return str(text)
3516
3517 return text.encode("utf-8")
3518
3519 def _collect_nodes(limit, sender, method_name, cacheable, params=None):
3520 """
3521 Returns a sequqnce of dom.Node objects about as close to
3522 limit as possible
3523 """
3524
3525 if not params:
3526 params = sender._get_params()
3527
3528 nodes = []
3529 page = 1
3530 end_of_pages = False
3531
3532 while not end_of_pages and (not limit or (limit and len(nodes) < limit)):
3533 params["page"] = str(page)
3534 doc = sender._request(method_name, cacheable, params)
3535
3536 main = doc.documentElement.childNodes[1]
3537
3538 if main.hasAttribute("totalPages"):
3539 total_pages = _number(main.getAttribute("totalPages"))
3540 elif main.hasAttribute("totalpages"):
3541 total_pages = _number(main.getAttribute("totalpages"))
3542 else:
3543 raise Exception("No total pages attribute")
3544
3545 for node in main.childNodes:
3546 if not node.nodeType == xml.dom.Node.TEXT_NODE and len(nodes) < limit:
3547 nodes.append(node)
3548
3549 if page >= total_pages:
3550 end_of_pages = True
3551
3552 page += 1
3553
3554 return nodes
3555
3556 def _extract(node, name, index = 0):
3557 """Extracts a value from the xml string"""
3558
3559 nodes = node.getElementsByTagName(name)
3560
3561 if len(nodes):
3562 if nodes[index].firstChild:
3563 return _unescape_htmlentity(nodes[index].firstChild.data.strip())
3564 else:
3565 return None
3566
3567 def _extract_all(node, name, limit_count = None):
3568 """Extracts all the values from the xml string. returning a list."""
3569
3570 seq = []
3571
3572 for i in range(0, len(node.getElementsByTagName(name))):
3573 if len(seq) == limit_count:
3574 break
3575
3576 seq.append(_extract(node, name, i))
3577
3578 return seq
3579
3580 def _url_safe(text):
3581 """Does all kinds of tricks on a text to make it safe to use in a url."""
3582
3583 return url_quote_plus(url_quote_plus(_string(text))).lower()
3584
3585 def _number(string):
3586 """
3587 Extracts an int from a string. Returns a 0 if None or an empty string was passed
3588 """
3589
3590 if not string:
3591 return 0
3592 elif string == "":
3593 return 0
3594 else:
3595 try:
3596 return int(string)
3597 except ValueError:
3598 return float(string)
3599
3600 def _unescape_htmlentity(string):
3601
3602 #string = _unicode(string)
3603
3604 mapping = htmlentitydefs.name2codepoint
3605 for key in mapping:
3606 string = string.replace("&%s;" %key, unichr(mapping[key]))
3607
3608 return string
3609
3610 def extract_items(topitems_or_libraryitems):
3611 """Extracts a sequence of items from a sequence of TopItem or LibraryItem objects."""
3612
3613 seq = []
3614 for i in topitems_or_libraryitems:
3615 seq.append(i.item)
3616
3617 return seq
3618
3619 class ScrobblingError(Exception):
3620 def __init__(self, message):
3621 Exception.__init__(self)
3622 self.message = message
3623
3624 @_string_output
3625 def __str__(self):
3626 return self.message
3627
3628 class BannedClientError(ScrobblingError):
3629 def __init__(self):
3630 ScrobblingError.__init__(self, "This version of the client has been banned")
3631
3632 class BadAuthenticationError(ScrobblingError):
3633 def __init__(self):
3634 ScrobblingError.__init__(self, "Bad authentication token")
3635
3636 class BadTimeError(ScrobblingError):
3637 def __init__(self):
3638 ScrobblingError.__init__(self, "Time provided is not close enough to current time")
3639
3640 class BadSessionError(ScrobblingError):
3641 def __init__(self):
3642 ScrobblingError.__init__(self, "Bad session id, consider re-handshaking")
3643
3644 class _ScrobblerRequest(object):
3645
3646 def __init__(self, url, params, network, type="POST"):
3647
3648 for key in params:
3649 params[key] = str(params[key])
3650
3651 self.params = params
3652 self.type = type
3653 (self.hostname, self.subdir) = url_split_host(url[len("http:"):])
3654 self.network = network
3655
3656 def execute(self):
3657 """Returns a string response of this request."""
3658
3659 connection = HTTPConnection(self.hostname)
3660
3661 data = []
3662 for name in self.params.keys():
3663 value = url_quote_plus(self.params[name])
3664 data.append('='.join((name, value)))
3665 data = "&".join(data)
3666
3667 headers = {
3668 "Content-type": "application/x-www-form-urlencoded",
3669 "Accept-Charset": "utf-8",
3670 "User-Agent": "pylast" + "/" + __version__,
3671 "HOST": self.hostname
3672 }
3673
3674 if self.type == "GET":
3675 connection.request("GET", self.subdir + "?" + data, headers = headers)
3676 else:
3677 connection.request("POST", self.subdir, data, headers)
3678 response = _unicode(connection.getresponse().read())
3679
3680 self._check_response_for_errors(response)
3681
3682 return response
3683
3684 def _check_response_for_errors(self, response):
3685 """When passed a string response it checks for erros, raising
3686 any exceptions as necessary."""
3687
3688 lines = response.split("\n")
3689 status_line = lines[0]
3690
3691 if status_line == "OK":
3692 return
3693 elif status_line == "BANNED":
3694 raise BannedClientError()
3695 elif status_line == "BADAUTH":
3696 raise BadAuthenticationError()
3697 elif status_line == "BADTIME":
3698 raise BadTimeError()
3699 elif status_line == "BADSESSION":
3700 raise BadSessionError()
3701 elif status_line.startswith("FAILED "):
3702 reason = status_line[status_line.find("FAILED ")+len("FAILED "):]
3703 raise ScrobblingError(reason)
3704
3705 class Scrobbler(object):
3706 """A class for scrobbling tracks to Last.fm"""
3707
3708 session_id = None
3709 nowplaying_url = None
3710 submissions_url = None
3711
3712 def __init__(self, network, client_id, client_version):
3713 self.client_id = client_id
3714 self.client_version = client_version
3715 self.username = network.username
3716 self.password = network.password_hash
3717 self.network = network
3718
3719 def _do_handshake(self):
3720 """Handshakes with the server"""
3721
3722 timestamp = str(int(time.time()))
3723
3724 if self.password and self.username:
3725 token = md5(self.password + timestamp)
3726 elif self.network.api_key and self.network.api_secret and self.network.session_key:
3727 if not self.username:
3728 self.username = self.network.get_authenticated_user().get_name()
3729 token = md5(self.network.api_secret + timestamp)
3730
3731 params = {"hs": "true", "p": "1.2.1", "c": self.client_id,
3732 "v": self.client_version, "u": self.username, "t": timestamp,
3733 "a": token}
3734
3735 if self.network.session_key and self.network.api_key:
3736 params["sk"] = self.network.session_key
3737 params["api_key"] = self.network.api_key
3738
3739 server = self.network.submission_server
3740 response = _ScrobblerRequest(server, params, self.network, "GET").execute().split("\n")
3741
3742 self.session_id = response[1]
3743 self.nowplaying_url = response[2]
3744 self.submissions_url = response[3]
3745
3746 def _get_session_id(self, new = False):
3747 """Returns a handshake. If new is true, then it will be requested from the server
3748 even if one was cached."""
3749
3750 if not self.session_id or new:
3751 self._do_handshake()
3752
3753 return self.session_id
3754
3755 def report_now_playing(self, artist, title, album = "", duration = "", track_number = "", mbid = ""):
3756
3757 _deprecation_warning("DeprecationWarning: Use Netowrk.update_now_playing(...) instead")
3758
3759 params = {"s": self._get_session_id(), "a": artist, "t": title,
3760 "b": album, "l": duration, "n": track_number, "m": mbid}
3761
3762 try:
3763 _ScrobblerRequest(self.nowplaying_url, params, self.network).execute()
3764 except BadSessionError:
3765 self._do_handshake()
3766 self.report_now_playing(artist, title, album, duration, track_number, mbid)
3767
3768 def scrobble(self, artist, title, time_started, source, mode, duration, album="", track_number="", mbid=""):
3769 """Scrobble a track. parameters:
3770 artist: Artist name.
3771 title: Track title.
3772 time_started: UTC timestamp of when the track started playing.
3773 source: The source of the track
3774 SCROBBLE_SOURCE_USER: Chosen by the user (the most common value, unless you have a reason for choosing otherwise, use this).
3775 SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1).
3776 SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast).
3777 SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the 5-digit recommendation_key value must be set.
3778 SCROBBLE_SOURCE_UNKNOWN: Source unknown.
3779 mode: The submission mode
3780 SCROBBLE_MODE_PLAYED: The track was played.
3781 SCROBBLE_MODE_LOVED: The user manually loved the track (implies a listen)
3782 SCROBBLE_MODE_SKIPPED: The track was skipped (Only if source was Last.fm)
3783 SCROBBLE_MODE_BANNED: The track was banned (Only if source was Last.fm)
3784 duration: Track duration in seconds.
3785 album: The album name.
3786 track_number: The track number on the album.
3787 mbid: MusicBrainz ID.
3788 """
3789
3790 _deprecation_warning("DeprecationWarning: Use Network.scrobble(...) instead")
3791
3792 params = {"s": self._get_session_id(), "a[0]": _string(artist), "t[0]": _string(title),
3793 "i[0]": str(time_started), "o[0]": source, "r[0]": mode, "l[0]": str(duration),
3794 "b[0]": _string(album), "n[0]": track_number, "m[0]": mbid}
3795
3796 _ScrobblerRequest(self.submissions_url, params, self.network).execute()
3797
3798 def scrobble_many(self, tracks):
3799 """
3800 Scrobble several tracks at once.
3801
3802 tracks: A sequence of a sequence of parameters for each trach. The order of parameters
3803 is the same as if passed to the scrobble() method.
3804 """
3805
3806 _deprecation_warning("DeprecationWarning: Use Network.scrobble_many(...) instead")
3807
3808 remainder = []
3809
3810 if len(tracks) > 50:
3811 remainder = tracks[50:]
3812 tracks = tracks[:50]
3813
3814 params = {"s": self._get_session_id()}
3815
3816 i = 0
3817 for t in tracks:
3818 _pad_list(t, 9, "")
3819 params["a[%s]" % str(i)] = _string(t[0])
3820 params["t[%s]" % str(i)] = _string(t[1])
3821 params["i[%s]" % str(i)] = str(t[2])
3822 params["o[%s]" % str(i)] = t[3]
3823 params["r[%s]" % str(i)] = t[4]
3824 params["l[%s]" % str(i)] = str(t[5])
3825 params["b[%s]" % str(i)] = _string(t[6])
3826 params["n[%s]" % str(i)] = t[7]
3827 params["m[%s]" % str(i)] = t[8]
3828
3829 i += 1
3830
3831 _ScrobblerRequest(self.submissions_url, params, self.network).execute()
3832
3833 if remainder:
3834 self.scrobble_many(remainder)