From d61fbb3a04620f1e96882a7ba492e6794f1705a7 Mon Sep 17 00:00:00 2001 From: Amr Hassan Date: Mon, 31 Aug 2009 04:18:37 +0000 Subject: [PATCH] Enter v0.4 --- .build | 1 + README | 7 + pylast.py | 1971 +++++++++++++++++++++++++++++++---------------------- setup.py | 26 +- 4 files changed, 1202 insertions(+), 803 deletions(-) create mode 100644 .build create mode 100644 README diff --git a/.build b/.build new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/.build @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..64b1b7f --- /dev/null +++ b/README @@ -0,0 +1,7 @@ +pylast +------ + +A python interface to Last.fm. Try using the pydoc utility for help +on usage. +For more info check out the project's home page at http://code.google.com/p/pylast/ +or the mailing list http://groups.google.com/group/pylast/ diff --git a/pylast.py b/pylast.py index 57a4113..a244f3c 100644 --- a/pylast.py +++ b/pylast.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# pylast - A Python interface to Last.fm +# 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 @@ -19,47 +19,38 @@ # USA # # http://code.google.com/p/pylast/ - -__name__ = 'pylast' -__version__ = '0.3' -__revision__ = "$Revision$" -__doc__ = 'A Python interface to Last.fm' + +__version__ = '0.4' __author__ = 'Amr Hassan' __copyright__ = "Copyright (C) 2008-2009 Amr Hassan" __license__ = "gpl" __email__ = 'amr.hassan@gmail.com' -# Parse revision and add it to __version__ -r = __revision__ -__version__ = __version__ + "." + r[r.find(" ")+1:r.rfind("$")-1] - -# Default values for Last.fm. -WS_SERVER = ('ws.audioscrobbler.com', '/2.0/') -SUBMISSION_SERVER = "http://post.audioscrobbler.com:80/" - -__proxy = None -__proxy_enabled = False -__cache_backend = None -__last_call_time = 0 - import hashlib import httplib import urllib import codecs import threading from xml.dom import minidom +import xml.dom import os import time from logging import info, warn, debug import shelve import tempfile import sys +import htmlentitydefs try: import sqlite3 except ImportError: pass +try: + import collections +except ImportError: + pass + STATUS_INVALID_SERVICE = 2 STATUS_INVALID_METHOD = 3 STATUS_AUTH_FAILED = 4 @@ -84,23 +75,24 @@ PERIOD_3MONTHS = '3month' PERIOD_6MONTHS = '6month' PERIOD_12MONTHS = '12month' -IMAGE_SMALL = 0 -IMAGE_MEDIUM = 1 -IMAGE_LARGE = 2 -IMAGE_EXTRA_LARGE = 3 +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 -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' +COVER_SMALL = 0 +COVER_MEDIUM = 1 +COVER_LARGE = 2 +COVER_EXTRA_LARGE = 3 +COVER_MEGA = 4 USER_MALE = 'Male' USER_FEMALE = 'Female' @@ -115,6 +107,511 @@ SCROBBLE_MODE_PLAYED = "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 + + 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, generate_session_key_if_can=True): + """ + Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple. + If SESSION_KEY == None and username and password_hash were provided, a SESSION_KEY will be + generated and stored + """ + + if generate_session_key_if_can 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) + + return (self.api_key, self.api_secret, self.session_key) + + def _delay_call(): + """ + 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) + + id = doc.getElementsByTagName("id")[0].firstChild.data + user = doc.getElementsByTagName('playlists')[0].getAttribute('user') + + return Playlist(user, id, self) + + def get_authenticated_user(self): + """Returns the authenticated user.""" + + return AuthenticatedUser(self) + + def get_top_tags(self): + """Returns a sequence of the most used tags as a sequence of TopItem objects.""" + + doc = self._Request(self, "tag.getTopTags").execute(True) + list = [] + for node in doc.getElementsByTagName("tag"): + tag = Tag(_extract(node, "name"), network) + weight = _extract(node, "count") + + list.append(TopItem(tag, weight)) + + return list + + 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_") + + if "sqlite3" in sys.modules.keys(): + self.cache_backend = _SqliteCacheBackend(file_path) + debug("Caching to Sqlite3 at " + file_path) + else: + self.cache_backend = _ShelfCacheBackend(file_path) + debug("Caching to Shelf at " + 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("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("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("album.getInfo", params).execute(True) + + return Album(_extract(doc, "artist"), _extract(doc, "name"), self) + +def get_lastfm_network(api_key=None, api_secret=None, session_key = None, username = None, password_hash = None): + """ + 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=None, api_secret=None, session_key = None, username = None, password_hash = None): + """ + 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.""" @@ -182,22 +679,21 @@ class _ThreadedCall(threading.Thread): class _Request(object): """Representing an abstract web service operation.""" - global WS_SERVER - (HOST_NAME, HOST_SUBDIR) = WS_SERVER - - def __init__(self, method_name, params, api_key, api_secret, session_key = None): - - self.params = params - self.api_secret = api_secret + def __init__(self, network, method_name, params = {}): - self.params["api_key"] = api_key + self.params = params + self.network = network + + (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth(False) + + self.params["api_key"] = self.api_key self.params["method"] = method_name - if is_caching_enabled(): - self.cache = _get_cache_backend() + if network.is_caching_enabled(): + self.cache = network._get_cache_backend() - if session_key: - self.params["sk"] = session_key + if self.session_key: + self.params["sk"] = self.session_key self.sign_it() def sign_it(self): @@ -233,7 +729,7 @@ class _Request(object): for key in keys: if key != "api_sig" and key != "api_key" and key != "sk": - cache_key += key + self.params[key].encode("utf-8") + cache_key += key + _string(self.params[key]) return hashlib.sha1(cache_key).hexdigest() @@ -255,11 +751,11 @@ class _Request(object): """Returns a response body string from the server.""" # Delay the call if necessary - _delay_call() + #self.network._delay_call() # enable it if you want. data = [] for name in self.params.keys(): - data.append('='.join((name, urllib.quote_plus(self.params[name].encode("utf-8"))))) + data.append('='.join((name, urllib.quote_plus(_string(self.params[name]))))) data = '&'.join(data) headers = { @@ -267,68 +763,75 @@ class _Request(object): 'Accept-Charset': 'utf-8', 'User-Agent': __name__ + '/' + __version__ } - - if is_proxy_enabled(): + + (HOST_NAME, HOST_SUBDIR) = self.network.ws_server + + if self.network.is_proxy_enabled(): conn = httplib.HTTPConnection(host = _get_proxy()[0], port = _get_proxy()[1]) - conn.request(method='POST', url="http://" + self.HOST_NAME + self.HOST_SUBDIR, + conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR, body=data, headers=headers) else: - conn = httplib.HTTPConnection(host=self.HOST_NAME) - conn.request(method='POST', url=self.HOST_SUBDIR, body=data, headers=headers) + 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(), "utf-8") + 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 is_caching_enabled() and cacheable: + if self.network.is_caching_enabled() and cacheable: response = self._get_cached_response() else: response = self._download_response() - return minidom.parseString(response.encode("utf-8")) + 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(response.encode("utf-8")) + 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 ServiceException(status, details) + raise WSError(self.network, status, details) class SessionKeyGenerator(object): """Methods of generating a session key: 1) Web Authentication: - a. sg = SessionKeyGenerator(API_KEY, API_SECRET) - b. url = sg.get_web_auth_url() - c. Ask the user to open the url and authorize you, and wait for it. - d. session_key = sg.get_web_auth_session_key(url) + 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. username = raw_input("Please enter your username: ") - b. md5_password = pylast.md5(raw_input("Please enter your password: ") - c. session_key = SessionKeyGenerator(API_KEY, API_SECRET).get_session_key(username, md5_password) + 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, api_key, api_secret): - self.api_key = api_key - self.api_secret = api_secret + def __init__(self, network): + self.network = network self.web_auth_tokens = {} def _get_web_auth_token(self): - """Retrieves a token from Last.fm for web authentication. + """Retrieves a token from the network for web authentication. The token then has to be authorized from getAuthURL before creating session. """ - request = _Request('auth.getToken', dict(), self.api_key, self.api_secret) + request = _Request(self.network, 'auth.getToken') # default action is that a request is signed only when # a session key is provided. @@ -344,8 +847,8 @@ class SessionKeyGenerator(object): token = self._get_web_auth_token() - url = 'http://www.last.fm/api/auth/?api_key=%(api)s&token=%(token)s' % \ - {'api': self.api_key, 'token': token} + url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \ + {"homepage": self.network.homepage, "api": self.api_key, "token": token} self.web_auth_tokens[url] = token @@ -357,9 +860,9 @@ class SessionKeyGenerator(object): if url in self.web_auth_tokens.keys(): token = self.web_auth_tokens[url] else: - token = "" #that's gonna raise a ServiceException of an unauthorized token when the request is executed. + token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed. - request = _Request('auth.getSession', {'token': token}, self.api_key, self.api_secret) + request = _Request(self.network, 'auth.getSession', {'token': token}) # default action is that a request is signed only when # a session key is provided. @@ -369,11 +872,11 @@ class SessionKeyGenerator(object): return doc.getElementsByTagName('key')[0].firstChild.data - def get_session_key(self, username, md5_password): + 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 + md5_password)} - request = _Request("auth.getMobileSession", params, self.api_key, self.api_secret) + 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. @@ -383,27 +886,54 @@ class SessionKeyGenerator(object): 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"]) +LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"]) +PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_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.""" - def __init__(self, api_key, api_secret, session_key=""): - - self.api_key = api_key - self.api_secret = api_secret - self.session_key = session_key - - self.auth_data = (self.api_key, self.api_secret, self.session_key) + 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(method_name, params, *self.auth_data).execute(cacheable) + return _Request(self.network, method_name, params).execute(cacheable) def _get_params(): """Returns the most common set of parameters between all objects.""" - return dict() + return {} class _Taggable(object): """Common functions for classes with tags.""" @@ -428,7 +958,7 @@ class _Taggable(object): tag = tag.get_name() params = self._get_params() - params['tags'] = unicode(tag) + params['tags'] = _unicode(tag) self._request(self.ws_prefix + '.addTags', False, params) info("Tagged " + repr(self) + " as (" + repr(tag) + ")") @@ -440,7 +970,7 @@ class _Taggable(object): single_tag = single_tag.get_name() params = self._get_params() - params['tag'] = unicode(single_tag) + params['tag'] = _unicode(single_tag) self._request(self.ws_prefix + '.removeTag', False, params) info("Removed tag (" + repr(tag) + ") from " + repr(self)) @@ -450,12 +980,12 @@ class _Taggable(object): # Uncacheable because it can be dynamically changed by the user. params = self._get_params() - doc = _Request(self.ws_prefix + '.getTags', params, *self.auth_data).execute(cacheable = False) + 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.auth_data)) + tags.append(Tag(tag, self.network)) return tags @@ -520,17 +1050,19 @@ class _Taggable(object): tag_name = _extract(element, 'name') tagcount = _extract(element, 'count') - list.append(TopItem(Tag(tag_name, *self.auth_data), tagcount)) + list.append(TopItem(Tag(tag_name, self.network), tagcount)) return list -class ServiceException(Exception): - """Exception related to the Last.fm web service""" +class WSError(Exception): + """Exception related to the Network web service""" - def __init__(self, lastfm_status, details): - self.lastfm_status = lastfm_status + def __init__(self, network, status, details): + self.status = status self.details = details - + self.message = repr(self) + + @_string_output def __repr__(self): return self.details @@ -551,97 +1083,12 @@ class ServiceException(Exception): STATUS_TOKEN_EXPIRED = 15 """ - return self.lastfm_status - -class TopItem (object): - """A top item in a list that has a weight. Returned from functions like get_top_tracks() and get_top_artists().""" - - def __init__(self, item, weight): - object.__init__(self) - - self.item = item - self.weight = _number(weight) - - def __repr__(self): - return "Item: " + self.get_item().__repr__() + ", Weight: " + str(self.get_weight()) - - def get_item(self): - """Returns the item.""" - - return self.item - - def get_weight(self): - """Returns the weight of the itme in the list.""" - - return self.weight - - -class LibraryItem (object): - """An item in a User's Library. It could be an artist, an album or a track.""" - - def __init__(self, item, playcount, tagcount): - object.__init__(self) - - self.item = item - self.playcount = _number(playcount) - self.tagcount = _number(tagcount) - - def __repr__(self): - return "Item: " + self.get_item().__repr__() + ", Playcount: " + str(self.get_playcount()) + ", Tagcount: " + str(self.get_tagcount()) - - def get_item(self): - """Returns the itme.""" - - return self.item - - def get_playcount(self): - """Returns the item's playcount in the Library.""" - - return self.playcount - - def get_tagcount(self): - """Returns the item's tagcount in the Library.""" - - return self.tagcount - -class PlayedTrack (object): - """A track with a playback date.""" - - def __init__(self, track, date, timestamp): - object.__init__(self) - - self.track = track - self.date = date - self.timestamp = timestamp - - def __repr__(self): - return repr(self.track) + " played at " + self.date - - def get_track(self): - """Return the track.""" - - return self.track - - def get_item(self): - """Returns the played track. An alias to get_track().""" - - return self.get_track(); - - def get_date(self): - """Returns the playback date.""" - - return self.date - - def get_timestamp(self): - """Returns the unix timestamp of the playback date.""" - - return self.timestamp - + return self.status class Album(_BaseObject, _Taggable): - """A Last.fm album.""" + """An album.""" - def __init__(self, artist, title, api_key, api_secret, session_key=""): + def __init__(self, artist, title, network): """ Create an album instance. # Parameters: @@ -649,18 +1096,19 @@ class Album(_BaseObject, _Taggable): * title: The album title. """ - _BaseObject.__init__(self, api_key, api_secret, session_key) + _BaseObject.__init__(self, network) _Taggable.__init__(self, 'album') if isinstance(artist, Artist): self.artist = artist else: - self.artist = Artist(artist, *self.auth_data) + self.artist = Artist(artist, self.network) self.title = title + @_string_output def __repr__(self): - return self.get_artist().get_name() + ' - ' + self.get_title() + 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()) @@ -691,46 +1139,52 @@ class Album(_BaseObject, _Taggable): return _extract(self._request("album.getInfo", cacheable = True), "releasedate") - def get_image_url(self, size = IMAGE_EXTRA_LARGE): - """Returns the associated image URL. - # Parameters: - * size int: The image size. Possible values: - o IMAGE_EXTRA_LARGE - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL + 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 Last.fm ID.""" + """Returns the ID""" return _extract(self._request("album.getInfo", cacheable = True), "id") def get_playcount(self): - """Returns the number of plays on Last.fm.""" + """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 Last.fm.""" + """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): + def get_top_tags(self): """Returns a list of the most-applied tags to this album.""" - # BROKEN: Web service is currently broken. + doc = self._request("album.getInfo", True) + e = doc.getElementsByTagName("toptags")[0] - return None + list = [] + for name in _extract_all(e, "name"): + list.append(Tag(name, self.network)) + + return list def get_tracks(self): """Returns the list of Tracks on this album.""" uri = 'lastfm://playlist/album/%s' %self.get_id() - return XSPF(uri, *self.auth_data).get_tracks() + return XSPF(uri, self.network).get_tracks() def get_mbid(self): """Returns the MusicBrainz id of the album.""" @@ -738,9 +1192,9 @@ class Album(_BaseObject, _Taggable): 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 Last.fm. + """Returns the url of the album page on the network. # Parameters: - * domain_name str: Last.fm's language domain. Possible values: + * domain_name str: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -755,29 +1209,64 @@ class Album(_BaseObject, _Taggable): o DOMAIN_CHINESE """ - url = 'http://%(domain)s/music/%(artist)s/%(album)s' + artist = _url_safe(self.get_artist().get_name()) + album = _url_safe(self.get_title()) - artist = _get_url_safe(self.get_artist().get_name()) - album = _get_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.""" - return url %{'domain': domain_name, 'artist': artist, 'album': album} + 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): - """A Last.fm artist.""" + """An artist.""" - def __init__(self, name, api_key, api_secret, session_key=""): + def __init__(self, name, network): """Create an artist object. # Parameters: * name str: The artist's name. """ - _BaseObject.__init__(self, api_key, api_secret, session_key) + _BaseObject.__init__(self, network) _Taggable.__init__(self, 'artist') self.name = name + @_string_output def __repr__(self): - return unicode(self.get_name()) + return self.get_name() def __eq__(self, other): return self.get_name().lower() == other.get_name().lower() @@ -793,19 +1282,21 @@ class Artist(_BaseObject, _Taggable): return self.name - def get_image_url(self, size = IMAGE_LARGE): - """Returns the associated image URL. - # Parameters: - * size int: The image size. Possible values: - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL + 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 Last.fm.""" + """Returns the number of plays on the network.""" return _number(_extract(self._request("artist.getInfo", True), "playcount")) @@ -817,7 +1308,7 @@ class Artist(_BaseObject, _Taggable): return _extract(doc, "mbid") def get_listener_count(self): - """Returns the number of liteners on Last.fm.""" + """Returns the number of liteners on the network.""" return _number(_extract(self._request("artist.getInfo", True), "listeners")) @@ -850,16 +1341,16 @@ class Artist(_BaseObject, _Taggable): events = [] for id in ids: - events.append(Event(id, *self.auth_data)) + events.append(Event(id, self.network)) return events def get_similar(self, limit = None): - """Returns the similar artists on Last.fm.""" + """Returns the similar artists on the network.""" params = self._get_params() if limit: - params['limit'] = unicode(limit) + params['limit'] = _unicode(limit) doc = self._request('artist.getSimilar', True, params) @@ -867,7 +1358,7 @@ class Artist(_BaseObject, _Taggable): artists = [] for name in names: - artists.append(Artist(name, *self.auth_data)) + artists.append(Artist(name, self.network)) return artists @@ -883,7 +1374,7 @@ class Artist(_BaseObject, _Taggable): artist = _extract(node, "name", 1) playcount = _extract(node, "playcount") - list.append(TopItem(Album(artist, name, *self.auth_data), playcount)) + list.append(TopItem(Album(artist, name, self.network), playcount)) return list @@ -899,7 +1390,7 @@ class Artist(_BaseObject, _Taggable): artist = _extract(track, "name", 1) playcount = _number(_extract(track, "playcount")) - list.append( TopItem(Track(artist, title, *self.auth_data), playcount) ) + list.append( TopItem(Track(artist, title, self.network), playcount) ) return list @@ -923,7 +1414,7 @@ class Artist(_BaseObject, _Taggable): name = _extract(element, 'name') weight = _number(_extract(element, 'weight')) - list.append(TopItem(User(name, *self.auth_data), weight)) + list.append(TopItem(User(name, self.network), weight)) return list @@ -950,15 +1441,15 @@ class Artist(_BaseObject, _Taggable): params = self._get_params() recipients = ','.join(nusers) params['recipient'] = recipients - if message: params['message'] = unicode(message) + if message: params['message'] = _unicode(message) self._request('artist.share', False, params) info(repr(self) + " was shared with " + repr(users)) def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the artist page on Last.fm. + """Returns the url of the artist page on the network. # Parameters: - * domain_name: Last.fm's language domain. Possible values: + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -973,21 +1464,72 @@ class Artist(_BaseObject, _Taggable): o DOMAIN_CHINESE """ - url = 'http://%(domain)s/music/%(artist)s' + artist = _url_safe(self.get_name()) - artist = _get_url_safe(self.get_name()) + return self.network._get_url(domain_name, "artist") %{'artist': artist} + + def get_images(self, limit=None): + """ + Returns a sequence of Image objects + if limit is None it will return all + """ - return url %{'domain': domain_name, 'artist': artist} + images = [] + + nodes = _collect_nodes(limit, self, "artist.getImages", True) + for e in nodes: + if _extract(e, "name"): + user = User(_extract(e, "name"), self.network) + else: + user = None + + images.append(Image( + _extract(e, "title"), + _extract(e, "url"), + _extract(e, "dateadded"), + _extract(e, "format"), + user, + ImageSizes(*_extract_all(e, "size")), + (_extract(e, "thumbsup"), _extract(e, "thumbsdown")) + ) + ) + return images + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "artist.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("artist.Shout", False, params) + class Event(_BaseObject): - """A Last.fm event.""" + """An event.""" - def __init__(self, event_id, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, event_id, network): + _BaseObject.__init__(self, network) - self.id = unicode(event_id) + self.id = _unicode(event_id) + @_string_output def __repr__(self): return "Event #" + self.get_id() @@ -1009,13 +1551,27 @@ class Event(_BaseObject): """ params = self._get_params() - params['status'] = unicode(attending_status) + params['status'] = _unicode(attending_status) doc = self._request('event.attend', False, params) info("Attendance to " + repr(self) + " was set to " + repr(attending_status)) + def get_attendees(self): + """ + Get a list of attendees for an event + """ + + doc = self._request("event.getAttendees", False) + + users = [] + for name in _extract_all(doc, "name"): + users.append(User(name, self.network)) + + return users + def get_id(self): - """Returns the id of the event on Last.fm. """ + """Returns the id of the event on the network. """ + return self.id def get_title(self): @@ -1030,7 +1586,7 @@ class Event(_BaseObject): doc = self._request("event.getInfo", True) - return Artist(_extract(doc, "headliner"), *self.auth_data) + return Artist(_extract(doc, "headliner"), self.network) def get_artists(self): """Returns a list of the participating Artists. """ @@ -1040,7 +1596,7 @@ class Event(_BaseObject): artists = [] for name in names: - artists.append(Artist(name, *self.auth_data)) + artists.append(Artist(name, self.network)) return artists @@ -1049,10 +1605,10 @@ class Event(_BaseObject): doc = self._request("event.getInfo", True) - venue_url = _extract(doc, "url") - venue_id = _number(venue_url[venue_url.rfind("/") + 1:]) + v = doc.getElementsByTagName("venue")[0] + venue_id = _number(_extract(v, "id")) - return Venue(venue_id, *self.auth_data) + return Venue(venue_id, self.network) def get_start_date(self): """Returns the date when the event starts.""" @@ -1068,12 +1624,15 @@ class Event(_BaseObject): return _extract(doc, "description") - def get_image_url(self, size = IMAGE_LARGE): - """Returns the associated image URL. - * size: The image size. Possible values: - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL + 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 """ doc = self._request("event.getInfo", True) @@ -1095,8 +1654,8 @@ class Event(_BaseObject): return _number(_extract(doc, "reviews")) def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the event page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the event page on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -1111,9 +1670,7 @@ class Event(_BaseObject): o DOMAIN_CHINESE """ - url = 'http://%(domain)s/event/%(id)s' - - return url %{'domain': domain_name, 'id': self.get_id()} + return self.network._get_url(domain_name, "event") %{'id': self.get_id()} def share(self, users, message = None): """Shares this event (sends out recommendations). @@ -1137,19 +1694,45 @@ class Event(_BaseObject): params = self._get_params() recipients = ','.join(nusers) params['recipient'] = recipients - if message: params['message'] = unicode(message) + if message: params['message'] = _unicode(message) self._request('event.share', False, params) info(repr(self) + " was shared with " + repr(users)) + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "event.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("event.Shout", False, params) + class Country(_BaseObject): """A country at Last.fm.""" - def __init__(self, name, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, name, network): + _BaseObject.__init__(self, network) self.name = name + @_string_output def __repr__(self): return self.get_name() @@ -1182,7 +1765,7 @@ class Country(_BaseObject): name = _extract(node, 'name') playcount = _extract(node, "playcount") - list.append(TopItem(Artist(name, *self.auth_data), playcount)) + list.append(TopItem(Artist(name, self.network), playcount)) return list @@ -1199,13 +1782,13 @@ class Country(_BaseObject): artist = _extract(n, 'name', 1) playcount = _number(_extract(n, "playcount")) - list.append( TopItem(Track(artist, title, *self.auth_data), playcount)) + list.append( TopItem(Track(artist, title, self.network), playcount)) return list def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the event page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the event page on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -1220,30 +1803,29 @@ class Country(_BaseObject): o DOMAIN_CHINESE """ - url = 'http://%(domain)s/place/%(country_name)s' + country_name = _url_safe(self.get_name()) - country_name = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'country_name': country_name} + return self.network._get_url(domain_name, "country") %{'country_name': country_name} class Library(_BaseObject): """A user's Last.fm library.""" - def __init__(self, user, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, user, network): + _BaseObject.__init__(self, network) if isinstance(user, User): self.user = user else: - self.user = User(user, *self.auth_data) + self.user = User(user, self.network) self._albums_index = 0 self._artists_index = 0 self._tracks_index = 0 + @_string_output def __repr__(self): - return self.get_user().__repr__() + "'s Library" + return repr(self.get_user()) + "'s Library" def _get_params(self): return {'user': self.user.get_name()} @@ -1281,138 +1863,54 @@ class Library(_BaseObject): self._request("library.addTrack", False, params) info(repr(track) + " was added to " + repr(self)) - def _get_albums_pagecount(self): - """Returns the number of album pages in this library.""" - - doc = self._request("library.getAlbums", True) - - return _number(doc.getElementsByTagName("albums")[0].getAttribute("totalPages")) - - def is_end_of_albums(self): - """Returns True when the last page of albums has ben retrieved.""" - - if self._albums_index >= self._get_albums_pagecount(): - return True - else: - return False - - def _get_artists_pagecount(self): - """Returns the number of artist pages in this library.""" - - doc = self._request("library.getArtists", True) - - return _number(doc.getElementsByTagName("artists")[0].getAttribute("totalPages")) - - def is_end_of_artists(self): - """Returns True when the last page of artists has ben retrieved.""" - - if self._artists_index >= self._get_artists_pagecount(): - return True - else: - return False - - def _get_tracks_pagecount(self): - """Returns the number of track pages in this library.""" - - doc = self._request("library.getTracks", True) - - return _number(doc.getElementsByTagName("tracks")[0].getAttribute("totalPages")) - - def is_end_of_tracks(self): - """Returns True when the last page of tracks has ben retrieved.""" - - if self._tracks_index >= self._get_tracks_pagecount(): - return True - else: - return False - - def get_albums_page(self): - """Retreives the next page of albums in the Library. Returns a sequence of TopItem objects. - Use the function extract_items like extract_items(Library.get_albums_page()) to return only a sequence of - Album objects with no extra data. - - Example: - ------- - library = Library("rj", API_KEY, API_SECRET, SESSION_KEY) - - while not library.is_end_of_albums(): - print library.get_albums_page() + def get_albums(self, limit=50): + """ + Returns a sequence of Album objects + if limit==None it will return all (may take a while) """ - self._albums_index += 1 - - params = self._get_params() - params["page"] = str(self._albums_index) - list = [] - doc = self._request("library.getAlbums", True, params) - for node in doc.getElementsByTagName("album"): + for node in _collect_nodes(limit, self, "library.getAlbums", True): name = _extract(node, "name") artist = _extract(node, "name", 1) playcount = _number(_extract(node, "playcount")) tagcount = _number(_extract(node, "tagcount")) - list.append(LibraryItem(Album(artist, name, *self.auth_data), playcount, tagcount)) + list.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount)) return list - def get_artists_page(self): - """Retreives the next page of artists in the Library. Returns a sequence of TopItem objects. - Use the function extract_items like extract_items(Library.get_artists_page()) to return only a sequence of - Artist objects with no extra data. - - Example: - ------- - library = Library("rj", API_KEY, API_SECRET, SESSION_KEY) - - while not library.is_end_of_artists(): - print library.get_artists_page() + def get_artists(self, limit=50): + """ + Returns a sequence of Album objects + if limit==None it will return all (may take a while) """ - - self._artists_index += 1 - - params = self._get_params() - params["page"] = str(self._artists_index) list = [] - doc = self._request("library.getArtists", True, params) - for node in doc.getElementsByTagName("artist"): + for node in _collect_nodes(limit, self, "library.getArtists", True): name = _extract(node, "name") playcount = _number(_extract(node, "playcount")) tagcount = _number(_extract(node, "tagcount")) - list.append(LibraryItem(Artist(name, *self.auth_data), playcount, tagcount)) + list.append(LibraryItem(Artist(name, self.network), playcount, tagcount)) return list - def get_tracks_page(self): - """Retreives the next page of tracks in the Library. Returns a sequence of TopItem objects. - Use the function extract_items like extract_items(Library.get_tracks_page()) to return only a sequence of - Track objects with no extra data. - - Example: - ------- - library = Library("rj", API_KEY, API_SECRET, SESSION_KEY) - - while not library.is_end_of_tracks(): - print library.get_tracks_page() + def get_tracks(self, limit=50): + """ + Returns a sequence of Album objects + if limit==None it will return all (may take a while) """ - self._tracks_index += 1 - - params = self._get_params() - params["page"] = str(self._tracks_index) - list = [] - doc = self._request("library.getTracks", True, params) - for node in doc.getElementsByTagName("track"): + for node in _collect_nodes(limit, self, "library.getTracks", True): name = _extract(node, "name") artist = _extract(node, "name", 1) playcount = _number(_extract(node, "playcount")) tagcount = _number(_extract(node, "tagcount")) - list.append(LibraryItem(Track(artist, name, *self.auth_data), playcount, tagcount)) + list.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount)) return list @@ -1420,16 +1918,17 @@ class Library(_BaseObject): class Playlist(_BaseObject): """A Last.fm user playlist.""" - def __init__(self, user, id, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, user, id, network): + _BaseObject.__init__(self, network) if isinstance(user, User): self.user = user else: - self.user = User(user, *self.auth_data) + self.user = User(user, self.network) - self.id = unicode(id) + self.id = _unicode(id) + @_string_output def __repr__(self): return repr(self.user) + "'s playlist # " + repr(self.id) @@ -1460,7 +1959,7 @@ class Playlist(_BaseObject): uri = u'lastfm://playlist/%s' %self.get_id() - return XSPF(uri, *self.auth_data).get_tracks() + return XSPF(uri, self.network).get_tracks() def add_track(self, track): """Adds a Track to this Playlist.""" @@ -1513,19 +2012,22 @@ class Playlist(_BaseObject): return track in self.get_tracks() - def get_image_url(self, size = IMAGE_LARGE): - """Returns the associated image URL. - * size: The image size. Possible values: - o IMAGE_LARGE - o IMAGE_MEDIUM - o IMAGE_SMALL + 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(self._get_info_node(), "image")[size] def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the playlist on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the playlist on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -1539,12 +2041,12 @@ class Playlist(_BaseObject): o DOMAIN_JAPANESE o DOMAIN_CHINESE """ - url = "http://%(domain)s/user/%(user)s/library/playlists/%(appendix)s" + url = "user/%(user)s/library/playlists/%(appendix)s" english_url = _extract(self._get_info_node(), "url") appendix = english_url[english_url.rfind("/") + 1:] - return url %{'domain': domain_name, 'appendix': appendix, "user": self.get_user().get_name()} + return self.network._get_url(domain_name, "playlist") %{'appendix': appendix, "user": self.get_user().get_name()} class Tag(_BaseObject): @@ -1552,14 +2054,15 @@ class Tag(_BaseObject): # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it) - def __init__(self, name, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, name, network): + _BaseObject.__init__(self, network) self.name = name def _get_params(self): return {'tag': self.get_name()} + @_string_output def __repr__(self): return self.get_name() @@ -1582,7 +2085,7 @@ class Tag(_BaseObject): list = [] names = _extract_all(doc, 'name') for name in names: - list.append(Tag(name, *self.auth_data)) + list.append(Tag(name, self.network)) return list @@ -1598,7 +2101,7 @@ class Tag(_BaseObject): artist = _extract(node, "name", 1) playcount = _extract(node, "playcount") - list.append(TopItem(Album(artist, name, *self.auth_data), playcount)) + list.append(TopItem(Album(artist, name, self.network), playcount)) return list @@ -1614,7 +2117,7 @@ class Tag(_BaseObject): artist = _extract(track, "name", 1) playcount = _number(_extract(track, "playcount")) - list.append( TopItem(Track(artist, title, *self.auth_data), playcount) ) + list.append( TopItem(Track(artist, title, self.network), playcount) ) return list @@ -1628,7 +2131,7 @@ class Tag(_BaseObject): name = _extract(node, 'name') playcount = _extract(node, "playcount") - list.append(TopItem(Artist(name, *self.auth_data), playcount)) + list.append(TopItem(Artist(name, self.network), playcount)) return list @@ -1655,15 +2158,15 @@ class Tag(_BaseObject): list = [] for node in doc.getElementsByTagName("artist"): - item = Artist(_extract(node, "name"), *self.auth_data) + item = Artist(_extract(node, "name"), self.network) weight = _extract(node, "weight") list.append(TopItem(item, weight)) return list def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the tag page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the tag page on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -1678,26 +2181,25 @@ class Tag(_BaseObject): o DOMAIN_CHINESE """ - url = 'http://%(domain)s/tag/%(name)s' + name = _url_safe(self.get_name()) - name = _get_url_safe(self.get_name()) - - return url %{'domain': domain_name, 'name': name} + return self.network._get_url(domain_name, "tag") %{'name': name} class Track(_BaseObject, _Taggable): """A Last.fm track.""" - def __init__(self, artist, title, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, artist, title, network): + _BaseObject.__init__(self, network) _Taggable.__init__(self, 'track') if isinstance(artist, Artist): self.artist = artist else: - self.artist = Artist(artist, *self.auth_data) + self.artist = Artist(artist, self.network) self.title = title + @_string_output def __repr__(self): return self.get_artist().get_name() + ' - ' + self.get_title() @@ -1726,7 +2228,7 @@ class Track(_BaseObject, _Taggable): return self.get_title() def get_id(self): - """Returns the track id on Last.fm.""" + """Returns the track id on the network.""" doc = self._request("track.getInfo", True) @@ -1782,7 +2284,7 @@ class Track(_BaseObject, _Taggable): return node = doc.getElementsByTagName("album")[0] - return Album(_extract(node, "artist"), _extract(node, "title"), *self.auth_data) + return Album(_extract(node, "artist"), _extract(node, "title"), self.network) def get_wiki_published_date(self): """Returns the date of publishing this version of the wiki.""" @@ -1831,7 +2333,7 @@ class Track(_BaseObject, _Taggable): self._request('track.ban') def get_similar(self): - """Returns similar tracks for this track on Last.fm, based on listening data. """ + """Returns similar tracks for this track on the network, based on listening data. """ doc = self._request('track.getSimilar', True) @@ -1840,7 +2342,7 @@ class Track(_BaseObject, _Taggable): title = _extract(node, 'name') artist = _extract(node, 'name', 1) - list.append(Track(artist, title, *self.auth_data)) + list.append(Track(artist, title, self.network)) return list @@ -1860,7 +2362,7 @@ class Track(_BaseObject, _Taggable): name = _extract(element, 'name') weight = _number(_extract(element, 'weight')) - list.append(TopItem(User(name, *self.auth_data), weight)) + list.append(TopItem(User(name, self.network), weight)) return list @@ -1886,13 +2388,13 @@ class Track(_BaseObject, _Taggable): params = self._get_params() recipients = ','.join(nusers) params['recipient'] = recipients - if message: params['message'] = unicode(message) + if message: params['message'] = _unicode(message) self._request('track.share', False, params) def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the track page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the track page on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -1906,21 +2408,46 @@ class Track(_BaseObject, _Taggable): o DOMAIN_JAPANESE o DOMAIN_CHINESE """ - url = 'http://%(domain)s/music/%(artist)s/_/%(title)s' - artist = _get_url_safe(self.get_artist().get_name()) - title = _get_url_safe(self.get_title()) + artist = _url_safe(self.get_artist().get_name()) + title = _url_safe(self.get_title()) - return url %{'domain': domain_name, 'artist': artist, 'title': title} + return self.network._get_url(domain_name, "track") %{'domain': self.network._get_language_domain(domain_name), 'artist': artist, 'title': title} + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "track.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("track.Shout", False, params) + class Group(_BaseObject): """A Last.fm group.""" - def __init__(self, group_name, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, group_name, network): + _BaseObject.__init__(self, network) self.name = group_name + @_string_output def __repr__(self): return self.get_name() @@ -1960,7 +2487,7 @@ class Group(_BaseObject): list = [] for node in doc.getElementsByTagName("artist"): - item = Artist(_extract(node, "name"), *self.auth_data) + item = Artist(_extract(node, "name"), self.network) weight = _extract(node, "playcount") list.append(TopItem(item, weight)) @@ -1978,7 +2505,7 @@ class Group(_BaseObject): list = [] for node in doc.getElementsByTagName("album"): - item = Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) + item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) weight = _extract(node, "playcount") list.append(TopItem(item, weight)) @@ -1996,15 +2523,15 @@ class Group(_BaseObject): list = [] for node in doc.getElementsByTagName("track"): - item = Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) + item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) weight = _extract(node, "playcount") list.append(TopItem(item, weight)) return list def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the group page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the group page on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -2019,23 +2546,37 @@ class Group(_BaseObject): o DOMAIN_CHINESE """ - url = 'http://%(domain)s/group/%(name)s' + name = _url_safe(self.get_name()) - name = _get_url_safe(self.get_name()) + return self.network._get_url(domain_name, "group") %{'name': name} + + def get_members(self, limit=50): + """ + Returns a sequence of User objects + if limit==None it will return all + """ - return url %{'domain': domain_name, 'name': name} + nodes = _collect_nodes(limit, self, "group.getMembers", False) + + users = [] + + for node in nodes: + users.append(User(_extract(node, "name"), self.network)) + + return users class XSPF(_BaseObject): "A Last.fm XSPF playlist.""" - def __init__(self, uri, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, uri, network): + _BaseObject.__init__(self, network) self.uri = uri def _get_params(self): return {'playlistURL': self.get_uri()} + @_string_output def __repr__(self): return self.get_uri() @@ -2060,15 +2601,15 @@ class XSPF(_BaseObject): title = _extract(n, 'title') artist = _extract(n, 'creator') - list.append(Track(artist, title, *self.auth_data)) + list.append(Track(artist, title, self.network)) return list class User(_BaseObject): """A Last.fm user.""" - def __init__(self, user_name, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, user_name, network): + _BaseObject.__init__(self, network) self.name = user_name @@ -2076,6 +2617,7 @@ class User(_BaseObject): self._recommended_events_index = 0 self._recommended_artists_index = 0 + @_string_output def __repr__(self): return self.get_name() @@ -2102,47 +2644,39 @@ class User(_BaseObject): events = [] for id in ids: - events.append(Event(id, *self.auth_data)) + events.append(Event(id, self.network)) return events - def get_friends(self, limit = None): + def get_friends(self, limit = 50): """Returns a list of the user's friends. """ - params = self._get_params() - if limit: - params['limit'] = unicode(limit) - - doc = self._request('user.getFriends', True, params) - - names = _extract_all(doc, 'name') - list = [] - for name in names: - list.append(User(name, *self.auth_data)) + for node in _collect_nodes(limit, self, "user.getFriends", False): + list.append(User(_extract(node, "name"), self)) return list - def get_loved_tracks(self, limit=50, page=1): - """Returns the loved tracks by this user """ + def get_loved_tracks(self, limit=50): + """Returns the loved tracks by this user + if limit is None, it will return all of them + """ - doc = self._request('user.getLovedTracks', True, {"limit": str(limit), "page": str(page)}) - - list = [] - for track in doc.getElementsByTagName('track'): + tracks = [] + for track in _collect_nodes(limit, self, "user.getLovedTracks", False): title = _extract(track, 'name', 0) artist = _extract(track, 'name', 1) - list.append(Track(artist, title, *self.auth_data)) + tracks.append(Track(artist, title, self.network)) - return list + return tracks - def get_neighbours(self, limit = None): + def get_neighbours(self, limit = 50): """Returns a list of the user's friends.""" params = self._get_params() if limit: - params['limit'] = unicode(limit) + params['limit'] = _unicode(limit) doc = self._request('user.getNeighbours', True, params) @@ -2150,45 +2684,20 @@ class User(_BaseObject): names = _extract_all(doc, 'name') for name in names: - list.append(User(name, *self.auth_data)) + list.append(User(name, self.network)) return list - - def _get_past_events_pagecount(self): - """Returns the number of pages in the past events.""" - - params = self._get_params() - params["page"] = str(self._past_events_index) - doc = self._request("user.getPastEvents", True, params) - - return _number(doc.getElementsByTagName("events")[0].getAttribute("totalPages")) - - def is_end_of_past_events(self): - """Returns True if the end of Past Events was reached.""" - - return self._past_events_index >= self._get_past_events_pagecount() - - def get_past_events_page(self, ): - """Retruns a paginated list of all events a user has attended in the past. - - Example: - -------- - - while not user.is_end_of_past_events(): - print user.get_past_events_page() + def get_past_events(self, limit=50): + """ + Returns a sequence of Event objects + if limit==None it will return all """ - self._past_events_index += 1 - params = self._get_params() - params["page"] = str(self._past_events_index) - - doc = self._request('user.getPastEvents', True, params) - list = [] - for id in _extract_all(doc, 'id'): - list.append(Event(id, *self.auth_data)) - + for n in _collect_nodes(limit, self, "user.getPastEvents", False): + list.append(Event(_exctract(n, "id"), self.network)) + return list def get_playlists(self): @@ -2198,7 +2707,7 @@ class User(_BaseObject): playlists = [] for id in _extract_all(doc, "id"): - playlists.append(Playlist(self.get_name(), id, *self.auth_data)) + playlists.append(Playlist(self.get_name(), id, self.network)) return playlists @@ -2220,7 +2729,7 @@ class User(_BaseObject): artist = _extract(e, 'artist') title = _extract(e, 'name') - return Track(artist, title, *self.auth_data) + return Track(artist, title, self.network) def get_recent_tracks(self, limit = None): @@ -2231,7 +2740,7 @@ class User(_BaseObject): params = self._get_params() if limit: - params['limit'] = unicode(limit) + params['limit'] = _unicode(limit) doc = self._request('user.getRecentTracks', False, params) @@ -2245,7 +2754,7 @@ class User(_BaseObject): if track.hasAttribute('nowplaying'): continue #to prevent the now playing track from sneaking in here - list.append(PlayedTrack(Track(artist, title, *self.auth_data), date, timestamp)) + list.append(PlayedTrack(Track(artist, title, self.network), date, timestamp)) return list @@ -2269,7 +2778,7 @@ class User(_BaseObject): artist = _extract(album, 'name', 1) playcount = _extract(album, "playcount") - list.append(TopItem(Album(artist, name, *self.auth_data), playcount)) + list.append(TopItem(Album(artist, name, self.network), playcount)) return list @@ -2292,7 +2801,7 @@ class User(_BaseObject): name = _extract(node, 'name') playcount = _extract(node, "playcount") - list.append(TopItem(Artist(name, *self.auth_data), playcount)) + list.append(TopItem(Artist(name, self.network), playcount)) return list @@ -2305,7 +2814,7 @@ class User(_BaseObject): list = [] for node in doc.getElementsByTagName("tag"): - list.append(TopItem(Tag(_extract(node, "name"), *self.auth_data), _extract(node, "count"))) + list.append(TopItem(Tag(_extract(node, "name"), self.network), _extract(node, "count"))) return list @@ -2329,7 +2838,7 @@ class User(_BaseObject): artist = _extract(track, 'name', 1) playcount = _extract(track, "playcount") - list.append(TopItem(Track(artist, name, *self.auth_data), playcount)) + list.append(TopItem(Track(artist, name, self.network), playcount)) return list @@ -2356,7 +2865,7 @@ class User(_BaseObject): list = [] for node in doc.getElementsByTagName("artist"): - item = Artist(_extract(node, "name"), *self.auth_data) + item = Artist(_extract(node, "name"), self.network) weight = _extract(node, "playcount") list.append(TopItem(item, weight)) @@ -2374,7 +2883,7 @@ class User(_BaseObject): list = [] for node in doc.getElementsByTagName("album"): - item = Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) + item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) weight = _extract(node, "playcount") list.append(TopItem(item, weight)) @@ -2392,7 +2901,7 @@ class User(_BaseObject): list = [] for node in doc.getElementsByTagName("track"): - item = Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data) + item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) weight = _extract(node, "playcount") list.append(TopItem(item, weight)) @@ -2409,13 +2918,13 @@ class User(_BaseObject): params = self._get_params() if shared_artists_limit: - params['limit'] = unicode(shared_artists_limit) + params['limit'] = _unicode(shared_artists_limit) params['type1'] = 'user' params['type2'] = 'user' params['value1'] = self.get_name() params['value2'] = user - doc = _Request('tasteometer.compare', params, *self.auth_data).execute() + doc = self._request('tasteometer.compare', False, params) score = _extract(doc, 'score') @@ -2425,11 +2934,11 @@ class User(_BaseObject): shared_artists_list = [] for name in shared_artists_names: - shared_artists_list.append(Artist(name, *self.auth_data)) + shared_artists_list.append(Artist(name, self.network)) return (score, shared_artists_list) - def getRecommendedEvents(self, page = None, limit = None): + def get_recommended_events(self, page = None, limit = None): """Returns a paginated list of all events recommended to a user by Last.fm, based on their listening profile. * page: The page number of results to return. * limit: The limit of events to return. @@ -2437,22 +2946,22 @@ class User(_BaseObject): params = self._get_params() if page: - params['page'] = unicode(page) + params['page'] = _unicode(page) if limit: - params['limit'] = unicode(limit) + params['limit'] = _unicode(limit) - doc = _Request('user.getRecommendedEvents', params, *self.auth_data).execute() + doc = self._request('user.getRecommendedEvents', False, params) ids = _extract_all(doc, 'id') list = [] for id in ids: - list.append(Event(id, *self.auth_data)) + list.append(Event(id, self.network)) return list def get_url(self, domain_name = DOMAIN_ENGLISH): - """Returns the url of the user page on Last.fm. - * domain_name: Last.fm's language domain. Possible values: + """Returns the url of the user page on the network. + * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN o DOMAIN_SPANISH @@ -2466,28 +2975,52 @@ class User(_BaseObject): o DOMAIN_JAPANESE o DOMAIN_CHINESE """ - url = 'http://%(domain)s/user/%(name)s' - name = _get_url_safe(self.get_name()) + name = _url_safe(self.get_name()) - return url %{'domain': domain_name, 'name': name} + return self.network._get_url(domain_name, "user") %{'name': name} def get_library(self): """Returns the associated Library object. """ - return Library(self, *self.auth_data) + return Library(self, self.network) + + def get_shouts(self, limit=50): + """ + Returns a sequqence of Shout objects + """ + + shouts = [] + for node in _collect_nodes(limit, self, "user.getShouts", False): + shouts.append(Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) + return shouts + + def shout(self, message): + """ + Post a shout + """ + + params = self._get_params() + params["message"] = message + + self._request("user.Shout", False, params) class AuthenticatedUser(User): - def __init__(self, api_key, api_secret, session_key=""): - User.__init__(self, "", api_key, api_secret, session_key); + def __init__(self, network): + User.__init__(self, "", network); def _get_params(self): - return {} + return {"user": self.get_name()} def get_name(self): """Returns the name of the authenticated user.""" - doc = self._request("user.getInfo", True) + doc = self._request("user.getInfo", True, {"user": ""}) # hack self.name = _extract(doc, "name") return self.name @@ -2499,7 +3032,7 @@ class AuthenticatedUser(User): return _extract(doc, "id") - def get_image_url(self): + def get_cover_image(self): """Returns the user's avatar.""" doc = self._request("user.getInfo", True) @@ -2518,7 +3051,7 @@ class AuthenticatedUser(User): doc = self._request("user.getInfo", True) - return Country(_extract(doc, "country"), *self.auth_data) + return Country(_extract(doc, "country"), self.network) def get_age(self): """Returns the user's age.""" @@ -2555,85 +3088,35 @@ class AuthenticatedUser(User): return _number(_extract(doc, "playcount")) - def _get_recommended_events_pagecount(self): - """Returns the number of pages in the past events.""" - - params = self._get_params() - params["page"] = str(self._recommended_events_index) - doc = self._request("user.getRecommendedEvents", True, params) - - return _number(doc.getElementsByTagName("events")[0].getAttribute("totalPages")) - - def is_end_of_recommended_events(self): - """Returns True if the end of Past Events was reached.""" - - return self._recommended_events_index >= self._get_recommended_events_pagecount() - - def get_recommended_events_page(self, ): - """Retruns a paginated list of all events a user has attended in the past. - - Example: - -------- - - while not user.is_end_of_recommended_events(): - print user.get_recommended_events_page() - + def get_recommended_events(self, limit=50): + """ + Returns a sequence of Event objects + if limit==None it will return all """ - self._recommended_events_index += 1 - params = self._get_params() - params["page"] = str(self._recommended_events_index) - - doc = self._request('user.getRecommendedEvents', True, params) - list = [] - for id in _extract_all(doc, 'id'): - list.append(Event(id, *self.auth_data)) + for node in _collect_nodes(limit, self, "user.getRecommendedEvents", False): + list.append(Event(_extract(node, "id"), self.network)) return list - - def _get_recommended_artists_pagecount(self): - """Returns the number of pages in the past artists.""" - - params = self._get_params() - params["page"] = str(self._recommended_artists_index) - doc = self._request("user.getRecommendedArtists", True, params) - - return _number(doc.getElementsByTagName("recommendations")[0].getAttribute("totalPages")) - - def is_end_of_recommended_artists(self): - """Returns True if the end of Past Artists was reached.""" - - return self._recommended_artists_index >= self._get_recommended_artists_pagecount() - - def get_recommended_artists_page(self, ): - """Retruns a paginated list of all artists a user has attended in the past. - - Example: - -------- - - while not user.is_end_of_recommended_artists(): - print user.get_recommended_artists_page() + def get_recommended_artists(self, limit=50): + """ + Returns a sequence of Event objects + if limit==None it will return all """ - self._recommended_artists_index += 1 - params = self._get_params() - params["page"] = str(self._recommended_artists_index) - - doc = self._request('user.getRecommendedArtists', True, params) - list = [] - for name in _extract_all(doc, 'name'): - list.append(Artist(name, *self.auth_data)) + for node in _collect_nodes(limit, self, "user.getRecommendedArtists", False): + list.append(Artist(_extract(node, "name"), self.network)) return list class _Search(_BaseObject): """An abstract class. Use one of its derivatives.""" - def __init__(self, ws_prefix, search_terms, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, ws_prefix, search_terms, network): + _BaseObject.__init__(self, network) self._ws_prefix = ws_prefix self.search_terms = search_terms @@ -2671,9 +3154,9 @@ class _Search(_BaseObject): class AlbumSearch(_Search): """Search for an album by name.""" - def __init__(self, album_name, api_key, api_secret, session_key=""): + def __init__(self, album_name, network): - _Search.__init__(self, "album", {"album": album_name}, api_key, api_secret, session_key) + _Search.__init__(self, "album", {"album": album_name}, network) def get_next_page(self): """Returns the next page of results as a sequence of Album objects.""" @@ -2682,15 +3165,15 @@ class AlbumSearch(_Search): list = [] for node in master_node.getElementsByTagName("album"): - list.append(Album(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)) + list.append(Album(_extract(node, "artist"), _extract(node, "name"), self.network)) return list class ArtistSearch(_Search): """Search for an artist by artist name.""" - def __init__(self, artist_name, api_key, api_secret, session_key=""): - _Search.__init__(self, "artist", {"artist": artist_name}, api_key, api_secret, session_key) + def __init__(self, artist_name, network): + _Search.__init__(self, "artist", {"artist": artist_name}, network) def get_next_page(self): """Returns the next page of results as a sequence of Artist objects.""" @@ -2699,16 +3182,16 @@ class ArtistSearch(_Search): list = [] for node in master_node.getElementsByTagName("artist"): - list.append(Artist(_extract(node, "name"), *self.auth_data)) + list.append(Artist(_extract(node, "name"), self.network)) return list class TagSearch(_Search): """Search for a tag by tag name.""" - def __init__(self, tag_name, api_key, api_secret, session_key=""): + def __init__(self, tag_name, network): - _Search.__init__(self, "tag", {"tag": tag_name}, api_key, api_secret, session_key) + _Search.__init__(self, "tag", {"tag": tag_name}, network) def get_next_page(self): """Returns the next page of results as a sequence of Tag objects.""" @@ -2717,7 +3200,7 @@ class TagSearch(_Search): list = [] for node in master_node.getElementsByTagName("tag"): - list.append(Tag(_extract(node, "name"), *self.auth_data)) + list.append(Tag(_extract(node, "name"), self.network)) return list @@ -2725,9 +3208,9 @@ class TrackSearch(_Search): """Search for a track by track title. If you don't wanna narrow the results down by specifying the artist name, set it to empty string.""" - def __init__(self, artist_name, track_title, api_key, api_secret, session_key=""): + def __init__(self, artist_name, track_title, network): - _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, api_key, api_secret, session_key) + _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, network) def get_next_page(self): """Returns the next page of results as a sequence of Track objects.""" @@ -2736,7 +3219,7 @@ class TrackSearch(_Search): list = [] for node in master_node.getElementsByTagName("track"): - list.append(Track(_extract(node, "artist"), _extract(node, "name"), *self.auth_data)) + list.append(Track(_extract(node, "artist"), _extract(node, "name"), self.network)) return list @@ -2744,9 +3227,9 @@ class VenueSearch(_Search): """Search for a venue by its name. If you don't wanna narrow the results down by specifying a country, set it to empty string.""" - def __init__(self, venue_name, country_name, api_key, api_secret, session_key=""): + def __init__(self, venue_name, country_name, network): - _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, api_key, api_secret, session_key) + _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, network) def get_next_page(self): """Returns the next page of results as a sequence of Track objects.""" @@ -2755,7 +3238,7 @@ class VenueSearch(_Search): list = [] for node in master_node.getElementsByTagName("venue"): - list.append(Venue(_extract(node, "id"), *self.auth_data)) + list.append(Venue(_extract(node, "id"), self.network)) return list @@ -2764,11 +3247,12 @@ class Venue(_BaseObject): # TODO: waiting for a venue.getInfo web service to use. - def __init__(self, id, api_key, api_secret, session_key=""): - _BaseObject.__init__(self, api_key, api_secret, session_key) + def __init__(self, id, network): + _BaseObject.__init__(self, network) self.id = _number(id) + @_string_output def __repr__(self): return "Venue #" + str(self.id) @@ -2790,7 +3274,7 @@ class Venue(_BaseObject): list = [] for node in doc.getElementsByTagName("event"): - list.append(Event(_extract(node, "id"), *self.auth_data)) + list.append(Event(_extract(node, "id"), self.network)) return list @@ -2801,70 +3285,18 @@ class Venue(_BaseObject): list = [] for node in doc.getElementsByTagName("event"): - list.append(Event(_extract(node, "id"), *self.auth_data)) + list.append(Event(_extract(node, "id"), self.network)) return list - -def create_new_playlist(title, description, api_key, api_secret, session_key=""): - """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 = dict() - params['title'] = unicode(title) - params['description'] = unicode(description) - - doc = _Request('playlist.create', params, api_key, api_secret, session_key).execute() - - id = doc.getElementsByTagName("id")[0].firstChild.data - user = doc.getElementsByTagName('playlists')[0].getAttribute('user') - - return Playlist(user, id, api_key, api_secret, session_key) - -def get_authenticated_user(api_key, api_secret, session_key=""): - """Returns the authenticated user.""" - - return AuthenticatedUser(api_key, api_secret, session_key) def md5(text): """Returns the md5 hash of a string.""" hash = hashlib.md5() - hash.update(text.encode("utf-8")) + hash.update(_string(text)) return hash.hexdigest() -def enable_proxy(host, port): - """Enable a default web proxy.""" - - global __proxy - global __proxy_enabled - - __proxy = [host, _number(port)] - __proxy_enabled = True - -def disable_proxy(): - """Disable using the web proxy.""" - - global __proxy_enabled - - __proxy_enabled = False - -def is_proxy_enabled(): - """Returns True if a web proxy is enabled.""" - - global __proxy_enabled - - return __proxy_enabled - -def _get_proxy(): - """Returns proxy details.""" - - global __proxy - - return __proxy - def async_call(sender, call, callback = None, call_args = None, callback_args = None): """This is the function for setting up an asynchronous operation. * call: The function to call asynchronously. @@ -2877,43 +3309,62 @@ def async_call(sender, call, callback = None, call_args = None, callback_args = thread = _ThreadedCall(sender, call, call_args, callback, callback_args) thread.start() -def enable_caching(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. +def _unicode(text): + if type(text) == unicode: + return text - * file_path: A file path for the backend storage file. If - None set, a temp file would probably be created, according the backend. + if type(text) == int: + return unicode(text) + + return unicode(text, "utf-8") + +def _string(text): + if type(text) == str: + return text + + if type(text) == int: + return str(text) + + return text.encode("utf-8") + +def _collect_nodes(limit, sender, method_name, cacheable, params=None): + """ + Returns a sequqnce of dom.Node objects about as close to + limit as possible """ - global __cache_backend + if not limit: limit = sys.maxint + if not params: params = sender._get_params() - if not file_path: - file_path = tempfile.mktemp(prefix="pylast_tmp_") + nodes = [] + page = 1 + end_of_pages = False - if "sqlite3" in sys.modules.keys(): - __cache_backend = _SqliteCacheBackend(file_path) - debug("Caching to Sqlite3 at " + file_path) - else: - __cache_backend = _ShelfCacheBackend(file_path) - debug("Caching to Shelf at " + file_path) + while len(nodes) < limit and not end_of_pages: + params["page"] = str(page) + doc = sender._request(method_name, cacheable, params) -def disable_caching(): - """Disables all caching features.""" - - global __cache_backend - __cache_backend = None - -def is_caching_enabled(): - """Returns True if caching is enabled.""" + main = doc.documentElement.childNodes[1] + + if main.hasAttribute("totalPages"): + total_pages = _number(main.getAttribute("totalPages")) + elif main.hasAttribute("totalpages"): + total_pages = _number(main.getAttribute("totalpages")) + else: + raise Exception("No total pages attribute") + + for node in main.childNodes: + if not node.nodeType == xml.dom.Node.TEXT_NODE and len(nodes) < limit: + nodes.append(node) + + print "total_pages", total_pages, "page", page + + if page >= total_pages: + end_of_pages = True + + page += 1 - global __cache_backend - return not (__cache_backend == None) - -def _get_cache_backend(): - - global __cache_backend - return __cache_backend + return nodes def _extract(node, name, index = 0): """Extracts a value from the xml string""" @@ -2922,7 +3373,7 @@ def _extract(node, name, index = 0): if len(nodes): if nodes[index].firstChild: - return nodes[index].firstChild.data.strip() + return _unescape_htmlentity(nodes[index].firstChild.data.strip()) else: return None @@ -2939,16 +3390,18 @@ def _extract_all(node, name, limit_count = None): return list -def _get_url_safe(text): +def _url_safe(text): """Does all kinds of tricks on a text to make it safe to use in a url.""" - if type(text) == type(unicode()): + if type(text) == unicode: text = text return urllib.quote_plus(urllib.quote_plus(text)).lower() def _number(string): - """Extracts an int from a string. Returns a 0 if None or an empty string was passed.""" + """ + Extracts an int from a string. Returns a 0 if None or an empty string was passed + """ if not string: return 0 @@ -2957,38 +3410,16 @@ def _number(string): else: return int(string) -def search_for_album(album_name, api_key, api_secret, session_key=""): - """Searches for an album by its name. Returns a AlbumSearch object. - Use get_next_page() to retreive sequences of results.""" +def _unescape_htmlentity(string): - return AlbumSearch(album_name, api_key, api_secret, session_key) - -def search_for_artist(artist_name, api_key, api_secret, session_key=""): - """Searches of an artist by its name. Returns a ArtistSearch object. - Use get_next_page() to retreive sequences of results.""" + string = _unicode(string) - return ArtistSearch(artist_name, api_key, api_secret, session_key) - -def search_for_tag(tag_name, api_key, api_secret, session_key=""): - """Searches of a tag by its name. Returns a TagSearch object. - Use get_next_page() to retreive sequences of results.""" + mapping = htmlentitydefs.name2codepoint + for key in mapping: + string = string.replace("&%s;" %key, unichr(mapping[key])) - return TagSearch(tag_name, api_key, api_secret, session_key) - -def search_for_track(artist_name, track_name, api_key, api_secret, session_key=""): - """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 string - return TrackSearch(artist_name, track_name, api_key, api_secret, session_key) - -def search_for_venue(venue_name, country_name, api_key, api_secret, session_key=""): - """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, api_key, api_secret, session_key) - def extract_items(topitems_or_libraryitems): """Extracts a sequence of items from a sequence of TopItem or LibraryItem objects.""" @@ -2998,85 +3429,30 @@ def extract_items(topitems_or_libraryitems): return list -def get_top_tags(api_key, api_secret, session_key=""): - """Returns a sequence of the most used Last.fm tags as a sequence of TopItem objects.""" - - doc = _Request("tag.getTopTags", dict(), api_key, api_secret, session_key).execute(True) - list = [] - for node in doc.getElementsByTagName("tag"): - tag = Tag(_extract(node, "name"), api_key, api_secret, session_key) - weight = _extract(node, "count") - - list.append(TopItem(tag, weight)) - - return list - -def get_track_by_mbid(mbid, api_key, api_secret, session_key=""): - """Looks up a track by its MusicBrainz ID.""" - - params = {"mbid": unicode(mbid)} - - doc = _Request("track.getInfo", params, api_key, api_secret, session_key).execute(True) - - return Track(_extract(doc, "name", 1), _extract(doc, "name"), api_key, api_secret, session_key) - -def get_artist_by_mbid(mbid, api_key, api_secret, session_key=""): - """Loooks up an artist by its MusicBrainz ID.""" - - params = {"mbid": unicode(mbid)} - - doc = _Request("artist.getInfo", params, api_key, api_secret, session_key).execute(True) - - return Artist(_extract(doc, "name"), api_key, api_secret, session_key) - -def get_album_by_mbid(mbid, api_key, api_secret, session_key=""): - """Looks up an album by its MusicBrainz ID.""" - - params = {"mbid": unicode(mbid)} - - doc = _Request("album.getInfo", params, api_key, api_secret, session_key).execute(True) - - return Album(_extract(doc, "artist"), _extract(doc, "name"), api_key, api_secret, session_key) - -def _delay_call(): - """Makes sure that web service calls are at least a second apart.""" - - global __last_call_time - - # delay time in seconds - DELAY_TIME = 1.0 - now = time.time() - - if (now - __last_call_time) < DELAY_TIME: - time.sleep(1) - - __last_call_time = now - -# ------------------------------------------------------------ - -class ScrobblingException(Exception): +class ScrobblingError(Exception): def __inint__(self, message): Exception.__init__(self) self.message = message - + + @_string_output def __repr__(self): return self.message -class BannedClient(ScrobblingException): +class BannedClientError(ScrobblingError): def __init__(self): - ScrobblingException.__init__(self, "This version of the client has been banned") + ScrobblingError.__init__(self, "This version of the client has been banned") -class BadAuthentication(ScrobblingException): +class BadAuthenticationError(ScrobblingError): def __init__(self): - ScrobblingException.__init__(self, "Bad authentication token") + ScrobblingError.__init__(self, "Bad authentication token") -class BadTime(ScrobblingException): +class BadTimeError(ScrobblingError): def __init__(self): - ScrobblingException.__init__(self, "Time provided is not close enough to current time") + ScrobblingError.__init__(self, "Time provided is not close enough to current time") -class BadSession(ScrobblingException): +class BadSessionError(ScrobblingError): def __init__(self): - ScrobblingException.__init__(self, "Bad session id, consider re-handshaking") + ScrobblingError.__init__(self, "Bad session id, consider re-handshaking") class _ScrobblerRequest(object): @@ -3123,16 +3499,16 @@ class _ScrobblerRequest(object): if status_line == "OK": return elif status_line == "BANNED": - raise BannedClient() + raise BannedClientError() elif status_line == "BADAUTH": - raise BadAuthentication() - elif status_line == "BADTIME": - raise BadTime() - elif status_line == "BADSESSION": - raise BadSession() + raise BadAuthenticationError() + elif status_line == "BadTimeError": + raise BadTimeError() + elif status_line == "BadSessionError": + raise BadSessionError() elif status_line.startswith("FAILED "): reason = status_line[status_line.find("FAILED ")+len("FAILED "):] - raise ScrobblingException(reason) + raise ScrobblingError(reason) class Scrobbler(object): """A class for scrobbling tracks to Last.fm""" @@ -3141,11 +3517,12 @@ class Scrobbler(object): nowplaying_url = None submissions_url = None - def __init__(self, client_id, client_version, username, md5_password): + def __init__(self, network, client_id, client_version): self.client_id = client_id self.client_version = client_version - self.username = username - self.password = md5_password + self.username = network.username + self.password = network.password_hash + self.network = network def _do_handshake(self): """Handshakes with the server""" @@ -3157,7 +3534,7 @@ class Scrobbler(object): "v": self.client_version, "u": self.username, "t": timestamp, "a": token} - global SUBMISSION_SERVER + SUBMISSION_SERVER = self.network.submission_server response = _ScrobblerRequest(SUBMISSION_SERVER, params).execute().split("\n") self.session_id = response[1] @@ -3183,7 +3560,7 @@ class Scrobbler(object): try: _ScrobblerRequest(self.nowplaying_url, params).execute() - except BadSession: + except BadSessionError: self._do_handshake() self.report_now_playing(artist, title, album, duration, track_number, mbid) @@ -3210,9 +3587,9 @@ class Scrobbler(object): mbid: MusicBrainz ID. """ - params = {"s": self._get_session_id(), "a[0]": artist.encode("utf-8"), "t[0]": title.encode("utf-8"), + params = {"s": self._get_session_id(), "a[0]": _string(artist), "t[0]": _string(title), "i[0]": str(time_started), "o[0]": source, "r[0]": mode, "l[0]": str(duration), - "b[0]": album.encode("utf-8"), "n[0]": track_number, "m[0]": mbid} + "b[0]": _string(album), "n[0]": track_number, "m[0]": mbid} response = _ScrobblerRequest(self.submissions_url, params).execute() info(artist + " - " + title + " was scrobbled") diff --git a/setup.py b/setup.py index 4aeebd4..2508012 100755 --- a/setup.py +++ b/setup.py @@ -2,13 +2,27 @@ from distutils.core import setup -import pylast +import os +def get_build(): + path = "./.build" + if os.path.exists(path): + fp = open(path, "r") + build = eval(fp.read())+1 + fp.close() + else: + build = 1 + + fp = open(path, "w") + fp.write(str(build)) + fp.close() + + return unicode(build) -setup(name = pylast.__name__, - version = pylast.__version__, - author = pylast.__author__, - description = pylast.__doc__, - author_email = pylast.__email__, +setup(name = "pylast", + version = "0.4." + get_build(), + author = "Amr Hassan ", + description = "A Python interface to Last.fm (and other API compatible social networks)", + author_email = "amr.hassan@gmail.com", url = "http://code.google.com/p/pylast/", py_modules = ("pylast",), license = "GPLv3"