annotate toolboxes/_python_lib/pylast/pylast.py @ 0:cc4b1211e677 tip

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