PageRenderTime 95ms CodeModel.GetById 21ms app.highlight 63ms RepoModel.GetById 1ms app.codeStats 0ms

/pylast.py

http://radioappz.googlecode.com/
Python | 2040 lines | 1933 code | 56 blank | 51 comment | 4 complexity | c08f326bfb837c1d91ab13df70e62b8d MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1# -*- coding: utf-8 -*-
   2#
   3# pylast - A Python interface to Last.fm (and other API compatible social networks)
   4# Copyright (C) 2008-2009  Amr Hassan
   5#
   6# This program is free software; you can redistribute it and/or modify
   7# it under the terms of the GNU General Public License as published by
   8# the Free Software Foundation; either version 2 of the License, or
   9# (at your option) any later version.
  10#
  11# This program is distributed in the hope that it will be useful,
  12# but WITHOUT ANY WARRANTY; without even the implied warranty of
  13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14# GNU General Public License for more details.
  15#
  16# You should have received a copy of the GNU General Public License
  17# along with this program; if not, write to the Free Software
  18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  19# USA
  20#
  21# http://code.google.com/p/pylast/
  22    
  23__version__ = '0.4'
  24__author__ = 'Amr Hassan'
  25__copyright__ = "Copyright (C) 2008-2009  Amr Hassan"
  26__license__ = "gpl"
  27__email__ = 'amr.hassan@gmail.com'
  28
  29import hashlib
  30import httplib
  31import urllib
  32import threading
  33from xml.dom import minidom
  34import xml.dom
  35import time
  36import shelve
  37import tempfile
  38import sys
  39import htmlentitydefs
  40
  41try:
  42    import collections
  43except ImportError:
  44    pass
  45
  46STATUS_INVALID_SERVICE = 2
  47STATUS_INVALID_METHOD = 3
  48STATUS_AUTH_FAILED = 4
  49STATUS_INVALID_FORMAT = 5
  50STATUS_INVALID_PARAMS = 6
  51STATUS_INVALID_RESOURCE = 7
  52STATUS_TOKEN_ERROR = 8
  53STATUS_INVALID_SK = 9
  54STATUS_INVALID_API_KEY = 10
  55STATUS_OFFLINE = 11
  56STATUS_SUBSCRIBERS_ONLY = 12
  57STATUS_INVALID_SIGNATURE = 13
  58STATUS_TOKEN_UNAUTHORIZED = 14
  59STATUS_TOKEN_EXPIRED = 15
  60
  61EVENT_ATTENDING = '0'
  62EVENT_MAYBE_ATTENDING = '1'
  63EVENT_NOT_ATTENDING = '2'
  64
  65PERIOD_OVERALL = 'overall'
  66PERIOD_3MONTHS = '3month'
  67PERIOD_6MONTHS = '6month'
  68PERIOD_12MONTHS = '12month'
  69
  70DOMAIN_ENGLISH = 0
  71DOMAIN_GERMAN = 1
  72DOMAIN_SPANISH = 2
  73DOMAIN_FRENCH = 3
  74DOMAIN_ITALIAN = 4
  75DOMAIN_POLISH = 5
  76DOMAIN_PORTUGUESE = 6
  77DOMAIN_SWEDISH = 7
  78DOMAIN_TURKISH = 8
  79DOMAIN_RUSSIAN = 9
  80DOMAIN_JAPANESE = 10
  81DOMAIN_CHINESE = 11
  82
  83COVER_SMALL = 0
  84COVER_MEDIUM = 1
  85COVER_LARGE = 2
  86COVER_EXTRA_LARGE = 3
  87COVER_MEGA = 4
  88
  89IMAGES_ORDER_POPULARITY = "popularity"
  90IMAGES_ORDER_DATE = "dateadded"
  91
  92
  93USER_MALE = 'Male'
  94USER_FEMALE = 'Female'
  95
  96SCROBBLE_SOURCE_USER = "P"
  97SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R"
  98SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E"
  99SCROBBLE_SOURCE_LASTFM = "L"
 100SCROBBLE_SOURCE_UNKNOWN = "U"
 101
 102SCROBBLE_MODE_PLAYED = ""
 103SCROBBLE_MODE_LOVED = "L"
 104SCROBBLE_MODE_BANNED = "B"
 105SCROBBLE_MODE_SKIPPED = "S"
 106
 107"""
 108A list of the implemented webservices (from http://www.last.fm/api/intro)
 109=====================================
 110# Album
 111
 112    * album.addTags            DONE
 113    * album.getInfo            DONE
 114    * album.getTags            DONE
 115    * album.removeTag        DONE
 116    * album.search             DONE
 117
 118# Artist
 119
 120    * artist.addTags         DONE
 121    * artist.getEvents         DONE
 122    * artist.getImages         DONE
 123    * artist.getInfo         DONE
 124    * artist.getPodcast     TODO
 125    * artist.getShouts         DONE
 126    * artist.getSimilar     DONE
 127    * artist.getTags         DONE
 128    * artist.getTopAlbums     DONE
 129    * artist.getTopFans     DONE
 130    * artist.getTopTags     DONE
 131    * artist.getTopTracks     DONE
 132    * artist.removeTag         DONE
 133    * artist.search         DONE
 134    * artist.share             DONE
 135    * artist.shout             DONE
 136
 137# Auth
 138
 139    * auth.getMobileSession DONE
 140    * auth.getSession         DONE
 141    * auth.getToken         DONE
 142
 143# Event
 144
 145    * event.attend             DONE
 146    * event.getAttendees     DONE
 147    * event.getInfo            DONE
 148    * event.getShouts         DONE
 149    * event.share             DONE
 150    * event.shout             DONE
 151
 152# Geo
 153
 154    * geo.getEvents
 155    * geo.getTopArtists
 156    * geo.getTopTracks
 157
 158# Group
 159
 160    * group.getMembers                DONE
 161    * group.getWeeklyAlbumChart        DONE
 162    * group.getWeeklyArtistChart    DONE
 163    * group.getWeeklyChartList        DONE
 164    * group.getWeeklyTrackChart        DONE
 165
 166# Library
 167
 168    * library.addAlbum        DONE
 169    * library.addArtist        DONE
 170    * library.addTrack        DONE
 171    * library.getAlbums        DONE
 172    * library.getArtists    DONE
 173    * library.getTracks        DONE
 174
 175# Playlist
 176
 177    * playlist.addTrack    DONE
 178    * playlist.create    DONE
 179    * playlist.fetch    DONE
 180
 181# Radio
 182
 183    * radio.getPlaylist
 184    * radio.tune
 185
 186# Tag
 187
 188    * tag.getSimilar    DONE
 189    * tag.getTopAlbums    DONE
 190    * tag.getTopArtists    DONE
 191    * tag.getTopTags    DONE
 192    * tag.getTopTracks    DONE
 193    * tag.getWeeklyArtistChart    DONE
 194    * tag.getWeeklyChartList    DONE
 195    * tag.search    DONE
 196
 197# Tasteometer
 198
 199    * tasteometer.compare    DONE
 200
 201# Track
 202
 203    * track.addTags    DONE
 204    * track.ban    DONE
 205    * track.getInfo    DONE
 206    * track.getSimilar    DONE
 207    * track.getTags    DONE
 208    * track.getTopFans    DONE
 209    * track.getTopTags    DONE
 210    * track.love    DONE
 211    * track.removeTag    DONE
 212    * track.search    DONE
 213    * track.share    DONE
 214
 215# User
 216
 217    * user.getEvents    DONE
 218    * user.getFriends    DONE
 219    * user.getInfo    DONE
 220    * user.getLovedTracks    DONE
 221    * user.getNeighbours    DONE
 222    * user.getPastEvents    DONE
 223    * user.getPlaylists    DONE
 224    * user.getRecentStations    TODO
 225    * user.getRecentTracks    DONE
 226    * user.getRecommendedArtists    DONE
 227    * user.getRecommendedEvents    DONE
 228    * user.getShouts    DONE
 229    * user.getTopAlbums    DONE
 230    * user.getTopArtists    DONE
 231    * user.getTopTags    DONE
 232    * user.getTopTracks    DONE
 233    * user.getWeeklyAlbumChart    DONE
 234    * user.getWeeklyArtistChart    DONE
 235    * user.getWeeklyChartList    DONE
 236    * user.getWeeklyTrackChart    DONE
 237    * user.shout    DONE
 238
 239# Venue
 240
 241    * venue.getEvents    DONE
 242    * venue.getPastEvents    DONE
 243    * venue.search    DONE
 244"""
 245
 246class Network(object):
 247    """
 248        A music social network website that is Last.fm or one exposing a Last.fm compatible API
 249    """
 250    
 251    def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash,
 252                    domain_names, urls):
 253        """
 254            name: the name of the network
 255            homepage: the homepage url
 256            ws_server: the url of the webservices server
 257            api_key: a provided API_KEY
 258            api_secret: a provided API_SECRET
 259            session_key: a generated session_key or None
 260            submission_server: the url of the server to which tracks are submitted (scrobbled)
 261            username: a username of a valid user
 262            password_hash: the output of pylast.md5(password) where password is the user's password thingy
 263            domain_names: a dict mapping each DOMAIN_* value to a string domain name
 264            urls: a dict mapping types to urls
 265            
 266            if username and password_hash were provided and not session_key, session_key will be
 267            generated automatically when needed.
 268            
 269            Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
 270            
 271            You should use a preconfigured network object through a get_*_network(...) method instead of creating an object
 272            of this class, unless you know what you're doing.
 273        """
 274            
 275        self.ws_server = ws_server
 276        self.submission_server = submission_server
 277        self.name = name
 278        self.homepage = homepage
 279        self.api_key = api_key
 280        self.api_secret = api_secret
 281        self.session_key = session_key
 282        self.username = username
 283        self.password_hash = password_hash
 284        self.domain_names = domain_names
 285        self.urls = urls
 286        
 287        self.cache_backend = None
 288        self.proxy_enabled = False
 289        self.proxy = None
 290        self.last_call_time = 0
 291        
 292        #generate a session_key if necessary
 293        if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash):
 294            sk_gen = SessionKeyGenerator(self)
 295            self.session_key = sk_gen.get_session_key(self.username, self.password_hash)
 296    
 297    def get_artist(self, artist_name):
 298        """
 299            Return an Artist object
 300        """
 301        
 302        return Artist(artist_name, self)
 303    
 304    def get_track(self, artist, title):
 305        """
 306            Return a Track object
 307        """
 308        
 309        return Track(artist, title, self)
 310    
 311    def get_album(self, artist, title):
 312        """
 313            Return an Album object
 314        """
 315        
 316        return Album(artist, title, self)
 317    
 318    def get_authenticated_user(self):
 319        """
 320            Returns the authenticated user
 321        """
 322        
 323        return AuthenticatedUser(self)
 324    
 325    def get_country(self, country_name):
 326        """
 327            Returns a country object
 328        """
 329        
 330        return Country(country_name, self)
 331    
 332    def get_group(self, name):
 333        """
 334            Returns a Group object
 335        """
 336        
 337        return Group(name, self)
 338    
 339    def get_user(self, username):
 340        """
 341            Returns a user object
 342        """
 343        
 344        return User(username, self)
 345    
 346    def get_tag(self, name):
 347        """
 348            Returns a tag object
 349        """
 350        
 351        return Tag(name, self)
 352    
 353    def get_scrobbler(self, client_id, client_version):
 354        """
 355            Returns a Scrobbler object used for submitting tracks to the server
 356            
 357            Quote from http://www.last.fm/api/submissions:
 358            ========
 359            Client identifiers are used to provide a centrally managed database of 
 360            the client versions, allowing clients to be banned if they are found to 
 361            be behaving undesirably. The client ID is associated with a version 
 362            number on the server, however these are only incremented if a client is 
 363            banned and do not have to reflect the version of the actual client application.
 364
 365            During development, clients which have not been allocated an identifier should 
 366            use the identifier tst, with a version number of 1.0. Do not distribute code or
 367            client implementations which use this test identifier. Do not use the identifiers
 368            used by other clients.
 369            =========
 370            
 371            To obtain a new client identifier please contact:
 372                * Last.fm: submissions@last.fm
 373                * # TODO: list others
 374            
 375            ...and provide us with the name of your client and its homepage address. 
 376        """
 377        
 378        return Scrobbler(self, client_id, client_version)
 379    
 380    def _get_language_domain(self, domain_language):
 381        """
 382            Returns the mapped domain name of the network to a DOMAIN_* value
 383        """
 384        
 385        if domain_language in self.domain_names:
 386            return self.domain_names[domain_language]
 387    
 388    def _get_url(self, domain, type):
 389        return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type])
 390    
 391    def _get_ws_auth(self):
 392        """
 393            Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple.
 394        """
 395        return (self.api_key, self.api_secret, self.session_key)
 396
 397    def _delay_call(self):
 398        """
 399            Makes sure that web service calls are at least a second apart
 400        """
 401        
 402        # delay time in seconds
 403        DELAY_TIME = 1.0
 404        now = time.time()
 405            
 406        if (now - self.last_call_time) < DELAY_TIME:
 407            time.sleep(1)
 408        
 409        self.last_call_time = now
 410        
 411    def create_new_playlist(self, title, description):
 412        """
 413            Creates a playlist for the authenticated user and returns it
 414                title: The title of the new playlist.
 415                description: The description of the new playlist.
 416        """
 417        
 418        params = {}
 419        params['title'] = _unicode(title)
 420        params['description'] = _unicode(description)
 421        
 422        doc = _Request(self, 'playlist.create', params).execute(False)
 423        
 424        e_id = doc.getElementsByTagName("id")[0].firstChild.data
 425        user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
 426        
 427        return Playlist(user, e_id, self)
 428
 429    def get_top_tags(self, limit=None):
 430        """Returns a sequence of the most used tags as a sequence of TopItem objects."""
 431        
 432        doc = _Request(self, "tag.getTopTags").execute(True)
 433        seq = []
 434        for node in doc.getElementsByTagName("tag"):
 435            tag = Tag(_extract(node, "name"), self)
 436            weight = _number(_extract(node, "count"))
 437            
 438            if len(seq) < limit:
 439                seq.append(TopItem(tag, weight))
 440        
 441        return seq
 442
 443    def enable_proxy(self, host, port):
 444        """Enable a default web proxy"""
 445        
 446        self.proxy = [host, _number(port)]
 447        self.proxy_enabled = True
 448
 449    def disable_proxy(self):
 450        """Disable using the web proxy"""
 451        
 452        self.proxy_enabled = False
 453
 454    def is_proxy_enabled(self):
 455        """Returns True if a web proxy is enabled."""
 456        
 457        return self.proxy_enabled
 458
 459    def _get_proxy(self):
 460        """Returns proxy details."""
 461        
 462        return self.proxy
 463        
 464    def enable_caching(self, file_path = None):
 465        """Enables caching request-wide for all cachable calls.
 466        In choosing the backend used for caching, it will try _SqliteCacheBackend first if
 467        the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects.
 468        
 469        * file_path: A file path for the backend storage file. If 
 470        None set, a temp file would probably be created, according the backend.
 471        """
 472        
 473        if not file_path:
 474            file_path = tempfile.mktemp(prefix="pylast_tmp_")
 475        
 476        self.cache_backend = _ShelfCacheBackend(file_path)
 477            
 478    def disable_caching(self):
 479        """Disables all caching features."""
 480
 481        self.cache_backend = None
 482
 483    def is_caching_enabled(self):
 484        """Returns True if caching is enabled."""
 485        
 486        return not (self.cache_backend == None)
 487
 488    def _get_cache_backend(self):
 489        
 490        return self.cache_backend
 491        
 492    def search_for_album(self, album_name):
 493        """Searches for an album by its name. Returns a AlbumSearch object.
 494        Use get_next_page() to retreive sequences of results."""
 495        
 496        return AlbumSearch(album_name, self)
 497
 498    def search_for_artist(self, artist_name):
 499        """Searches of an artist by its name. Returns a ArtistSearch object.
 500        Use get_next_page() to retreive sequences of results."""
 501        
 502        return ArtistSearch(artist_name, self)
 503
 504    def search_for_tag(self, tag_name):
 505        """Searches of a tag by its name. Returns a TagSearch object.
 506        Use get_next_page() to retreive sequences of results."""
 507        
 508        return TagSearch(tag_name, self)
 509
 510    def search_for_track(self, artist_name, track_name):
 511        """Searches of a track by its name and its artist. Set artist to an empty string if not available.
 512        Returns a TrackSearch object.
 513        Use get_next_page() to retreive sequences of results."""
 514        
 515        return TrackSearch(artist_name, track_name, self)
 516
 517    def search_for_venue(self, venue_name, country_name):
 518        """Searches of a venue by its name and its country. Set country_name to an empty string if not available.
 519        Returns a VenueSearch object.
 520        Use get_next_page() to retreive sequences of results."""
 521
 522        return VenueSearch(venue_name, country_name, self)
 523        
 524    def get_track_by_mbid(self, mbid):
 525        """Looks up a track by its MusicBrainz ID"""
 526        
 527        params = {"mbid": _unicode(mbid)}
 528        
 529        doc = _Request(self, "track.getInfo", params).execute(True)
 530        
 531        return Track(_extract(doc, "name", 1), _extract(doc, "name"), self)
 532
 533    def get_artist_by_mbid(self, mbid):
 534        """Loooks up an artist by its MusicBrainz ID"""
 535        
 536        params = {"mbid": _unicode(mbid)}
 537        
 538        doc = _Request(self, "artist.getInfo", params).execute(True)
 539        
 540        return Artist(_extract(doc, "name"), self)
 541
 542    def get_album_by_mbid(self, mbid):
 543        """Looks up an album by its MusicBrainz ID"""
 544        
 545        params = {"mbid": _unicode(mbid)}
 546        
 547        doc = _Request(self, "album.getInfo", params).execute(True)
 548        
 549        return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
 550
 551def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
 552    """
 553    Returns a preconfigured Network object for Last.fm
 554    
 555    api_key: a provided API_KEY
 556    api_secret: a provided API_SECRET
 557    session_key: a generated session_key or None
 558    username: a username of a valid user
 559    password_hash: the output of pylast.md5(password) where password is the user's password
 560    
 561    if username and password_hash were provided and not session_key, session_key will be
 562    generated automatically when needed.
 563            
 564    Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
 565    
 566    Most read-only webservices only require an api_key and an api_secret, see about obtaining them from:
 567    http://www.last.fm/api/account
 568    """
 569    
 570    return Network (
 571                    name = "Last.fm",
 572                    homepage = "http://last.fm",
 573                    ws_server = ("ws.audioscrobbler.com", "/2.0/"),
 574                    api_key = api_key,
 575                    api_secret = api_secret,
 576                    session_key = session_key,
 577                    submission_server = "http://post.audioscrobbler.com:80/",
 578                    username = username,
 579                    password_hash = password_hash,
 580                    domain_names = {
 581                        DOMAIN_ENGLISH: 'www.last.fm',
 582                        DOMAIN_GERMAN: 'www.lastfm.de',
 583                        DOMAIN_SPANISH: 'www.lastfm.es',
 584                        DOMAIN_FRENCH: 'www.lastfm.fr',
 585                        DOMAIN_ITALIAN: 'www.lastfm.it',
 586                        DOMAIN_POLISH: 'www.lastfm.pl',
 587                        DOMAIN_PORTUGUESE: 'www.lastfm.com.br',
 588                        DOMAIN_SWEDISH: 'www.lastfm.se',
 589                        DOMAIN_TURKISH: 'www.lastfm.com.tr',
 590                        DOMAIN_RUSSIAN: 'www.lastfm.ru',
 591                        DOMAIN_JAPANESE: 'www.lastfm.jp',
 592                        DOMAIN_CHINESE: 'cn.last.fm',
 593                        },
 594                    urls = {
 595                        "album": "music/%(artist)s/%(album)s",
 596                        "artist": "music/%(artist)s",
 597                        "event": "event/%(id)s",
 598                        "country": "place/%(country_name)s",
 599                        "playlist": "user/%(user)s/library/playlists/%(appendix)s",
 600                        "tag": "tag/%(name)s",
 601                        "track": "music/%(artist)s/_/%(title)s",
 602                        "group": "group/%(name)s",
 603                        "user": "user/%(name)s",
 604                        }
 605                    )
 606
 607def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
 608    """
 609    Returns a preconfigured Network object for Libre.fm
 610    
 611    api_key: a provided API_KEY
 612    api_secret: a provided API_SECRET
 613    session_key: a generated session_key or None
 614    username: a username of a valid user
 615    password_hash: the output of pylast.md5(password) where password is the user's password
 616    
 617    if username and password_hash were provided and not session_key, session_key will be
 618    generated automatically when needed.
 619    """
 620    
 621    return Network (
 622                    name = "Libre.fm",
 623                    homepage = "http://alpha.dev.libre.fm",
 624                    ws_server = ("alpha.dev.libre.fm", "/2.0/"),
 625                    api_key = api_key,
 626                    api_secret = api_secret,
 627                    session_key = session_key,
 628                    submission_server = "http://turtle.libre.fm:80/",
 629                    username = username,
 630                    password_hash = password_hash,
 631                    domain_names = {
 632                        DOMAIN_ENGLISH: "alpha.dev.libre.fm",
 633                        DOMAIN_GERMAN: "alpha.dev.libre.fm",
 634                        DOMAIN_SPANISH: "alpha.dev.libre.fm",
 635                        DOMAIN_FRENCH: "alpha.dev.libre.fm",
 636                        DOMAIN_ITALIAN: "alpha.dev.libre.fm",
 637                        DOMAIN_POLISH: "alpha.dev.libre.fm",
 638                        DOMAIN_PORTUGUESE: "alpha.dev.libre.fm",
 639                        DOMAIN_SWEDISH: "alpha.dev.libre.fm",
 640                        DOMAIN_TURKISH: "alpha.dev.libre.fm",
 641                        DOMAIN_RUSSIAN: "alpha.dev.libre.fm",
 642                        DOMAIN_JAPANESE: "alpha.dev.libre.fm",
 643                        DOMAIN_CHINESE: "alpha.dev.libre.fm",
 644                        },
 645                    urls = {
 646                        "album": "artist/%(artist)s/album/%(album)s",
 647                        "artist": "artist/%(artist)s",
 648                        "event": "event/%(id)s",
 649                        "country": "place/%(country_name)s",
 650                        "playlist": "user/%(user)s/library/playlists/%(appendix)s",
 651                        "tag": "tag/%(name)s",
 652                        "track": "music/%(artist)s/_/%(title)s",
 653                        "group": "group/%(name)s",
 654                        "user": "user/%(name)s",
 655                        }
 656                    )
 657
 658class _ShelfCacheBackend(object):
 659    """Used as a backend for caching cacheable requests."""
 660    def __init__(self, file_path = None):
 661        self.shelf = shelve.open(file_path)
 662    
 663    def get_xml(self, key):
 664        return self.shelf[key]
 665    
 666    def set_xml(self, key, xml_string):
 667        self.shelf[key] = xml_string
 668    
 669    def has_key(self, key):
 670        return key in self.shelf.keys()
 671        
 672class _ThreadedCall(threading.Thread):
 673    """Facilitates calling a function on another thread."""
 674    
 675    def __init__(self, sender, funct, funct_args, callback, callback_args):
 676        
 677        threading.Thread.__init__(self)
 678        
 679        self.funct = funct
 680        self.funct_args = funct_args
 681        self.callback = callback
 682        self.callback_args = callback_args
 683        
 684        self.sender = sender
 685    
 686    def run(self):
 687        
 688        output = []
 689        
 690        if self.funct:
 691            if self.funct_args:
 692                output = self.funct(*self.funct_args)
 693            else:
 694                output = self.funct()
 695                
 696        if self.callback:
 697            if self.callback_args:
 698                self.callback(self.sender, output, *self.callback_args)
 699            else:
 700                self.callback(self.sender, output)
 701    
 702class _Request(object):
 703    """Representing an abstract web service operation."""
 704    
 705    def __init__(self, network, method_name, params = {}):
 706        
 707        self.params = params
 708        self.network = network
 709        
 710        (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth()
 711        
 712        self.params["api_key"] = self.api_key
 713        self.params["method"] = method_name
 714        
 715        if network.is_caching_enabled():
 716            self.cache = network._get_cache_backend()
 717        
 718        if self.session_key:
 719            self.params["sk"] = self.session_key
 720            self.sign_it()
 721    
 722    def sign_it(self):
 723        """Sign this request."""
 724        
 725        if not "api_sig" in self.params.keys():
 726            self.params['api_sig'] = self._get_signature()
 727    
 728    def _get_signature(self):
 729        """Returns a 32-character hexadecimal md5 hash of the signature string."""
 730        
 731        keys = self.params.keys()[:]
 732        
 733        keys.sort()
 734        
 735        string = ""
 736        
 737        for name in keys:
 738            string += _unicode(name)
 739            string += _unicode(self.params[name])
 740        
 741        string += _unicode(self.api_secret)
 742        
 743        return md5(string)
 744    
 745    def _get_cache_key(self):
 746        """The cache key is a string of concatenated sorted names and values."""
 747        
 748        keys = self.params.keys()
 749        keys.sort()
 750        
 751        cache_key = str()
 752        
 753        for key in keys:
 754            if key != "api_sig" and key != "api_key" and key != "sk":
 755                cache_key += key + _string(self.params[key])
 756        
 757        return hashlib.sha1(cache_key).hexdigest()
 758    
 759    def _get_cached_response(self):
 760        """Returns a file object of the cached response."""
 761        
 762        if not self._is_cached():
 763            response = self._download_response()
 764            self.cache.set_xml(self._get_cache_key(), response)
 765        
 766        return self.cache.get_xml(self._get_cache_key())
 767    
 768    def _is_cached(self):
 769        """Returns True if the request is already in cache."""
 770        
 771        return self.cache.has_key(self._get_cache_key())
 772        
 773    def _download_response(self):
 774        """Returns a response body string from the server."""
 775        
 776        # Delay the call if necessary
 777        #self.network._delay_call()    # enable it if you want.
 778        
 779        data = []
 780        for name in self.params.keys():
 781            data.append('='.join((name, urllib.quote_plus(_string(self.params[name])))))
 782        data = '&'.join(data)
 783        
 784        headers = {
 785            "Content-type": "application/x-www-form-urlencoded",
 786            'Accept-Charset': 'utf-8',
 787            'User-Agent': "pylast" + '/' + __version__
 788            }        
 789        
 790        (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
 791        
 792        if self.network.is_proxy_enabled():
 793            conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
 794            conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR, 
 795                body=data, headers=headers)
 796        else:
 797            conn = httplib.HTTPConnection(host=HOST_NAME)
 798            conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers)
 799        
 800        response = conn.getresponse()
 801        response_text = _unicode(response.read())
 802        self._check_response_for_errors(response_text)
 803        return response_text
 804        
 805    def execute(self, cacheable = False):
 806        """Returns the XML DOM response of the POST Request from the server"""
 807        
 808        if self.network.is_caching_enabled() and cacheable:
 809            response = self._get_cached_response()
 810        else:
 811            response = self._download_response()
 812        
 813        return minidom.parseString(_string(response))
 814    
 815    def _check_response_for_errors(self, response):
 816        """Checks the response for errors and raises one if any exists."""
 817        
 818        doc = minidom.parseString(_string(response))
 819        e = doc.getElementsByTagName('lfm')[0]
 820        
 821        if e.getAttribute('status') != "ok":
 822            e = doc.getElementsByTagName('error')[0]
 823            status = e.getAttribute('code')
 824            details = e.firstChild.data.strip()
 825            raise WSError(self.network, status, details)
 826
 827class SessionKeyGenerator(object):
 828    """Methods of generating a session key:
 829    1) Web Authentication:
 830        a. network = get_*_network(API_KEY, API_SECRET)
 831        b. sg = SessionKeyGenerator(network)
 832        c. url = sg.get_web_auth_url()
 833        d. Ask the user to open the url and authorize you, and wait for it.
 834        e. session_key = sg.get_web_auth_session_key(url)
 835    2) Username and Password Authentication:
 836        a. network = get_*_network(API_KEY, API_SECRET)
 837        b. username = raw_input("Please enter your username: ")
 838        c. password_hash = pylast.md5(raw_input("Please enter your password: ")
 839        d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash)
 840    
 841    A session key's lifetime is infinie, unless the user provokes the rights of the given API Key.
 842    
 843    If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a
 844    SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this
 845    manually, unless you want to.
 846    """
 847    
 848    def __init__(self, network):        
 849        self.network = network
 850        self.web_auth_tokens = {}
 851    
 852    def _get_web_auth_token(self):
 853        """Retrieves a token from the network for web authentication.
 854        The token then has to be authorized from getAuthURL before creating session.
 855        """
 856        
 857        request = _Request(self.network, 'auth.getToken')
 858        
 859        # default action is that a request is signed only when
 860        # a session key is provided.
 861        request.sign_it()
 862        
 863        doc = request.execute()
 864        
 865        e = doc.getElementsByTagName('token')[0]
 866        return e.firstChild.data
 867    
 868    def get_web_auth_url(self):
 869        """The user must open this page, and you first, then call get_web_auth_session_key(url) after that."""
 870        
 871        token = self._get_web_auth_token()
 872        
 873        url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \
 874            {"homepage": self.network.homepage, "api": self.network.api_key, "token": token}
 875        
 876        self.web_auth_tokens[url] = token
 877        
 878        return url
 879
 880    def get_web_auth_session_key(self, url):
 881        """Retrieves the session key of a web authorization process by its url."""
 882        
 883        if url in self.web_auth_tokens.keys():
 884            token = self.web_auth_tokens[url]
 885        else:
 886            token = ""    #that's gonna raise a WSError of an unauthorized token when the request is executed.
 887        
 888        request = _Request(self.network, 'auth.getSession', {'token': token})
 889        
 890        # default action is that a request is signed only when
 891        # a session key is provided.
 892        request.sign_it()
 893        
 894        doc = request.execute()
 895        
 896        return doc.getElementsByTagName('key')[0].firstChild.data
 897    
 898    def get_session_key(self, username, password_hash):
 899        """Retrieve a session key with a username and a md5 hash of the user's password."""
 900        
 901        params = {"username": username, "authToken": md5(username + password_hash)}
 902        request = _Request(self.network, "auth.getMobileSession", params)
 903        
 904        # default action is that a request is signed only when
 905        # a session key is provided.
 906        request.sign_it()
 907        
 908        doc = request.execute()
 909        
 910        return _extract(doc, "key")
 911
 912def _namedtuple(name, children):
 913    """
 914        collections.namedtuple is available in (python >= 2.6)
 915    """
 916    
 917    v = sys.version_info
 918    if v[1] >= 6 and v[0] < 3:
 919        return collections.namedtuple(name, children)
 920    else:
 921        def fancydict(*args):
 922            d = {}
 923            i = 0
 924            for child in children:
 925                d[child.strip()] = args[i]
 926                i += 1
 927            return d
 928        
 929        return fancydict
 930
 931TopItem = _namedtuple("TopItem", ["item", "weight"])
 932SimilarItem = _namedtuple("SimilarItem", ["item", "match"])
 933LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
 934PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
 935LovedTrack = _namedtuple("LovedTrack", ["track", "date", "timestamp"])
 936ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
 937Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
 938Shout = _namedtuple("Shout", ["body", "author", "date"])
 939
 940def _string_output(funct):
 941    def r(*args):
 942        return _string(funct(*args))
 943        
 944    return r
 945
 946class _BaseObject(object):
 947    """An abstract webservices object."""
 948    
 949    network = None
 950    
 951    def __init__(self, network):
 952        self.network = network
 953    
 954    def _request(self, method_name, cacheable = False, params = None):
 955        if not params:
 956            params = self._get_params()
 957            
 958        return _Request(self.network, method_name, params).execute(cacheable)
 959    
 960    def _get_params(self):
 961        """Returns the most common set of parameters between all objects."""
 962        
 963        return {}
 964    
 965    def __hash__(self):
 966        return hash(self.network) + \
 967            hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower())
 968
 969class _Taggable(object):
 970    """Common functions for classes with tags."""
 971    
 972    def __init__(self, ws_prefix):
 973        self.ws_prefix = ws_prefix
 974    
 975    def add_tags(self, *tags):
 976        """Adds one or several tags.
 977        * *tags: Any number of tag names or Tag objects. 
 978        """
 979        
 980        for tag in tags:
 981            self._add_tag(tag)    
 982    
 983    def _add_tag(self, tag):
 984        """Adds one or several tags.
 985        * tag: one tag name or a Tag object.
 986        """
 987        
 988        if isinstance(tag, Tag):
 989            tag = tag.get_name()
 990        
 991        params = self._get_params()
 992        params['tags'] = _unicode(tag)
 993        
 994        self._request(self.ws_prefix + '.addTags', False, params)
 995    
 996    def _remove_tag(self, single_tag):
 997        """Remove a user's tag from this object."""
 998        
 999        if isinstance(single_tag, Tag):
1000            single_tag = single_tag.get_name()
1001        
1002        params = self._get_params()
1003        params['tag'] = _unicode(single_tag)
1004        
1005        self._request(self.ws_prefix + '.removeTag', False, params)
1006
1007    def get_tags(self):
1008        """Returns a list of the tags set by the user to this object."""
1009        
1010        # Uncacheable because it can be dynamically changed by the user.
1011        params = self._get_params()
1012        
1013        doc = self._request(self.ws_prefix + '.getTags', False, params)
1014        tag_names = _extract_all(doc, 'name')
1015        tags = []
1016        for tag in tag_names:
1017            tags.append(Tag(tag, self.network))
1018        
1019        return tags
1020    
1021    def remove_tags(self, *tags):
1022        """Removes one or several tags from this object.
1023        * *tags: Any number of tag names or Tag objects. 
1024        """
1025        
1026        for tag in tags:
1027            self._remove_tag(tag)
1028    
1029    def clear_tags(self):
1030        """Clears all the user-set tags. """
1031        
1032        self.remove_tags(*(self.get_tags()))
1033    
1034    def set_tags(self, *tags):
1035        """Sets this object's tags to only those tags.
1036        * *tags: any number of tag names.
1037        """
1038        
1039        c_old_tags = []
1040        old_tags = []
1041        c_new_tags = []
1042        new_tags = []
1043        
1044        to_remove = []
1045        to_add = []
1046        
1047        tags_on_server = self.get_tags()
1048        
1049        for tag in tags_on_server:
1050            c_old_tags.append(tag.get_name().lower())
1051            old_tags.append(tag.get_name())
1052        
1053        for tag in tags:
1054            c_new_tags.append(tag.lower())
1055            new_tags.append(tag)
1056        
1057        for i in range(0, len(old_tags)):
1058            if not c_old_tags[i] in c_new_tags:
1059                to_remove.append(old_tags[i])
1060        
1061        for i in range(0, len(new_tags)):
1062            if not c_new_tags[i] in c_old_tags:
1063                to_add.append(new_tags[i])
1064        
1065        self.remove_tags(*to_remove)
1066        self.add_tags(*to_add)
1067        
1068    def get_top_tags(self, limit = None):
1069        """Returns a list of the most frequently used Tags on this object."""
1070        
1071        doc = self._request(self.ws_prefix + '.getTopTags', True)
1072        
1073        elements = doc.getElementsByTagName('tag')
1074        seq = []
1075        
1076        for element in elements:
1077            if limit and len(seq) >= limit:
1078                break
1079            tag_name = _extract(element, 'name')
1080            tagcount = _extract(element, 'count')
1081            
1082            seq.append(TopItem(Tag(tag_name, self.network), tagcount))
1083        
1084        return seq
1085        
1086class WSError(Exception):
1087    """Exception related to the Network web service"""
1088    
1089    def __init__(self, network, status, details):
1090        self.status = status
1091        self.details = details
1092        self.network = network
1093
1094    @_string_output
1095    def __str__(self):
1096        return self.details
1097    
1098    def get_id(self):
1099        """Returns the exception ID, from one of the following:
1100            STATUS_INVALID_SERVICE = 2
1101            STATUS_INVALID_METHOD = 3
1102            STATUS_AUTH_FAILED = 4
1103            STATUS_INVALID_FORMAT = 5
1104            STATUS_INVALID_PARAMS = 6
1105            STATUS_INVALID_RESOURCE = 7
1106            STATUS_TOKEN_ERROR = 8
1107            STATUS_INVALID_SK = 9
1108            STATUS_INVALID_API_KEY = 10
1109            STATUS_OFFLINE = 11
1110            STATUS_SUBSCRIBERS_ONLY = 12
1111            STATUS_TOKEN_UNAUTHORIZED = 14
1112            STATUS_TOKEN_EXPIRED = 15
1113        """
1114        
1115        return self.status
1116
1117class Album(_BaseObject, _Taggable):
1118    """An album."""
1119    
1120    title = None
1121    artist = None
1122    
1123    def __init__(self, artist, title, network):
1124        """
1125        Create an album instance.
1126        # Parameters:
1127            * artist: An artist name or an Artist object.
1128            * title: The album title.
1129        """
1130        
1131        _BaseObject.__init__(self, network)
1132        _Taggable.__init__(self, 'album')
1133        
1134        if isinstance(artist, Artist):
1135            self.artist = artist
1136        else:
1137            self.artist = Artist(artist, self.network)
1138        
1139        self.title = title
1140
1141    @_string_output
1142    def __repr__(self):
1143        return u"%s - %s" %(self.get_artist().get_name(), self.get_title())
1144    
1145    def __eq__(self, other):
1146        return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
1147    
1148    def __ne__(self, other):
1149        return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
1150    
1151    def _get_params(self):
1152        return {'artist': self.get_artist().get_name(), 'album': self.get_title(), }
1153    
1154    def get_artist(self):
1155        """Returns the associated Artist object."""
1156        
1157        return self.artist
1158    
1159    def get_title(self):
1160        """Returns the album title."""
1161        
1162        return self.title
1163    
1164    def get_name(self):
1165        """Returns the album title (alias to Album.get_title)."""
1166        
1167        return self.get_title()
1168    
1169    def get_release_date(self):
1170        """Retruns the release date of the album."""
1171        
1172        return _extract(self._request("album.getInfo", cacheable = True), "releasedate")
1173    
1174    def get_cover_image(self, size = COVER_EXTRA_LARGE):
1175        """
1176        Returns a uri to the cover image
1177        size can be one of:
1178            COVER_MEGA
1179            COVER_EXTRA_LARGE
1180            COVER_LARGE
1181            COVER_MEDIUM
1182            COVER_SMALL
1183        """
1184        
1185        return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size]
1186    
1187    def get_id(self):
1188        """Returns the ID"""
1189        
1190        return _extract(self._request("album.getInfo", cacheable = True), "id")
1191    
1192    def get_playcount(self):
1193        """Returns the number of plays on the network"""
1194        
1195        return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
1196    
1197    def get_listener_count(self):
1198        """Returns the number of liteners on the network"""
1199        
1200        return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners"))
1201    
1202    def get_top_tags(self, limit=None):
1203        """Returns a list of the most-applied tags to this album."""
1204        
1205        doc = self._request("album.getInfo", True)
1206        e = doc.getElementsByTagName("toptags")[0]
1207        
1208        seq = []
1209        for name in _extract_all(e, "name"):
1210            if len(seq) < limit:
1211                seq.append(Tag(name, self.network))
1212        
1213        return seq
1214
1215    def get_tracks(self):
1216        """Returns the list of Tracks on this album."""
1217        
1218        uri = 'lastfm://playlist/album/%s' %self.get_id()
1219        
1220        return XSPF(uri, self.network).get_tracks()
1221    
1222    def get_mbid(self):
1223        """Returns the MusicBrainz id of the album."""
1224        
1225        return _extract(self._request("album.getInfo", cacheable = True), "mbid")
1226        
1227    def get_url(self, domain_name = DOMAIN_ENGLISH):
1228        """Returns the url of the album page on the network. 
1229        # Parameters:
1230        * domain_name str: The network's language domain. Possible values:
1231            o DOMAIN_ENGLISH
1232            o DOMAIN_GERMAN
1233            o DOMAIN_SPANISH
1234            o DOMAIN_FRENCH
1235            o DOMAIN_ITALIAN
1236            o DOMAIN_POLISH
1237            o DOMAIN_PORTUGUESE
1238            o DOMAIN_SWEDISH
1239            o DOMAIN_TURKISH
1240            o DOMAIN_RUSSIAN
1241            o DOMAIN_JAPANESE
1242            o DOMAIN_CHINESE
1243        """
1244        
1245        artist = _url_safe(self.get_artist().get_name())
1246        album = _url_safe(self.get_title())
1247        
1248        return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album}
1249    
1250    def get_wiki_published_date(self):
1251        """Returns the date of publishing this version of the wiki."""
1252        
1253        doc = self._request("album.getInfo", True)
1254        
1255        if len(doc.getElementsByTagName("wiki")) == 0:
1256            return
1257        
1258        node = doc.getElementsByTagName("wiki")[0]
1259        
1260        return _extract(node, "published")
1261    
1262    def get_wiki_summary(self):
1263        """Returns the summary of the wiki."""
1264        
1265        doc = self._request("album.getInfo", True)
1266        
1267        if len(doc.getElementsByTagName("wiki")) == 0:
1268            return
1269        
1270        node = doc.getElementsByTagName("wiki")[0]
1271        
1272        return _extract(node, "summary")
1273    
1274    def get_wiki_content(self):
1275        """Returns the content of the wiki."""
1276        
1277        doc = self._request("album.getInfo", True)
1278        
1279        if len(doc.getElementsByTagName("wiki")) == 0:
1280            return
1281        
1282        node = doc.getElementsByTagName("wiki")[0]
1283        
1284        return _extract(node, "content")
1285
1286class Artist(_BaseObject, _Taggable):
1287    """An artist."""
1288    
1289    name = None
1290    
1291    def __init__(self, name, network):
1292        """Create an artist object.
1293        # Parameters:
1294            * name str: The artist's name.
1295        """
1296        
1297        _BaseObject.__init__(self, network)
1298        _Taggable.__init__(self, 'artist')
1299        
1300        self.name = name
1301
1302    @_string_output
1303    def __repr__(self):
1304        return self.get_name()
1305    
1306    def __eq__(self, other):
1307        return self.get_name().lower() == other.get_name().lower()
1308    
1309    def __ne__(self, other):
1310        return self.get_name().lower() != other.get_name().lower()
1311    
1312    def _get_params(self):
1313        return {'artist': self.get_name()}
1314    
1315    def get_name(self):
1316        """Returns the name of the artist."""
1317        
1318        return self.name
1319    
1320    def get_cover_image(self, size = COVER_LARGE):
1321        """
1322        Returns a uri to the cover image
1323        size can be one of:
1324            COVER_MEGA
1325            COVER_EXTRA_LARGE
1326            COVER_LARGE
1327            COVER_MEDIUM
1328            COVER_SMALL
1329        """
1330        
1331        return _extract_all(self._request("artist.getInfo", True), "image")[size]
1332    
1333    def get_playcount(self):
1334        """Returns the number of plays on the network."""
1335        
1336        return _number(_extract(self._request("artist.getInfo", True), "playcount"))
1337
1338    def get_mbid(self):
1339        """Returns the MusicBrainz ID of this artist."""
1340        
1341        doc = self._request("artist.getInfo", True)
1342        
1343        return _extract(doc, "mbid")
1344        
1345    def get_listener_count(self):
1346        """Returns the number of liteners on the network."""
1347        
1348        return _number(_extract(self._request("artist.getInfo", True), "listeners"))
1349    
1350    def is_streamable(self):
1351        """Returns True if the artist is streamable."""
1352        
1353        return bool(_number(_extract(self._request("artist.getInfo", True), "streamable")))
1354    
1355    def get_bio_published_date(self):
1356        """Returns the date on which the artist's biography was published."""
1357        
1358        return _extract(self._request("artist.getInfo", True), "published")
1359    
1360    def get_bio_summary(self):
1361        """Returns the summary of the artist's biography."""
1362        
1363        return _extract(self._request("artist.getInfo", True), "summary")
1364    
1365    def get_bio_content(self):
1366        """Returns the content of the artist's biography."""
1367        
1368        return _extract(self._request("artist.getInfo", True), "content")
1369    
1370    def get_upcoming_events(self):
1371        """Returns a list of the upcoming Events for this artist."""
1372        
1373        doc = self._request('artist.getEvents', True)
1374        
1375        ids = _extract_all(doc, 'id')
1376        
1377        events = []
1378        for e_id in ids:
1379            events.append(Event(e_id, self.network))
1380        
1381        return events
1382    
1383    def get_similar(self, limit = None):
1384        """Returns the similar artists on the network."""
1385        
1386        params = self._get_params()
1387        if limit:
1388            params['limit'] = _unicode(limit)
1389        
1390        doc = self._request('artist.getSimilar', True, params)
1391        
1392        names = _extract_all(doc, "name")
1393        matches = _extract_all(doc, "match")
1394        
1395        artists = []
1396        for i in range(0, len(names)):
1397            artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i])))
1398        
1399        return artists
1400
1401    def get_top_albums(self):
1402        """Retuns a list of the top albums."""
1403        
1404        doc = self._request('artist.getTopAlbums', True)
1405        
1406        seq = []
1407        
1408        for node in doc.getElementsByTagName("album"):
1409            name = _extract(node, "name")
1410            artist = _extract(node, "name", 1)
1411            playcount = _extract(node, "playcount")
1412            
1413            seq.append(TopItem(Album(artist, name, self.network), playcount))
1414        
1415        return seq
1416        
1417    def get_top_tracks(self):
1418        """Returns a list of the most played Tracks by this artist."""
1419        
1420        doc = self._request("artist.getTopTracks", True)
1421        
1422        seq = []
1423        for track in doc.getElementsByTagName('track'):
1424            
1425            title = _extract(track, "name")
1426            artist = _extract(track, "name", 1)
1427            playcount = _number(_extract(track, "playcount"))
1428            
1429            seq.append( TopItem(Track(artist, title, self.network), playcount) )
1430        
1431        return seq
1432    
1433    def get_top_fans(self, limit = None):
1434        """Returns a list of the Users who played this artist the most.
1435        # Parameters:
1436            * limit int: Max elements.
1437        """
1438        
1439        doc = self._request('artist.getTopFans', True)
1440        
1441        seq = []
1442        
1443        elements = doc.getElementsByTagName('user')
1444        
1445        for element in elements:
1446            if limit and len(seq) >= limit:
1447                break
1448                
1449            name = _extract(element, 'name')
1450            weight = _number(_extract(element, 'weight'))
1451            
1452            seq.append(TopItem(User(name, self.network), weight))
1453        
1454        return seq
1455
1456    def share(self, users, message = None):
1457        """Shares this artist (sends out recommendations). 
1458        # Parameters:
1459            * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
1460            * message str: A message to include in the recommendation message. 
1461        """
1462        
1463        #last.fm currently accepts a max of 10 recipient at a time
1464        while(len(users) > 10):
1465            section = users[0:9]
1466            users = users[9:]
1467            self.share(section, message)
1468        
1469        nusers = []
1470        for user in users:
1471            if isinstance(user, User):
1472                nusers.append(user.get_name())
1473            else:
1474                nusers.append(user)
1475        
1476        params = self._get_params()
1477        recipients = ','.join(nusers)
1478        params['recipient'] = recipients
1479        if message: params['message'] = _unicode(message)
1480        
1481        self._request('artist.share', False, params)
1482    
1483    def get_url(self, domain_name = DOMAIN_ENGLISH):
1484        """Returns the url of the artist page on the network. 
1485        # Parameters:
1486        * domain_name: The network's language domain. Possible values:
1487          o DOMAIN_ENGLISH
1488          o DOMAIN_GERMAN
1489          o DOMAIN_SPANISH
1490          o DOMAIN_FRENCH
1491          o DOMAIN_ITALIAN
1492          o DOMAIN_POLISH
1493          o DOMAIN_PORTUGUESE
1494          o DOMAIN_SWEDISH
1495          o DOMAIN_TURKISH
1496          o DOMAIN_RUSSIAN
1497          o DOMAIN_JAPANESE
1498          o DOMAIN_CHINESE 
1499        """
1500        
1501        artist = _url_safe(self.get_name())
1502        
1503        return self.network._get_url(domain_name, "artist") %{'artist': artist}
1504    
1505    def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None):
1506        """
1507            Returns a sequence of Image objects
1508            if limit is None it will return all
1509            order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE
1510        """
1511        
1512        images = []
1513        
1514        params = self._get_params()
1515        params["order"] = order
1516        nodes = _collect_nodes(limit, self, "artist.getImages", True, params)
1517        for e in nodes:
1518            if _extract(e, "name"):
1519                user = User(_extract(e, "name"), self.network)
1520            else:
1521                user = None
1522                
1523            

Large files files are truncated, but you can click here to view the full file