works with CDATA tags

This commit is contained in:
Matt Jeffery 2013-03-01 19:38:41 +00:00
parent 78aac5c437
commit e2cc69a7e8
7 changed files with 116 additions and 130 deletions

2
.build
View file

@ -1 +1 @@
14
11

2
AUTHORS Normal file
View file

@ -0,0 +1,2 @@
Amr Hassan <amr.hassan@gmail.com>
Lukas Lipka <lukaslipka@gmail.com>

View file

@ -1,6 +0,0 @@
include pylast.py
include setup.py
include README
include COPYING
include INSTALL
include .build

10
PKG-INFO Normal file
View file

@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: pylast
Version: 0.5.11
Summary: A Python interface to Last.fm (and other API compatible social networks)
Home-page: http://code.google.com/p/pylast/
Author: Amr Hassan <amr.hassan@gmail.com>
Author-email: amr.hassan@gmail.com
License: Apache2
Description: UNKNOWN
Platform: UNKNOWN

4
README
View file

@ -3,5 +3,5 @@ pylast
A python interface to Last.fm. Try using the pydoc utility for help
on usage.
Original code can be found at: http://code.google.com/p/pylast/
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/

213
pylast.py
View file

@ -47,7 +47,7 @@ if sys.version_info[0] == 3:
elif sys.version_info[0] == 2:
from httplib import HTTPConnection
import htmlentitydefs
import htmlentitydefs
from urllib import splithost as url_split_host
from urllib import quote_plus as url_quote_plus
@ -143,7 +143,7 @@ class _Network(object):
"""
self.name = name
self.homepage = homepage
self.homepage = homepage
self.ws_server = ws_server
self.api_key = api_key
self.api_secret = api_secret
@ -241,13 +241,13 @@ class _Network(object):
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
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
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.
@ -257,7 +257,7 @@ class _Network(object):
* Last.fm: submissions@last.fm
* # TODO: list others
...and provide us with the name of your client and its homepage address.
...and provide us with the name of your client and its homepage address.
"""
_deprecation_warning("Use _Network.scrobble(...), _Network.scrobble_many(...), and Netowrk.update_now_playing(...) instead")
@ -313,6 +313,37 @@ class _Network(object):
return Playlist(user, e_id, self)
def get_top_artists(self, limit=None):
"""Returns a sequence of the most played artists."""
doc = _Request(self, "chart.getTopArtists").execute(True)
seq = []
for node in doc.getElementsByTagName("artist"):
title = _extract(node, "name")
artist = Artist(title, self)
seq.append(artist)
if limit:
seq = seq[:limit]
return seq
def get_top_tracks(self, limit=None):
"""Returns a sequence of the most played tracks."""
doc = _Request(self, "chart.getTopTracks").execute(True)
seq = []
for node in doc.getElementsByTagName("track"):
title = _extract(node, "name")
artist = _extract(node, "name", 1)
track = Track(artist, title, self)
seq.append(track)
if limit:
seq = seq[:limit]
return seq
def get_top_tags(self, limit=None):
"""Returns a sequence of the most used tags as a sequence of TopItem objects."""
@ -352,8 +383,10 @@ class _Network(object):
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
* file_path: A file path for the backend storage file. If
None set, a temp file would probably be created, according the backend.
"""
@ -435,10 +468,10 @@ class _Network(object):
return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
def update_now_playing(self, artist, title, album = None, album_artist = None,
def update_now_playing(self, artist, title, album = None, album_artist = None,
duration = None, track_number = None, mbid = None, context = None):
"""
Used to notify Last.fm that a user has started listening to a track.
Used to notify Last.fm that a user has started listening to a track.
Parameters:
artist (Required) : The artist name
@ -462,7 +495,7 @@ class _Network(object):
_Request(self, "track.updateNowPlaying", params).execute()
def scrobble(self, artist, title, timestamp, album = None, album_artist = None, track_number = None,
def scrobble(self, artist, title, timestamp, album = None, album_artist = None, track_number = None,
duration = None, stream_id = None, context = None, mbid = None):
"""Used to add a track-play to a user's profile.
@ -578,7 +611,7 @@ class LastFMNetwork(_Network):
)
def __repr__(self):
return "pylast.LastFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key,
return "pylast.LastFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key,
"'%s'" %self.username, "'%s'" %self.password_hash)))
def __str__(self):
@ -661,7 +694,7 @@ class LibreFMNetwork(_Network):
)
def __repr__(self):
return "pylast.LibreFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key,
return "pylast.LibreFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key,
"'%s'" %self.username, "'%s'" %self.password_hash)))
def __str__(self):
@ -788,7 +821,7 @@ class _Request(object):
"Content-type": "application/x-www-form-urlencoded",
'Accept-Charset': 'utf-8',
'User-Agent': "pylast" + '/' + __version__
}
}
(HOST_NAME, HOST_SUBDIR) = self.network.ws_server
@ -796,7 +829,7 @@ class _Request(object):
conn = HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1])
try:
conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
body=data, headers=headers)
except Exception as e:
raise NetworkError(self.network, e)
@ -864,7 +897,7 @@ class SessionKeyGenerator(object):
manually, unless you want to.
"""
def __init__(self, network):
def __init__(self, network):
self.network = network
self.web_auth_tokens = {}
@ -1150,9 +1183,8 @@ class Album(_BaseObject, _Taggable):
title = None
artist = None
username = None
def __init__(self, artist, title, network, username=None):
def __init__(self, artist, title, network):
"""
Create an album instance.
# Parameters:
@ -1169,7 +1201,6 @@ class Album(_BaseObject, _Taggable):
self.artist = Artist(artist, self.network)
self.title = title
self.username = username
def __repr__(self):
return "pylast.Album(%s, %s, %s)" %(repr(self.artist.name), repr(self.title), repr(self.network))
@ -1229,16 +1260,6 @@ class Album(_BaseObject, _Taggable):
return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount"))
def get_userplaycount(self):
"""Returns the number of plays by a given username"""
if not self.username: return
params = self._get_params()
params['username'] = self.username
return _number(_extract(self._request("album.getInfo", True, params), "userplaycount"))
def get_listener_count(self):
"""Returns the number of liteners on the network"""
@ -1272,7 +1293,7 @@ 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 the network.
"""Returns the url of the album page on the network.
# Parameters:
* domain_name str: The network's language domain. Possible values:
o DOMAIN_ENGLISH
@ -1334,9 +1355,8 @@ class Artist(_BaseObject, _Taggable):
"""An artist."""
name = None
username = None
def __init__(self, name, network, username=None):
def __init__(self, name, network):
"""Create an artist object.
# Parameters:
* name str: The artist's name.
@ -1346,7 +1366,6 @@ class Artist(_BaseObject, _Taggable):
_Taggable.__init__(self, 'artist')
self.name = name
self.username = username
def __repr__(self):
return "pylast.Artist(%s, %s)" %(repr(self.get_name()), repr(self.network))
@ -1392,16 +1411,6 @@ class Artist(_BaseObject, _Taggable):
return _number(_extract(self._request("artist.getInfo", True), "playcount"))
def get_userplaycount(self):
"""Returns the number of plays by a given username"""
if not self.username: return
params = self._get_params()
params['username'] = self.username
return _number(_extract(self._request("artist.getInfo", True, params), "userplaycount"))
def get_mbid(self):
"""Returns the MusicBrainz ID of this artist."""
@ -1537,10 +1546,10 @@ class Artist(_BaseObject, _Taggable):
return seq
def share(self, users, message = None):
"""Shares this artist (sends out recommendations).
"""Shares this artist (sends out recommendations).
# Parameters:
* users [User|str,]: A list that can contain usernames, emails, User objects, or all of them.
* message str: A message to include in the recommendation message.
* message str: A message to include in the recommendation message.
"""
#last.fm currently accepts a max of 10 recipient at a time
@ -1565,7 +1574,7 @@ class Artist(_BaseObject, _Taggable):
self._request('artist.share', False, params)
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the artist page on the network.
"""Returns the url of the artist page on the network.
# Parameters:
* domain_name: The network's language domain. Possible values:
o DOMAIN_ENGLISH
@ -1579,7 +1588,7 @@ class Artist(_BaseObject, _Taggable):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
artist = _url_safe(self.get_name())
@ -1675,7 +1684,7 @@ class Event(_BaseObject):
* attending_status: The attending status. Possible values:
o EVENT_ATTENDING
o EVENT_MAYBE_ATTENDING
o EVENT_NOT_ATTENDING
o EVENT_NOT_ATTENDING
"""
params = self._get_params()
@ -1781,7 +1790,7 @@ class Event(_BaseObject):
return _number(_extract(doc, "reviews"))
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the event page on the network.
"""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
@ -1794,15 +1803,15 @@ class Event(_BaseObject):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
return self.network._get_url(domain_name, "event") %{'id': self.get_id()}
def share(self, users, message = None):
"""Shares this event (sends out recommendations).
"""Shares this event (sends out recommendations).
* users: A list that can contain usernames, emails, User objects, or all of them.
* message: A message to include in the recommendation message.
* message: A message to include in the recommendation message.
"""
#last.fm currently accepts a max of 10 recipient at a time
@ -1919,7 +1928,7 @@ class Country(_BaseObject):
return seq
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the event page on the network.
"""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
@ -1932,7 +1941,7 @@ class Country(_BaseObject):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
country_name = _url_safe(self.get_name())
@ -2173,7 +2182,7 @@ class Playlist(_BaseObject):
return _extract(self._get_info_node(), "image")[size]
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the playlist on the network.
"""Returns the url of the playlist on the network.
* domain_name: The network's language domain. Possible values:
o DOMAIN_ENGLISH
o DOMAIN_GERMAN
@ -2186,7 +2195,7 @@ class Playlist(_BaseObject):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
english_url = _extract(self._get_info_node(), "url")
@ -2319,7 +2328,7 @@ class Tag(_BaseObject):
return seq
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the tag page on the network.
"""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
@ -2332,7 +2341,7 @@ class Tag(_BaseObject):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
name = _url_safe(self.get_name())
@ -2344,9 +2353,8 @@ class Track(_BaseObject, _Taggable):
artist = None
title = None
username = None
def __init__(self, artist, title, network, username=None):
def __init__(self, artist, title, network):
_BaseObject.__init__(self, network)
_Taggable.__init__(self, 'track')
@ -2357,8 +2365,6 @@ class Track(_BaseObject, _Taggable):
self.title = title
self.username = username
def __repr__(self):
return "pylast.Track(%s, %s, %s)" %(repr(self.artist.name), repr(self.title), repr(self.network))
@ -2430,17 +2436,6 @@ class Track(_BaseObject, _Taggable):
doc = self._request("track.getInfo", True)
return _number(_extract(doc, "playcount"))
def get_userplaycount(self):
"""Returns the number of plays by a given username"""
if not self.username: return
params = self._get_params()
params['username'] = self.username
doc = self._request("track.getInfo", True, params)
return _number(_extract(doc, "userplaycount"))
def is_streamable(self):
"""Returns True if the track is available at Last.fm."""
@ -2548,9 +2543,9 @@ class Track(_BaseObject, _Taggable):
return seq
def share(self, users, message = None):
"""Shares this track (sends out recommendations).
"""Shares this track (sends out recommendations).
* users: A list that can contain usernames, emails, User objects, or all of them.
* message: A message to include in the recommendation message.
* message: A message to include in the recommendation message.
"""
#last.fm currently accepts a max of 10 recipient at a time
@ -2575,7 +2570,7 @@ class Track(_BaseObject, _Taggable):
self._request('track.share', False, params)
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the track page on the network.
"""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
@ -2588,7 +2583,7 @@ class Track(_BaseObject, _Taggable):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
artist = _url_safe(self.get_artist().get_name())
@ -2707,7 +2702,7 @@ class Group(_BaseObject):
return seq
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the group page on the network.
"""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
@ -2720,7 +2715,7 @@ class Group(_BaseObject):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
name = _url_safe(self.get_name())
@ -2832,30 +2827,12 @@ class User(_BaseObject):
for e_id in ids:
events.append(Event(e_id, self.network))
return events
def get_artist_tracks(self, artist):
"""Get a list of tracks by a given artist scrobbled by this user, including scrobble time."""
# Not implemented: "Can be limited to specific timeranges, defaults to all time."
params = self._get_params()
params['artist'] = artist
seq = []
for track in _collect_nodes(None, self, "user.getArtistTracks", False, params):
title = _extract(track, "name")
artist = _extract(track, "artist")
date = _extract(track, "date")
timestamp = track.getElementsByTagName("date")[0].getAttribute("uts")
seq.append(PlayedTrack(Track(artist, title, self.network), date, timestamp))
return seq
def get_friends(self, limit = 50):
"""Returns a list of the user's friends. """
seq = []
for node in _collect_nodes(limit, self, "user.getFriends", False):
seq.append(User(_extract(node, "name"), self.network))
@ -2946,7 +2923,7 @@ class User(_BaseObject):
artist = _extract(e, 'artist')
title = _extract(e, 'name')
return Track(artist, title, self.network, self.name)
return Track(artist, title, self.network)
def get_recent_tracks(self, limit = 10):
@ -3037,13 +3014,13 @@ class User(_BaseObject):
return _number(_extract(doc, "playcount"))
def get_top_albums(self, period = PERIOD_OVERALL):
"""Returns the top albums played by a user.
"""Returns the top albums played by a user.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_7DAYS
o PERIOD_3MONTHS
o PERIOD_6MONTHS
o PERIOD_12MONTHS
o PERIOD_12MONTHS
"""
params = self._get_params()
@ -3062,13 +3039,13 @@ class User(_BaseObject):
return seq
def get_top_artists(self, period = PERIOD_OVERALL):
"""Returns the top artists played by a user.
"""Returns the top artists played by a user.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_7DAYS
o PERIOD_3MONTHS
o PERIOD_6MONTHS
o PERIOD_12MONTHS
o PERIOD_12MONTHS
"""
params = self._get_params()
@ -3086,8 +3063,8 @@ class User(_BaseObject):
return seq
def get_top_tags(self, limit=None):
"""Returns a sequence of the top tags used by this user with their counts as TopItem objects.
* limit: The limit of how many tags to return.
"""Returns a sequence of the top tags used by this user with their counts as TopItem objects.
* limit: The limit of how many tags to return.
"""
doc = self._request("user.getTopTags", True)
@ -3102,13 +3079,13 @@ class User(_BaseObject):
return seq
def get_top_tracks(self, period = PERIOD_OVERALL):
"""Returns the top tracks played by a user.
"""Returns the top tracks played by a user.
* period: The period of time. Possible values:
o PERIOD_OVERALL
o PERIOD_7DAYS
o PERIOD_3MONTHS
o PERIOD_6MONTHS
o PERIOD_12MONTHS
o PERIOD_12MONTHS
"""
params = self._get_params()
@ -3230,7 +3207,7 @@ class User(_BaseObject):
return _extract(doc, "image")
def get_url(self, domain_name = DOMAIN_ENGLISH):
"""Returns the url of the user page on the network.
"""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
@ -3243,7 +3220,7 @@ class User(_BaseObject):
o DOMAIN_TURKISH
o DOMAIN_RUSSIAN
o DOMAIN_JAPANESE
o DOMAIN_CHINESE
o DOMAIN_CHINESE
"""
name = _url_safe(self.get_name())
@ -3552,7 +3529,7 @@ def _string(text):
def _collect_nodes(limit, sender, method_name, cacheable, params=None):
"""
Returns a sequence of dom.Node objects about as close to
Returns a sequqnce of dom.Node objects about as close to
limit as possible
"""
@ -3575,14 +3552,14 @@ def _collect_nodes(limit, sender, method_name, cacheable, params=None):
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 (not limit or (len(nodes) < limit)):
if not node.nodeType == xml.dom.Node.TEXT_NODE and len(nodes) < limit:
nodes.append(node)
if page >= total_pages:
end_of_pages = True
page += 1
return nodes
@ -3594,7 +3571,7 @@ def _extract(node, name, index = 0):
if len(nodes):
if nodes[index].firstChild:
return _unescape_htmlentity(nodes[index].firstChild.data.strip())
return _unescape_htmlentity(nodes[index].firstChild.wholeText.strip())
else:
return None
@ -3633,7 +3610,7 @@ def _number(string):
def _unescape_htmlentity(string):
#string = _unicode(string)
#string = _unicode(string)
mapping = htmlentitydefs.name2codepoint
for key in mapping:

9
setup.py Executable file → Normal file
View file

@ -1,6 +1,9 @@
#!/usr/bin/env python
from distutils.core import setup
try:
from setuptools import setup
except:
from distutils.core import setup
import os
def get_build():
@ -22,11 +25,11 @@ def get_build():
return str(build)
setup(name = "pylast",
version = "0.1+0.5." + get_build(),
version = "0.5." + get_build(),
author = "Amr Hassan <amr.hassan@gmail.com>",
description = "A Python interface to Last.fm (and other API compatible social networks)",
author_email = "amr.hassan@gmail.com",
url = "https://github.com/Elizacat/",
url = "http://code.google.com/p/pylast/",
py_modules = ("pylast",),
license = "Apache2"
)