* Now plays nice with Python3. (Hopefully)

This commit is contained in:
Amr Hassan 2011-01-05 05:07:07 +00:00
parent b7129a564b
commit d0344f40d4
2 changed files with 79 additions and 252 deletions

2
.build
View file

@ -1 +1 @@
29 30

325
pylast.py
View file

@ -18,28 +18,34 @@
# #
# http://code.google.com/p/pylast/ # http://code.google.com/p/pylast/
__version__ = '0.4' __version__ = '0.5'
__author__ = 'Amr Hassan' __author__ = 'Amr Hassan'
__copyright__ = "Copyright (C) 2008-2009 Amr Hassan" __copyright__ = "Copyright (C) 2008-2010 Amr Hassan"
__license__ = "gpl" __license__ = "apache2"
__email__ = 'amr.hassan@gmail.com' __email__ = 'amr.hassan@gmail.com'
import hashlib import hashlib
import httplib
import urllib
import threading
from xml.dom import minidom from xml.dom import minidom
import xml.dom import xml.dom
import time import time
import shelve import shelve
import tempfile import tempfile
import sys import sys
import htmlentitydefs import collections
try: if sys.version_info.major == 3:
import collections from http.client import HTTPConnection
except ImportError: import html.entities as htmlentitydefs
pass from urllib.parse import splithost as url_split_host
from urllib.parse import quote_plus as url_quote_plus
unichr = chr
elif sys.version_info.major == 2:
from httplib import HTTPConnection
import htmlentitydefs
from urllib import splithost as url_split_host
from urllib import quote_plus as url_quote_plus
STATUS_INVALID_SERVICE = 2 STATUS_INVALID_SERVICE = 2
STATUS_INVALID_METHOD = 3 STATUS_INVALID_METHOD = 3
@ -103,145 +109,6 @@ SCROBBLE_MODE_LOVED = "L"
SCROBBLE_MODE_BANNED = "B" SCROBBLE_MODE_BANNED = "B"
SCROBBLE_MODE_SKIPPED = "S" 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): class Network(object):
""" """
A music social network website that is Last.fm or one exposing a Last.fm compatible API A music social network website that is Last.fm or one exposing a Last.fm compatible API
@ -258,7 +125,7 @@ class Network(object):
session_key: a generated session_key or None session_key: a generated session_key or None
submission_server: the url of the server to which tracks are submitted (scrobbled) submission_server: the url of the server to which tracks are submitted (scrobbled)
username: a username of a valid user username: a username of a valid user
password_hash: the output of pylast.md5(password) where password is the user's password thingy password_hash: the output of pylast.md5(password) where password is the user's password
domain_names: a dict mapping each DOMAIN_* value to a string domain name domain_names: a dict mapping each DOMAIN_* value to a string domain name
urls: a dict mapping types to urls urls: a dict mapping types to urls
@ -271,13 +138,13 @@ class Network(object):
of this class, unless you know what you're doing. of this class, unless you know what you're doing.
""" """
self.ws_server = ws_server
self.submission_server = submission_server
self.name = name self.name = name
self.homepage = homepage self.homepage = homepage
self.ws_server = ws_server
self.api_key = api_key self.api_key = api_key
self.api_secret = api_secret self.api_secret = api_secret
self.session_key = session_key self.session_key = session_key
self.submission_server = submission_server
self.username = username self.username = username
self.password_hash = password_hash self.password_hash = password_hash
self.domain_names = domain_names self.domain_names = domain_names
@ -293,6 +160,17 @@ class Network(object):
sk_gen = SessionKeyGenerator(self) sk_gen = SessionKeyGenerator(self)
self.session_key = sk_gen.get_session_key(self.username, self.password_hash) self.session_key = sk_gen.get_session_key(self.username, self.password_hash)
def __repr__(self):
attributes = ("name", "homepage", "ws_server", "api_key", "api_secret", "session_key", "submission_server",
"username", "password_hash", "domain_names", "urls")
text = "pylast.Network(%s)"
args = []
for attr in attributes:
args.append("=".join((attr, repr(getattr(self, attr)))))
return text % ", ".join(args)
def get_artist(self, artist_name): def get_artist(self, artist_name):
""" """
Return an Artist object Return an Artist object
@ -668,36 +546,6 @@ class _ShelfCacheBackend(object):
def has_key(self, key): def has_key(self, key):
return key in self.shelf.keys() return key in self.shelf.keys()
class _ThreadedCall(threading.Thread):
"""Facilitates calling a function on another thread."""
def __init__(self, sender, funct, funct_args, callback, callback_args):
threading.Thread.__init__(self)
self.funct = funct
self.funct_args = funct_args
self.callback = callback
self.callback_args = callback_args
self.sender = sender
def run(self):
output = []
if self.funct:
if self.funct_args:
output = self.funct(*self.funct_args)
else:
output = self.funct()
if self.callback:
if self.callback_args:
self.callback(self.sender, output, *self.callback_args)
else:
self.callback(self.sender, output)
class _Request(object): class _Request(object):
"""Representing an abstract web service operation.""" """Representing an abstract web service operation."""
@ -727,7 +575,7 @@ class _Request(object):
def _get_signature(self): def _get_signature(self):
"""Returns a 32-character hexadecimal md5 hash of the signature string.""" """Returns a 32-character hexadecimal md5 hash of the signature string."""
keys = self.params.keys()[:] keys = list(self.params.keys())
keys.sort() keys.sort()
@ -744,7 +592,7 @@ class _Request(object):
def _get_cache_key(self): def _get_cache_key(self):
"""The cache key is a string of concatenated sorted names and values.""" """The cache key is a string of concatenated sorted names and values."""
keys = self.params.keys() keys = list(self.params.keys())
keys.sort() keys.sort()
cache_key = str() cache_key = str()
@ -777,7 +625,7 @@ class _Request(object):
data = [] data = []
for name in self.params.keys(): for name in self.params.keys():
data.append('='.join((name, urllib.quote_plus(_string(self.params[name]))))) data.append('='.join((name, url_quote_plus(_string(self.params[name])))))
data = '&'.join(data) data = '&'.join(data)
headers = { headers = {
@ -789,11 +637,11 @@ class _Request(object):
(HOST_NAME, HOST_SUBDIR) = self.network.ws_server (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
if self.network.is_proxy_enabled(): if self.network.is_proxy_enabled():
conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1]) conn = HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR, conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
body=data, headers=headers) body=data, headers=headers)
else: else:
conn = httplib.HTTPConnection(host=HOST_NAME) conn = HTTPConnection(host=HOST_NAME)
conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers) conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers)
response = conn.getresponse() response = conn.getresponse()
@ -908,33 +756,14 @@ class SessionKeyGenerator(object):
return _extract(doc, "key") return _extract(doc, "key")
def _namedtuple(name, children): TopItem = collections.namedtuple("TopItem", ["item", "weight"])
""" SimilarItem = collections.namedtuple("SimilarItem", ["item", "match"])
collections.namedtuple is available in (python >= 2.6) LibraryItem = collections.namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
""" PlayedTrack = collections.namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
LovedTrack = collections.namedtuple("LovedTrack", ["track", "date", "timestamp"])
v = sys.version_info ImageSizes = collections.namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
if v[1] >= 6 and v[0] < 3: Image = collections.namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
return collections.namedtuple(name, children) Shout = collections.namedtuple("Shout", ["body", "author", "date"])
else:
def fancydict(*args):
d = {}
i = 0
for child in children:
d[child.strip()] = args[i]
i += 1
return d
return fancydict
TopItem = _namedtuple("TopItem", ["item", "weight"])
SimilarItem = _namedtuple("SimilarItem", ["item", "match"])
LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"])
PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"])
LovedTrack = _namedtuple("LovedTrack", ["track", "date", "timestamp"])
ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"])
Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"])
Shout = _namedtuple("Shout", ["body", "author", "date"])
def _string_output(funct): def _string_output(funct):
def r(*args): def r(*args):
@ -973,7 +802,7 @@ class _BaseObject(object):
def __hash__(self): def __hash__(self):
return hash(self.network) + \ return hash(self.network) + \
hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower()) hash(str(type(self)) + "".join(list(self._get_params().keys()) + list(self._get_params().values())).lower())
class _Taggable(object): class _Taggable(object):
"""Common functions for classes with tags.""" """Common functions for classes with tags."""
@ -1149,7 +978,7 @@ class Album(_BaseObject, _Taggable):
@_string_output @_string_output
def __repr__(self): def __repr__(self):
return u"%s - %s" %(self.get_artist().get_name(), self.get_title()) return _unicode("%s - %s") %(self.get_artist().get_name(), self.get_title())
def __eq__(self, other): 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()) return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower())
@ -2010,7 +1839,7 @@ class Playlist(_BaseObject):
def get_tracks(self): def get_tracks(self):
"""Returns a list of the tracks on this user playlist.""" """Returns a list of the tracks on this user playlist."""
uri = u'lastfm://playlist/%s' %self.get_id() uri = _unicode('lastfm://playlist/%s') %self.get_id()
return XSPF(uri, self.network).get_tracks() return XSPF(uri, self.network).get_tracks()
@ -3371,35 +3200,37 @@ def md5(text):
return h.hexdigest() return h.hexdigest()
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.
* callback: The function to call after the operation is complete, Its prototype has to be like:
callback(sender, output[, param1, param3, ... ])
* call_args: A sequence of args to be passed to call.
* callback_args: A sequence of args to be passed to callback.
"""
thread = _ThreadedCall(sender, call, call_args, callback, callback_args)
thread.start()
def _unicode(text): def _unicode(text):
if type(text) == unicode:
return text
if type(text) == int: if sys.version_info.major == 3:
return unicode(text) return str(text, "utf-8")
return unicode(text, "utf-8") elif sys.version_info.major ==2:
if type(text) == unicode:
return text
if type(text) == int:
return unicode(text)
return unicode(text, "utf-8")
def _string(text): def _string(text):
if type(text) == str: """For Python2 routines that can only process str type."""
return text
if type(text) == int: if sys.version_info.major == 3:
return str(text) if type(text) != str:
return str(text)
else:
return text
return text.encode("utf-8") elif sys.version_info.major == 2:
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): def _collect_nodes(limit, sender, method_name, cacheable, params=None):
""" """
@ -3407,7 +3238,6 @@ def _collect_nodes(limit, sender, method_name, cacheable, params=None):
limit as possible limit as possible
""" """
if not limit: limit = sys.maxint
if not params: params = sender._get_params() if not params: params = sender._get_params()
nodes = [] nodes = []
@ -3465,10 +3295,7 @@ def _extract_all(node, name, limit_count = None):
def _url_safe(text): def _url_safe(text):
"""Does all kinds of tricks on a text to make it safe to use in a url.""" """Does all kinds of tricks on a text to make it safe to use in a url."""
if type(text) == unicode: return url_quote_plus(url_quote_plus(_string(text))).lower()
text = text.encode('utf-8')
return urllib.quote_plus(urllib.quote_plus(text)).lower()
def _number(string): def _number(string):
""" """
@ -3487,7 +3314,7 @@ def _number(string):
def _unescape_htmlentity(string): def _unescape_htmlentity(string):
string = _unicode(string) #string = _unicode(string)
mapping = htmlentitydefs.name2codepoint mapping = htmlentitydefs.name2codepoint
for key in mapping: for key in mapping:
@ -3538,17 +3365,17 @@ class _ScrobblerRequest(object):
self.params = params self.params = params
self.type = type self.type = type
(self.hostname, self.subdir) = urllib.splithost(url[len("http:"):]) (self.hostname, self.subdir) = url_split_host(url[len("http:"):])
self.network = network self.network = network
def execute(self): def execute(self):
"""Returns a string response of this request.""" """Returns a string response of this request."""
connection = httplib.HTTPConnection(self.hostname) connection = HTTPConnection(self.hostname)
data = [] data = []
for name in self.params.keys(): for name in self.params.keys():
value = urllib.quote_plus(self.params[name]) value = url_quote_plus(self.params[name])
data.append('='.join((name, value))) data.append('='.join((name, value)))
data = "&".join(data) data = "&".join(data)