/pylast.py
http://radioappz.googlecode.com/ · Python · 2040 lines · 1743 code · 29 blank · 268 comment · 38 complexity · c08f326bfb837c1d91ab13df70e62b8d MD5 · raw file
Large files are truncated click here to view the full file
- # -*- coding: utf-8 -*-
- #
- # pylast - A Python interface to Last.fm (and other API compatible social networks)
- # Copyright (C) 2008-2009 Amr Hassan
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- # USA
- #
- # http://code.google.com/p/pylast/
-
- __version__ = '0.4'
- __author__ = 'Amr Hassan'
- __copyright__ = "Copyright (C) 2008-2009 Amr Hassan"
- __license__ = "gpl"
- __email__ = 'amr.hassan@gmail.com'
- import hashlib
- import httplib
- import urllib
- import threading
- from xml.dom import minidom
- import xml.dom
- import time
- import shelve
- import tempfile
- import sys
- import htmlentitydefs
- try:
- import collections
- except ImportError:
- pass
- STATUS_INVALID_SERVICE = 2
- STATUS_INVALID_METHOD = 3
- STATUS_AUTH_FAILED = 4
- STATUS_INVALID_FORMAT = 5
- STATUS_INVALID_PARAMS = 6
- STATUS_INVALID_RESOURCE = 7
- STATUS_TOKEN_ERROR = 8
- STATUS_INVALID_SK = 9
- STATUS_INVALID_API_KEY = 10
- STATUS_OFFLINE = 11
- STATUS_SUBSCRIBERS_ONLY = 12
- STATUS_INVALID_SIGNATURE = 13
- STATUS_TOKEN_UNAUTHORIZED = 14
- STATUS_TOKEN_EXPIRED = 15
- EVENT_ATTENDING = '0'
- EVENT_MAYBE_ATTENDING = '1'
- EVENT_NOT_ATTENDING = '2'
- PERIOD_OVERALL = 'overall'
- PERIOD_3MONTHS = '3month'
- PERIOD_6MONTHS = '6month'
- PERIOD_12MONTHS = '12month'
- DOMAIN_ENGLISH = 0
- DOMAIN_GERMAN = 1
- DOMAIN_SPANISH = 2
- DOMAIN_FRENCH = 3
- DOMAIN_ITALIAN = 4
- DOMAIN_POLISH = 5
- DOMAIN_PORTUGUESE = 6
- DOMAIN_SWEDISH = 7
- DOMAIN_TURKISH = 8
- DOMAIN_RUSSIAN = 9
- DOMAIN_JAPANESE = 10
- DOMAIN_CHINESE = 11
- COVER_SMALL = 0
- COVER_MEDIUM = 1
- COVER_LARGE = 2
- COVER_EXTRA_LARGE = 3
- COVER_MEGA = 4
- IMAGES_ORDER_POPULARITY = "popularity"
- IMAGES_ORDER_DATE = "dateadded"
- USER_MALE = 'Male'
- USER_FEMALE = 'Female'
- SCROBBLE_SOURCE_USER = "P"
- SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R"
- SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E"
- SCROBBLE_SOURCE_LASTFM = "L"
- SCROBBLE_SOURCE_UNKNOWN = "U"
- SCROBBLE_MODE_PLAYED = ""
- SCROBBLE_MODE_LOVED = "L"
- SCROBBLE_MODE_BANNED = "B"
- SCROBBLE_MODE_SKIPPED = "S"
- """
- A list of the implemented webservices (from http://www.last.fm/api/intro)
- =====================================
- # Album
- * album.addTags DONE
- * album.getInfo DONE
- * album.getTags DONE
- * album.removeTag DONE
- * album.search DONE
- # Artist
- * artist.addTags DONE
- * artist.getEvents DONE
- * artist.getImages DONE
- * artist.getInfo DONE
- * artist.getPodcast TODO
- * artist.getShouts DONE
- * artist.getSimilar DONE
- * artist.getTags DONE
- * artist.getTopAlbums DONE
- * artist.getTopFans DONE
- * artist.getTopTags DONE
- * artist.getTopTracks DONE
- * artist.removeTag DONE
- * artist.search DONE
- * artist.share DONE
- * artist.shout DONE
- # Auth
- * auth.getMobileSession DONE
- * auth.getSession DONE
- * auth.getToken DONE
- # Event
- * event.attend DONE
- * event.getAttendees DONE
- * event.getInfo DONE
- * event.getShouts DONE
- * event.share DONE
- * event.shout DONE
- # Geo
- * geo.getEvents
- * geo.getTopArtists
- * geo.getTopTracks
- # Group
- * group.getMembers DONE
- * group.getWeeklyAlbumChart DONE
- * group.getWeeklyArtistChart DONE
- * group.getWeeklyChartList DONE
- * group.getWeeklyTrackChart DONE
- # Library
- * library.addAlbum DONE
- * library.addArtist DONE
- * library.addTrack DONE
- * library.getAlbums DONE
- * library.getArtists DONE
- * library.getTracks DONE
- # Playlist
- * playlist.addTrack DONE
- * playlist.create DONE
- * playlist.fetch DONE
- # Radio
- * radio.getPlaylist
- * radio.tune
- # Tag
- * tag.getSimilar DONE
- * tag.getTopAlbums DONE
- * tag.getTopArtists DONE
- * tag.getTopTags DONE
- * tag.getTopTracks DONE
- * tag.getWeeklyArtistChart DONE
- * tag.getWeeklyChartList DONE
- * tag.search DONE
- # Tasteometer
- * tasteometer.compare DONE
- # Track
- * track.addTags DONE
- * track.ban DONE
- * track.getInfo DONE
- * track.getSimilar DONE
- * track.getTags DONE
- * track.getTopFans DONE
- * track.getTopTags DONE
- * track.love DONE
- * track.removeTag DONE
- * track.search DONE
- * track.share DONE
- # User
- * user.getEvents DONE
- * user.getFriends DONE
- * user.getInfo DONE
- * user.getLovedTracks DONE
- * user.getNeighbours DONE
- * user.getPastEvents DONE
- * user.getPlaylists DONE
- * user.getRecentStations TODO
- * user.getRecentTracks DONE
- * user.getRecommendedArtists DONE
- * user.getRecommendedEvents DONE
- * user.getShouts DONE
- * user.getTopAlbums DONE
- * user.getTopArtists DONE
- * user.getTopTags DONE
- * user.getTopTracks DONE
- * user.getWeeklyAlbumChart DONE
- * user.getWeeklyArtistChart DONE
- * user.getWeeklyChartList DONE
- * user.getWeeklyTrackChart DONE
- * user.shout DONE
- # Venue
- * venue.getEvents DONE
- * venue.getPastEvents DONE
- * venue.search DONE
- """
- class Network(object):
- """
- A music social network website that is Last.fm or one exposing a Last.fm compatible API
- """
-
- def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash,
- domain_names, urls):
- """
- name: the name of the network
- homepage: the homepage url
- ws_server: the url of the webservices server
- api_key: a provided API_KEY
- api_secret: a provided API_SECRET
- session_key: a generated session_key or None
- submission_server: the url of the server to which tracks are submitted (scrobbled)
- username: a username of a valid user
- password_hash: the output of pylast.md5(password) where password is the user's password thingy
- domain_names: a dict mapping each DOMAIN_* value to a string domain name
- urls: a dict mapping types to urls
-
- if username and password_hash were provided and not session_key, session_key will be
- generated automatically when needed.
-
- Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
-
- You should use a preconfigured network object through a get_*_network(...) method instead of creating an object
- of this class, unless you know what you're doing.
- """
-
- self.ws_server = ws_server
- self.submission_server = submission_server
- self.name = name
- self.homepage = homepage
- self.api_key = api_key
- self.api_secret = api_secret
- self.session_key = session_key
- self.username = username
- self.password_hash = password_hash
- self.domain_names = domain_names
- self.urls = urls
-
- self.cache_backend = None
- self.proxy_enabled = False
- self.proxy = None
- self.last_call_time = 0
-
- #generate a session_key if necessary
- if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash):
- sk_gen = SessionKeyGenerator(self)
- self.session_key = sk_gen.get_session_key(self.username, self.password_hash)
-
- def get_artist(self, artist_name):
- """
- Return an Artist object
- """
-
- return Artist(artist_name, self)
-
- def get_track(self, artist, title):
- """
- Return a Track object
- """
-
- return Track(artist, title, self)
-
- def get_album(self, artist, title):
- """
- Return an Album object
- """
-
- return Album(artist, title, self)
-
- def get_authenticated_user(self):
- """
- Returns the authenticated user
- """
-
- return AuthenticatedUser(self)
-
- def get_country(self, country_name):
- """
- Returns a country object
- """
-
- return Country(country_name, self)
-
- def get_group(self, name):
- """
- Returns a Group object
- """
-
- return Group(name, self)
-
- def get_user(self, username):
- """
- Returns a user object
- """
-
- return User(username, self)
-
- def get_tag(self, name):
- """
- Returns a tag object
- """
-
- return Tag(name, self)
-
- def get_scrobbler(self, client_id, client_version):
- """
- Returns a Scrobbler object used for submitting tracks to the server
-
- Quote from http://www.last.fm/api/submissions:
- ========
- Client identifiers are used to provide a centrally managed database of
- the client versions, allowing clients to be banned if they are found to
- be behaving undesirably. The client ID is associated with a version
- number on the server, however these are only incremented if a client is
- banned and do not have to reflect the version of the actual client application.
- During development, clients which have not been allocated an identifier should
- use the identifier tst, with a version number of 1.0. Do not distribute code or
- client implementations which use this test identifier. Do not use the identifiers
- used by other clients.
- =========
-
- To obtain a new client identifier please contact:
- * Last.fm: submissions@last.fm
- * # TODO: list others
-
- ...and provide us with the name of your client and its homepage address.
- """
-
- return Scrobbler(self, client_id, client_version)
-
- def _get_language_domain(self, domain_language):
- """
- Returns the mapped domain name of the network to a DOMAIN_* value
- """
-
- if domain_language in self.domain_names:
- return self.domain_names[domain_language]
-
- def _get_url(self, domain, type):
- return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type])
-
- def _get_ws_auth(self):
- """
- Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple.
- """
- return (self.api_key, self.api_secret, self.session_key)
- def _delay_call(self):
- """
- Makes sure that web service calls are at least a second apart
- """
-
- # delay time in seconds
- DELAY_TIME = 1.0
- now = time.time()
-
- if (now - self.last_call_time) < DELAY_TIME:
- time.sleep(1)
-
- self.last_call_time = now
-
- def create_new_playlist(self, title, description):
- """
- Creates a playlist for the authenticated user and returns it
- title: The title of the new playlist.
- description: The description of the new playlist.
- """
-
- params = {}
- params['title'] = _unicode(title)
- params['description'] = _unicode(description)
-
- doc = _Request(self, 'playlist.create', params).execute(False)
-
- e_id = doc.getElementsByTagName("id")[0].firstChild.data
- user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
-
- return Playlist(user, e_id, self)
- def get_top_tags(self, limit=None):
- """Returns a sequence of the most used tags as a sequence of TopItem objects."""
-
- doc = _Request(self, "tag.getTopTags").execute(True)
- seq = []
- for node in doc.getElementsByTagName("tag"):
- tag = Tag(_extract(node, "name"), self)
- weight = _number(_extract(node, "count"))
-
- if len(seq) < limit:
- seq.append(TopItem(tag, weight))
-
- return seq
- def enable_proxy(self, host, port):
- """Enable a default web proxy"""
-
- self.proxy = [host, _number(port)]
- self.proxy_enabled = True
- def disable_proxy(self):
- """Disable using the web proxy"""
-
- self.proxy_enabled = False
- def is_proxy_enabled(self):
- """Returns True if a web proxy is enabled."""
-
- return self.proxy_enabled
- def _get_proxy(self):
- """Returns proxy details."""
-
- return self.proxy
-
- def enable_caching(self, file_path = None):
- """Enables caching request-wide for all cachable calls.
- In choosing the backend used for caching, it will try _SqliteCacheBackend first if
- the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects.
-
- * file_path: A file path for the backend storage file. If
- None set, a temp file would probably be created, according the backend.
- """
-
- if not file_path:
- file_path = tempfile.mktemp(prefix="pylast_tmp_")
-
- self.cache_backend = _ShelfCacheBackend(file_path)
-
- def disable_caching(self):
- """Disables all caching features."""
- self.cache_backend = None
- def is_caching_enabled(self):
- """Returns True if caching is enabled."""
-
- return not (self.cache_backend == None)
- def _get_cache_backend(self):
-
- return self.cache_backend
-
- def search_for_album(self, album_name):
- """Searches for an album by its name. Returns a AlbumSearch object.
- Use get_next_page() to retreive sequences of results."""
-
- return AlbumSearch(album_name, self)
- def search_for_artist(self, artist_name):
- """Searches of an artist by its name. Returns a ArtistSearch object.
- Use get_next_page() to retreive sequences of results."""
-
- return ArtistSearch(artist_name, self)
- def search_for_tag(self, tag_name):
- """Searches of a tag by its name. Returns a TagSearch object.
- Use get_next_page() to retreive sequences of results."""
-
- return TagSearch(tag_name, self)
- def search_for_track(self, artist_name, track_name):
- """Searches of a track by its name and its artist. Set artist to an empty string if not available.
- Returns a TrackSearch object.
- Use get_next_page() to retreive sequences of results."""
-
- return TrackSearch(artist_name, track_name, self)
- def search_for_venue(self, venue_name, country_name):
- """Searches of a venue by its name and its country. Set country_name to an empty string if not available.
- Returns a VenueSearch object.
- Use get_next_page() to retreive sequences of results."""
- return VenueSearch(venue_name, country_name, self)
-
- def get_track_by_mbid(self, mbid):
- """Looks up a track by its MusicBrainz ID"""
-
- params = {"mbid": _unicode(mbid)}
-
- doc = _Request(self, "track.getInfo", params).execute(True)
-
- return Track(_extract(doc, "name", 1), _extract(doc, "name"), self)
- def get_artist_by_mbid(self, mbid):
- """Loooks up an artist by its MusicBrainz ID"""
-
- params = {"mbid": _unicode(mbid)}
-
- doc = _Request(self, "artist.getInfo", params).execute(True)
-
- return Artist(_extract(doc, "name"), self)
- def get_album_by_mbid(self, mbid):
- """Looks up an album by its MusicBrainz ID"""
-
- params = {"mbid": _unicode(mbid)}
-
- doc = _Request(self, "album.getInfo", params).execute(True)
-
- return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
- def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
- """
- Returns a preconfigured Network object for Last.fm
-
- api_key: a provided API_KEY
- api_secret: a provided API_SECRET
- session_key: a generated session_key or None
- username: a username of a valid user
- password_hash: the output of pylast.md5(password) where password is the user's password
-
- if username and password_hash were provided and not session_key, session_key will be
- generated automatically when needed.
-
- Either a valid session_key or a combination of username and password_hash must be present for scrobbling.
-
- Most read-only webservices only require an api_key and an api_secret, see about obtaining them from:
- http://www.last.fm/api/account
- """
-
- return Network (
- name = "Last.fm",
- homepage = "http://last.fm",
- ws_server = ("ws.audioscrobbler.com", "/2.0/"),
- api_key = api_key,
- api_secret = api_secret,
- session_key = session_key,
- submission_server = "http://post.audioscrobbler.com:80/",
- username = username,
- password_hash = password_hash,
- domain_names = {
- DOMAIN_ENGLISH: 'www.last.fm',
- DOMAIN_GERMAN: 'www.lastfm.de',
- DOMAIN_SPANISH: 'www.lastfm.es',
- DOMAIN_FRENCH: 'www.lastfm.fr',
- DOMAIN_ITALIAN: 'www.lastfm.it',
- DOMAIN_POLISH: 'www.lastfm.pl',
- DOMAIN_PORTUGUESE: 'www.lastfm.com.br',
- DOMAIN_SWEDISH: 'www.lastfm.se',
- DOMAIN_TURKISH: 'www.lastfm.com.tr',
- DOMAIN_RUSSIAN: 'www.lastfm.ru',
- DOMAIN_JAPANESE: 'www.lastfm.jp',
- DOMAIN_CHINESE: 'cn.last.fm',
- },
- urls = {
- "album": "music/%(artist)s/%(album)s",
- "artist": "music/%(artist)s",
- "event": "event/%(id)s",
- "country": "place/%(country_name)s",
- "playlist": "user/%(user)s/library/playlists/%(appendix)s",
- "tag": "tag/%(name)s",
- "track": "music/%(artist)s/_/%(title)s",
- "group": "group/%(name)s",
- "user": "user/%(name)s",
- }
- )
- def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""):
- """
- Returns a preconfigured Network object for Libre.fm
-
- api_key: a provided API_KEY
- api_secret: a provided API_SECRET
- session_key: a generated session_key or None
- username: a username of a valid user
- password_hash: the output of pylast.md5(password) where password is the user's password
-
- if username and password_hash were provided and not session_key, session_key will be
- generated automatically when needed.
- """
-
- return Network (
- name = "Libre.fm",
- homepage = "http://alpha.dev.libre.fm",
- ws_server = ("alpha.dev.libre.fm", "/2.0/"),
- api_key = api_key,
- api_secret = api_secret,
- session_key = session_key,
- submission_server = "http://turtle.libre.fm:80/",
- username = username,
- password_hash = password_hash,
- domain_names = {
- DOMAIN_ENGLISH: "alpha.dev.libre.fm",
- DOMAIN_GERMAN: "alpha.dev.libre.fm",
- DOMAIN_SPANISH: "alpha.dev.libre.fm",
- DOMAIN_FRENCH: "alpha.dev.libre.fm",
- DOMAIN_ITALIAN: "alpha.dev.libre.fm",
- DOMAIN_POLISH: "alpha.dev.libre.fm",
- DOMAIN_PORTUGUESE: "alpha.dev.libre.fm",
- DOMAIN_SWEDISH: "alpha.dev.libre.fm",
- DOMAIN_TURKISH: "alpha.dev.libre.fm",
- DOMAIN_RUSSIAN: "alpha.dev.libre.fm",
- DOMAIN_JAPANESE: "alpha.dev.libre.fm",
- DOMAIN_CHINESE: "alpha.dev.libre.fm",
- },
- urls = {
- "album": "artist/%(artist)s/album/%(album)s",
- "artist": "artist/%(artist)s",
- "event": "event/%(id)s",
- "country": "place/%(country_name)s",
- "playlist": "user/%(user)s/library/playlists/%(appendix)s",
- "tag": "tag/%(name)s",
- "track": "music/%(artist)s/_/%(title)s",
- "group": "group/%(name)s",
- "user": "user/%(name)s",
- }
- )
- class _ShelfCacheBackend(object):
- """Used as a backend for caching cacheable requests."""
- def __init__(self, file_path = None):
- self.shelf = shelve.open(file_path)
-
- def get_xml(self, key):
- return self.shelf[key]
-
- def set_xml(self, key, xml_string):
- self.shelf[key] = xml_string
-
- def has_key(self, key):
- return key in self.shelf.keys()
-
- class _ThreadedCall(threading.Thread):
- """Facilitates calling a function on another thread."""
-
- def __init__(self, sender, funct, funct_args, callback, callback_args):
-
- threading.Thread.__init__(self)
-
- self.funct = funct
- self.funct_args = funct_args
- self.callback = callback
- self.callback_args = callback_args
-
- self.sender = sender
-
- def run(self):
-
- output = []
-
- if self.funct:
- if self.funct_args:
- output = self.funct(*self.funct_args)
- else:
- output = self.funct()
-
- if self.callback:
- if self.callback_args:
- self.callback(self.sender, output, *self.callback_args)
- else:
- self.callback(self.sender, output)
-
- class _Request(object):
- """Representing an abstract web service operation."""
-
- def __init__(self, network, method_name, params = {}):
-
- self.params = params
- self.network = network
-
- (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth()
-
- self.params["api_key"] = self.api_key
- self.params["method"] = method_name
-
- if network.is_caching_enabled():
- self.cache = network._get_cache_backend()
-
- if self.session_key:
- self.params["sk"] = self.session_key
- self.sign_it()
-
- def sign_it(self):
- """Sign this request."""
-
- if not "api_sig" in self.params.keys():
- self.params['api_sig'] = self._get_signature()
-
- def _get_signature(self):
- """Returns a 32-character hexadecimal md5 hash of the signature string."""
-
- keys = self.params.keys()[:]
-
- keys.sort()
-
- string = ""
-
- for name in keys:
- string += _unicode(name)
- string += _unicode(self.params[name])
-
- string += _unicode(self.api_secret)
-
- return md5(string)
-
- def _get_cache_key(self):
- """The cache key is a string of concatenated sorted names and values."""
-
- keys = self.params.keys()
- keys.sort()
-
- cache_key = str()
-
- for key in keys:
- if key != "api_sig" and key != "api_key" and key != "sk":
- cache_key += key + _string(self.params[key])
-
- return hashlib.sha1(cache_key).hexdigest()
-
- def _get_cached_response(self):
- """Returns a file object of the cached response."""
-
- if not self._is_cached():
- response = self._download_response()
- self.cache.set_xml(self._get_cache_key(), response)
-
- return self.cache.get_xml(self._get_cache_key())
-
- def _is_cached(self):
- """Returns True if the request is already in cache."""
-
- return self.cache.has_key(self._get_cache_key())
-
- def _download_response(self):
- """Returns a response body string from the server."""
-
- # Delay the call if necessary
- #self.network._delay_call() # enable it if you want.
-
- data = []
- for name in self.params.keys():
- data.append('='.join((name, urllib.quote_plus(_string(self.params[name])))))
- data = '&'.join(data)
-
- headers = {
- "Content-type": "application/x-www-form-urlencoded",
- 'Accept-Charset': 'utf-8',
- 'User-Agent': "pylast" + '/' + __version__
- }
-
- (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
-
- if self.network.is_proxy_enabled():
- conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
- conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
- body=data, headers=headers)
- else:
- conn = httplib.HTTPConnection(host=HOST_NAME)
- conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers)
-
- response = conn.getresponse()
- response_text = _unicode(response.read())
- self._check_response_for_errors(response_text)
- return response_text
-
- def execute(self, cacheable = False):
- """Returns the XML DOM response of the POST Request from the server"""
-
- if self.network.is_caching_enabled() and cacheable:
- response = self._get_cached_response()
- else:
- response = self._download_response()
-
- return minidom.parseString(_string(response))
-
- def _check_response_for_errors(self, response):
- """Checks the response for errors and raises one if any exists."""
-
- doc = minidom.parseString(_string(response))
- e = doc.getElementsByTagName('lfm')[0]
-
- if e.getAttribute('status') != "ok":
- e = doc.getElementsByTagName('error')[0]
- status = e.getAttribute('code')
- details = e.firstChild.data.strip()
- raise WSError(self.network, status, details)
- class SessionKeyGenerator(object):
- """Methods of generating a session key:
- 1) Web Authentication:
- a. network = get_*_network(API_KEY, API_SECRET)
- b. sg = SessionKeyGenerator(network)
- c. url = sg.get_web_auth_url()
- d. Ask the user to open the url and authorize you, and wait for it.
- e. session_key = sg.get_web_auth_session_key(url)
- 2) Username and Password Authentication:
- a. network = get_*_network(API_KEY, API_SECRET)
- b. username = raw_input("Please enter your username: ")
- c. password_hash = pylast.md5(raw_input("Please enter your password: ")
- d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash)
-
- A session key's lifetime is infinie, unless the user provokes the rights of the given API Key.
-
- If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a
- SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this
- manually, unless you want to.
- """
-
- def __init__(self, network):
- self.network = network
- self.web_auth_tokens = {}
-
- def _get_web_auth_token(self):
- """Retrieves a token from the network for web authentication.
- The token then has to be authorized from getAuthURL before creating session.
- """
-
- request = _Request(self.network, 'auth.getToken')
-
- # default action is that a request is signed only when
- # a session key is provided.
- request.sign_it()
-
- doc = request.execute()
-
- e = doc.getElementsByTagName('token')[0]
- return e.firstChild.data
-
- def get_web_auth_url(self):
- """The user must open this page, and you first, then call get_web_auth_session_key(url) after that."""
-
- token = self._get_web_auth_token()
-
- url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \
- {"homepage": self.network.homepage, "api": self.network.api_key, "token": token}
-
- self.web_auth_tokens[url] = token
-
- return url
- def get_web_auth_session_key(self, url):
- """Retrieves the session key of a web authorization process by its url."""
-
- if url in self.web_auth_tokens.keys():
- token = self.web_auth_tokens[url]
- else:
- token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed.
-
- request = _Request(self.network, 'auth.getSession', {'token': token})
-
- # default action is that a request is signed only when
- # a session key is provided.
- request.sign_it()
-
- doc = request.execute()
-
- return doc.getElementsByTagName('key')[0].firstChild.data
-
- def get_session_key(self, username, password_hash):
- """Retrieve a session key with a username and a md5 hash of the user's password."""
-
- params = {"username": username, "authToken": md5(username + password_hash)}
- request = _Request(self.network, "auth.getMobileSession", params)
-
- # default action is that a request is signed only when
- # a session key is provided.
- request.sign_it()
-
- doc = request.execute()
-
- return _extract(doc, "key")
- def _namedtuple(name, children):
- """
- collections.namedtuple is available in (python >= 2.6)
- """
-
- v = sys.version_info
- if v[1] >= 6 and v[0] < 3:
- return collections.namedtuple(name, children)
- else:
- def fancydict(*args):
- d = {}
- i = 0
- for child in children:
- d[child.strip()] = args[i]
- i += 1
- return d
-
- return fancydict
- TopItem = _namedtuple("TopItem", ["item", "weight"])
- SimilarItem = _namedtuple("SimilarItem", ["item", "match"])
- LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
- PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
- LovedTrack = _namedtuple("LovedTrack", ["track", "date", "timestamp"])
- ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
- Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
- Shout = _namedtuple("Shout", ["body", "author", "date"])
- def _string_output(funct):
- def r(*args):
- return _string(funct(*args))
-
- return r
- class _BaseObject(object):
- """An abstract webservices object."""
-
- network = None
-
- def __init__(self, network):
- self.network = network
-
- def _request(self, method_name, cacheable = False, params = None):
- if not params:
- params = self._get_params()
-
- return _Request(self.network, method_name, params).execute(cacheable)
-
- def _get_params(self):
- """Returns the most common set of parameters between all objects."""
-
- return {}
-
- def __hash__(self):
- return hash(self.network) + \
- hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower())
- class _Taggable(object):
- """Common functions for classes with tags."""
-
- def __init__(self, ws_prefix):
- self.ws_prefix = ws_prefix
-
- def add_tags(self, *tags):
- """Adds one or several tags.
- * *tags: Any number of tag names or Tag objects.
- """
-
- for tag in tags:
- self._add_tag(tag)
-
- def _add_tag(self, tag):
- """Adds one or several tags.
- * tag: one tag name or a Tag object.
- """
-
- if isinstance(tag, Tag):
- tag = tag.get_name()
-
- params = self._get_params()
- params['tags'] = _unicode(tag)
-
- self._request(self.ws_prefix + '.addTags', False, params)
-
- def _remove_tag(self, single_tag):
- """Remove a user's tag from this object."""
-
- if isinstance(single_tag, Tag):
- single_tag = single_tag.get_name()
-
- params = self._get_params()
- params['tag'] = _unicode(single_tag)
-
- self._request(self.ws_prefix + '.removeTag', False, params)
- def get_tags(self):
- """Returns a list of the tags set by the user to this object."""
-
- # Uncacheable because it can be dynamically changed by the user.
- params = self._get_params()
-
- doc = self._request(self.ws_prefix + '.getTags', False, params)
- tag_names = _extract_all(doc, 'name')
- tags = []
- for tag in tag_names:
- tags.append(Tag(tag, self.network))
-
- return tags
-
- def remove_tags(self, *tags):
- """Removes one or several tags from this object.
- * *tags: Any number of tag names or Tag objects.
- """
-
- for tag in tags:
- self._remove_tag(tag)
-
- def clear_tags(self):
- """Clears all the user-set tags. """
-
- self.remove_tags(*(self.get_tags()))
-
- def set_tags(self, *tags):
- """Sets this object's tags to only those tags.
- * *tags: any number of tag names.
- """
-
- c_old_tags = []
- old_tags = []
- c_new_tags = []
- new_tags = []
-
- to_remove = []
- to_add = []
-
- tags_on_server = self.get_tags()
-
- for tag in tags_on_server:
- c_old_tags.append(tag.get_name().lower())
- old_tags.append(tag.get_name())
-
- for tag in tags:
- c_new_tags.append(tag.lower())
- new_tags.append(tag)
-
- for i in range(0, len(old_tags)):
- if not c_old_tags[i] in c_new_tags:
- to_remove.append(old_tags[i])
-
- for i in range(0, len(new_tags)):
- if not c_new_tags[i] in c_old_tags:
- to_add.append(new_tags[i])
-
- self.remove_tags(*to_remove)
- self.add_tags(*to_add)
-
- def get_top_tags(self, limit = None):
- """Returns a list of the most frequently used Tags on this object."""
-
- doc = self._request(self.ws_prefix + '.getTopTags', True)
-
- elements = doc.getElementsByTagName('tag')
- seq = []
-
- for element in elements:
- if limit and len(seq) >= limit:
- break
- tag_name = _extract(element, 'name')
- tagcount = _extract(element, 'count')
-
- seq.append(TopItem(Tag(tag_name, self.network), tagcount))
-
- return seq
-
- class WSError(Exception):
- """Exception related to the Network web service"""
-
- def __init__(self, network, status, details):
- self.status = status
- self.details = details
- self.network = network
- @_string_output
- def __str__(self):
- return self.details
-
- def get_id(self):
- """Returns the exception ID, from one of the following:
- STATUS_INVALID_SERVICE = 2
- STATUS_INVALID_METHOD = 3
- STATUS_AUTH_FAILED = 4
- STATUS_INVALID_FORMAT = 5
- STATUS_INVALID_PARAMS = 6
- STATUS_INVALID_RESOURCE = 7
- STATUS_TOKEN_ERROR = 8
- STATUS_INVALID_SK = 9
- STATUS_INVALID_API_KEY = 10
- STATUS_OFFLINE = 11
- STATUS_SUBSCRIBERS_ONLY = 12
- STATUS_TOKEN_UNAUTHORIZED = 14
- STATUS_TOKEN_EXPIRED = 15
- """
-
- return self.status
- class Album(_BaseObject, _Taggable):
- """An album."""
-
- title = None
- artist = None
-
- def __init__(self, artist, title, network):
- """
- Create an album instance.
- # Parameters:
- * artist: An artist name or an Artist object.
- * title: The album title.
- """
-
- _BaseObject.__init__(self, network)
- _Taggable.__init__(self, 'album')
-
- if isinstance(artist, Artist):
- self.artist = artist
- else:
- self.artist = Artist(artist, self.network)
-
- self.title = title
- @_string_output
- def __repr__(self):
- return u"%s - %s" %(self.get_artist().get_name(), self.get_title())
-
- def __eq__(self, other):
- return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
-
- def __ne__(self, other):
- return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower())
-
- def _get_params(self):
- return {'artist': self.get_artist().get_name(), 'album': self.get_title(), }
-
- def get_artist(self):
- """Returns the associated Artist object."""
-
- return self.artist
-
- def get_title(self):
- """Returns the album title."""
-
- return self.title
-
- def get_name(self):
- """Returns the album title (alias to Album.get_title)."""
-
- return self.get_title()
-
- def get_release_date(self):
- """Retruns the release date of the album."""
-
- return _extract(self._request("album.getInfo", cacheable = True), "releasedate")
-
- def get_cover_image(self, size = COVER_EXTRA_LARGE):
- """
- Returns a uri to the cover image
- size can be one of:
- COVER_MEGA
- COVER_EXTRA_LARGE
- COVER_LARGE
- COVER_MEDIUM
- COVER_SMALL
- """
-
- return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size]
-
- def get_id(self):
- """Returns the ID"""
-
- return _extract(self._request("album.getInfo", cacheable = True), "id")
-
- def get_playcount(self):
- """Returns the number of plays on the network"""
-
- return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
-
- def get_listener_count(self):
- """Returns the number of liteners on the network"""
-
- return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners"))
-
- def get_top_tags(self, limit=None):
- """Returns a list of the most-applied tags to this album."""
-
- doc = self._request("album.getInfo", True)
- e = doc.getElementsByTagName("toptags")[0]
-
- seq = []
- for name in _extract_all(e, "name"):
- if len(seq) < limit:
- seq.append(Tag(name, self.network))
-
- return seq
- def get_tracks(self):
- """Returns the list of Tracks on this album."""
-
- uri = 'lastfm://playlist/album/%s' %self.get_id()
-
- return XSPF(uri, self.network).get_tracks()
-
- def get_mbid(self):
- """Returns the MusicBrainz id of the album."""
-
- return _extract(self._request("album.getInfo", cacheable = True), "mbid")
-
- def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the album page on the network.
- # Parameters:
- * domain_name str: The network's language domain. Possible values:
- o DOMAIN_ENGLISH
- o DOMAIN_GERMAN
- o DOMAIN_SPANISH
- o DOMAIN_FRENCH
- o DOMAIN_ITALIAN
- o DOMAIN_POLISH
- o DOMAIN_PORTUGUESE
- o DOMAIN_SWEDISH
- o DOMAIN_TURKISH
- o DOMAIN_RUSSIAN
- o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
- """
-
- artist = _url_safe(self.get_artist().get_name())
- album = _url_safe(self.get_title())
-
- return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album}
-
- def get_wiki_published_date(self):
- """Returns the date of publishing this version of the wiki."""
-
- doc = self._request("album.getInfo", True)
-
- if len(doc.getElementsByTagName("wiki")) == 0:
- return
-
- node = doc.getElementsByTagName("wiki")[0]
-
- return _extract(node, "published")
-
- def get_wiki_summary(self):
- """Returns the summary of the wiki."""
-
- doc = self._request("album.getInfo", True)
-
- if len(doc.getElementsByTagName("wiki")) == 0:
- return
-
- node = doc.getElementsByTagName("wiki")[0]
-
- return _extract(node, "summary")
-
- def get_wiki_content(self):
- """Returns the content of the wiki."""
-
- doc = self._request("album.getInfo", True)
-
- if len(doc.getElementsByTagName("wiki")) == 0:
- return
-
- node = doc.getElementsByTagName("wiki")[0]
-
- return _extract(node, "content")
- class Artist(_BaseObject, _Taggable):
- """An artist."""
-
- name = None
-
- def __init__(self, name, network):
- """Create an artist object.
- # Parameters:
- * name str: The artist's name.
- """
-
- _BaseObject.__init__(self, network)
- _Taggable.__init__(self, 'artist')
-
- self.name = name
- @_string_output
- def __repr__(self):
- return self.get_name()
-
- def __eq__(self, other):
- return self.get_name().lower() == other.get_name().lower()
-
- def __ne__(self, other):
- return self.get_name().lower() != other.get_name().lower()
-
- def _get_params(self):
- return {'artist': self.get_name()}
-
- def get_name(self):
- """Returns the name of the artist."""
-
- return self.name
-
- def get_cover_image(self, size = COVER_LARGE):
- """
- Returns a uri to the cover image
- size can be one of:
- COVER_MEGA
- COVER_EXTRA_LARGE
- COVER_LARGE
- COVER_MEDIUM
- COVER_SMALL
- """
-
- return _extract_all(self._request("artist.getInfo", True), "image")[size]
-
- def get_playcount(self):
- """Returns the number of plays on the network."""
-
- return _number(_extract(self._request("artist.getInfo", True), "playcount"))
- def get_mbid(self):
- """Returns the MusicBrainz ID of this artist."""
-
- doc = self._request("artist.getInfo", True)
-
- return _extract(doc, "mbid")
-
- def get_listener_count(self):
- """Returns the number of liteners on the network."""
-
- return _number(_extract(self._request("artist.getInfo", True), "listeners"))
-
- def is_streamable(self):
- """Returns True if the artist is streamable."""
-
- return bool(_number(_extract(self._request("artist.getInfo", True), "streamable")))
-
- def get_bio_published_date(self):
- """Returns the date on which the artist's biography was published."""
-
- return _extract(self._request("artist.getInfo", True), "published")
-
- def get_bio_summary(self):
- """Returns the summary of the artist's biography."""
-
- return _extract(self._request("artist.getInfo", True), "summary")
-
- def get_bio_content(self):
- """Returns the content of the artist's biography."""
-
- return _extract(self._request("artist.getInfo", True), "content")
-
- def get_upcoming_events(self):
- """Returns a list of the upcoming Events for this artist."""
-
- doc = self._request('artist.getEvents', True)
-
- ids = _extract_all(doc, 'id')
-
- events = []
- for e_id in ids:
- events.append(Event(e_id, self.network))
-
- return events
-
- def get_similar(self, limit = None):
- """Returns the similar artists on the network."""
-
- params = self._get_params()
- if limit:
- params['limit'] = _unicode(limit)
-
- doc = self._request('artist.getSimilar', True, params)
-
- names = _extract_all(doc, "name")
- matches = _extract_all(doc, "match")
-
- artists = []
- for i in range(0, len(names)):
- artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i])))
-
- return artists
- def get_top_albums(self):
- """Retuns a list of the top albums."""
-
- doc = self._request('artist.getTopAlbums', True)
-
- seq = []
-
- for node in doc.getElementsByTagName("album"):
- name = _extract(node, "name")
- artist = _extract(node, "name", 1)
- playcount = _extract(node, "playcount")
-
- seq.append(TopItem(Album(artist, name, self.network), playcount))
-
- return seq
-
- def get_top_tracks(self):
- """Returns a list of the most played Tracks by this artist."""
-
- doc = self._request("artist.getTopTracks", True)
-
- seq = []
- for track in doc.getElementsByTagName('track'):
-
- title = _extract(track, "name")
- artist = _extract(track, "name", 1)
- playcount = _number(_extract(track, "playcount"))
-
- seq.append( TopItem(Track(artist, title, self.network), playcount) )
-
- return seq
-
- def get_top_fans(self, limit = None):
- """Returns a list of the Users who played this artist the most.
- # Parameters:
- * limit int: Max elements.
- """
-
- doc = self._request('artist.getTopFans', True)
-
- seq = []
-
- elements = doc.getElementsByTagName('user')
-
- for element in elements:
- if limit and len(seq) >= limit:
- break
-
- name = _extract(element, 'name')
- weight = _number(_extract(element, 'weight'))
-
- seq.append(TopItem(User(name, self.network), weight))
-
- return seq
- def share(self, users, message = None):
- """Shares this artist (sends out recommendations).
- # Parameters:
- * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
- * message str: A message to include in the recommendation message.
- """
-
- #last.fm currently accepts a max of 10 recipient at a time
- while(len(users) > 10):
- section = users[0:9]
- users = users[9:]
- self.share(section, message)
-
- nusers = []
- for user in users:
- if isinstance(user, User):
- nusers.append(user.get_name())
- else:
- nusers.append(user)
-
- params = self._get_params()
- recipients = ','.join(nusers)
- params['recipient'] = recipients
- if message: params['message'] = _unicode(message)
-
- self._request('artist.share', False, params)
-
- def get_url(self, domain_name = DOMAIN_ENGLISH):
- """Returns the url of the artist page on the network.
- # Parameters:
- * domain_name: The network's language domain. Possible values:
- o DOMAIN_ENGLISH
- o DOMAIN_GERMAN
- o DOMAIN_SPANISH
- o DOMAIN_FRENCH
- o DOMAIN_ITALIAN
- o DOMAIN_POLISH
- o DOMAIN_PORTUGUESE
- o DOMAIN_SWEDISH
- o DOMAIN_TURKISH
- o DOMAIN_RUSSIAN
- o DOMAIN_JAPANESE
- o DOMAIN_CHINESE
- """
-
- artist = _url_safe(self.get_name())
-
- return self.network._get_url(domain_name, "artist") %{'artist': artist}
-
- def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None):
- """
- Returns a sequence of Image objects
- if limit is None it will return all
- order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE
- """
-
- images = []
-
- params = self._get_params()
- params["order"] = order
- nodes = _collect_nodes(limit, self, "artist.getImages", True, params)
- for e in nodes:
- if _extract(e, "name"):
- user = User(_extract(e, "name"), self.network)
- else:
- user = None…