From 908060736f9a5169366059c87162f92a7e4d97c8 Mon Sep 17 00:00:00 2001 From: Andrew Moss Date: Thu, 26 Dec 2013 23:11:23 +0000 Subject: [PATCH] Allow getting venue info through Event.get_venue() as a workaround until the Venue.getInfo API call is added to Last.fm. Changed version to 0.6 in setup.py and pylast.py to reflect update. --- pylast.py | 176 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 116 insertions(+), 60 deletions(-) diff --git a/pylast.py b/pylast.py index e2d6a64..fa961ad 100644 --- a/pylast.py +++ b/pylast.py @@ -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") @@ -353,7 +353,7 @@ class _Network(object): def enable_caching(self, file_path = None): """Enables caching request-wide for all cachable calls. - * 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 +435,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 +462,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 +578,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 +661,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 +788,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 +796,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 +864,7 @@ class SessionKeyGenerator(object): manually, unless you want to. """ - def __init__(self, network): + def __init__(self, network): self.network = network self.web_auth_tokens = {} @@ -1272,7 +1272,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 @@ -1537,10 +1537,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 +1565,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 +1579,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 +1675,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() @@ -1735,7 +1735,7 @@ class Event(_BaseObject): v = doc.getElementsByTagName("venue")[0] venue_id = _number(_extract(v, "id")) - return Venue(venue_id, self.network) + return Venue(venue_id, self.network, venue_element=v) def get_start_date(self): """Returns the date when the event starts.""" @@ -1781,7 +1781,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 +1794,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 +1919,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 +1932,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 +2173,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 +2186,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 +2319,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 +2332,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()) @@ -2548,9 +2548,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 +2575,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 +2588,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 +2707,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 +2720,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,13 +2832,13 @@ 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 @@ -2848,14 +2848,14 @@ class User(_BaseObject): 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)) @@ -3037,13 +3037,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 +3062,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 +3086,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 +3102,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 +3230,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 +3243,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()) @@ -3459,13 +3459,25 @@ class Venue(_BaseObject): """A venue where events are held.""" # TODO: waiting for a venue.getInfo web service to use. + # TODO: As an intermediate use case, can pass the venue DOM element when using + # Event.get_venue() to populate the venue info, if the venue.getInfo API + # call becomes available this workaround should be removed id = None + info = None + name = None + location = None + url = None - def __init__(self, id, network): + def __init__(self, id, network, venue_element=None): _BaseObject.__init__(self, network) self.id = _number(id) + if venue_element is not None: + self.info = _extract_element_tree(venue_element) + self.name = self.info.get('name') + self.url = self.info.get('url') + self.location = self.info.get('location') def __repr__(self): return "pylast.Venue(%s, %s)" %(repr(self.id), repr(self.network)) @@ -3485,6 +3497,21 @@ class Venue(_BaseObject): return self.id + def get_name(self): + """Returns the name of the venue.""" + + return self.name + + def get_url(self): + """Returns the URL of the venue page.""" + + return self.url + + def get_location(self): + """Returns the location of the venue (dictionary).""" + + return self.location + def get_upcoming_events(self): """Returns the upcoming events in this venue.""" @@ -3575,14 +3602,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)): nodes.append(node) - + if page >= total_pages: end_of_pages = True - + page += 1 return nodes @@ -3598,6 +3625,35 @@ def _extract(node, name, index = 0): else: return None +def _extract_element_tree(node, index = 0): + """Extract an element tree into a multi-level dictionary + + NB: If any elements have text nodes as well as nested + elements this will ignore the text nodes""" + + def _recurse_build_tree(rootNode, targetDict): + """Recursively build a multi-level dict""" + + def _has_child_elements(rootNode): + """Check if an element has any nested (child) elements""" + + for node in rootNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + return True + return False + + for node in rootNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + if _has_child_elements(node): + targetDict[node.tagName] = {} + _recurse_build_tree(node, targetDict[node.tagName]) + else: + targetDict[node.tagName] = _unescape_htmlentity(node.firstChild.data.strip()) + + return targetDict + + return _recurse_build_tree(node, {}) + def _extract_all(node, name, limit_count = None): """Extracts all the values from the xml string. returning a list.""" @@ -3633,7 +3689,7 @@ def _number(string): def _unescape_htmlentity(string): - #string = _unicode(string) + #string = _unicode(string) mapping = htmlentitydefs.name2codepoint for key in mapping: