Mercurial > hg > camir-aes2014
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) |