diff --git a/.build b/.build index 60d3b2f..b6a7d89 100644 --- a/.build +++ b/.build @@ -1 +1 @@ -15 +16 diff --git a/pylast.py b/pylast.py index f5c73da..595f617 100644 --- a/pylast.py +++ b/pylast.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # -# pylast - A Python interface to Last.fm (and other API compatible social networks) +# pylast - +# A Python interface to Last.fm (and other API compatible social networks) # # Copyright 2008-2010 Amr Hassan # @@ -35,6 +36,7 @@ import collections import warnings import re + def _deprecation_warning(message): warnings.warn(message, DeprecationWarning) @@ -115,41 +117,51 @@ SCROBBLE_MODE_BANNED = "B" SCROBBLE_MODE_SKIPPED = "S" # From http://boodebr.org/main/python/all-about-python-and-unicode#UNI_XML -RE_XML_ILLEGAL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \ - u'|' + \ - u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \ - (unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), - unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), - unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff)) +RE_XML_ILLEGAL = (u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + + u'|' + + u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' + % + (unichr(0xd800), unichr(0xdbff), unichr(0xdc00), + unichr(0xdfff), unichr(0xd800), unichr(0xdbff), + unichr(0xdc00), unichr(0xdfff), unichr(0xd800), + unichr(0xdbff), unichr(0xdc00), unichr(0xdfff))) XML_ILLEGAL = re.compile(RE_XML_ILLEGAL) + 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 such as Last.fm or + one with 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): + 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 + 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) + 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 - domain_names: a dict mapping each DOMAIN_* value to a string domain name - urls: a dict mapping types to urls + 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 + 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. + 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. + 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 + 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. """ @@ -171,14 +183,17 @@ class _Network(object): self.last_call_time = 0 self.limit_rate = False - #generate a session_key if necessary - if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash): + # Generate a session_key if necessary + if ((self.api_key and self.api_secret) and not self.session_key and + (self.username and self.password_hash)): sk_gen = SessionKeyGenerator(self) - self.session_key = sk_gen.get_session_key(self.username, self.password_hash) + 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") + attributes = ("name", "homepage", "ws_server", "api_key", "api_secret", + "session_key", "submission_server", "username", "password_hash", + "domain_names", "urls") text = "pylast._Network(%s)" args = [] @@ -189,7 +204,7 @@ class _Network(object): """ def __str__(self): - return "The %s Network" %self.name + return "The %s Network" % self.name def get_artist(self, artist_name): """ @@ -260,26 +275,31 @@ 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 - banned and do not have to reflect the version of the actual client application. + 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. + 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. + ...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") + _deprecation_warning( + "Use _Network.scrobble(...), _Network.scrobble_many(...)," + " and Network.update_now_playing(...) instead") return Scrobbler(self, client_id, client_version) @@ -292,7 +312,8 @@ class _Network(object): return self.domain_names[domain_language] def _get_url(self, domain, type): - return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type]) + return "http://%s/%s" % ( + self._get_language_domain(domain), self.urls[type]) def _get_ws_auth(self): """ @@ -385,17 +406,25 @@ class _Network(object): return seq - def get_geo_events(self, long=None, lat=None, location=None, distance=None, tag=None, festivalsonly=None, limit=None, cacheable=True): + def get_geo_events( + self, long=None, lat=None, location=None, distance=None, + tag=None, festivalsonly=None, limit=None, cacheable=True): """ Returns all events in a specific location by country or city name. Parameters: - long (Optional) : Specifies a longitude value to retrieve events for (service returns nearby events by default) - lat (Optional) : Specifies a latitude value to retrieve events for (service returns nearby events by default) - location (Optional) : Specifies a location to retrieve events for (service returns nearby events by default) - distance (Optional) : Find events within a specified radius (in kilometres) + long (Optional) : Specifies a longitude value to retrieve events for + (service returns nearby events by default) + lat (Optional) : Specifies a latitude value to retrieve events for + (service returns nearby events by default) + location (Optional) : Specifies a location to retrieve events for + (service returns nearby events by default) + distance (Optional) : Find events within a specified radius + (in kilometres) tag (Optional) : Specifies a tag to filter by. - festivalsonly[0|1] (Optional) : Whether only festivals should be returned, or all events. - limit (Optional) : The number of results to fetch per page. Defaults to 10. + festivalsonly[0|1] (Optional) : Whether only festivals should be + returned, or all events. + limit (Optional) : The number of results to fetch per page. + Defaults to 10. """ params = {} @@ -416,20 +445,26 @@ class _Network(object): return _extract_events_from_doc(doc, self) def get_metro_weekly_chart_dates(self, cacheable=True): - """Returns a list of From and To tuples for the available metro charts.""" + """ + Returns a list of From and To tuples for the available metro charts. + """ doc = _Request(self, "geo.getMetroWeeklyChartlist").execute(cacheable) seq = [] for node in doc.getElementsByTagName("chart"): - seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + seq.append((node.getAttribute("from"), node.getAttribute("to"))) return seq def get_metros(self, country=None, cacheable=True): - """Get a list of valid countries and metros for use in the other webservices. + """ + Get a list of valid countries and metros for use in the other + webservices. Parameters: - country (Optional) : Optionally restrict the results to those Metros from a particular country, as defined by the ISO 3166-1 country names standard + country (Optional) : Optionally restrict the results to those Metros + from a particular country, as defined by the ISO 3166-1 country + names standard. """ params = {} @@ -448,12 +483,13 @@ class _Network(object): return seq - def get_geo_top_artists(self, country, limit=None, cacheable=True): """Get the most popular artists on Last.fm by country. Parameters: - country (Required) : A country name, as defined by the ISO 3166-1 country names standard - limit (Optional) : The number of results to fetch per page. Defaults to 50. + country (Required) : A country name, as defined by the ISO 3166-1 + country names standard. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ params = {"country": country} @@ -472,12 +508,16 @@ class _Network(object): return seq - def get_geo_top_tracks(self, country, location=None, limit=None, cacheable=True): + def get_geo_top_tracks( + self, country, location=None, limit=None, cacheable=True): """Get the most popular tracks on Last.fm last week by country. Parameters: - country (Required) : A country name, as defined by the ISO 3166-1 country names standard - location (Optional) : A metro name, to fetch the charts for (must be within the country specified) - limit (Optional) : The number of results to fetch per page. Defaults to 50. + country (Required) : A country name, as defined by the ISO 3166-1 + country names standard + location (Optional) : A metro name, to fetch the charts for + (must be within the country specified) + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ params = {"country": country} @@ -531,10 +571,8 @@ class _Network(object): """Return True if web service calls are rate limited""" return self.limit_rate - 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. + def enable_caching(self, file_path=None): + """Enables caching request-wide for all cacheable calls. * file_path: A file path for the backend storage file. If None set, a temp file would probably be created, according the backend. @@ -578,14 +616,16 @@ class _Network(object): 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. + """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 retrieve 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. + """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 retrieve sequences of results.""" @@ -618,20 +658,24 @@ class _Network(object): return Album(_extract(doc, "artist"), _extract(doc, "name"), self) - def update_now_playing(self, artist, title, album = None, album_artist = None, - duration = None, track_number = None, mbid = None, context = 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 title (Required) : The track title album (Optional) : The album name. - album_artist (Optional) : The album artist - if this differs from the track artist. + album_artist (Optional) : The album artist - if this differs + from the track artist. duration (Optional) : The length of the track in seconds. - track_number (Optional) : The track number of the track on the album. + track_number (Optional) : The track number of the track on the + album. mbid (Optional) : The MusicBrainz Track ID. - context (Optional) : Sub-client version (not public, only enabled for certain API keys) + context (Optional) : Sub-client version + (not public, only enabled for certain API keys) """ params = {"track": title, "artist": artist} @@ -645,31 +689,43 @@ class _Network(object): _Request(self, "track.updateNowPlaying", params).execute() - def scrobble(self, artist, title, timestamp, album = None, album_artist = None, track_number = None, - duration = None, stream_id = None, context = None, mbid = 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. Parameters: artist (Required) : The artist name. title (Required) : The track name. - timestamp (Required) : The time the track started playing, in UNIX timestamp format (integer number of seconds since 00:00:00, January 1st 1970 UTC). This must be in the UTC time zone. + timestamp (Required) : The time the track started playing, in UNIX + timestamp format (integer number of seconds since 00:00:00, + January 1st 1970 UTC). This must be in the UTC time zone. album (Optional) : The album name. - album_artist (Optional) : The album artist - if this differs from the track artist. - context (Optional) : Sub-client version (not public, only enabled for certain API keys) - stream_id (Optional) : The stream id for this track received from the radio.getPlaylist service. - track_number (Optional) : The track number of the track on the album. + album_artist (Optional) : The album artist - if this differs from + the track artist. + context (Optional) : Sub-client version (not public, only enabled + for certain API keys) + stream_id (Optional) : The stream id for this track received from + the radio.getPlaylist service. + track_number (Optional) : The track number of the track on the + album. mbid (Optional) : The MusicBrainz Track ID. duration (Optional) : The length of the track in seconds. """ - return self.scrobble_many(({"artist": artist, "title": title, "timestamp": timestamp, "album": album, "album_artist": album_artist, - "track_number": track_number, "duration": duration, "stream_id": stream_id, "context": context, "mbid": mbid},)) + return self.scrobble_many(({ + "artist": artist, "title": title, "timestamp": timestamp, + "album": album, "album_artist": album_artist, + "track_number": track_number, "duration": duration, + "stream_id": stream_id, "context": context, "mbid": mbid},)) def scrobble_many(self, tracks): """ - Used to scrobble a batch of tracks at once. The parameter tracks is a sequence of dicts per - track containing the keyword arguments as if passed to the scrobble() method. + Used to scrobble a batch of tracks at once. The parameter tracks is a + sequence of dicts per track containing the keyword arguments as if + passed to the scrobble() method. """ tracks_to_scrobble = tracks[:50] @@ -684,8 +740,13 @@ class _Network(object): params["artist[%d]" % i] = tracks_to_scrobble[i]["artist"] params["track[%d]" % i] = tracks_to_scrobble[i]["title"] - additional_args = ("timestamp", "album", "album_artist", "context", "stream_id", "track_number", "mbid", "duration") - args_map_to = {"album_artist": "albumArtist", "track_number": "trackNumber", "stream_id": "streamID"} # so friggin lazy + additional_args = ( + "timestamp", "album", "album_artist", "context", + "stream_id", "track_number", "mbid", "duration") + args_map_to = { # so friggin lazy + "album_artist": "albumArtist", + "track_number": "trackNumber", + "stream_id": "streamID"} for arg in additional_args: @@ -695,8 +756,8 @@ class _Network(object): else: maps_to = arg - params["%s[%d]" %(maps_to, i)] = tracks_to_scrobble[i][arg] - + params[ + "%s[%d]" % (maps_to, i)] = tracks_to_scrobble[i][arg] _Request(self, "track.scrobble", params).execute() @@ -712,10 +773,10 @@ class _Network(object): params['artist[' + str(i) + ']'] = thing elif type == "album": params['artist[' + str(i) + ']'] = thing.artist - params['album[' + str(i) + ']'] = thing.title + params['album[' + str(i) + ']'] = thing.title elif type == "track": params['artist[' + str(i) + ']'] = thing.artist - params['track[' + str(i) + ']'] = thing.title + params['track[' + str(i) + ']'] = thing.title doc = _Request(self, method, params).execute(cacheable) @@ -736,6 +797,7 @@ class _Network(object): def get_track_play_links(self, tracks, cacheable=True): return self.get_play_links("track", tracks) + class LastFMNetwork(_Network): """A Last.fm network object @@ -744,63 +806,76 @@ class LastFMNetwork(_Network): 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 + 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. + 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. + 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: + Most read-only webservices only require an api_key and an api_secret, see + about obtaining them from: http://www.last.fm/api/account """ - def __init__(self, api_key="", api_secret="", session_key="", username="", password_hash=""): - _Network.__init__(self, - 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 __init__( + self, api_key="", api_secret="", session_key="", username="", + password_hash=""): + _Network.__init__( + self, + 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 __repr__(self): - return "pylast.LastFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key, - "'%s'" %self.username, "'%s'" %self.password_hash))) + 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): return "Last.fm Network" -def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""): + +def get_lastfm_network( + api_key="", api_secret="", session_key="", username="", + password_hash=""): """ Returns a preconfigured _Network object for Last.fm @@ -808,20 +883,25 @@ def get_lastfm_network(api_key="", api_secret="", session_key = "", username = " 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 + 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. + 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. + 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: + Most read-only webservices only require an api_key and an api_secret, see + about obtaining them from: http://www.last.fm/api/account """ _deprecation_warning("Create a LastFMNetwork object instead") - return LastFMNetwork(api_key, api_secret, session_key, username, password_hash) + return LastFMNetwork( + api_key, api_secret, session_key, username, password_hash) + class LibreFMNetwork(_Network): """ @@ -831,59 +911,70 @@ class LibreFMNetwork(_Network): 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 + 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. + if username and password_hash were provided and not session_key, + session_key will be generated automatically when needed. """ - def __init__(self, api_key="", api_secret="", session_key = "", username = "", password_hash = ""): + def __init__( + self, api_key="", api_secret="", session_key="", username="", + password_hash=""): - _Network.__init__(self, - name = "Libre.fm", - homepage = "http://alpha.libre.fm", - ws_server = ("alpha.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.libre.fm", - DOMAIN_GERMAN: "alpha.libre.fm", - DOMAIN_SPANISH: "alpha.libre.fm", - DOMAIN_FRENCH: "alpha.libre.fm", - DOMAIN_ITALIAN: "alpha.libre.fm", - DOMAIN_POLISH: "alpha.libre.fm", - DOMAIN_PORTUGUESE: "alpha.libre.fm", - DOMAIN_SWEDISH: "alpha.libre.fm", - DOMAIN_TURKISH: "alpha.libre.fm", - DOMAIN_RUSSIAN: "alpha.libre.fm", - DOMAIN_JAPANESE: "alpha.libre.fm", - DOMAIN_CHINESE: "alpha.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", - } - ) + _Network.__init__( + self, + name="Libre.fm", + homepage="http://alpha.libre.fm", + ws_server=("alpha.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.libre.fm", + DOMAIN_GERMAN: "alpha.libre.fm", + DOMAIN_SPANISH: "alpha.libre.fm", + DOMAIN_FRENCH: "alpha.libre.fm", + DOMAIN_ITALIAN: "alpha.libre.fm", + DOMAIN_POLISH: "alpha.libre.fm", + DOMAIN_PORTUGUESE: "alpha.libre.fm", + DOMAIN_SWEDISH: "alpha.libre.fm", + DOMAIN_TURKISH: "alpha.libre.fm", + DOMAIN_RUSSIAN: "alpha.libre.fm", + DOMAIN_JAPANESE: "alpha.libre.fm", + DOMAIN_CHINESE: "alpha.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", + } + ) def __repr__(self): - return "pylast.LibreFMNetwork(%s)" %(", ".join(("'%s'" %self.api_key, "'%s'" %self.api_secret, "'%s'" %self.session_key, - "'%s'" %self.username, "'%s'" %self.password_hash))) + 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): return "Libre.fm Network" -def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""): + +def get_librefm_network( + api_key="", api_secret="", session_key="", username="", + password_hash=""): """ Returns a preconfigured _Network object for Libre.fm @@ -891,19 +982,23 @@ def get_librefm_network(api_key="", api_secret="", session_key = "", username = 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 + 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. + if username and password_hash were provided and not session_key, + session_key will be generated automatically when needed. """ - _deprecation_warning("DeprecationWarning: Create a LibreFMNetwork object instead") + _deprecation_warning( + "DeprecationWarning: Create a LibreFMNetwork object instead") + + return LibreFMNetwork( + api_key, api_secret, session_key, username, password_hash) - return LibreFMNetwork(api_key, api_secret, session_key, username, password_hash) class _ShelfCacheBackend(object): """Used as a backend for caching cacheable requests.""" - def __init__(self, file_path = None): + def __init__(self, file_path=None): self.shelf = shelve.open(file_path) def get_xml(self, key): @@ -915,10 +1010,11 @@ class _ShelfCacheBackend(object): def has_key(self, key): return key in self.shelf.keys() + class _Request(object): """Representing an abstract web service operation.""" - def __init__(self, network, method_name, params = {}): + def __init__(self, network, method_name, params={}): self.network = network self.params = {} @@ -926,7 +1022,8 @@ class _Request(object): for key in params: self.params[key] = _unicode(params[key]) - (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth() + (self.api_key, self.api_secret, self.session_key) = \ + network._get_ws_auth() self.params["api_key"] = self.api_key self.params["method"] = method_name @@ -945,7 +1042,9 @@ class _Request(object): self.params['api_sig'] = self._get_signature() 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 = list(self.params.keys()) @@ -962,7 +1061,9 @@ class _Request(object): return md5(string) 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 = list(self.params.keys()) keys.sort() @@ -997,22 +1098,25 @@ class _Request(object): data = [] for name in self.params.keys(): - data.append('='.join((name, url_quote_plus(_string(self.params[name]))))) + data.append('='.join(( + name, url_quote_plus(_string(self.params[name]))))) data = '&'.join(data) headers = { "Content-type": "application/x-www-form-urlencoded", 'Accept-Charset': 'utf-8', 'User-Agent': "pylast" + '/' + __version__ - } + } (HOST_NAME, HOST_SUBDIR) = self.network.ws_server if self.network.is_proxy_enabled(): - conn = HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1]) + 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) @@ -1021,7 +1125,8 @@ class _Request(object): conn = HTTPConnection(host=HOST_NAME) try: - conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers) + conn.request( + method='POST', url=HOST_SUBDIR, body=data, headers=headers) except Exception as e: raise NetworkError(self.network, e) @@ -1035,7 +1140,7 @@ class _Request(object): self._check_response_for_errors(response_text) return response_text - def execute(self, cacheable = False): + def execute(self, cacheable=False): """Returns the XML DOM response of the POST Request from the server""" if self.network.is_caching_enabled() and cacheable: @@ -1061,6 +1166,7 @@ class _Request(object): details = e.firstChild.data.strip() raise WSError(self.network, status, details) + class SessionKeyGenerator(object): """Methods of generating a session key: 1) Web Authentication: @@ -1073,13 +1179,16 @@ class SessionKeyGenerator(object): 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) + d. session_key = SessionKeyGenerator(network).get_session_key(username, + password_hash) - A session key's lifetime is infinite, unless the user provokes the rights of the given API Key. + A session key's lifetime is infinite, 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. + If you create a Network object with just a API_KEY and API_SECRET and a + username and a password_hash, a SESSION_KEY will be automatically generated + for that network and stored in it so you don't have to do this manually, + unless you want to. """ def __init__(self, network): @@ -1087,8 +1196,10 @@ class SessionKeyGenerator(object): self.web_auth_tokens = {} def _get_web_auth_token(self): - """Retrieves a token from the network for web authentication. - The token then has to be authorized from getAuthURL before creating session. + """ + Retrieves a token from the network for web authentication. + The token then has to be authorized from getAuthURL before creating + session. """ request = _Request(self.network, 'auth.getToken') @@ -1103,24 +1214,32 @@ class SessionKeyGenerator(object): return e.firstChild.data def get_web_auth_url(self): - """The user must open this page, and you first, then call get_web_auth_session_key(url) after that.""" + """ + The user must open this page, and you first, then + call get_web_auth_session_key(url) after that. + """ token = self._get_web_auth_token() url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \ - {"homepage": self.network.homepage, "api": self.network.api_key, "token": token} + {"homepage": self.network.homepage, + "api": self.network.api_key, "token": token} self.web_auth_tokens[url] = token return url def get_web_auth_session_key(self, url): - """Retrieves the session key of a web authorization process by its url.""" + """ + Retrieves the session key of a web authorization process by its url. + """ if url in self.web_auth_tokens.keys(): token = self.web_auth_tokens[url] else: - token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed. + # That's going to raise a WSError of an unauthorized token when the + # request is executed. + token = "" request = _Request(self.network, 'auth.getSession', {'token': token}) @@ -1133,9 +1252,13 @@ class SessionKeyGenerator(object): return doc.getElementsByTagName('key')[0].firstChild.data def get_session_key(self, username, password_hash): - """Retrieve a session key with a username and a md5 hash of the user's password.""" + """ + Retrieve a session key with a username and a md5 hash of the user's + password. + """ - params = {"username": username, "authToken": md5(username + password_hash)} + 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 @@ -1148,12 +1271,21 @@ class SessionKeyGenerator(object): TopItem = collections.namedtuple("TopItem", ["item", "weight"]) SimilarItem = collections.namedtuple("SimilarItem", ["item", "match"]) -LibraryItem = collections.namedtuple("LibraryItem", ["item", "playcount", "tagcount"]) -PlayedTrack = collections.namedtuple("PlayedTrack", ["track", "album", "playback_date", "timestamp"]) -LovedTrack = collections.namedtuple("LovedTrack", ["track", "date", "timestamp"]) -ImageSizes = collections.namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"]) -Image = collections.namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"]) -Shout = collections.namedtuple("Shout", ["body", "author", "date"]) +LibraryItem = collections.namedtuple( + "LibraryItem", ["item", "playcount", "tagcount"]) +PlayedTrack = collections.namedtuple( + "PlayedTrack", ["track", "album", "playback_date", "timestamp"]) +LovedTrack = collections.namedtuple( + "LovedTrack", ["track", "date", "timestamp"]) +ImageSizes = collections.namedtuple( + "ImageSizes", [ + "original", "large", "largesquare", "medium", "small", "extralarge"]) +Image = collections.namedtuple( + "Image", [ + "title", "url", "dateadded", "format", "owner", "sizes", "votes"]) +Shout = collections.namedtuple( + "Shout", ["body", "author", "date"]) + def _string_output(funct): def r(*args): @@ -1161,7 +1293,8 @@ def _string_output(funct): return r -def _pad_list(given_list, desired_length, padding = None): + +def _pad_list(given_list, desired_length, padding=None): """ Pads a list to be of the desired_length. """ @@ -1171,6 +1304,7 @@ def _pad_list(given_list, desired_length, padding = None): return given_list + class _BaseObject(object): """An abstract webservices object.""" @@ -1179,7 +1313,7 @@ class _BaseObject(object): def __init__(self, network): self.network = network - def _request(self, method_name, cacheable = False, params = None): + def _request(self, method_name, cacheable=False, params=None): if not params: params = self._get_params() @@ -1194,13 +1328,16 @@ class _BaseObject(object): # Convert any ints (or whatever) into strings values = map(str, self._get_params().values()) - return hash(self.network) + \ - hash(str(type(self)) + "".join(list(self._get_params().keys()) + list(values)).lower()) + return hash(self.network) + hash(str(type(self)) + "".join( + list(self._get_params().keys()) + list(values) + ).lower()) def _extract_cdata_from_request(self, method_name, tag_name, params): doc = self._request(method_name, True, params) - return doc.getElementsByTagName(tag_name)[0].firstChild.wholeText.strip() + return doc.getElementsByTagName( + tag_name)[0].firstChild.wholeText.strip() + class _Taggable(object): """Common functions for classes with tags.""" @@ -1320,6 +1457,7 @@ class _Taggable(object): return seq + class WSError(Exception): """Exception related to the Network web service""" @@ -1351,6 +1489,7 @@ class WSError(Exception): return self.status + class MalformedResponseError(Exception): """Exception conveying a malformed response from Last.fm.""" @@ -1359,7 +1498,9 @@ class MalformedResponseError(Exception): self.underlying_error = underlying_error def __str__(self): - return "Malformed response from Last.fm. Underlying error: %s" %str(self.underlying_error) + return "Malformed response from Last.fm. Underlying error: %s" % str( + self.underlying_error) + class NetworkError(Exception): """Exception conveying a problem in sending a request to Last.fm""" @@ -1369,7 +1510,8 @@ class NetworkError(Exception): self.underlying_error = underlying_error def __str__(self): - return "NetworkError: %s" %str(self.underlying_error) + return "NetworkError: %s" % str(self.underlying_error) + class Album(_BaseObject, _Taggable): """An album.""" @@ -1400,20 +1542,32 @@ class Album(_BaseObject, _Taggable): self.username = username def __repr__(self): - return "pylast.Album(%s, %s, %s)" %(repr(self.artist.name), repr(self.title), repr(self.network)) + return "pylast.Album(%s, %s, %s)" % ( + repr(self.artist.name), repr(self.title), repr(self.network)) @_string_output def __str__(self): - return _unicode("%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): - return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) + a = self.get_title().lower() + b = other.get_title().lower() + c = self.get_artist().get_name().lower() + d = other.get_artist().get_name().lower() + return (a == b) and (c == d) def __ne__(self, other): - return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) + a = self.get_title().lower() + b = other.get_title().lower() + c = self.get_artist().get_name().lower() + d = other.get_artist().get_name().lower() + return (a != b) or (c != d) def _get_params(self): - return {'artist': self.get_artist().get_name(), 'album': self.get_title(), } + return { + 'artist': self.get_artist().get_name(), 'album': self.get_title(), + } def get_artist(self): """Returns the associated Artist object.""" @@ -1433,9 +1587,10 @@ class Album(_BaseObject, _Taggable): def get_release_date(self): """Retruns the release date of the album.""" - return _extract(self._request("album.getInfo", cacheable = True), "releasedate") + return _extract( + self._request("album.getInfo", cacheable=True), "releasedate") - def get_cover_image(self, size = COVER_EXTRA_LARGE): + def get_cover_image(self, size=COVER_EXTRA_LARGE): """ Returns a uri to the cover image size can be one of: @@ -1445,17 +1600,19 @@ class Album(_BaseObject, _Taggable): COVER_SMALL """ - return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size] + return _extract_all( + self._request("album.getInfo", cacheable=True), 'image')[size] def get_id(self): """Returns the ID""" - return _extract(self._request("album.getInfo", cacheable = True), "id") + return _extract(self._request("album.getInfo", cacheable=True), "id") def get_playcount(self): """Returns the number of plays on the network""" - return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount")) + return _number(_extract( + self._request("album.getInfo", cacheable=True), "playcount")) def get_userplaycount(self): """Returns the number of plays by a given username""" @@ -1465,26 +1622,28 @@ class Album(_BaseObject, _Taggable): params = self._get_params() params['username'] = self.username - return _number(_extract(self._request("album.getInfo", True, params), "userplaycount")) + return _number(_extract( + self._request("album.getInfo", True, params), "userplaycount")) def get_listener_count(self): """Returns the number of listeners on the network""" - return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners")) + return _number(_extract( + self._request("album.getInfo", cacheable=True), "listeners")) def get_tracks(self): """Returns the list of Tracks on this album.""" - uri = 'lastfm://playlist/album/%s' %self.get_id() + uri = 'lastfm://playlist/album/%s' % self.get_id() return XSPF(uri, self.network).get_tracks() def get_mbid(self): """Returns the MusicBrainz id of the album.""" - return _extract(self._request("album.getInfo", cacheable = True), "mbid") + return _extract(self._request("album.getInfo", cacheable=True), "mbid") - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the album page on the network. # Parameters: * domain_name str: The network's language domain. Possible values: @@ -1505,7 +1664,8 @@ class Album(_BaseObject, _Taggable): artist = _url_safe(self.get_artist().get_name()) album = _url_safe(self.get_title()) - return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album} + 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.""" @@ -1543,6 +1703,7 @@ class Album(_BaseObject, _Taggable): return _extract(node, "content") + class Artist(_BaseObject, _Taggable): """An artist.""" @@ -1564,7 +1725,8 @@ class Artist(_BaseObject, _Taggable): self.username = username def __repr__(self): - return "pylast.Artist(%s, %s)" %(repr(self.get_name()), repr(self.network)) + return "pylast.Artist(%s, %s)" % ( + repr(self.get_name()), repr(self.network)) @_string_output def __str__(self): @@ -1589,7 +1751,7 @@ class Artist(_BaseObject, _Taggable): return self.name - def get_cover_image(self, size = COVER_MEGA): + def get_cover_image(self, size=COVER_MEGA): """ Returns a uri to the cover image size can be one of: @@ -1600,12 +1762,14 @@ class Artist(_BaseObject, _Taggable): COVER_SMALL """ - return _extract_all(self._request("artist.getInfo", True), "image")[size] + return _extract_all( + self._request("artist.getInfo", True), "image")[size] def get_playcount(self): """Returns the number of plays on the network.""" - return _number(_extract(self._request("artist.getInfo", True), "playcount")) + return _number(_extract( + self._request("artist.getInfo", True), "playcount")) def get_userplaycount(self): """Returns the number of plays by a given username""" @@ -1615,7 +1779,8 @@ class Artist(_BaseObject, _Taggable): params = self._get_params() params['username'] = self.username - return _number(_extract(self._request("artist.getInfo", True, params), "userplaycount")) + return _number(_extract( + self._request("artist.getInfo", True, params), "userplaycount")) def get_mbid(self): """Returns the MusicBrainz ID of this artist.""" @@ -1630,13 +1795,15 @@ class Artist(_BaseObject, _Taggable): if hasattr(self, "listener_count"): return self.listener_count else: - self.listener_count = _number(_extract(self._request("artist.getInfo", True), "listeners")) + self.listener_count = _number(_extract( + self._request("artist.getInfo", True), "listeners")) return self.listener_count def is_streamable(self): """Returns True if the artist is streamable.""" - return bool(_number(_extract(self._request("artist.getInfo", True), "streamable"))) + return bool(_number(_extract( + self._request("artist.getInfo", True), "streamable"))) def get_bio_published_date(self): """Returns the date on which the artist's biography was published.""" @@ -1652,7 +1819,8 @@ class Artist(_BaseObject, _Taggable): else: params = None - return self._extract_cdata_from_request("artist.getInfo", "summary", params) + return self._extract_cdata_from_request( + "artist.getInfo", "summary", params) def get_bio_content(self, language=None): """Returns the content of the artist's biography.""" @@ -1663,7 +1831,8 @@ class Artist(_BaseObject, _Taggable): else: params = None - return self._extract_cdata_from_request("artist.getInfo", "content", params) + return self._extract_cdata_from_request( + "artist.getInfo", "content", params) def get_upcoming_events(self): """Returns a list of the upcoming Events for this artist.""" @@ -1672,7 +1841,7 @@ class Artist(_BaseObject, _Taggable): return _extract_events_from_doc(doc, self.network) - def get_similar(self, limit = None): + def get_similar(self, limit=None): """Returns the similar artists on the network.""" params = self._get_params() @@ -1686,7 +1855,8 @@ class Artist(_BaseObject, _Taggable): artists = [] for i in range(0, len(names)): - artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i]))) + artists.append(SimilarItem( + Artist(names[i], self.network), _number(matches[i]))) return artists @@ -1718,11 +1888,11 @@ class Artist(_BaseObject, _Taggable): artist = _extract(track, "name", 1) playcount = _number(_extract(track, "playcount")) - seq.append( TopItem(Track(artist, title, self.network), playcount) ) + seq.append(TopItem(Track(artist, title, self.network), playcount)) return seq - def get_top_fans(self, limit = None): + def get_top_fans(self, limit=None): """Returns a list of the Users who played this artist the most. # Parameters: * limit int: Max elements. @@ -1745,14 +1915,15 @@ class Artist(_BaseObject, _Taggable): return seq - def share(self, users, message = None): + def share(self, users, message=None): """Shares this artist (sends out recommendations). # Parameters: - * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them. + * users [User|str,]: A list that can contain usernames, emails, + User objects, or all of them. * message str: A message to include in the recommendation message. """ - #last.fm currently accepts a max of 10 recipient at a time + # Last.fm currently accepts a max of 10 recipient at a time while(len(users) > 10): section = users[0:9] users = users[9:] @@ -1773,7 +1944,7 @@ class Artist(_BaseObject, _Taggable): self._request('artist.share', False, params) - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the artist page on the network. # Parameters: * domain_name: The network's language domain. Possible values: @@ -1793,7 +1964,8 @@ class Artist(_BaseObject, _Taggable): artist = _url_safe(self.get_name()) - return self.network._get_url(domain_name, "artist") %{'artist': artist} + return self.network._get_url( + domain_name, "artist") % {'artist': artist} def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None): """ @@ -1815,16 +1987,17 @@ class Artist(_BaseObject, _Taggable): 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")) - ) - ) + 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, cacheable=False): @@ -1834,12 +2007,13 @@ class Artist(_BaseObject, _Taggable): shouts = [] for node in _collect_nodes(limit, self, "artist.getShouts", cacheable): - shouts.append(Shout( - _extract(node, "body"), - User(_extract(node, "author"), self.network), - _extract(node, "date") - ) - ) + shouts.append( + Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) return shouts def shout(self, message): @@ -1866,7 +2040,7 @@ class Event(_BaseObject): self.id = event_id def __repr__(self): - return "pylast.Event(%s, %s)" %(repr(self.id), repr(self.network)) + return "pylast.Event(%s, %s)" % (repr(self.id), repr(self.network)) @_string_output def __str__(self): @@ -1962,7 +2136,7 @@ class Event(_BaseObject): return _extract(doc, "description") - def get_cover_image(self, size = COVER_MEGA): + def get_cover_image(self, size=COVER_MEGA): """ Returns a uri to the cover image size can be one of: @@ -1991,7 +2165,7 @@ class Event(_BaseObject): return _number(_extract(doc, "reviews")) - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the event page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -2008,15 +2182,17 @@ class Event(_BaseObject): o DOMAIN_CHINESE """ - return self.network._get_url(domain_name, "event") %{'id': self.get_id()} + return self.network._get_url( + domain_name, "event") % {'id': self.get_id()} - def share(self, users, message = None): + def share(self, users, message=None): """Shares this event (sends out recommendations). - * users: A list that can contain usernames, emails, User objects, or all of them. + * users: A list that can contain usernames, emails, User objects, + or all of them. * message: A message to include in the recommendation message. """ - #last.fm currently accepts a max of 10 recipient at a time + # Last.fm currently accepts a max of 10 recipient at a time while(len(users) > 10): section = users[0:9] users = users[9:] @@ -2044,12 +2220,13 @@ class Event(_BaseObject): shouts = [] for node in _collect_nodes(limit, self, "event.getShouts", cacheable): - shouts.append(Shout( - _extract(node, "body"), - User(_extract(node, "author"), self.network), - _extract(node, "date") - ) - ) + shouts.append( + Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) return shouts def shout(self, message): @@ -2062,6 +2239,7 @@ class Event(_BaseObject): self._request("event.Shout", False, params) + class Country(_BaseObject): """A country at Last.fm.""" @@ -2075,7 +2253,7 @@ class Country(_BaseObject): self.name = name def __repr__(self): - return "pylast.Country(%s, %s)" %(repr(self.name), repr(self.network)) + return "pylast.Country(%s, %s)" % (repr(self.name), repr(self.network)) @_string_output def __str__(self): @@ -2091,7 +2269,8 @@ class Country(_BaseObject): return {'country': self.get_name()} def _get_name_from_code(self, alpha2code): - # TODO: Have this function lookup the alpha-2 code and return the country name. + # TODO: Have this function lookup the alpha-2 code and return the + # country name. return alpha2code @@ -2127,11 +2306,11 @@ class Country(_BaseObject): artist = _extract(n, 'name', 1) playcount = _number(_extract(n, "playcount")) - seq.append( TopItem(Track(artist, title, self.network), playcount)) + seq.append(TopItem(Track(artist, title, self.network), playcount)) return seq - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the event page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -2150,7 +2329,8 @@ class Country(_BaseObject): country_name = _url_safe(self.get_name()) - return self.network._get_url(domain_name, "country") %{'country_name': country_name} + return self.network._get_url( + domain_name, "country") % {'country_name': country_name} class Metro(_BaseObject): @@ -2168,19 +2348,20 @@ class Metro(_BaseObject): self.country = country def __repr__(self): - return "pylast.Metro(%s, %s, %s)" %(repr(self.name), repr(self.country), repr(self.network)) + return "pylast.Metro(%s, %s, %s)" % ( + repr(self.name), repr(self.country), repr(self.network)) @_string_output def __str__(self): return self.get_name() + ", " + self.get_country() def __eq__(self, other): - return self.get_name().lower() == other.get_name().lower() and \ - self.get_country().lower() == other.get_country().lower() + return (self.get_name().lower() == other.get_name().lower() and + self.get_country().lower() == other.get_country().lower()) def __ne__(self, other): - return self.get_name() != other.get_name() or \ - self.get_country().lower() != other.get_country().lower() + return (self.get_name() != other.get_name() or + self.get_country().lower() != other.get_country().lower()) def _get_params(self): return {'metro': self.get_name(), 'country': self.get_country()} @@ -2195,7 +2376,9 @@ class Metro(_BaseObject): return self.country - def _get_chart(self, method, tag="artist", limit=None, from_date=None, to_date=None, cacheable=True): + def _get_chart( + self, method, tag="artist", limit=None, from_date=None, + to_date=None, cacheable=True): """Internal helper for getting geo charts.""" params = self._get_params() if limit: params["limit"] = limit @@ -2220,59 +2403,97 @@ class Metro(_BaseObject): return seq - def get_artist_chart(self, tag="artist", limit=None, from_date=None, to_date=None, cacheable=True): + def get_artist_chart( + self, tag="artist", limit=None, from_date=None, to_date=None, + cacheable=True): """Get a chart of artists for a metro. Parameters: - from_date (Optional) : Beginning timestamp of the weekly range requested + from_date (Optional) : Beginning timestamp of the weekly range + requested to_date (Optional) : Ending timestamp of the weekly range requested - limit (Optional) : The number of results to fetch per page. Defaults to 50. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ - return self._get_chart("geo.getMetroArtistChart", tag = tag, limit = limit, from_date = from_date, to_date = to_date, cacheable = cacheable) + return self._get_chart( + "geo.getMetroArtistChart", tag=tag, limit=limit, + from_date=from_date, to_date=to_date, cacheable=cacheable) - def get_hype_artist_chart(self, tag="artist", limit=None, from_date=None, to_date=None, cacheable=True): + def get_hype_artist_chart( + self, tag="artist", limit=None, from_date=None, to_date=None, + cacheable=True): """Get a chart of hyped (up and coming) artists for a metro. Parameters: - from_date (Optional) : Beginning timestamp of the weekly range requested + from_date (Optional) : Beginning timestamp of the weekly range + requested to_date (Optional) : Ending timestamp of the weekly range requested - limit (Optional) : The number of results to fetch per page. Defaults to 50. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ - return self._get_chart("geo.getMetroHypeArtistChart", tag = tag, limit = limit, from_date = from_date, to_date = to_date, cacheable = cacheable) + return self._get_chart( + "geo.getMetroHypeArtistChart", tag=tag, limit=limit, + from_date=from_date, to_date=to_date, cacheable=cacheable) - def get_unique_artist_chart(self, tag="artist", limit=None, from_date=None, to_date=None, cacheable=True): + def get_unique_artist_chart( + self, tag="artist", limit=None, from_date=None, to_date=None, + cacheable=True): """Get a chart of the artists which make that metro unique. Parameters: - from_date (Optional) : Beginning timestamp of the weekly range requested + from_date (Optional) : Beginning timestamp of the weekly range + requested to_date (Optional) : Ending timestamp of the weekly range requested - limit (Optional) : The number of results to fetch per page. Defaults to 50. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ - return self._get_chart("geo.getMetroUniqueArtistChart", tag = tag, limit = limit, from_date = from_date, to_date = to_date, cacheable = cacheable) + return self._get_chart( + "geo.getMetroUniqueArtistChart", tag=tag, limit=limit, + from_date=from_date, to_date=to_date, cacheable=cacheable) - def get_track_chart(self, tag="track", limit=None, from_date=None, to_date=None, cacheable=True): + def get_track_chart( + self, tag="track", limit=None, from_date=None, to_date=None, + cacheable=True): """Get a chart of tracks for a metro. Parameters: - from_date (Optional) : Beginning timestamp of the weekly range requested + from_date (Optional) : Beginning timestamp of the weekly range + requested to_date (Optional) : Ending timestamp of the weekly range requested - limit (Optional) : The number of results to fetch per page. Defaults to 50. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ - return self._get_chart("geo.getMetroTrackChart", tag = tag, limit = limit, from_date = from_date, to_date = to_date, cacheable = cacheable) + return self._get_chart( + "geo.getMetroTrackChart", tag=tag, limit=limit, + from_date=from_date, to_date=to_date, cacheable=cacheable) - def get_hype_track_chart(self, tag="track", limit=None, from_date=None, to_date=None, cacheable=True): + def get_hype_track_chart( + self, tag="track", limit=None, from_date=None, to_date=None, + cacheable=True): """Get a chart of tracks for a metro. Parameters: - from_date (Optional) : Beginning timestamp of the weekly range requested + from_date (Optional) : Beginning timestamp of the weekly range + requested to_date (Optional) : Ending timestamp of the weekly range requested - limit (Optional) : The number of results to fetch per page. Defaults to 50. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ - return self._get_chart("geo.getMetroHypeTrackChart", tag = tag, limit = limit, from_date = from_date, to_date = to_date, cacheable = cacheable) + return self._get_chart( + "geo.getMetroHypeTrackChart", tag=tag, + limit=limit, from_date=from_date, to_date=to_date, + cacheable=cacheable) - def get_unique_track_chart(self, tag="track", limit=None, from_date=None, to_date=None, cacheable=True): + def get_unique_track_chart( + self, tag="track", limit=None, from_date=None, to_date=None, + cacheable=True): """Get a chart of tracks for a metro. Parameters: - from_date (Optional) : Beginning timestamp of the weekly range requested + from_date (Optional) : Beginning timestamp of the weekly range + requested to_date (Optional) : Ending timestamp of the weekly range requested - limit (Optional) : The number of results to fetch per page. Defaults to 50. + limit (Optional) : The number of results to fetch per page. + Defaults to 50. """ - return self._get_chart("geo.getMetroUniqueTrackChart", tag = tag, limit = limit, from_date = from_date, to_date = to_date, cacheable = cacheable) + return self._get_chart( + "geo.getMetroUniqueTrackChart", tag=tag, limit=limit, + from_date=from_date, to_date=to_date, cacheable=cacheable) + class Library(_BaseObject): """A user's Last.fm library.""" @@ -2294,7 +2515,7 @@ class Library(_BaseObject): self._tracks_index = 0 def __repr__(self): - return "pylast.Library(%s, %s)" %(repr(self.user), repr(self.network)) + return "pylast.Library(%s, %s)" % (repr(self.user), repr(self.network)) @_string_output def __str__(self): @@ -2359,7 +2580,8 @@ class Library(_BaseObject): def get_albums(self, artist=None, limit=50, cacheable=True): """ Returns a sequence of Album objects - If no artist is specified, it will return all, sorted by decreasing play count. + If no artist is specified, it will return all, sorted by decreasing + play count. If limit==None it will return all (may take a while) """ @@ -2368,13 +2590,15 @@ class Library(_BaseObject): params["artist"] = artist seq = [] - for node in _collect_nodes(limit, self, "library.getAlbums", cacheable, params): + for node in _collect_nodes( + limit, self, "library.getAlbums", cacheable, params): name = _extract(node, "name") artist = _extract(node, "name", 1) playcount = _number(_extract(node, "playcount")) tagcount = _number(_extract(node, "tagcount")) - seq.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount)) + seq.append(LibraryItem( + Album(artist, name, self.network), playcount, tagcount)) return seq @@ -2385,13 +2609,15 @@ class Library(_BaseObject): """ seq = [] - for node in _collect_nodes(limit, self, "library.getArtists", cacheable): + for node in _collect_nodes( + limit, self, "library.getArtists", cacheable): name = _extract(node, "name") playcount = _number(_extract(node, "playcount")) tagcount = _number(_extract(node, "tagcount")) - seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount)) + seq.append(LibraryItem( + Artist(name, self.network), playcount, tagcount)) return seq @@ -2408,13 +2634,15 @@ class Library(_BaseObject): params["album"] = album seq = [] - for node in _collect_nodes(limit, self, "library.getTracks", cacheable, params): + for node in _collect_nodes( + limit, self, "library.getTracks", cacheable, params): name = _extract(node, "name") artist = _extract(node, "name", 1) playcount = _number(_extract(node, "playcount")) tagcount = _number(_extract(node, "tagcount")) - seq.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount)) + seq.append(LibraryItem( + Track(artist, name, self.network), playcount, tagcount)) return seq @@ -2457,7 +2685,9 @@ class Playlist(_BaseObject): return repr(self.user) + "'s playlist # " + repr(self.id) def _get_info_node(self): - """Returns the node from user.getPlaylists where this playlist's info is.""" + """ + Returns the node from user.getPlaylists where this playlist's info is. + """ doc = self._request("user.getPlaylists", True) @@ -2481,7 +2711,7 @@ class Playlist(_BaseObject): def get_tracks(self): """Returns a list of the tracks on this user playlist.""" - uri = _unicode('lastfm://playlist/%s') %self.get_id() + uri = _unicode('lastfm://playlist/%s') % self.get_id() return XSPF(uri, self.network).get_tracks() @@ -2520,8 +2750,10 @@ class Playlist(_BaseObject): return _number(_extract(self._get_info_node(), "duration")) def is_streamable(self): - """Returns True if the playlist is streamable. - For a playlist to be streamable, it needs at least 45 tracks by 15 different artists.""" + """ + Returns True if the playlist is streamable. + For a playlist to be streamable, it needs at least 45 tracks by 15 + different artists.""" if _extract(self._get_info_node(), "streamable") == '1': return True @@ -2535,7 +2767,7 @@ class Playlist(_BaseObject): return track in self.get_tracks() - def get_cover_image(self, size = COVER_EXTRA_LARGE): + def get_cover_image(self, size=COVER_EXTRA_LARGE): """ Returns a uri to the cover image size can be one of: @@ -2548,7 +2780,7 @@ class Playlist(_BaseObject): return _extract(self._get_info_node(), "image")[size] - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the playlist on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -2568,13 +2800,15 @@ class Playlist(_BaseObject): english_url = _extract(self._get_info_node(), "url") appendix = english_url[english_url.rfind("/") + 1:] - return self.network._get_url(domain_name, "playlist") %{'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): """A Last.fm object tag.""" - # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it) + # TODO: getWeeklyArtistChart + # (too lazy, i'll wait for when someone requests it) name = None @@ -2586,7 +2820,7 @@ class Tag(_BaseObject): self.name = name def __repr__(self): - return "pylast.Tag(%s, %s)" %(repr(self.name), repr(self.network)) + return "pylast.Tag(%s, %s)" % (repr(self.name), repr(self.network)) @_string_output def __str__(self): @@ -2649,7 +2883,7 @@ class Tag(_BaseObject): artist = _extract(track, "name", 1) playcount = _number(_extract(track, "playcount")) - seq.append( TopItem(Track(artist, title, self.network), playcount) ) + seq.append(TopItem(Track(artist, title, self.network), playcount)) return seq @@ -2674,12 +2908,15 @@ class Tag(_BaseObject): seq = [] for node in doc.getElementsByTagName("chart"): - seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + seq.append((node.getAttribute("from"), node.getAttribute("to"))) return seq - def get_weekly_artist_charts(self, from_date = None, to_date = None): - """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_artist_charts(self, from_date=None, to_date=None): + """ + Returns the weekly artist charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -2696,7 +2933,7 @@ class Tag(_BaseObject): return seq - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the tag page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -2715,7 +2952,8 @@ class Tag(_BaseObject): name = _url_safe(self.get_name()) - return self.network._get_url(domain_name, "tag") %{'name': name} + return self.network._get_url(domain_name, "tag") % {'name': name} + class Track(_BaseObject, _Taggable): """A Last.fm track.""" @@ -2740,20 +2978,30 @@ class Track(_BaseObject, _Taggable): self.username = username def __repr__(self): - return "pylast.Track(%s, %s, %s)" %(repr(self.artist.name), repr(self.title), repr(self.network)) + return "pylast.Track(%s, %s, %s)" % ( + repr(self.artist.name), repr(self.title), repr(self.network)) @_string_output def __str__(self): return 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()) + a = self.get_title().lower() + b = other.get_title().lower() + c = self.get_artist().get_name().lower() + d = other.get_artist().get_name().lower() + return (a == b) and (c == d) def __ne__(self, other): - return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) + a = self.get_title().lower() + b = other.get_title().lower() + c = self.get_artist().get_name().lower() + d = other.get_artist().get_name().lower() + return (a != b) or (c != d) def _get_params(self): - return {'artist': self.get_artist().get_name(), 'track': self.get_title()} + return { + 'artist': self.get_artist().get_name(), 'track': self.get_title()} def get_artist(self): """Returns the associated Artist object.""" @@ -2843,7 +3091,8 @@ class Track(_BaseObject, _Taggable): """Returns True if the fulltrack is available for streaming.""" doc = self._request("track.getInfo", True) - return doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1" + return doc.getElementsByTagName( + "streamable")[0].getAttribute("fulltrack") == "1" def get_album(self): """Returns the album object of this track.""" @@ -2856,7 +3105,8 @@ class Track(_BaseObject, _Taggable): return node = doc.getElementsByTagName("album")[0] - return Album(_extract(node, "artist"), _extract(node, "title"), self.network) + 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.""" @@ -2910,7 +3160,10 @@ class Track(_BaseObject, _Taggable): self._request('track.ban') def get_similar(self): - """Returns similar tracks for this track on the network, based on listening data. """ + """ + Returns similar tracks for this track on the network, + based on listening data. + """ doc = self._request('track.getSimilar', True) @@ -2924,7 +3177,7 @@ class Track(_BaseObject, _Taggable): return seq - def get_top_fans(self, limit = None): + def get_top_fans(self, limit=None): """Returns a list of the Users who played this track.""" doc = self._request('track.getTopFans', True) @@ -2944,13 +3197,14 @@ class Track(_BaseObject, _Taggable): return seq - def share(self, users, message = None): + def share(self, users, message=None): """Shares this track (sends out recommendations). - * users: A list that can contain usernames, emails, User objects, or all of them. + * users: A list that can contain usernames, emails, User objects, + or all of them. * message: A message to include in the recommendation message. """ - #last.fm currently accepts a max of 10 recipient at a time + # Last.fm currently accepts a max of 10 recipient at a time while(len(users) > 10): section = users[0:9] users = users[9:] @@ -2971,7 +3225,7 @@ class Track(_BaseObject, _Taggable): self._request('track.share', False, params) - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the track page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -2991,7 +3245,9 @@ class Track(_BaseObject, _Taggable): artist = _url_safe(self.get_artist().get_name()) title = _url_safe(self.get_title()) - return self.network._get_url(domain_name, "track") %{'domain': self.network._get_language_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, cacheable=False): """ @@ -3000,14 +3256,16 @@ class Track(_BaseObject, _Taggable): shouts = [] for node in _collect_nodes(limit, self, "track.getShouts", cacheable): - shouts.append(Shout( - _extract(node, "body"), - User(_extract(node, "author"), self.network), - _extract(node, "date") - ) - ) + shouts.append( + Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) return shouts + class Group(_BaseObject): """A Last.fm group.""" @@ -3021,7 +3279,7 @@ class Group(_BaseObject): self.name = group_name def __repr__(self): - return "pylast.Group(%s, %s)" %(repr(self.name), repr(self.network)) + return "pylast.Group(%s, %s)" % (repr(self.name), repr(self.network)) @_string_output def __str__(self): @@ -3047,12 +3305,15 @@ class Group(_BaseObject): seq = [] for node in doc.getElementsByTagName("chart"): - seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + seq.append((node.getAttribute("from"), node.getAttribute("to"))) return seq - def get_weekly_artist_charts(self, from_date = None, to_date = None): - """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_artist_charts(self, from_date=None, to_date=None): + """ + Returns the weekly artist charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -3069,8 +3330,11 @@ class Group(_BaseObject): return seq - def get_weekly_album_charts(self, from_date = None, to_date = None): - """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_album_charts(self, from_date=None, to_date=None): + """ + Returns the weekly album charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -3081,14 +3345,18 @@ class Group(_BaseObject): seq = [] for node in doc.getElementsByTagName("album"): - item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) + item = Album( + _extract(node, "artist"), _extract(node, "name"), self.network) weight = _number(_extract(node, "playcount")) seq.append(TopItem(item, weight)) return seq - def get_weekly_track_charts(self, from_date = None, to_date = None): - """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_track_charts(self, from_date=None, to_date=None): + """ + Returns the weekly track charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -3099,13 +3367,14 @@ class Group(_BaseObject): seq = [] for node in doc.getElementsByTagName("track"): - item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) + item = Track( + _extract(node, "artist"), _extract(node, "name"), self.network) weight = _number(_extract(node, "playcount")) seq.append(TopItem(item, weight)) return seq - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the group page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -3124,7 +3393,7 @@ class Group(_BaseObject): name = _url_safe(self.get_name()) - return self.network._get_url(domain_name, "group") %{'name': name} + return self.network._get_url(domain_name, "group") % {'name': name} def get_members(self, limit=50, cacheable=False): """ @@ -3141,6 +3410,7 @@ class Group(_BaseObject): return users + class XSPF(_BaseObject): "A Last.fm XSPF playlist.""" @@ -3185,6 +3455,7 @@ class XSPF(_BaseObject): return seq + class User(_BaseObject): """A Last.fm user.""" @@ -3202,7 +3473,7 @@ class User(_BaseObject): self._recommended_artists_index = 0 def __repr__(self): - return "pylast.User(%s, %s)" %(repr(self.name), repr(self.network)) + return "pylast.User(%s, %s)" % (repr(self.name), repr(self.network)) @_string_output def __str__(self): @@ -3239,25 +3510,32 @@ class User(_BaseObject): return _extract_events_from_doc(doc, self.network) def get_artist_tracks(self, artist, cacheable=False): - """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." + """ + 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", cacheable, params): + for track in _collect_nodes( + None, self, "user.getArtistTracks", cacheable, params): title = _extract(track, "name") artist = _extract(track, "artist") date = _extract(track, "date") album = _extract(track, "album") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + timestamp = track.getElementsByTagName( + "date")[0].getAttribute("uts") - seq.append(PlayedTrack(Track(artist, title, self.network), album, date, timestamp)) + seq.append(PlayedTrack( + Track(artist, title, self.network), album, date, timestamp)) return seq - def get_friends(self, limit = 50, cacheable=False): + def get_friends(self, limit=50, cacheable=False): """Returns a list of the user's friends. """ seq = [] @@ -3267,8 +3545,9 @@ class User(_BaseObject): return seq def get_loved_tracks(self, limit=50, cacheable=True): - """Returns this user's loved track as a sequence of LovedTrack objects - in reverse order of their timestamp, all the way back to the first track. + """ + Returns this user's loved track as a sequence of LovedTrack objects in + reverse order of their timestamp, all the way back to the first track. If limit==None, it will try to pull all the available data. @@ -3276,25 +3555,29 @@ class User(_BaseObject): large amount of data. Use extract_items() with the return of this function to - get only a sequence of Track objects with no playback dates. """ + get only a sequence of Track objects with no playback dates. + """ params = self._get_params() if limit: params['limit'] = limit seq = [] - for track in _collect_nodes(limit, self, "user.getLovedTracks", cacheable, params): + for track in _collect_nodes( + limit, self, "user.getLovedTracks", cacheable, params): title = _extract(track, "name") artist = _extract(track, "name", 1) date = _extract(track, "date") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + timestamp = track.getElementsByTagName( + "date")[0].getAttribute("uts") - seq.append(LovedTrack(Track(artist, title, self.network), date, timestamp)) + seq.append(LovedTrack( + Track(artist, title, self.network), date, timestamp)) return seq - def get_neighbours(self, limit = 50): + def get_neighbours(self, limit=50): """Returns a list of the user's friends.""" params = self._get_params() @@ -3330,12 +3613,15 @@ class User(_BaseObject): playlists = [] for playlist_id in _extract_all(doc, "id"): - playlists.append(Playlist(self.get_name(), playlist_id, self.network)) + playlists.append( + Playlist(self.get_name(), playlist_id, self.network)) return playlists def get_now_playing(self): - """Returns the currently playing track, or None if nothing is playing. """ + """ + Returns the currently playing track, or None if nothing is playing. + """ params = self._get_params() params['limit'] = '1' @@ -3357,10 +3643,10 @@ class User(_BaseObject): return Track(artist, title, self.network, self.name) - def get_recent_tracks(self, limit=10, cacheable=True): - """Returns this user's played track as a sequence of PlayedTrack objects - in reverse order of their playtime, all the way back to the first track. + """ + Returns this user's played track as a sequence of PlayedTrack objects + in reverse order of playtime, all the way back to the first track. If limit==None, it will try to pull all the available data. @@ -3368,25 +3654,29 @@ class User(_BaseObject): large amount of data. Use extract_items() with the return of this function to - get only a sequence of Track objects with no playback dates. """ + get only a sequence of Track objects with no playback dates. + """ params = self._get_params() if limit: params['limit'] = limit seq = [] - for track in _collect_nodes(limit, self, "user.getRecentTracks", cacheable, params): + for track in _collect_nodes( + limit, self, "user.getRecentTracks", cacheable, params): if track.hasAttribute('nowplaying'): - continue #to prevent the now playing track from sneaking in here + continue # to prevent the now playing track from sneaking in title = _extract(track, "name") artist = _extract(track, "artist") date = _extract(track, "date") album = _extract(track, "album") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + timestamp = track.getElementsByTagName( + "date")[0].getAttribute("uts") - seq.append(PlayedTrack(Track(artist, title, self.network), album, date, timestamp)) + seq.append(PlayedTrack( + Track(artist, title, self.network), album, date, timestamp)) return seq @@ -3463,9 +3753,10 @@ class User(_BaseObject): doc = self._request("user.getInfo", True) - return doc.getElementsByTagName("registered")[0].getAttribute("unixtime") + return doc.getElementsByTagName( + "registered")[0].getAttribute("unixtime") - def get_top_albums(self, period = PERIOD_OVERALL): + def get_top_albums(self, period=PERIOD_OVERALL): """Returns the top albums played by a user. * period: The period of time. Possible values: o PERIOD_OVERALL @@ -3490,7 +3781,7 @@ class User(_BaseObject): return seq - def get_top_artists(self, period = PERIOD_OVERALL): + def get_top_artists(self, period=PERIOD_OVERALL): """Returns the top artists played by a user. * period: The period of time. Possible values: o PERIOD_OVERALL @@ -3515,7 +3806,9 @@ class User(_BaseObject): return seq def get_top_tags(self, limit=None, cacheable=True): - """Returns a sequence of the top tags used by this user with their counts as TopItem objects. + """ + 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. * cacheable: Whether to cache results. """ @@ -3527,11 +3820,13 @@ class User(_BaseObject): seq = [] for node in doc.getElementsByTagName("tag"): - seq.append(TopItem(Tag(_extract(node, "name"), self.network), _extract(node, "count"))) + seq.append(TopItem( + Tag(_extract(node, "name"), self.network), + _extract(node, "count"))) return seq - def get_top_tracks(self, period = PERIOD_OVERALL): + def get_top_tracks(self, period=PERIOD_OVERALL): """Returns the top tracks played by a user. * period: The period of time. Possible values: o PERIOD_OVERALL @@ -3563,12 +3858,15 @@ class User(_BaseObject): seq = [] for node in doc.getElementsByTagName("chart"): - seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) + seq.append((node.getAttribute("from"), node.getAttribute("to"))) return seq - def get_weekly_artist_charts(self, from_date = None, to_date = None): - """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_artist_charts(self, from_date=None, to_date=None): + """ + Returns the weekly artist charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -3585,8 +3883,11 @@ class User(_BaseObject): return seq - def get_weekly_album_charts(self, from_date = None, to_date = None): - """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_album_charts(self, from_date=None, to_date=None): + """ + Returns the weekly album charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -3597,14 +3898,18 @@ class User(_BaseObject): seq = [] for node in doc.getElementsByTagName("album"): - item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) + item = Album( + _extract(node, "artist"), _extract(node, "name"), self.network) weight = _number(_extract(node, "playcount")) seq.append(TopItem(item, weight)) return seq - def get_weekly_track_charts(self, from_date = None, to_date = None): - """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" + def get_weekly_track_charts(self, from_date=None, to_date=None): + """ + Returns the weekly track charts for the week starting from the + from_date value to the to_date value. + """ params = self._get_params() if from_date and to_date: @@ -3615,15 +3920,18 @@ class User(_BaseObject): seq = [] for node in doc.getElementsByTagName("track"): - item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) + item = Track( + _extract(node, "artist"), _extract(node, "name"), self.network) weight = _number(_extract(node, "playcount")) seq.append(TopItem(item, weight)) return seq - def compare_with_user(self, user, shared_artists_limit = None): - """Compare this user with another Last.fm user. - Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...)) + def compare_with_user(self, user, shared_artists_limit=None): + """ + Compare this user with another Last.fm user. + Returns a sequence: + (tasteometer_score, (shared_artist1, shared_artist2, ...)) user: A User object or a username string/unicode object. """ @@ -3659,7 +3967,7 @@ class User(_BaseObject): return _extract(doc, "image") - def get_url(self, domain_name = DOMAIN_ENGLISH): + def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the url of the user page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -3678,7 +3986,7 @@ class User(_BaseObject): name = _url_safe(self.get_name()) - return self.network._get_url(domain_name, "user") %{'name': name} + return self.network._get_url(domain_name, "user") % {'name': name} def get_library(self): """Returns the associated Library object. """ @@ -3687,17 +3995,18 @@ class User(_BaseObject): def get_shouts(self, limit=50, cacheable=False): """ - Returns a sequqence of Shout objects + Returns a sequence of Shout objects """ shouts = [] for node in _collect_nodes(limit, self, "user.getShouts", cacheable): - shouts.append(Shout( - _extract(node, "body"), - User(_extract(node, "author"), self.network), - _extract(node, "date") - ) - ) + shouts.append( + Shout( + _extract(node, "body"), + User(_extract(node, "author"), self.network), + _extract(node, "date") + ) + ) return shouts def shout(self, message): @@ -3710,9 +4019,10 @@ class User(_BaseObject): self._request("user.Shout", False, params) + class AuthenticatedUser(User): def __init__(self, network): - User.__init__(self, "", network); + User.__init__(self, "", network) def _get_params(self): return {"user": self.get_name()} @@ -3732,7 +4042,8 @@ class AuthenticatedUser(User): """ seq = [] - for node in _collect_nodes(limit, self, "user.getRecommendedEvents", cacheable): + for node in _collect_nodes( + limit, self, "user.getRecommendedEvents", cacheable): seq.append(Event(_extract(node, "id"), self.network)) return seq @@ -3744,11 +4055,13 @@ class AuthenticatedUser(User): """ seq = [] - for node in _collect_nodes(limit, self, "user.getRecommendedArtists", cacheable): + for node in _collect_nodes( + limit, self, "user.getRecommendedArtists", cacheable): seq.append(Artist(_extract(node, "name"), self.network)) return seq + class _Search(_BaseObject): """An abstract class. Use one of its derivatives.""" @@ -3788,6 +4101,7 @@ class _Search(_BaseObject): self._last_page_index += 1 return self._retrieve_page(self._last_page_index) + class AlbumSearch(_Search): """Search for an album by name.""" @@ -3802,10 +4116,14 @@ class AlbumSearch(_Search): seq = [] for node in master_node.getElementsByTagName("album"): - seq.append(Album(_extract(node, "artist"), _extract(node, "name"), self.network)) + seq.append(Album( + _extract(node, "artist"), + _extract(node, "name"), + self.network)) return seq + class ArtistSearch(_Search): """Search for an artist by artist name.""" @@ -3825,6 +4143,7 @@ class ArtistSearch(_Search): return seq + class TagSearch(_Search): """Search for a tag by tag name.""" @@ -3845,13 +4164,20 @@ class TagSearch(_Search): return seq + 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.""" + """ + Search for a track by track title. If you don't want to narrow the results + down by specifying the artist name, set it to empty string. + """ def __init__(self, artist_name, track_title, network): - _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, network) + _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.""" @@ -3860,19 +4186,29 @@ class TrackSearch(_Search): seq = [] for node in master_node.getElementsByTagName("track"): - track = Track(_extract(node, "artist"), _extract(node, "name"), self.network) + track = Track( + _extract(node, "artist"), + _extract(node, "name"), + self.network) track.listener_count = _number(_extract(node, "listeners")) seq.append(track) return seq + 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.""" + """ + Search for a venue by its name. If you don't want to narrow the results + down by specifying a country, set it to empty string. + """ def __init__(self, venue_name, country_name, network): - _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, network) + _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.""" @@ -3885,13 +4221,14 @@ class VenueSearch(_Search): return seq + 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 + # 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 @@ -3912,7 +4249,7 @@ class Venue(_BaseObject): self.location = self.info.get('location') def __repr__(self): - return "pylast.Venue(%s, %s)" %(repr(self.id), repr(self.network)) + return "pylast.Venue(%s, %s)" % (repr(self.id), repr(self.network)) @_string_output def __str__(self): @@ -3958,6 +4295,7 @@ class Venue(_BaseObject): return _extract_events_from_doc(doc, self.network) + def md5(text): """Returns the md5 hash of a string.""" @@ -3966,6 +4304,7 @@ def md5(text): return h.hexdigest() + def _unicode(text): if sys.version_info[0] == 3: if type(text) in (bytes, bytearray): @@ -3975,7 +4314,7 @@ def _unicode(text): else: return str(text) - elif sys.version_info[0] ==2: + elif sys.version_info[0] == 2: if type(text) in (str,): return unicode(text, "utf-8") elif type(text) == unicode: @@ -3983,6 +4322,7 @@ def _unicode(text): else: return unicode(text) + def _string(text): """For Python2 routines that can only process str type.""" @@ -4001,10 +4341,10 @@ def _string(text): return text.encode("utf-8") + def _collect_nodes(limit, sender, method_name, cacheable, params=None): """ - Returns a sequence of dom.Node objects about as close to - limit as possible + Returns a sequence of dom.Node objects about as close to limit as possible """ if not params: @@ -4028,7 +4368,8 @@ def _collect_nodes(limit, sender, method_name, cacheable, params=None): 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 ( + not limit or (len(nodes) < limit)): nodes.append(node) if page >= total_pages: @@ -4038,7 +4379,8 @@ def _collect_nodes(limit, sender, method_name, cacheable, params=None): return nodes -def _extract(node, name, index = 0): + +def _extract(node, name, index=0): """Extracts a value from the xml string""" nodes = node.getElementsByTagName(name) @@ -4049,7 +4391,8 @@ def _extract(node, name, index = 0): else: return None -def _extract_element_tree(node, index = 0): + +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 @@ -4072,13 +4415,15 @@ def _extract_element_tree(node, index = 0): targetDict[node.tagName] = {} _recurse_build_tree(node, targetDict[node.tagName]) else: - val = None if node.firstChild is None else _unescape_htmlentity(node.firstChild.data.strip()) + val = None if node.firstChild is None else \ + _unescape_htmlentity(node.firstChild.data.strip()) targetDict[node.tagName] = val return targetDict return _recurse_build_tree(node, {}) -def _extract_all(node, name, limit_count = None): + +def _extract_all(node, name, limit_count=None): """Extracts all the values from the xml string. returning a list.""" seq = [] @@ -4091,20 +4436,24 @@ def _extract_all(node, name, limit_count = None): return seq + def _extract_events_from_doc(doc, network): events = [] for node in doc.getElementsByTagName("event"): events.append(Event(_extract(node, "id"), network)) return events + def _url_safe(text): """Does all kinds of tricks on a text to make it safe to use in a url.""" return url_quote_plus(url_quote_plus(_string(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: @@ -4117,18 +4466,23 @@ def _number(string): except ValueError: return float(string) + def _unescape_htmlentity(string): #string = _unicode(string) mapping = htmlentitydefs.name2codepoint for key in mapping: - string = string.replace("&%s;" %key, unichr(mapping[key])) + string = string.replace("&%s;" % key, unichr(mapping[key])) return string + def extract_items(topitems_or_libraryitems): - """Extracts a sequence of items from a sequence of TopItem or LibraryItem objects.""" + """ + Extracts a sequence of items from a sequence of TopItem or + LibraryItem objects. + """ seq = [] for i in topitems_or_libraryitems: @@ -4136,6 +4490,7 @@ def extract_items(topitems_or_libraryitems): return seq + class ScrobblingError(Exception): def __init__(self, message): Exception.__init__(self) @@ -4145,21 +4500,29 @@ class ScrobblingError(Exception): def __str__(self): return self.message + class BannedClientError(ScrobblingError): def __init__(self): - ScrobblingError.__init__(self, "This version of the client has been banned") + ScrobblingError.__init__( + self, "This version of the client has been banned") + class BadAuthenticationError(ScrobblingError): def __init__(self): ScrobblingError.__init__(self, "Bad authentication token") + class BadTimeError(ScrobblingError): def __init__(self): - ScrobblingError.__init__(self, "Time provided is not close enough to current time") + ScrobblingError.__init__( + self, "Time provided is not close enough to current time") + class BadSessionError(ScrobblingError): def __init__(self): - ScrobblingError.__init__(self, "Bad session id, consider re-handshaking") + ScrobblingError.__init__( + self, "Bad session id, consider re-handshaking") + class _ScrobblerRequest(object): @@ -4189,10 +4552,11 @@ class _ScrobblerRequest(object): "Accept-Charset": "utf-8", "User-Agent": "pylast" + "/" + __version__, "HOST": self.hostname - } + } if self.type == "GET": - connection.request("GET", self.subdir + "?" + data, headers = headers) + connection.request( + "GET", self.subdir + "?" + data, headers=headers) else: connection.request("POST", self.subdir, data, headers) response = _unicode(connection.getresponse().read()) @@ -4202,8 +4566,10 @@ class _ScrobblerRequest(object): return response def _check_response_for_errors(self, response): - """When passed a string response it checks for erros, raising - any exceptions as necessary.""" + """ + When passed a string response it checks for errors, raising any + exceptions as necessary. + """ lines = response.split("\n") status_line = lines[0] @@ -4219,9 +4585,10 @@ class _ScrobblerRequest(object): elif status_line == "BADSESSION": raise BadSessionError() elif status_line.startswith("FAILED "): - reason = status_line[status_line.find("FAILED ")+len("FAILED "):] + reason = status_line[status_line.find("FAILED ") + len("FAILED "):] raise ScrobblingError(reason) + class Scrobbler(object): """A class for scrobbling tracks to Last.fm""" @@ -4243,12 +4610,15 @@ class Scrobbler(object): if self.password and self.username: token = md5(self.password + timestamp) - elif self.network.api_key and self.network.api_secret and self.network.session_key: + elif self.network.api_key and self.network.api_secret and \ + self.network.session_key: if not self.username: - self.username = self.network.get_authenticated_user().get_name() + self.username = self.network.get_authenticated_user()\ + .get_name() token = md5(self.network.api_secret + timestamp) - params = {"hs": "true", "p": "1.2.1", "c": self.client_id, + params = { + "hs": "true", "p": "1.2.1", "c": self.client_id, "v": self.client_version, "u": self.username, "t": timestamp, "a": token} @@ -4257,61 +4627,91 @@ class Scrobbler(object): params["api_key"] = self.network.api_key server = self.network.submission_server - response = _ScrobblerRequest(server, params, self.network, "GET").execute().split("\n") + response = _ScrobblerRequest( + server, params, self.network, "GET").execute().split("\n") self.session_id = response[1] self.nowplaying_url = response[2] self.submissions_url = response[3] - def _get_session_id(self, new = False): - """Returns a handshake. If new is true, then it will be requested from the server - even if one was cached.""" + def _get_session_id(self, new=False): + """ + Returns a handshake. If new is true, then it will be requested from + the server even if one was cached. + """ if not self.session_id or new: self._do_handshake() return self.session_id - def report_now_playing(self, artist, title, album = "", duration = "", track_number = "", mbid = ""): + def report_now_playing( + self, artist, title, album="", duration="", track_number="", + mbid=""): - _deprecation_warning("DeprecationWarning: Use Network.update_now_playing(...) instead") + _deprecation_warning( + "DeprecationWarning: Use Network.update_now_playing(...) instead") - params = {"s": self._get_session_id(), "a": artist, "t": title, + params = { + "s": self._get_session_id(), "a": artist, "t": title, "b": album, "l": duration, "n": track_number, "m": mbid} try: - _ScrobblerRequest(self.nowplaying_url, params, self.network).execute() + _ScrobblerRequest( + self.nowplaying_url, params, self.network + ).execute() except BadSessionError: self._do_handshake() - self.report_now_playing(artist, title, album, duration, track_number, mbid) + self.report_now_playing( + artist, title, album, duration, track_number, mbid) - def scrobble(self, artist, title, time_started, source, mode, duration, album="", track_number="", mbid=""): + def scrobble( + self, artist, title, time_started, source, mode, duration, + album="", track_number="", mbid=""): """Scrobble a track. parameters: artist: Artist name. title: Track title. time_started: UTC timestamp of when the track started playing. source: The source of the track - SCROBBLE_SOURCE_USER: Chosen by the user (the most common value, unless you have a reason for choosing otherwise, use this). - SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1). - SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast). - SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the 5-digit recommendation_key value must be set. + SCROBBLE_SOURCE_USER: Chosen by the user + (the most common value, unless you have a reason for + choosing otherwise, use this). + SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised + broadcast (e.g. Shoutcast, BBC Radio 1). + SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised + recommendation except Last.fm (e.g. Pandora, Launchcast). + SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the + 5-digit recommendation_key value must be set. SCROBBLE_SOURCE_UNKNOWN: Source unknown. mode: The submission mode SCROBBLE_MODE_PLAYED: The track was played. - SCROBBLE_MODE_LOVED: The user manually loved the track (implies a listen) - SCROBBLE_MODE_SKIPPED: The track was skipped (Only if source was Last.fm) - SCROBBLE_MODE_BANNED: The track was banned (Only if source was Last.fm) + SCROBBLE_MODE_LOVED: The user manually loved the track + (implies a listen) + SCROBBLE_MODE_SKIPPED: The track was skipped + (Only if source was Last.fm) + SCROBBLE_MODE_BANNED: The track was banned + (Only if source was Last.fm) duration: Track duration in seconds. album: The album name. track_number: The track number on the album. mbid: MusicBrainz ID. """ - _deprecation_warning("DeprecationWarning: Use Network.scrobble(...) instead") + _deprecation_warning( + "DeprecationWarning: Use Network.scrobble(...) instead") - 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]": _string(album), "n[0]": track_number, "m[0]": mbid} + 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]": _string(album), + "n[0]": track_number, + "m[0]": mbid + } _ScrobblerRequest(self.submissions_url, params, self.network).execute() @@ -4319,11 +4719,13 @@ class Scrobbler(object): """ Scrobble several tracks at once. - tracks: A sequence of a sequence of parameters for each trach. The order of parameters - is the same as if passed to the scrobble() method. + tracks: A sequence of a sequence of parameters for each track. + The order of parameters is the same as if passed to the + scrobble() method. """ - _deprecation_warning("DeprecationWarning: Use Network.scrobble_many(...) instead") + _deprecation_warning( + "DeprecationWarning: Use Network.scrobble_many(...) instead") remainder = [] diff --git a/test_pylast.py b/test_pylast.py index c7bb82c..ef6f4e7 100755 --- a/test_pylast.py +++ b/test_pylast.py @@ -9,11 +9,12 @@ import unittest import pylast + def load_secrets(): secrets_file = "test_pylast.yaml" if os.path.isfile(secrets_file): - import yaml # pip install pyyaml - with open(secrets_file, "r") as f: # see example_test_pylast.yaml + import yaml # pip install pyyaml + with open(secrets_file, "r") as f: # see example_test_pylast.yaml doc = yaml.load(f) else: doc = {} @@ -38,12 +39,12 @@ class TestPyLast(unittest.TestCase): self.username = self.__class__.secrets["username"] password_hash = self.__class__.secrets["password_hash"] - API_KEY = self.__class__.secrets["api_key"] - API_SECRET = self.__class__.secrets["api_secret"] - - self.network = pylast.LastFMNetwork(api_key = API_KEY, api_secret = - API_SECRET, username = self.username, password_hash = password_hash) + API_KEY = self.__class__.secrets["api_key"] + API_SECRET = self.__class__.secrets["api_secret"] + self.network = pylast.LastFMNetwork( + api_key=API_KEY, api_secret=API_SECRET, + username=self.username, password_hash=password_hash) def test_scrobble(self): # Arrange @@ -53,35 +54,36 @@ class TestPyLast(unittest.TestCase): lastfm_user = self.network.get_user(self.username) # Act - self.network.scrobble(artist = artist, title = title, timestamp = timestamp) + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) # Assert - last_scrobble = lastfm_user.get_recent_tracks(limit = 2)[0] # 2 to ignore now-playing + # limit=2 to ignore now-playing: + last_scrobble = lastfm_user.get_recent_tracks(limit=2)[0] self.assertEqual(str(last_scrobble.track.artist), str(artist)) self.assertEqual(str(last_scrobble.track.title), str(title)) self.assertEqual(str(last_scrobble.timestamp), str(timestamp)) - def test_unscrobble(self): # Arrange artist = "Test Artist 2" title = "Test Title 2" timestamp = self.unix_timestamp() - library = pylast.Library(user = self.username, network = self.network) - self.network.scrobble(artist = artist, title = title, timestamp = timestamp) + library = pylast.Library(user=self.username, network=self.network) + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) lastfm_user = self.network.get_user(self.username) # Act - library.remove_scrobble(artist = artist, title = title, timestamp = timestamp) + library.remove_scrobble( + artist=artist, title=title, timestamp=timestamp) # Assert - last_scrobble = lastfm_user.get_recent_tracks(limit = 2)[0] # 2 to ignore now-playing + # limit=2 to ignore now-playing: + last_scrobble = lastfm_user.get_recent_tracks(limit=2)[0] self.assertNotEqual(str(last_scrobble.timestamp), str(timestamp)) - def test_add_album(self): # Arrange - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) album = self.network.get_album("Test Artist", "Test Album") # Act @@ -95,12 +97,11 @@ class TestPyLast(unittest.TestCase): break self.assertTrue(value) - def test_remove_album(self): # Arrange - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) # Pick an artist with plenty of albums - artist = self.network.get_top_artists(limit = 1)[0].item + artist = self.network.get_top_artists(limit=1)[0].item albums = artist.get_top_albums() # Pick a random one to avoid problems running concurrent tests album = choice(albums)[0] @@ -117,11 +118,10 @@ class TestPyLast(unittest.TestCase): break self.assertFalse(value) - def test_add_artist(self): # Arrange artist = "Test Artist 2" - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) # Act library.add_artist(artist) @@ -134,14 +134,13 @@ class TestPyLast(unittest.TestCase): break self.assertTrue(value) - def test_remove_artist(self): # Arrange # Get plenty of artists artists = self.network.get_top_artists() # Pick a random one to avoid problems running concurrent tests my_artist = choice(artists).item - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) library.add_artist(my_artist) # Act @@ -155,7 +154,6 @@ class TestPyLast(unittest.TestCase): break self.assertFalse(value) - def test_get_venue(self): # Arrange venue_name = "Last.fm Office" @@ -168,7 +166,6 @@ class TestPyLast(unittest.TestCase): # Assert self.assertEqual(str(venue.id), "8778225") - def test_get_user_registration(self): # Arrange username = "RJ" @@ -181,7 +178,6 @@ class TestPyLast(unittest.TestCase): # Just check date because of timezones self.assertIn(u"2002-11-20 ", registered) - def test_get_user_unixtime_registration(self): # Arrange username = "RJ" @@ -194,10 +190,10 @@ class TestPyLast(unittest.TestCase): # Just check date because of timezones self.assertEqual(unixtime_registered, u"1037793040") - def test_get_genderless_user(self): # Arrange - lastfm_user = self.network.get_user("test_user") # currently no gender set + # Currently test_user has no gender set: + lastfm_user = self.network.get_user("test_user") # Act gender = lastfm_user.get_gender() @@ -205,10 +201,10 @@ class TestPyLast(unittest.TestCase): # Assert self.assertIsNone(gender) - def test_get_countryless_user(self): # Arrange - lastfm_user = self.network.get_user("test_user") # currently no country set + # Currently test_user has no country set: + lastfm_user = self.network.get_user("test_user") # Act country = lastfm_user.get_country() @@ -216,7 +212,6 @@ class TestPyLast(unittest.TestCase): # Assert self.assertIsNone(country) - def test_love(self): # Arrange artist = "Test Artist" @@ -228,11 +223,10 @@ class TestPyLast(unittest.TestCase): track.love() # Assert - loved = lastfm_user.get_loved_tracks(limit = 1) + loved = lastfm_user.get_loved_tracks(limit=1) self.assertEqual(str(loved[0].track.artist), "Test Artist") self.assertEqual(str(loved[0].track.title), "Test Title") - def test_unlove(self): # Arrange artist = "Test Artist" @@ -245,59 +239,55 @@ class TestPyLast(unittest.TestCase): track.unlove() # Assert - loved = lastfm_user.get_loved_tracks(limit = 1) - if len(loved): # OK to be empty but if not: + loved = lastfm_user.get_loved_tracks(limit=1) + if len(loved): # OK to be empty but if not: self.assertNotEqual(str(loved.track.artist), "Test Artist") self.assertNotEqual(str(loved.track.title), "Test Title") - def test_get_100_albums(self): # Arrange - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) # Act - albums = library.get_albums(limit = 100) + albums = library.get_albums(limit=100) # Assert self.assertGreaterEqual(len(albums), 0) - def test_get_limitless_albums(self): # Arrange - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) # Act - albums = library.get_albums(limit = None) + albums = library.get_albums(limit=None) # Assert self.assertGreaterEqual(len(albums), 0) - def test_user_equals_none(self): # Arrange lastfm_user = self.network.get_user(self.username) # Act - value = (lastfm_user == None) + value = (lastfm_user is None) # Assert self.assertFalse(value) - def test_user_not_equal_to_none(self): # Arrange lastfm_user = self.network.get_user(self.username) # Act - value = (lastfm_user != None) + value = (lastfm_user is not None) # Assert self.assertTrue(value) - def test_now_playing_user_with_no_scrobbles(self): # Arrange - user = self.network.get_user('test-account') # currently has no scrobbles + # Currently test-account has no scrobbles: + user = self.network.get_user('test-account') # Act current_track = user.get_now_playing() @@ -305,10 +295,10 @@ class TestPyLast(unittest.TestCase): # Assert self.assertIsNone(current_track) - def test_love_limits(self): # Arrange - user = self.network.get_user("test-user") # currently at least 23 loved tracks + # Currently test-account has at least 23 loved tracks: + user = self.network.get_user("test-user") # Act/Assert self.assertEqual(len(user.get_loved_tracks(limit=20)), 20) @@ -316,7 +306,6 @@ class TestPyLast(unittest.TestCase): self.assertGreaterEqual(len(user.get_loved_tracks(limit=None)), 23) self.assertGreaterEqual(len(user.get_loved_tracks(limit=0)), 23) - def test_update_now_playing(self): # Arrange artist = "Test Artist" @@ -326,7 +315,8 @@ class TestPyLast(unittest.TestCase): lastfm_user = self.network.get_user(self.username) # Act - self.network.update_now_playing(artist = artist, title = title, album = album, track_number = track_number) + self.network.update_now_playing( + artist=artist, title=title, album=album, track_number=track_number) # Assert current_track = lastfm_user.get_now_playing() @@ -334,33 +324,31 @@ class TestPyLast(unittest.TestCase): self.assertEqual(str(current_track.title), "Test Title") self.assertEqual(str(current_track.artist), "Test Artist") - def test_libre_fm(self): # Arrange - username = self.__class__.secrets["username"] + username = self.__class__.secrets["username"] password_hash = self.__class__.secrets["password_hash"] # Act - network = pylast.LibreFMNetwork(password_hash = password_hash, username = username) - tags = network.get_top_tags(limit = 1) + network = pylast.LibreFMNetwork( + password_hash=password_hash, username=username) + tags = network.get_top_tags(limit=1) # Assert self.assertGreater(len(tags), 0) self.assertEqual(type(tags[0]), pylast.TopItem) - def test_album_tags_are_topitems(self): # Arrange albums = self.network.get_user('RJ').get_top_albums() # Act - tags = albums[0].item.get_top_tags(limit = 1) + tags = albums[0].item.get_top_tags(limit=1) # Assert self.assertGreater(len(tags), 0) self.assertEqual(type(tags[0]), pylast.TopItem) - def helper_is_thing_hashable(self, thing): # Arrange things = set() @@ -379,7 +367,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_is_thing_hashable(album) - def test_artist_is_hashable(self): # Arrange test_artist = self.network.get_artist("Test Artist") @@ -389,7 +376,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_is_thing_hashable(artist) - def test_country_is_hashable(self): # Arrange country = self.network.get_country("Italy") @@ -397,24 +383,21 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_is_thing_hashable(country) - - def test_country_is_hashable(self): + def test_metro_is_hashable(self): # Arrange metro = self.network.get_metro("Helsinki", "Finland") # Act/Assert self.helper_is_thing_hashable(metro) - def test_event_is_hashable(self): # Arrange user = self.network.get_user("RJ") - event = user.get_past_events(limit = 1)[0] + event = user.get_past_events(limit=1)[0] # Act/Assert self.helper_is_thing_hashable(event) - def test_group_is_hashable(self): # Arrange group = self.network.get_group("Audioscrobbler Beta") @@ -422,31 +405,28 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_is_thing_hashable(group) - def test_library_is_hashable(self): # Arrange - library = pylast.Library(user = self.username, network = self.network) + library = pylast.Library(user=self.username, network=self.network) # Act/Assert self.helper_is_thing_hashable(library) - def test_playlist_is_hashable(self): # Arrange - playlist = pylast.Playlist(user = "RJ", id = "1k1qp_doglist", network = self.network) + playlist = pylast.Playlist( + user="RJ", id="1k1qp_doglist", network=self.network) # Act/Assert self.helper_is_thing_hashable(playlist) - def test_tag_is_hashable(self): # Arrange - tag = self.network.get_top_tags(limit = 1)[0] + tag = self.network.get_top_tags(limit=1)[0] # Act/Assert self.helper_is_thing_hashable(tag) - def test_track_is_hashable(self): # Arrange artist = self.network.get_artist("Test Artist") @@ -456,7 +436,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_is_thing_hashable(track) - def test_user_is_hashable(self): # Arrange artist = self.network.get_artist("Test Artist") @@ -466,24 +445,22 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_is_thing_hashable(user) - def test_venue_is_hashable(self): # Arrange - venue_id = "8778225" # Last.fm office + venue_id = "8778225" # Last.fm office venue = pylast.Venue(venue_id, self.network) # Act/Assert self.helper_is_thing_hashable(venue) - def test_xspf_is_hashable(self): # Arrange - xspf = pylast.XSPF(uri = "lastfm://playlist/1k1qp_doglist", network = self.network) + xspf = pylast.XSPF( + uri="lastfm://playlist/1k1qp_doglist", network=self.network) # Act/Assert self.helper_is_thing_hashable(xspf) - def test_invalid_xml(self): # Arrange # Currently causes PCDATA invalid Char value 25 @@ -497,12 +474,13 @@ class TestPyLast(unittest.TestCase): # Assert self.assertGreaterEqual(int(total), 0) - def test_user_play_count_in_track_info(self): # Arrange artist = "Test Artist" title = "Test Title" - track = pylast.Track(artist = artist, title = title, network = self.network, username = self.username) + track = pylast.Track( + artist=artist, title=title, + network=self.network, username=self.username) # Act count = track.get_userplaycount() @@ -510,12 +488,13 @@ class TestPyLast(unittest.TestCase): # Assert self.assertGreaterEqual(count, 0) - def test_user_loved_in_track_info(self): # Arrange artist = "Test Artist" title = "Test Title" - track = pylast.Track(artist = artist, title = title, network = self.network, username = self.username) + track = pylast.Track( + artist=artist, title=title, + network=self.network, username=self.username) # Act loved = track.get_userloved() @@ -525,29 +504,27 @@ class TestPyLast(unittest.TestCase): self.assertIsInstance(loved, bool) self.assertNotIsInstance(loved, str) - def test_album_in_recent_tracks(self): # Arrange lastfm_user = self.network.get_user(self.username) # Act - track = lastfm_user.get_recent_tracks(limit = 2)[0] # 2 to ignore now-playing + # limit=2 to ignore now-playing: + track = lastfm_user.get_recent_tracks(limit=2)[0] # Assert self.assertTrue(hasattr(track, 'album')) - def test_album_in_artist_tracks(self): # Arrange lastfm_user = self.network.get_user(self.username) # Act - track = lastfm_user.get_artist_tracks(artist = "Test Artist")[0] + track = lastfm_user.get_artist_tracks(artist="Test Artist")[0] # Assert self.assertTrue(hasattr(track, 'album')) - def test_enable_rate_limiting(self): # Arrange self.assertFalse(self.network.is_rate_limited()) @@ -565,7 +542,6 @@ class TestPyLast(unittest.TestCase): self.assertTrue(self.network.is_rate_limited()) self.assertGreaterEqual(now - then, 0.2) - def test_disable_rate_limiting(self): # Arrange self.network.enable_rate_limit() @@ -573,32 +549,28 @@ class TestPyLast(unittest.TestCase): # Act self.network.disable_rate_limit() - then = time.time() # Make some network call, limit not applied first time self.network.get_user(self.username) # Make a second network call, limiting should be applied self.network.get_top_artists() - now = time.time() # Assert self.assertFalse(self.network.is_rate_limited()) - - # Commented out because (a) it'll take a long time and - # (b) it strangely fails due Last.fm's complaining of hitting the rate limit, - # even when limited to one call per second. The ToS allows 5 calls per second. + # Commented out because (a) it'll take a long time and (b) it strangely + # fails due Last.fm's complaining of hitting the rate limit, even when + # limited to one call per second. The ToS allows 5 calls per second. # def test_get_all_scrobbles(self): # # Arrange # lastfm_user = self.network.get_user("RJ") # self.network.enable_rate_limit() # this is going to be slow... # # Act - # tracks = lastfm_user.get_recent_tracks(limit = None) + # tracks = lastfm_user.get_recent_tracks(limit=None) # # Assert # self.assertGreaterEqual(len(tracks), 0) - def helper_past_events_have_valid_ids(self, thing): # Act events = thing.get_past_events() @@ -606,7 +578,6 @@ class TestPyLast(unittest.TestCase): # Assert self.helper_assert_events_have_valid_ids(events) - def helper_upcoming_events_have_valid_ids(self, thing): # Act events = thing.get_upcoming_events() @@ -614,14 +585,13 @@ class TestPyLast(unittest.TestCase): # Assert self.helper_assert_events_have_valid_ids(events) - def helper_assert_events_have_valid_ids(self, events): # Assert - self.assertGreaterEqual(len(events), 1) # if fails, add past/future event for user/Test Artist - for event in events[:2]: # checking first two should be enough + # If fails, add past/future event for user/Test Artist: + self.assertGreaterEqual(len(events), 1) + for event in events[:2]: # checking first two should be enough self.assertIsInstance(event.get_headliner(), pylast.Artist) - def test_artist_upcoming_events_returns_valid_ids(self): # Arrange artist = pylast.Artist("Test Artist", self.network) @@ -629,7 +599,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_upcoming_events_have_valid_ids(artist) - def test_user_past_events_returns_valid_ids(self): # Arrange lastfm_user = self.network.get_user(self.username) @@ -637,7 +606,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_past_events_have_valid_ids(lastfm_user) - def test_user_recommended_events_returns_valid_ids(self): # Arrange lastfm_user = self.network.get_user(self.username) @@ -648,7 +616,6 @@ class TestPyLast(unittest.TestCase): # Assert self.helper_assert_events_have_valid_ids(events) - def test_user_upcoming_events_returns_valid_ids(self): # Arrange lastfm_user = self.network.get_user(self.username) @@ -656,25 +623,22 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_upcoming_events_have_valid_ids(lastfm_user) - def test_venue_past_events_returns_valid_ids(self): # Arrange - venue_id = "8778225" # Last.fm office + venue_id = "8778225" # Last.fm office venue = pylast.Venue(venue_id, self.network) # Act/Assert self.helper_past_events_have_valid_ids(venue) - def test_venue_upcoming_events_returns_valid_ids(self): # Arrange - venue_id = "8778225" # Last.fm office + venue_id = "8778225" # Last.fm office venue = pylast.Venue(venue_id, self.network) # Act/Assert self.helper_upcoming_events_have_valid_ids(venue) - def test_pickle(self): # Arrange import pickle @@ -689,31 +653,28 @@ class TestPyLast(unittest.TestCase): # Assert self.assertEqual(lastfm_user, loaded_user) - def test_bio_content(self): # Arrange artist = pylast.Artist("Test Artist", self.network) # Act - bio = artist.get_bio_content(language = "en") + bio = artist.get_bio_content(language="en") # Assert self.assertIsNotNone(bio) self.assertGreaterEqual(len(bio), 1) - def test_bio_summary(self): # Arrange artist = pylast.Artist("Test Artist", self.network) # Act - bio = artist.get_bio_summary(language = "en") + bio = artist.get_bio_summary(language="en") # Assert self.assertIsNotNone(bio) self.assertGreaterEqual(len(bio), 1) - def test_album_wiki_content(self): # Arrange album = pylast.Album("Test Artist", "Test Album", self.network) @@ -725,7 +686,6 @@ class TestPyLast(unittest.TestCase): self.assertIsNotNone(wiki) self.assertGreaterEqual(len(wiki), 1) - def test_album_wiki_summary(self): # Arrange album = pylast.Album("Test Artist", "Test Album", self.network) @@ -737,7 +697,6 @@ class TestPyLast(unittest.TestCase): self.assertIsNotNone(wiki) self.assertGreaterEqual(len(wiki), 1) - def test_track_wiki_content(self): # Arrange track = pylast.Track("Test Artist", "Test Title", self.network) @@ -749,7 +708,6 @@ class TestPyLast(unittest.TestCase): self.assertIsNotNone(wiki) self.assertGreaterEqual(len(wiki), 1) - def test_track_wiki_summary(self): # Arrange track = pylast.Track("Test Artist", "Test Title", self.network) @@ -761,7 +719,6 @@ class TestPyLast(unittest.TestCase): self.assertIsNotNone(wiki) self.assertGreaterEqual(len(wiki), 1) - def test_lastfm_network_name(self): # Act name = str(self.network) @@ -769,7 +726,6 @@ class TestPyLast(unittest.TestCase): # Assert self.assertEqual(name, "Last.fm Network") - def test_artist_get_images_deprecated(self): # Arrange artist = self.network.get_artist("Test Artist") @@ -778,7 +734,6 @@ class TestPyLast(unittest.TestCase): with self.assertRaisesRegexp(pylast.WSError, 'deprecated'): artist.get_images() - def helper_validate_results(self, a, b, c): # Assert self.assertIsNotNone(a) @@ -790,21 +745,19 @@ class TestPyLast(unittest.TestCase): self.assertEqual(a, b) self.assertEqual(b, c) - def helper_validate_cacheable(self, thing, function_name): # Arrange # get thing.function_name() func = getattr(thing, function_name, None) # Act - result1 = func(limit = 1, cacheable = False) - result2 = func(limit = 1, cacheable = True) - result3 = func(limit = 1) + result1 = func(limit=1, cacheable=False) + result2 = func(limit=1, cacheable=True) + result3 = func(limit=1) # Assert self.helper_validate_results(result1, result2, result3) - def test_cacheable_artist_get_shouts(self): # Arrange artist = self.network.get_artist("Test Artist") @@ -812,16 +765,14 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_validate_cacheable(artist, "get_shouts") - def test_cacheable_event_get_shouts(self): # Arrange user = self.network.get_user("RJ") - event = user.get_past_events(limit = 1)[0] + event = user.get_past_events(limit=1)[0] # Act/Assert self.helper_validate_cacheable(event, "get_shouts") - def test_cacheable_track_get_shouts(self): # Arrange track = self.network.get_top_tracks()[0].item @@ -829,7 +780,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_validate_cacheable(track, "get_shouts") - def test_cacheable_group_get_members(self): # Arrange group = self.network.get_group("Audioscrobbler Beta") @@ -837,7 +787,6 @@ class TestPyLast(unittest.TestCase): # Act/Assert self.helper_validate_cacheable(group, "get_members") - def test_cacheable_library(self): # Arrange library = pylast.Library(self.username, self.network) @@ -847,20 +796,18 @@ class TestPyLast(unittest.TestCase): self.helper_validate_cacheable(library, "get_artists") self.helper_validate_cacheable(library, "get_tracks") - def test_cacheable_user_artist_tracks(self): # Arrange lastfm_user = self.network.get_authenticated_user() # Act - result1 = lastfm_user.get_artist_tracks(artist = "Test Artist", cacheable = False) - result2 = lastfm_user.get_artist_tracks(artist = "Test Artist", cacheable = True) - result3 = lastfm_user.get_artist_tracks(artist = "Test Artist") + result1 = lastfm_user.get_artist_tracks("Test Artist", cacheable=False) + result2 = lastfm_user.get_artist_tracks("Test Artist", cacheable=True) + result3 = lastfm_user.get_artist_tracks("Test Artist") # Assert self.helper_validate_results(result1, result2, result3) - def test_cacheable_user(self): # Arrange lastfm_user = self.network.get_authenticated_user() @@ -874,11 +821,11 @@ class TestPyLast(unittest.TestCase): self.helper_validate_cacheable(lastfm_user, "get_recommended_events") self.helper_validate_cacheable(lastfm_user, "get_shouts") - def test_geo_get_events_in_location(self): # Arrange # Act - events = self.network.get_geo_events(location = "London", tag = "blues", limit = 1) + events = self.network.get_geo_events( + location="London", tag="blues", limit=1) # Assert self.assertEqual(len(events), 1) @@ -886,11 +833,11 @@ class TestPyLast(unittest.TestCase): self.assertEqual(type(event), pylast.Event) self.assertEqual(event.get_venue().location['city'], "London") - def test_geo_get_events_in_latlong(self): # Arrange # Act - events = self.network.get_geo_events(lat = 40.67, long = -73.94, distance = 5, limit = 1) + events = self.network.get_geo_events( + lat=40.67, long=-73.94, distance=5, limit=1) # Assert self.assertEqual(len(events), 1) @@ -898,11 +845,11 @@ class TestPyLast(unittest.TestCase): self.assertEqual(type(event), pylast.Event) self.assertEqual(event.get_venue().location['city'], "New York") - def test_geo_get_events_festival(self): # Arrange # Act - events = self.network.get_geo_events(location = "Reading", festivalsonly = True, limit = 1) + events = self.network.get_geo_events( + location="Reading", festivalsonly=True, limit=1) # Assert self.assertEqual(len(events), 1) @@ -910,7 +857,6 @@ class TestPyLast(unittest.TestCase): self.assertEqual(type(event), pylast.Event) self.assertEqual(event.get_venue().location['city'], "Reading") - def test_get_metro_weekly_chart_dates(self): # Arrange # Act @@ -922,8 +868,7 @@ class TestPyLast(unittest.TestCase): (start, end) = dates[0] self.assertLess(start, end) - - def helper_geo_chart(self, function_name, expected_type = pylast.Artist): + def helper_geo_chart(self, function_name, expected_type=pylast.Artist): # Arrange metro = self.network.get_metro("Madrid", "Spain") dates = self.network.get_metro_weekly_chart_dates() @@ -933,77 +878,71 @@ class TestPyLast(unittest.TestCase): func = getattr(metro, function_name, None) # Act - chart = func(from_date = from_date, to_date = to_date, limit = 1) + chart = func(from_date=from_date, to_date=to_date, limit=1) # Assert self.assertEqual(len(chart), 1) self.assertEqual(type(chart[0]), pylast.TopItem) self.assertEqual(type(chart[0].item), expected_type) - def test_get_metro_artist_chart(self): # Arrange/Act/Assert self.helper_geo_chart("get_artist_chart") - def test_get_metro_hype_artist_chart(self): # Arrange/Act/Assert self.helper_geo_chart("get_hype_artist_chart") - def test_get_metro_unique_artist_chart(self): # Arrange/Act/Assert self.helper_geo_chart("get_unique_artist_chart") - def test_get_metro_track_chart(self): # Arrange/Act/Assert - self.helper_geo_chart("get_track_chart", expected_type = pylast.Track) - + self.helper_geo_chart("get_track_chart", expected_type=pylast.Track) def test_get_metro_hype_track_chart(self): # Arrange/Act/Assert - self.helper_geo_chart("get_hype_track_chart", expected_type = pylast.Track) - + self.helper_geo_chart( + "get_hype_track_chart", expected_type=pylast.Track) def test_get_metro_unique_track_chart(self): # Arrange/Act/Assert - self.helper_geo_chart("get_unique_track_chart", expected_type = pylast.Track) - + self.helper_geo_chart( + "get_unique_track_chart", expected_type=pylast.Track) def test_geo_get_metros(self): # Arrange # Act - metros = self.network.get_metros(country = "Poland") + metros = self.network.get_metros(country="Poland") # Assert self.assertGreaterEqual(len(metros), 1) self.assertEqual(type(metros[0]), pylast.Metro) self.assertEqual(metros[0].get_country(), "Poland") - def test_geo_get_top_artists(self): # Arrange # Act - artists = self.network.get_geo_top_artists(country = "United Kingdom", limit = 1) + artists = self.network.get_geo_top_artists( + country="United Kingdom", limit=1) # Assert self.assertEqual(len(artists), 1) self.assertEqual(type(artists[0]), pylast.TopItem) self.assertEqual(type(artists[0].item), pylast.Artist) - def test_geo_get_top_tracks(self): # Arrange # Act - tracks = self.network.get_geo_top_tracks(country = "United Kingdom", location = "Manchester", limit = 1) + tracks = self.network.get_geo_top_tracks( + country="United Kingdom", location="Manchester", limit=1) # Assert self.assertEqual(len(tracks), 1) self.assertEqual(type(tracks[0]), pylast.TopItem) self.assertEqual(type(tracks[0].item), pylast.Track) - def test_metro_class(self): # Arrange # Act @@ -1014,13 +953,14 @@ class TestPyLast(unittest.TestCase): self.assertEqual(metro.get_country(), "Norway") self.assertEqual(str(metro), "Bergen, Norway") self.assertEqual(metro, pylast.Metro("Bergen", "Norway", self.network)) - self.assertNotEqual(metro, pylast.Metro("Wellington", "New Zealand", self.network)) - + self.assertNotEqual( + metro, + pylast.Metro("Wellington", "New Zealand", self.network)) def test_get_album_play_links(self): # Arrange - album1 = self.network.get_album(artist = "Portishead", title = "Dummy") - album2 = self.network.get_album(artist = "Radiohead", title = "OK Computer") + album1 = self.network.get_album("Portishead", "Dummy") + album2 = self.network.get_album("Radiohead", "OK Computer") albums = [album1, album2] # Act @@ -1032,7 +972,6 @@ class TestPyLast(unittest.TestCase): self.assertIn("spotify:album:", links[0]) self.assertIn("spotify:album:", links[1]) - def test_get_artist_play_links(self): # Arrange artists = ["Portishead", "Radiohead"] @@ -1045,11 +984,10 @@ class TestPyLast(unittest.TestCase): self.assertIn("spotify:artist:", links[0]) self.assertIn("spotify:artist:", links[1]) - def test_get_track_play_links(self): # Arrange - track1 = self.network.get_track(artist = "Portishead", title = "Mysterons") - track2 = self.network.get_track(artist = "Radiohead", title = "Creep") + track1 = self.network.get_track(artist="Portishead", title="Mysterons") + track2 = self.network.get_track(artist="Radiohead", title="Creep") tracks = [track1, track2] # Act @@ -1061,7 +999,6 @@ class TestPyLast(unittest.TestCase): self.assertIn("spotify:track:", links[0]) self.assertIn("spotify:track:", links[1]) - def helper_only_one_thing_in_top_list(self, things, expected_type): # Assert self.assertEqual(len(things), 1) @@ -1074,34 +1011,31 @@ class TestPyLast(unittest.TestCase): user = self.network.get_user("RJ") # Act - tags = user.get_top_tags(limit = 1) + tags = user.get_top_tags(limit=1) # Assert self.helper_only_one_thing_in_top_list(tags, pylast.Tag) - def test_network_get_top_artists_with_limit(self): # Arrange # Act - artists = self.network.get_top_artists(limit = 1) + artists = self.network.get_top_artists(limit=1) # Assert self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - def test_network_get_top_tags_with_limit(self): # Arrange # Act - tags = self.network.get_top_tags(limit = 1) + tags = self.network.get_top_tags(limit=1) # Assert self.helper_only_one_thing_in_top_list(tags, pylast.Tag) - def test_network_get_top_tracks_with_limit(self): # Arrange # Act - tracks = self.network.get_top_tracks(limit = 1) + tracks = self.network.get_top_tracks(limit=1) # Assert self.helper_only_one_thing_in_top_list(tracks, pylast.Track)