diff --git a/.travis.yml b/.travis.yml index 9bd5c36..e10837f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: python +cache: pip + env: global: - secure: ivg6II471E9HV8xyqnawLIuP/sZ0J63Y+BC0BQcRVKtLn/K3zmD1ozM3TFL9S549Nxd0FqDKHXJvXsgaTGIDpK8sxE2AMKV5IojyM0iAVuN7YjPK9vwSlRw1u0EysPMFqxOZVQnoDyHrSGIUrP/VMdnhBu6dbUX0FyEkvZshXhY= @@ -14,17 +16,17 @@ env: matrix: include: - python: 2.7 - env: TOXENV=lint + env: TOXENV=py2lint - python: 2.7 env: TOXENV=py27 + - python: 3.6 + env: TOXENV=py3lint - python: 3.6 env: TOXENV=py36 - python: 3.5 env: TOXENV=py35 - python: 3.4 env: TOXENV=py34 - - python: 3.3 - env: TOXENV=py33 - python: pypy3 env: TOXENV=pypy3 - python: pypy @@ -37,7 +39,7 @@ matrix: sudo: false install: -- travis_retry pip install tox==2.1.1 +- travis_retry pip install tox - travis_retry pip install coverage script: tox diff --git a/COPYING b/COPYING index eec88ff..c4ff845 100644 --- a/COPYING +++ b/COPYING @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 -http://www.apache.org/licenses/ +https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION diff --git a/LICENSE.txt b/LICENSE.txt index 8dada3e..9b259bd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -192,7 +192,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md index a988b94..53b5d12 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ pyLast ====== -[![Build status](https://travis-ci.org/pylast/pylast.svg?branch=develop)](https://travis-ci.org/pylast/pylast) -[![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.python.org/pypi/pylast/) - +[![Build status](https://travis-ci.org/pylast/pylast.svg?branch=develop)](https://travis-ci.org/pylast/pylast) +[![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.python.org/pypi/pylast/) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.python.org/pypi/pylast/) [![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/develop/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) [![Coverage (Coveralls)](https://coveralls.io/repos/github/pylast/pylast/badge.svg?branch=develop)](https://coveralls.io/github/pylast/pylast?branch=develop) [![Code health](https://landscape.io/github/pylast/pylast/develop/landscape.svg)](https://landscape.io/github/hugovk/pylast/develop) -A Python interface to [Last.fm](http://www.last.fm/) and other API-compatible websites such as [Libre.fm](http://libre.fm/). +A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites such as [Libre.fm](https://libre.fm/). -Try using the pydoc utility for help on usage or see [test_pylast.py](tests/test_pylast.py) for examples. +Use the pydoc utility for help on usage or see [tests/](tests/) for examples. Installation ------------ @@ -20,6 +20,13 @@ Install via pip: pip install pylast +Note: + +* pyLast >= 2.0.0 supports Python 2.7.10+ and 3.4, 3.5, 3.6. +* pyLast >= 1.7.0 < 2.0.0 supports Python 2.7, 3.3, 3.4, 3.5, 3.6. +* pyLast >= 1.0.0 < 1.7.0 supports Python 2.7, 3.3, 3.4. +* pyLast >= 0.5 < 1.0.0 supports Python 2, 3. +* pyLast < 0.5 supports Python 2. Features -------- @@ -43,7 +50,7 @@ Here's some simple code example to get you started. In order to create any objec import pylast # You have to have your own unique two values for API_KEY and API_SECRET -# Obtain yours from http://www.last.fm/api/account/create for Last.fm +# Obtain yours from https://www.last.fm/api/account/create for Last.fm API_KEY = "b25b959554ed76058ac220b7b2e0a026" # this is a sample key API_SECRET = "425b55975eed76058ac220b7b4e8a054" @@ -67,12 +74,12 @@ track.add_tags(("awesome", "favorite")) # to get more help about anything and see examples of how it works ``` -More examples in hugovk/lastfm-tools and [test_pylast.py](test_pylast.py). +More examples in hugovk/lastfm-tools and [tests/](tests/). Testing ------- -[tests/test_pylast.py](tests/test_pylast.py) contains integration tests with Last.fm, and plenty of code examples. Unit tests are also in the [tests/](tests/) directory. +The [tests/](tests/) directory contains integration and unit tests with Last.fm, and plenty of code examples. For integration tests you need a test account at Last.fm that will become cluttered with test data, and an API key and secret. Either copy [example_test_pylast.yaml](example_test_pylast.yaml) to test_pylast.yaml and fill out the credentials, or set them as environment variables like: @@ -86,17 +93,17 @@ export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE To run all unit and integration tests: ```sh pip install pytest flaky mock -py.test +pytest ``` Or run just one test case: ```sh -py.test -k test_scrobble +pytest -k test_scrobble ``` To run with coverage: ```sh -py.test -v --cov pylast --cov-report term-missing +pytest -v --cov pylast --cov-report term-missing coverage report # for command-line report coverage html # for HTML report open htmlcov/index.html diff --git a/pylast/__init__.py b/pylast/__init__.py index c83c05f..96ca64b 100644 --- a/pylast/__init__.py +++ b/pylast/__init__.py @@ -10,7 +10,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -20,59 +20,35 @@ # # https://github.com/pylast/pylast -import hashlib from xml.dom import minidom, Node -import xml.dom -import time -import shelve -import tempfile -import sys import collections -import warnings -import re +import hashlib +import shelve import six +import ssl +import sys +import tempfile +import time +import xml.dom __version__ = '1.9.0' -__author__ = 'Amr Hassan, hugovk' -__copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2017 hugovk" +__author__ = 'Amr Hassan, hugovk, Mice Pápai' +__copyright__ = ('Copyright (C) 2008-2010 Amr Hassan, 2013-2017 hugovk, ' + '2017 Mice Pápai') __license__ = "apache2" __email__ = 'amr.hassan@gmail.com' -def _deprecation_warning(message): - warnings.warn(message, DeprecationWarning) - - -def _can_use_ssl_securely(): - # Python 3.3 doesn't support create_default_context() but can be made to - # work sanely. - # <2.7.9 and <3.2 never did any SSL verification so don't do SSL there. - # >3.4 and >2.7.9 has sane defaults so use SSL there. - v = sys.version_info - return v > (3, 3) or ((2, 7, 9) < v < (3, 0)) - - -if _can_use_ssl_securely(): - import ssl - if sys.version_info[0] == 3: - if _can_use_ssl_securely(): - from http.client import HTTPSConnection - else: - from http.client import HTTPConnection import html.entities as htmlentitydefs - from urllib.parse import splithost as url_split_host + from http.client import HTTPSConnection from urllib.parse import quote_plus as url_quote_plus unichr = chr elif sys.version_info[0] == 2: - if _can_use_ssl_securely(): - from httplib import HTTPSConnection - else: - from httplib import HTTPConnection import htmlentitydefs - from urllib import splithost as url_split_host + from httplib import HTTPSConnection from urllib import quote_plus as url_quote_plus STATUS_INVALID_SERVICE = 2 @@ -90,10 +66,6 @@ STATUS_INVALID_SIGNATURE = 13 STATUS_TOKEN_UNAUTHORIZED = 14 STATUS_TOKEN_EXPIRED = 15 -EVENT_ATTENDING = '0' -EVENT_MAYBE_ATTENDING = '1' -EVENT_NOT_ATTENDING = '2' - PERIOD_OVERALL = 'overall' PERIOD_7DAYS = '7day' PERIOD_1MONTH = '1month' @@ -124,9 +96,6 @@ IMAGES_ORDER_POPULARITY = "popularity" IMAGES_ORDER_DATE = "dateadded" -USER_MALE = 'Male' -USER_FEMALE = 'Female' - SCROBBLE_SOURCE_USER = "P" SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R" SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E" @@ -138,70 +107,8 @@ SCROBBLE_MODE_LOVED = "L" 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))) - -XML_ILLEGAL = re.compile(RE_XML_ILLEGAL) - -# Python <=3.3 doesn't support create_default_context() -# <2.7.9 and <3.2 never did any SSL verification -# FIXME This can be removed after 2017-09 when 3.3 is no longer supported and -# pypy3 uses 3.4 or later, see -# https://en.wikipedia.org/wiki/CPython#Version_history -if sys.version_info[0] == 3 and sys.version_info[1] == 3: - import certifi - SSL_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLSv1) - SSL_CONTEXT.verify_mode = ssl.CERT_REQUIRED - SSL_CONTEXT.options |= ssl.OP_NO_COMPRESSION - # Intermediate from https://wiki.mozilla.org/Security/Server_Side_TLS - # Create the cipher string - cipher_string = """ - ECDHE-ECDSA-CHACHA20-POLY1305 - ECDHE-RSA-CHACHA20-POLY1305 - ECDHE-ECDSA-AES128-GCM-SHA256 - ECDHE-RSA-AES128-GCM-SHA256 - ECDHE-ECDSA-AES256-GCM-SHA384 - ECDHE-RSA-AES256-GCM-SHA384 - DHE-RSA-AES128-GCM-SHA256 - DHE-RSA-AES256-GCM-SHA384 - ECDHE-ECDSA-AES128-SHA256 - ECDHE-RSA-AES128-SHA256 - ECDHE-ECDSA-AES128-SHA - ECDHE-RSA-AES256-SHA384 - ECDHE-RSA-AES128-SHA - ECDHE-ECDSA-AES256-SHA384 - ECDHE-ECDSA-AES256-SHA - ECDHE-RSA-AES256-SHA - DHE-RSA-AES128-SHA256 - DHE-RSA-AES128-SHA - DHE-RSA-AES256-SHA256 - DHE-RSA-AES256-SHA - ECDHE-ECDSA-DES-CBC3-SHA - ECDHE-RSA-DES-CBC3-SHA - EDH-RSA-DES-CBC3-SHA - AES128-GCM-SHA256 - AES256-GCM-SHA384 - AES128-SHA256 - AES256-SHA256 - AES128-SHA - AES256-SHA - DES-CBC3-SHA - !DSS - """ - cipher_string = ' '.join(cipher_string.split()) - SSL_CONTEXT.set_ciphers(cipher_string) - SSL_CONTEXT.load_verify_locations(certifi.where()) - # Python >3.4 and >2.7.9 has sane defaults -elif sys.version_info > (3, 4) or ((2, 7, 9) < sys.version_info < (3, 0)): - SSL_CONTEXT = ssl.create_default_context() +SSL_CONTEXT = ssl.create_default_context() class _Network(object): @@ -212,8 +119,7 @@ class _Network(object): def __init__( self, name, homepage, ws_server, api_key, api_secret, session_key, - submission_server, username, password_hash, domain_names, urls, - token=None): + username, password_hash, domain_names, urls, token=None): """ name: the name of the network homepage: the homepage URL @@ -221,8 +127,6 @@ class _Network(object): api_key: a provided API_KEY api_secret: a provided API_SECRET session_key: a generated session_key or None - submission_server: the URL of the server to which tracks are - submitted (scrobbled) username: a username of a valid user password_hash: the output of pylast.md5(password) where password is the user's password @@ -248,7 +152,6 @@ class _Network(object): self.api_key = api_key self.api_secret = api_secret self.session_key = session_key - self.submission_server = submission_server self.username = username self.password_hash = password_hash self.domain_names = domain_names @@ -311,20 +214,6 @@ class _Network(object): return Country(country_name, self) - def get_metro(self, metro_name, country_name): - """ - Returns a metro object - """ - - return Metro(metro_name, country_name, self) - - def get_group(self, name): - """ - Returns a Group object - """ - - return Group(name, self) - def get_user(self, username): """ Returns a user object @@ -339,40 +228,6 @@ class _Network(object): return Tag(name, self) - def get_scrobbler(self, client_id, client_version): - """ - Returns a Scrobbler object used for submitting tracks to the server - - Quote from https://www.last.fm/api/submissions: - ======== - Client identifiers are used to provide a centrally managed database - of the client versions, allowing clients to be banned if they are - found to be behaving undesirably. The client ID is associated with - a version number on the server, however these are only incremented - if a client is banned and do not have to reflect the version of the - actual client application. - - During development, clients which have not been allocated an - identifier should use the identifier tst, with a version number of - 1.0. Do not distribute code or client implementations which use - this test identifier. Do not use the identifiers used by other - clients. - ========= - - To obtain a new client identifier please contact: - * Last.fm: submissions@last.fm - * # TODO: list others - - ...and provide us with the name of your client and its homepage - address. - """ - - _deprecation_warning( - "Use _Network.scrobble(...), _Network.scrobble_many(...)," - " and Network.update_now_playing(...) instead") - - return Scrobbler(self, client_id, client_version) - def _get_language_domain(self, domain_language): """ Returns the mapped domain name of the network to a DOMAIN_* value @@ -389,7 +244,7 @@ class _Network(object): """ Returns an (API_KEY, API_SECRET, SESSION_KEY) tuple. """ - return (self.api_key, self.api_secret, self.session_key) + return self.api_key, self.api_secret, self.session_key def _delay_call(self): """ @@ -407,24 +262,6 @@ class _Network(object): self.last_call_time = now - def create_new_playlist(self, title, description): - """ - Creates a playlist for the authenticated user and returns it - title: The title of the new playlist. - description: The description of the new playlist. - """ - - params = {} - params['title'] = title - params['description'] = description - - doc = _Request(self, 'playlist.create', params).execute(False) - - e_id = doc.getElementsByTagName("id")[0].firstChild.data - user = doc.getElementsByTagName('playlists')[0].getAttribute('user') - - return Playlist(user, e_id, self) - def get_top_artists(self, limit=None, cacheable=True): """Returns the most played artists as a sequence of TopItem objects.""" @@ -472,90 +309,6 @@ class _Network(object): return seq - def get_geo_events( - self, longitude=None, latitude=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: - longitude (Optional) : Specifies a longitude value to retrieve events - for (service returns nearby events by default) - latitude (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. - """ - - params = {} - - if longitude: - params["long"] = longitude - if latitude: - params["lat"] = latitude - if location: - params["location"] = location - if limit: - params["limit"] = limit - if distance: - params["distance"] = distance - if tag: - params["tag"] = tag - if festivalsonly: - params["festivalsonly"] = 1 - elif not festivalsonly: - params["festivalsonly"] = 0 - - doc = _Request(self, "geo.getEvents", params).execute(cacheable) - - 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. - """ - - doc = _Request(self, "geo.getMetroWeeklyChartlist").execute(cacheable) - - seq = [] - for node in doc.getElementsByTagName("chart"): - 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. - Parameters: - country (Optional) : Optionally restrict the results to those Metros - from a particular country, as defined by the ISO 3166-1 country - names standard. - """ - params = {} - - if country: - params["country"] = country - - doc = _Request(self, "geo.getMetros", params).execute(cacheable) - - metros = doc.getElementsByTagName("metro") - seq = [] - - for metro in metros: - name = _extract(metro, "name") - country = _extract(metro, "country") - - seq.append(Metro(name, country, self)) - - return seq - def get_geo_top_artists(self, country, limit=None, cacheable=True): """Get the most popular artists on Last.fm by country. Parameters: @@ -676,12 +429,6 @@ class _Network(object): return ArtistSearch(artist_name, self) - def search_for_tag(self, tag_name): - """Searches of a tag by its name. Returns a TagSearch object. - Use get_next_page() to retrieve sequences of results.""" - - return TagSearch(tag_name, self) - def search_for_track(self, artist_name, track_name): """Searches of a track by its name and its artist. Set artist to an empty string if not available. @@ -690,14 +437,6 @@ class _Network(object): return TrackSearch(artist_name, track_name, self) - def search_for_venue(self, venue_name, country_name): - """Searches of a venue by its name and its country. Set country_name to - an empty string if not available. - Returns a VenueSearch object. - Use get_next_page() to retrieve sequences of results.""" - - return VenueSearch(venue_name, country_name, self) - def get_track_by_mbid(self, mbid): """Looks up a track by its MusicBrainz ID""" @@ -708,7 +447,7 @@ class _Network(object): return Track(_extract(doc, "name", 1), _extract(doc, "name"), self) def get_artist_by_mbid(self, mbid): - """Loooks up an artist by its MusicBrainz ID""" + """Looks up an artist by its MusicBrainz ID""" params = {"mbid": mbid} @@ -837,39 +576,6 @@ class _Network(object): if remaining_tracks: self.scrobble_many(remaining_tracks) - def get_play_links(self, link_type, things, cacheable=True): - method = link_type + ".getPlaylinks" - params = {} - - for i, thing in enumerate(things): - if link_type == "artist": - params['artist[' + str(i) + ']'] = thing - elif link_type == "album": - params['artist[' + str(i) + ']'] = thing.artist - params['album[' + str(i) + ']'] = thing.title - elif link_type == "track": - params['artist[' + str(i) + ']'] = thing.artist - params['track[' + str(i) + ']'] = thing.title - - doc = _Request(self, method, params).execute(cacheable) - - seq = [] - - for node in doc.getElementsByTagName("externalids"): - spotify = _extract(node, "spotify") - seq.append(spotify) - - return seq - - def get_artist_play_links(self, artists, cacheable=True): - return self.get_play_links("artist", artists, cacheable) - - def get_album_play_links(self, albums, cacheable=True): - return self.get_play_links("album", albums, cacheable) - - def get_track_play_links(self, tracks, cacheable=True): - return self.get_play_links("track", tracks, cacheable) - class LastFMNetwork(_Network): @@ -904,7 +610,6 @@ class LastFMNetwork(_Network): 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, token=token, @@ -925,12 +630,9 @@ class LastFMNetwork(_Network): 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", } ) @@ -944,37 +646,6 @@ class LastFMNetwork(_Network): "'%s'" % self.password_hash))) -def get_lastfm_network( - api_key="", api_secret="", session_key="", username="", - password_hash="", token=""): - """ - Returns a preconfigured _Network object for Last.fm - - api_key: a provided API_KEY - api_secret: a provided API_SECRET - session_key: a generated session_key or None - username: a username of a valid user - password_hash: the output of pylast.md5(password) where password is the - user's password - token: an authentication token to retrieve a session - - if username and password_hash were provided and not session_key, - session_key will be generated automatically when needed. - - Either a valid session_key, a combination of username and password_hash, - or token must be present for scrobbling. - - Most read-only webservices only require an api_key and an api_secret, see - about obtaining them from: - https://www.last.fm/api/account - """ - - _deprecation_warning("Create a LastFMNetwork object instead") - - return LastFMNetwork( - api_key, api_secret, session_key, username, password_hash, token) - - class LibreFMNetwork(_Network): """ A preconfigured _Network object for Libre.fm @@ -1002,7 +673,6 @@ class LibreFMNetwork(_Network): 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={ @@ -1022,12 +692,9 @@ class LibreFMNetwork(_Network): 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", } ) @@ -1041,30 +708,6 @@ class LibreFMNetwork(_Network): "'%s'" % self.password_hash))) -def get_librefm_network( - api_key="", api_secret="", session_key="", username="", - password_hash=""): - """ - Returns a preconfigured _Network object for Libre.fm - - api_key: a provided API_KEY - api_secret: a provided API_SECRET - session_key: a generated session_key or None - username: a username of a valid user - password_hash: the output of pylast.md5(password) where password is the - user's password - - if username and password_hash were provided and not session_key, - session_key will be generated automatically when needed. - """ - - _deprecation_warning( - "DeprecationWarning: Create a LibreFMNetwork object instead") - - 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): @@ -1180,33 +823,20 @@ class _Request(object): (HOST_NAME, HOST_SUBDIR) = self.network.ws_server if self.network.is_proxy_enabled(): - if _can_use_ssl_securely(): - conn = HTTPSConnection( - context=SSL_CONTEXT, - host=self.network._get_proxy()[0], - port=self.network._get_proxy()[1]) - else: - conn = HTTPConnection( - host=self.network._get_proxy()[0], - port=self.network._get_proxy()[1]) + conn = HTTPSConnection( + context=SSL_CONTEXT, + host=self.network._get_proxy()[0], + port=self.network._get_proxy()[1]) try: conn.request( - method='POST', url="http://" + HOST_NAME + HOST_SUBDIR, + method='POST', url="https://" + HOST_NAME + HOST_SUBDIR, body=data, headers=headers) except Exception as e: raise NetworkError(self.network, e) else: - if _can_use_ssl_securely(): - conn = HTTPSConnection( - context=SSL_CONTEXT, - host=HOST_NAME - ) - else: - conn = HTTPConnection( - host=HOST_NAME - ) + conn = HTTPSConnection(context=SSL_CONTEXT, host=HOST_NAME) try: conn.request( @@ -1219,9 +849,8 @@ class _Request(object): except Exception as e: raise MalformedResponseError(self.network, e) - response_text = XML_ILLEGAL.sub("?", response_text) - self._check_response_for_errors(response_text) + conn.close() return response_text def execute(self, cacheable=False): @@ -1369,28 +998,15 @@ ImageSizes = collections.namedtuple( Image = collections.namedtuple( "Image", [ "title", "url", "dateadded", "format", "owner", "sizes", "votes"]) -Shout = collections.namedtuple( - "Shout", ["body", "author", "date"]) -def _string_output(funct): +def _string_output(func): def r(*args): - return _string(funct(*args)) + return _string(func(*args)) return r -def _pad_list(given_list, desired_length, padding=None): - """ - Pads a list to be of the desired_length. - """ - - while len(given_list) < desired_length: - given_list.append(padding) - - return given_list - - class _BaseObject(object): """An abstract webservices object.""" @@ -1443,61 +1059,6 @@ class _BaseObject(object): return seq - def get_top_fans(self, limit=None, cacheable=True): - """Returns a list of the Users who played this the most. - # Parameters: - * limit int: Max elements. - # For Artist/Track - """ - - doc = self._request(self.ws_prefix + '.getTopFans', cacheable) - - seq = [] - - elements = doc.getElementsByTagName('user') - - for element in elements: - if limit and len(seq) >= limit: - break - - name = _extract(element, 'name') - weight = _number(_extract(element, 'weight')) - - seq.append(TopItem(User(name, self.network), weight)) - - return seq - - def share(self, users, message=None): - """ - Shares this (sends out recommendations). - Parameters: - * users [User|str,]: A list that can contain usernames, emails, - User objects, or all of them. - * message str: A message to include in the recommendation message. - Only for Artist/Event/Track. - """ - - # Last.fm currently accepts a max of 10 recipient at a time - while(len(users) > 10): - section = users[0:9] - users = users[9:] - self.share(section, message) - - nusers = [] - for user in users: - if isinstance(user, User): - nusers.append(user.get_name()) - else: - nusers.append(user) - - params = self._get_params() - recipients = ','.join(nusers) - params['recipient'] = recipients - if message: - params['message'] = message - - self._request(self.ws_prefix + '.share', False, params) - def get_wiki_published_date(self): """ Returns the summary of the wiki. @@ -1536,26 +1097,6 @@ class _BaseObject(object): return _extract(node, section) - def get_shouts(self, limit=50, cacheable=False): - """ - Returns a sequence of Shout objects - """ - - shouts = [] - for node in _collect_nodes( - limit, - self, - self.ws_prefix + ".getShouts", - cacheable): - shouts.append( - Shout( - _extract(node, "body"), - User(_extract(node, "author"), self.network), - _extract(node, "date") - ) - ) - return shouts - class _Chartable(object): """Common functions for classes with charts.""" @@ -1578,7 +1119,7 @@ class _Chartable(object): """ Returns the weekly album charts for the week starting from the from_date value to the to_date value. - Only for Group or User. + Only for User. """ return self.get_weekly_charts("album", from_date, to_date) @@ -1586,7 +1127,7 @@ class _Chartable(object): """ Returns the weekly artist charts for the week starting from the from_date value to the to_date value. - Only for Group, Tag or User. + Only for Tag or User. """ return self.get_weekly_charts("artist", from_date, to_date) @@ -1594,7 +1135,7 @@ class _Chartable(object): """ Returns the weekly track charts for the week starting from the from_date value to the to_date value. - Only for Group or User. + Only for User. """ return self.get_weekly_charts("track", from_date, to_date) @@ -1873,12 +1414,6 @@ class _Opus(_BaseObject, _Taggable): return self.get_title(properly_capitalized) - def get_id(self): - """Returns the ID on the network.""" - - return _extract( - self._request(self.ws_prefix + ".getInfo", cacheable=True), "id") - def get_playcount(self): """Returns the number of plays on the network""" @@ -1933,12 +1468,6 @@ class Album(_Opus): def __init__(self, artist, title, network, username=None): super(Album, self).__init__(artist, title, network, "album", username) - def get_release_date(self): - """Returns the release date of the album.""" - - return _extract(self._request( - self.ws_prefix + ".getInfo", cacheable=True), "releasedate") - def get_cover_image(self, size=COVER_EXTRA_LARGE): """ Returns a uri to the cover image @@ -1958,7 +1487,7 @@ class Album(_Opus): return _extract_tracks( self._request( - self.ws_prefix + ".getInfo", cacheable=True), "tracks") + self.ws_prefix + ".getInfo", cacheable=True), self.network) def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the URL of the album or track page on the network. @@ -2128,13 +1657,6 @@ class Artist(_BaseObject, _Taggable): """Returns the content of the artist's biography.""" return self.get_bio("content", language) - def get_upcoming_events(self): - """Returns a list of the upcoming Events for this artist.""" - - doc = self._request(self.ws_prefix + '.getEvents', True) - - return _extract_events_from_doc(doc, self.network) - def get_similar(self, limit=None): """Returns the similar artists on the network.""" @@ -2195,199 +1717,6 @@ class Artist(_BaseObject, _Taggable): return self.network._get_url( domain_name, "artist") % {'artist': artist} - def shout(self, message): - """ - Post a shout - """ - - params = self._get_params() - params["message"] = message - - self._request("artist.Shout", False, params) - - def get_band_members(self): - """Returns a list of band members or None if unknown.""" - - names = None - doc = self._request(self.ws_prefix + ".getInfo", True) - - for node in doc.getElementsByTagName("bandmembers"): - names = _extract_all(node, "name") - - return names - - -class Event(_BaseObject): - """An event.""" - - id = None - - __hash__ = _BaseObject.__hash__ - - def __init__(self, event_id, network): - _BaseObject.__init__(self, network, 'event') - - self.id = event_id - - def __repr__(self): - return "pylast.Event(%s, %s)" % (repr(self.id), repr(self.network)) - - @_string_output - def __str__(self): - return "Event #" + str(self.get_id()) - - def __eq__(self, other): - if type(self) is type(other): - return self.get_id() == other.get_id() - else: - return False - - def __ne__(self, other): - return not self.__eq__(other) - - def _get_params(self): - return {'event': self.get_id()} - - def attend(self, attending_status): - """Sets the attending status. - * attending_status: The attending status. Possible values: - o EVENT_ATTENDING - o EVENT_MAYBE_ATTENDING - o EVENT_NOT_ATTENDING - """ - - params = self._get_params() - params['status'] = attending_status - - self._request('event.attend', False, params) - - def get_attendees(self): - """ - Get a list of attendees for an event - """ - - doc = self._request("event.getAttendees", False) - - users = [] - for name in _extract_all(doc, "name"): - users.append(User(name, self.network)) - - return users - - def get_id(self): - """Returns the id of the event on the network. """ - - return self.id - - def get_title(self): - """Returns the title of the event. """ - - doc = self._request("event.getInfo", True) - - return _extract(doc, "title") - - def get_headliner(self): - """Returns the headliner of the event. """ - - doc = self._request("event.getInfo", True) - - return Artist(_extract(doc, "headliner"), self.network) - - def get_artists(self): - """Returns a list of the participating Artists. """ - - doc = self._request("event.getInfo", True) - names = _extract_all(doc, "artist") - - artists = [] - for name in names: - artists.append(Artist(name, self.network)) - - return artists - - def get_venue(self): - """Returns the venue where the event is held.""" - - doc = self._request("event.getInfo", True) - - v = doc.getElementsByTagName("venue")[0] - venue_id = _number(_extract(v, "id")) - - return Venue(venue_id, self.network, venue_element=v) - - def get_start_date(self): - """Returns the date when the event starts.""" - - doc = self._request("event.getInfo", True) - - return _extract(doc, "startDate") - - def get_description(self): - """Returns the description of the event. """ - - doc = self._request("event.getInfo", True) - - return _extract(doc, "description") - - def get_cover_image(self, size=COVER_MEGA): - """ - Returns a uri to the cover image - size can be one of: - COVER_MEGA - COVER_EXTRA_LARGE - COVER_LARGE - COVER_MEDIUM - COVER_SMALL - """ - - doc = self._request("event.getInfo", True) - - return _extract_all(doc, "image")[size] - - def get_attendance_count(self): - """Returns the number of attending people. """ - - doc = self._request("event.getInfo", True) - - return _number(_extract(doc, "attendance")) - - def get_review_count(self): - """Returns the number of available reviews for this event. """ - - doc = self._request("event.getInfo", True) - - return _number(_extract(doc, "reviews")) - - 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 - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - return self.network._get_url( - domain_name, "event") % {'id': self.get_id()} - - def shout(self, message): - """ - Post a shout - """ - - params = self._get_params() - params["message"] = message - - self._request("event.Shout", False, params) - class Country(_BaseObject): """A country at Last.fm.""" @@ -2417,12 +1746,6 @@ class Country(_BaseObject): def _get_params(self): # TODO can move to _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. - - return alpha2code - def get_name(self): """Returns the country name. """ @@ -2448,7 +1771,7 @@ class Country(_BaseObject): "getTopTracks", "track", Track, params, cacheable) def get_url(self, domain_name=DOMAIN_ENGLISH): - """Returns the url of the event page on the network. + """Returns the url of the country page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN @@ -2470,169 +1793,6 @@ class Country(_BaseObject): domain_name, "country") % {'country_name': country_name} -class Metro(_BaseObject): - """A metro at Last.fm.""" - - name = None - country = None - - __hash__ = _BaseObject.__hash__ - - def __init__(self, name, country, network): - _BaseObject.__init__(self, network, None) - - self.name = name - self.country = country - - def __repr__(self): - 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()) - - def __ne__(self, other): - 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()} - - def get_name(self): - """Returns the metro name.""" - - return self.name - - def get_country(self): - """Returns the metro country.""" - - return self.country - - 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 - if from_date and to_date: - params["from"] = from_date - params["to"] = to_date - - doc = self._request(method, cacheable, params) - - seq = [] - for node in doc.getElementsByTagName(tag): - if tag == "artist": - item = Artist(_extract(node, "name"), self.network) - elif tag == "track": - title = _extract(node, "name") - artist = _extract_element_tree(node).get('artist')['name'] - item = Track(artist, title, self.network) - else: - return None - weight = _number(_extract(node, "listeners")) - seq.append(TopItem(item, weight)) - - return seq - - 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 - to_date (Optional) : Ending timestamp of the weekly range requested - 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) - - 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 - to_date (Optional) : Ending timestamp of the weekly range requested - 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) - - 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 - to_date (Optional) : Ending timestamp of the weekly range requested - 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) - - 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 - to_date (Optional) : Ending timestamp of the weekly range requested - 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) - - 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 - to_date (Optional) : Ending timestamp of the weekly range requested - 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) - - 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 - to_date (Optional) : Ending timestamp of the weekly range requested - 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) - - class Library(_BaseObject): """A user's Last.fm library.""" @@ -2648,10 +1808,6 @@ class Library(_BaseObject): else: self.user = User(user, self.network) - self._albums_index = 0 - self._artists_index = 0 - self._tracks_index = 0 - def __repr__(self): return "pylast.Library(%s, %s)" % (repr(self.user), repr(self.network)) @@ -2664,86 +1820,8 @@ class Library(_BaseObject): def get_user(self): """Returns the user who owns this library.""" - return self.user - def add_album(self, album): - """Add an album to this library.""" - - params = self._get_params() - params["artist"] = album.get_artist().get_name() - params["album"] = album.get_name() - - self._request("library.addAlbum", False, params) - - def remove_album(self, album): - """Remove an album from this library.""" - - params = self._get_params() - params["artist"] = album.get_artist().get_name() - params["album"] = album.get_name() - - self._request(self.ws_prefix + ".removeAlbum", False, params) - - def add_artist(self, artist): - """Add an artist to this library.""" - - params = self._get_params() - if type(artist) == str: - params["artist"] = artist - else: - params["artist"] = artist.get_name() - - self._request(self.ws_prefix + ".addArtist", False, params) - - def remove_artist(self, artist): - """Remove an artist from this library.""" - - params = self._get_params() - if type(artist) == str: - params["artist"] = artist - else: - params["artist"] = artist.get_name() - - self._request(self.ws_prefix + ".removeArtist", False, params) - - def add_track(self, track): - """Add a track to this library.""" - - params = self._get_params() - params["track"] = track.get_title() - - self._request(self.ws_prefix + ".addTrack", False, params) - - 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 limit==None it will return all (may take a while) - """ - - params = self._get_params() - if artist: - params["artist"] = artist - - seq = [] - for node in _collect_nodes( - limit, - self, - self.ws_prefix + ".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)) - - return seq - def get_artists(self, limit=50, cacheable=True): """ Returns a sequence of Album objects @@ -2766,192 +1844,6 @@ class Library(_BaseObject): return seq - def get_tracks(self, artist=None, album=None, limit=50, cacheable=True): - """ - Returns a sequence of Album objects - If limit==None it will return all (may take a while) - """ - - params = self._get_params() - if artist: - params["artist"] = artist - if album: - params["album"] = album - - seq = [] - for node in _collect_nodes( - limit, - self, - self.ws_prefix + ".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)) - - return seq - - def remove_scrobble(self, artist, title, timestamp): - """Remove a scrobble from a user's Last.fm library. Parameters: - artist (Required) : The artist that composed the track - title (Required) : The name of the track - timestamp (Required) : The unix timestamp of the scrobble - that you wish to remove - """ - - params = self._get_params() - params["artist"] = artist - params["track"] = title - params["timestamp"] = timestamp - - self._request(self.ws_prefix + ".removeScrobble", False, params) - - -class Playlist(_BaseObject): - """A Last.fm user playlist.""" - - id = None - user = None - - __hash__ = _BaseObject.__hash__ - - def __init__(self, user, playlist_id, network): - _BaseObject.__init__(self, network, "playlist") - - if isinstance(user, User): - self.user = user - else: - self.user = User(user, self.network) - - self.id = playlist_id - - @_string_output - def __str__(self): - 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. - """ - - doc = self._request("user.getPlaylists", True) - - for node in doc.getElementsByTagName("playlist"): - if _extract(node, "id") == str(self.get_id()): - return node - - def _get_params(self): - return {'user': self.user.get_name(), 'playlistID': self.get_id()} - - def get_id(self): - """Returns the playlist ID.""" - - return self.id - - def get_user(self): - """Returns the owner user of this playlist.""" - - return self.user - - def get_tracks(self): - """Returns a list of the tracks on this user playlist.""" - - uri = _unicode('lastfm://playlist/%s') % self.get_id() - - return XSPF(uri, self.network).get_tracks() - - def add_track(self, track): - """Adds a Track to this Playlist.""" - - params = self._get_params() - params['artist'] = track.get_artist().get_name() - params['track'] = track.get_title() - - self._request('playlist.addTrack', False, params) - - def get_title(self): - """Returns the title of this playlist.""" - - return _extract(self._get_info_node(), "title") - - def get_creation_date(self): - """Returns the creation date of this playlist.""" - - return _extract(self._get_info_node(), "date") - - def get_size(self): - """Returns the number of tracks in this playlist.""" - - return _number(_extract(self._get_info_node(), "size")) - - def get_description(self): - """Returns the description of this playlist.""" - - return _extract(self._get_info_node(), "description") - - def get_duration(self): - """Returns the duration of this playlist in milliseconds.""" - - 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.""" - - if _extract(self._get_info_node(), "streamable") == '1': - return True - else: - return False - - def has_track(self, track): - """Checks to see if track is already in the playlist. - * track: Any Track object. - """ - - return track in self.get_tracks() - - def get_cover_image(self, size=COVER_EXTRA_LARGE): - """ - Returns a uri to the cover image - size can be one of: - COVER_MEGA - COVER_EXTRA_LARGE - COVER_LARGE - COVER_MEDIUM - COVER_SMALL - """ - - return _extract(self._get_info_node(), "image")[size] - - 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 - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - 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()} - class Tag(_BaseObject, _Chartable): """A Last.fm object tag.""" @@ -2991,18 +1883,6 @@ class Tag(_BaseObject, _Chartable): return self.name - def get_similar(self): - """Returns the tags similar to this one, ordered by similarity. """ - - doc = self._request(self.ws_prefix + '.getSimilar', True) - - seq = [] - names = _extract_all(doc, 'name') - for name in names: - seq.append(Tag(name, self.network)) - - return seq - def get_top_albums(self, limit=None, cacheable=True): """Returns a list of the top albums.""" params = self._get_params() @@ -3098,7 +1978,7 @@ class Track(_Opus): return _extract(doc, "streamable") == "1" def is_fulltrack_available(self): - """Returns True if the fulltrack is available for streaming.""" + """Returns True if the full track is available for streaming.""" doc = self._request(self.ws_prefix + ".getInfo", True) return doc.getElementsByTagName( @@ -3128,11 +2008,6 @@ class Track(_Opus): self._request(self.ws_prefix + '.unlove') - def ban(self): - """Ban this track from ever playing on the radio. """ - - self._request(self.ws_prefix + '.ban') - def get_similar(self): """ Returns similar tracks for this track on the network, @@ -3177,122 +2052,6 @@ class Track(_Opus): 'artist': artist, 'title': title} -class Group(_BaseObject, _Chartable): - """A Last.fm group.""" - - name = None - - __hash__ = _BaseObject.__hash__ - - def __init__(self, name, network): - _BaseObject.__init__(self, network, 'group') - _Chartable.__init__(self, 'group') - - self.name = name - - def __repr__(self): - return "pylast.Group(%s, %s)" % (repr(self.name), repr(self.network)) - - @_string_output - def __str__(self): - return self.get_name() - - def __eq__(self, other): - return self.get_name().lower() == other.get_name().lower() - - def __ne__(self, other): - return self.get_name() != other.get_name() - - def _get_params(self): - return {self.ws_prefix: self.get_name()} - - def get_name(self): - """Returns the group name. """ - return self.name - - 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 - o DOMAIN_GERMAN - o DOMAIN_SPANISH - o DOMAIN_FRENCH - o DOMAIN_ITALIAN - o DOMAIN_POLISH - o DOMAIN_PORTUGUESE - o DOMAIN_SWEDISH - o DOMAIN_TURKISH - o DOMAIN_RUSSIAN - o DOMAIN_JAPANESE - o DOMAIN_CHINESE - """ - - name = _url_safe(self.get_name()) - - return self.network._get_url(domain_name, "group") % {'name': name} - - def get_members(self, limit=50, cacheable=False): - """ - Returns a sequence of User objects - if limit==None it will return all - """ - - nodes = _collect_nodes( - limit, self, self.ws_prefix + ".getMembers", cacheable) - - users = [] - - for node in nodes: - users.append(User(_extract(node, "name"), self.network)) - - return users - - -class XSPF(_BaseObject): - "A Last.fm XSPF playlist.""" - - uri = None - - __hash__ = _BaseObject.__hash__ - - def __init__(self, uri, network): - _BaseObject.__init__(self, network, None) - - self.uri = uri - - def _get_params(self): - return {'playlistURL': self.get_uri()} - - @_string_output - def __str__(self): - return self.get_uri() - - def __eq__(self, other): - return self.get_uri() == other.get_uri() - - def __ne__(self, other): - return self.get_uri() != other.get_uri() - - def get_uri(self): - """Returns the Last.fm playlist URI. """ - - return self.uri - - def get_tracks(self): - """Returns the tracks on this playlist.""" - - doc = self._request('playlist.fetch', True) - - seq = [] - for node in doc.getElementsByTagName('track'): - title = _extract(node, 'title') - artist = _extract(node, 'creator') - - seq.append(Track(artist, title, self.network)) - - return seq - - class User(_BaseObject, _Chartable): """A Last.fm user.""" @@ -3306,10 +2065,6 @@ class User(_BaseObject, _Chartable): self.name = user_name - self._past_events_index = 0 - self._recommended_events_index = 0 - self._recommended_artists_index = 0 - def __repr__(self): return "pylast.User(%s, %s)" % (repr(self.name), repr(self.network)) @@ -3341,13 +2096,6 @@ class User(_BaseObject, _Chartable): return self.name - def get_upcoming_events(self): - """Returns all the upcoming events for this user.""" - - doc = self._request(self.ws_prefix + '.getEvents', True) - - 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, @@ -3400,9 +2148,6 @@ class User(_BaseObject, _Chartable): This method uses caching. Enable caching only if you're pulling a 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. """ params = self._get_params() @@ -3427,52 +2172,6 @@ class User(_BaseObject, _Chartable): return seq - def get_neighbours(self, limit=50, cacheable=True): - """Returns a list of the user's friends.""" - - params = self._get_params() - if limit: - params['limit'] = limit - - doc = self._request( - self.ws_prefix + '.getNeighbours', cacheable, params) - - seq = [] - names = _extract_all(doc, 'name') - - for name in names: - seq.append(User(name, self.network)) - - return seq - - def get_past_events(self, limit=50, cacheable=False): - """ - Returns a sequence of Event objects - if limit==None it will return all - """ - - seq = [] - for node in _collect_nodes( - limit, - self, - self.ws_prefix + ".getPastEvents", - cacheable): - seq.append(Event(_extract(node, "id"), self.network)) - - return seq - - def get_playlists(self): - """Returns a list of Playlists that this user owns.""" - - doc = self._request(self.ws_prefix + ".getPlaylists", True) - - playlists = [] - for playlist_id in _extract_all(doc, "id"): - 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. @@ -3517,9 +2216,6 @@ class User(_BaseObject, _Chartable): This method uses caching. Enable caching only if you're pulling a 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. """ params = self._get_params() @@ -3553,20 +2249,6 @@ class User(_BaseObject, _Chartable): return seq - def get_id(self): - """Returns the user ID.""" - - doc = self._request(self.ws_prefix + ".getInfo", True) - - return _extract(doc, "id") - - def get_language(self): - """Returns the language code of the language used by the user.""" - - doc = self._request(self.ws_prefix + ".getInfo", True) - - return _extract(doc, "lang") - def get_country(self): """Returns the name of the country of the user.""" @@ -3579,27 +2261,6 @@ class User(_BaseObject, _Chartable): else: return Country(country, self.network) - def get_age(self): - """Returns the user's age.""" - - doc = self._request(self.ws_prefix + ".getInfo", True) - - return _number(_extract(doc, "age")) - - def get_gender(self): - """Returns the user's gender. Either USER_MALE or USER_FEMALE.""" - - doc = self._request(self.ws_prefix + ".getInfo", True) - - value = _extract(doc, "gender") - - if value == 'm': - return USER_MALE - elif value == 'f': - return USER_FEMALE - - return None - def is_subscriber(self): """Returns whether the user is a subscriber or not. True or False.""" @@ -3748,39 +2409,6 @@ class User(_BaseObject, _Chartable): return self._get_things( "getTopTracks", "track", Track, params, cacheable) - 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. - """ - - if isinstance(user, User): - user = user.get_name() - - params = self._get_params() - if shared_artists_limit: - params['limit'] = shared_artists_limit - params['type1'] = 'user' - params['type2'] = 'user' - params['value1'] = self.get_name() - params['value2'] = user - - doc = self._request('tasteometer.compare', False, params) - - score = _extract(doc, 'score') - - artists = doc.getElementsByTagName('artists')[0] - shared_artists_names = _extract_all(artists, 'name') - - shared_artists_seq = [] - - for name in shared_artists_names: - shared_artists_seq.append(Artist(name, self.network)) - - return (score, shared_artists_seq) - def get_image(self): """Returns the user's avatar.""" @@ -3814,16 +2442,6 @@ class User(_BaseObject, _Chartable): return Library(self, self.network) - def shout(self, message): - """ - Post a shout - """ - - params = self._get_params() - params["message"] = message - - self._request(self.ws_prefix + ".Shout", False, params) - class AuthenticatedUser(User): def __init__(self, network): @@ -3840,32 +2458,6 @@ class AuthenticatedUser(User): self.name = _extract(doc, "name") return self.name - def get_recommended_events(self, limit=50, cacheable=False): - """ - Returns a sequence of Event objects - if limit==None it will return all - """ - - seq = [] - for node in _collect_nodes( - limit, self, "user.getRecommendedEvents", cacheable): - seq.append(Event(_extract(node, "id"), self.network)) - - return seq - - def get_recommended_artists(self, limit=50, cacheable=False): - """ - Returns a sequence of Artist objects - if limit==None it will return all - """ - - seq = [] - 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.""" @@ -3891,7 +2483,7 @@ class _Search(_BaseObject): doc = self._request(self._ws_prefix + ".search", True) - return _extract(doc, "opensearch:totalResults") + return _extract(doc, "totalResults") def _retrieve_page(self, page_index): """Returns the node of matches to be processed""" @@ -3949,27 +2541,6 @@ class ArtistSearch(_Search): return seq -class TagSearch(_Search): - """Search for a tag by tag name.""" - - def __init__(self, tag_name, network): - - _Search.__init__(self, "tag", {"tag": tag_name}, network) - - def get_next_page(self): - """Returns the next page of results as a sequence of Tag objects.""" - - master_node = self._retrieve_next_page() - - seq = [] - for node in master_node.getElementsByTagName("tag"): - tag = Tag(_extract(node, "name"), self.network) - tag.tag_count = _number(_extract(node, "count")) - seq.append(tag) - - return seq - - class TrackSearch(_Search): """ Search for a track by track title. If you don't want to narrow the results @@ -4001,106 +2572,6 @@ class TrackSearch(_Search): return seq -class VenueSearch(_Search): - """ - 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) - - def get_next_page(self): - """Returns the next page of results as a sequence of Track objects.""" - - master_node = self._retrieve_next_page() - - seq = [] - for node in master_node.getElementsByTagName("venue"): - seq.append(Venue(_extract(node, "id"), self.network)) - - 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 - - id = None - info = None - name = None - location = None - url = None - - __hash__ = _BaseObject.__hash__ - - def __init__(self, netword_id, network, venue_element=None): - _BaseObject.__init__(self, network, "venue") - - self.id = _number(netword_id) - if venue_element is not None: - self.info = _extract_element_tree(venue_element) - self.name = self.info.get('name') - self.url = self.info.get('url') - self.location = self.info.get('location') - - def __repr__(self): - return "pylast.Venue(%s, %s)" % (repr(self.id), repr(self.network)) - - @_string_output - def __str__(self): - return "Venue #" + str(self.id) - - def __eq__(self, other): - return self.get_id() == other.get_id() - - def _get_params(self): - return {self.ws_prefix: self.get_id()} - - def get_id(self): - """Returns the id of the venue.""" - - return self.id - - def get_name(self): - """Returns the name of the venue.""" - - return self.name - - def get_url(self): - """Returns the URL of the venue page.""" - - return self.url - - def get_location(self): - """Returns the location of the venue (dictionary).""" - - return self.location - - def get_upcoming_events(self): - """Returns the upcoming events in this venue.""" - - doc = self._request(self.ws_prefix + ".getEvents", True) - - return _extract_events_from_doc(doc, self.network) - - def get_past_events(self): - """Returns the past events held in this venue.""" - - doc = self._request(self.ws_prefix + ".getEvents", True) - - return _extract_events_from_doc(doc, self.network) - - def md5(text): """Returns the md5 hash of a string.""" @@ -4156,6 +2627,9 @@ def _collect_nodes(limit, sender, method_name, cacheable, params=None): doc = sender._request(method_name, cacheable, params) doc = cleanup_nodes(doc) + # break if there are no child nodes + if not doc.documentElement.childNodes: + break main = doc.documentElement.childNodes[0] if main.hasAttribute("totalPages"): @@ -4190,37 +2664,6 @@ def _extract(node, name, index=0): return None -def _extract_element_tree(node): - """Extract an element tree into a multi-level dictionary - - NB: If any elements have text nodes as well as nested - elements this will ignore the text nodes""" - - def _recurse_build_tree(rootNode, targetDict): - """Recursively build a multi-level dict""" - - def _has_child_elements(rootNode): - """Check if an element has any nested (child) elements""" - - for node in rootNode.childNodes: - if node.nodeType == node.ELEMENT_NODE: - return True - return False - - for node in rootNode.childNodes: - if node.nodeType == node.ELEMENT_NODE: - if _has_child_elements(node): - targetDict[node.tagName] = {} - _recurse_build_tree(node, targetDict[node.tagName]) - else: - 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): """Extracts all the values from the xml string. returning a list.""" @@ -4285,13 +2728,6 @@ def _extract_tracks(doc, network): 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.""" @@ -4326,289 +2762,4 @@ def _unescape_htmlentity(string): return string -def extract_items(topitems_or_libraryitems): - """ - Extracts a sequence of items from a sequence of TopItem or - LibraryItem objects. - """ - - seq = [] - for i in topitems_or_libraryitems: - seq.append(i.item) - - return seq - - -class ScrobblingError(Exception): - def __init__(self, message): - Exception.__init__(self) - self.message = message - - @_string_output - def __str__(self): - return self.message - - -class BannedClientError(ScrobblingError): - def __init__(self): - 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") - - -class BadSessionError(ScrobblingError): - def __init__(self): - ScrobblingError.__init__( - self, "Bad session id, consider re-handshaking") - - -class _ScrobblerRequest(object): - - def __init__(self, url, params, network, request_type="POST"): - - for key in params: - params[key] = str(params[key]) - - self.params = params - self.type = request_type - (self.hostname, self.subdir) = url_split_host(url[len("http:"):]) - self.network = network - - def execute(self): - """Returns a string response of this request.""" - - if _can_use_ssl_securely(): - connection = HTTPSConnection( - context=SSL_CONTEXT, - host=self.hostname - ) - else: - connection = HTTPConnection( - host=self.hostname - ) - - data = [] - for name in self.params.keys(): - value = url_quote_plus(self.params[name]) - data.append('='.join((name, value))) - data = "&".join(data) - - headers = { - "Content-type": "application/x-www-form-urlencoded", - "Accept-Charset": "utf-8", - "User-Agent": "pylast" + "/" + __version__, - "HOST": self.hostname - } - - if self.type == "GET": - connection.request( - "GET", self.subdir + "?" + data, headers=headers) - else: - connection.request("POST", self.subdir, data, headers) - response = _unicode(connection.getresponse().read()) - - self._check_response_for_errors(response) - - return response - - def _check_response_for_errors(self, response): - """ - When passed a string response it checks for errors, raising any - exceptions as necessary. - """ - - lines = response.split("\n") - status_line = lines[0] - - if status_line == "OK": - return - elif status_line == "BANNED": - raise BannedClientError() - elif status_line == "BADAUTH": - raise BadAuthenticationError() - elif status_line == "BADTIME": - raise BadTimeError() - elif status_line == "BADSESSION": - raise BadSessionError() - elif status_line.startswith("FAILED "): - reason = status_line[status_line.find("FAILED ") + len("FAILED "):] - raise ScrobblingError(reason) - - -class Scrobbler(object): - """A class for scrobbling tracks to Last.fm""" - - session_id = None - nowplaying_url = None - submissions_url = None - - def __init__(self, network, client_id, client_version): - self.client_id = client_id - self.client_version = client_version - self.username = network.username - self.password = network.password_hash - self.network = network - - def _do_handshake(self): - """Handshakes with the server""" - - timestamp = str(int(time.time())) - - 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: - if not self.username: - 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, - "v": self.client_version, "u": self.username, "t": timestamp, - "a": token} - - if self.network.session_key and self.network.api_key: - params["sk"] = self.network.session_key - params["api_key"] = self.network.api_key - - server = self.network.submission_server - 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. - """ - - 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=""): - - _deprecation_warning( - "DeprecationWarning: Use Network.update_now_playing(...) instead") - - 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() - except BadSessionError: - self._do_handshake() - 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=""): - """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_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) - 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") - - 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() - - def scrobble_many(self, tracks): - """ - Scrobble several tracks at once. - - 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") - - remainder = [] - - if len(tracks) > 50: - remainder = tracks[50:] - tracks = tracks[:50] - - params = {"s": self._get_session_id()} - - i = 0 - for t in tracks: - _pad_list(t, 9, "") - params["a[%s]" % str(i)] = _string(t[0]) - params["t[%s]" % str(i)] = _string(t[1]) - params["i[%s]" % str(i)] = str(t[2]) - params["o[%s]" % str(i)] = t[3] - params["r[%s]" % str(i)] = t[4] - params["l[%s]" % str(i)] = str(t[5]) - params["b[%s]" % str(i)] = _string(t[6]) - params["n[%s]" % str(i)] = t[7] - params["m[%s]" % str(i)] = t[8] - - i += 1 - - _ScrobblerRequest(self.submissions_url, params, self.network).execute() - - if remainder: - self.scrobble_many(remainder) - # End of file diff --git a/setup.py b/setup.py index 2bf413c..2a0de0d 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( @@ -7,14 +7,9 @@ setup( version="1.9.0", author="Amr Hassan ", install_requires=['six'], - # FIXME This can be removed after 2017-09 when 3.3 is no longer supported - # and pypy3 uses 3.4 or later, see - # https://en.wikipedia.org/wiki/CPython#Version_history - extras_require={ - ':python_version=="3.3"': ["certifi"], - }, - tests_require=['mock', 'pytest', 'coverage', 'pep8', 'pyyaml', 'pyflakes'], - description=("A Python interface to Last.fm and Libre.fm"), + tests_require=['mock', 'pytest', 'coverage', 'pycodestyle', 'pyyaml', + 'pyflakes', 'flaky'], + description="A Python interface to Last.fm and Libre.fm", author_email="amr.hassan@gmail.com", url="https://github.com/pylast/pylast", classifiers=[ @@ -26,10 +21,11 @@ setup( "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ], keywords=["Last.fm", "music", "scrobble", "scrobbling"], packages=find_packages(exclude=('tests*',)), diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 45cbd8f..9ebbcc1 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -2,13 +2,13 @@ """ Integration (not unit) tests for pylast.py """ -from flaky import flaky import os -import pytest -from random import choice import time import unittest +import pytest +from flaky import flaky + import pylast @@ -30,24 +30,8 @@ def load_secrets(): return doc -def handle_lastfm_exceptions(f): - """Skip exceptions caused by Last.fm's broken API""" - def wrapper(*args, **kw): - try: - return f(*args, **kw) - except pylast.WSError as e: - if (str(e) == "Invalid Method - " - "No method with that name in this package"): - msg = "Ignore broken Last.fm API: " + str(e) - print(msg) - pytest.skip(msg) - else: - raise(e) - return wrapper - - @flaky(max_runs=5, min_passes=1) -class TestPyLast(unittest.TestCase): +class PyLastTestCase(unittest.TestCase): secrets = None @@ -68,325 +52,6 @@ class TestPyLast(unittest.TestCase): api_key=API_KEY, api_secret=API_SECRET, username=self.username, password_hash=password_hash) - def skip_if_lastfm_api_broken(self, value): - """Skip things not yet restored in Last.fm's broken API""" - if value is None or len(value) == 0: - pytest.skip("Last.fm API is broken.") - - @handle_lastfm_exceptions - def test_scrobble(self): - # Arrange - artist = "Test Artist" - title = "test title" - timestamp = self.unix_timestamp() - lastfm_user = self.network.get_user(self.username) - - # Act - self.network.scrobble(artist=artist, title=title, timestamp=timestamp) - - # Assert - # 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)) - - @handle_lastfm_exceptions - 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) - lastfm_user = self.network.get_user(self.username) - - # Act - library.remove_scrobble( - artist=artist, title=title, timestamp=timestamp) - - # Assert - # limit=2 to ignore now-playing: - last_scrobble = lastfm_user.get_recent_tracks(limit=2)[0] - self.assertNotEqual(str(last_scrobble.timestamp), str(timestamp)) - - @handle_lastfm_exceptions - def test_add_album(self): - # Arrange - library = pylast.Library(user=self.username, network=self.network) - album = self.network.get_album("Test Artist", "Test Album") - - # Act - library.add_album(album) - - # Assert - my_albums = library.get_albums() - for my_album in my_albums: - value = (album == my_album[0]) - if value: - break - self.assertTrue(value) - - @handle_lastfm_exceptions - def test_remove_album(self): - # Arrange - 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 - albums = artist.get_top_albums() - # Pick a random one to avoid problems running concurrent tests - album = choice(albums)[0] - library.add_album(album) - - # Act - library.remove_album(album) - - # Assert - my_albums = library.get_albums() - for my_album in my_albums: - value = (album == my_album[0]) - if value: - break - self.assertFalse(value) - - @handle_lastfm_exceptions - def test_add_artist(self): - # Arrange - artist = "Test Artist 2" - library = pylast.Library(user=self.username, network=self.network) - - # Act - library.add_artist(artist) - - # Assert - artists = library.get_artists() - for artist in artists: - value = (str(artist[0]) == "Test Artist 2") - if value: - break - self.assertTrue(value) - - @handle_lastfm_exceptions - 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.add_artist(my_artist) - - # Act - library.remove_artist(my_artist) - - # Assert - artists = library.get_artists() - for artist in artists: - value = (artist[0] == my_artist) - if value: - break - self.assertFalse(value) - - @handle_lastfm_exceptions - def test_get_venue(self): - # Arrange - venue_name = "Last.fm Office" - country_name = "United Kingdom" - - # Act - venue_search = self.network.search_for_venue(venue_name, country_name) - venue = venue_search.get_next_page()[0] - - # Assert - self.assertEqual(str(venue.id), "8778225") - - @handle_lastfm_exceptions - def test_get_user_registration(self): - # Arrange - username = "RJ" - user = self.network.get_user(username) - - # Act - registered = user.get_registered() - - # Assert - # Last.fm API broken? Should be yyyy-mm-dd not Unix timestamp - if int(registered): - pytest.skip("Last.fm API is broken.") - - # Just check date because of timezones - self.assertIn(u"2002-11-20 ", registered) - - @handle_lastfm_exceptions - def test_get_user_unixtime_registration(self): - # Arrange - username = "RJ" - user = self.network.get_user(username) - - # Act - unixtime_registered = user.get_unixtime_registered() - - # Assert - # Just check date because of timezones - self.assertEqual(unixtime_registered, u"1037793040") - - @handle_lastfm_exceptions - def test_get_genderless_user(self): - # Arrange - # Currently test_user has no gender set: - lastfm_user = self.network.get_user("test_user") - - # Act - gender = lastfm_user.get_gender() - - # Assert - self.assertIsNone(gender) - - @handle_lastfm_exceptions - def test_get_countryless_user(self): - # Arrange - # Currently test_user has no country set: - lastfm_user = self.network.get_user("test_user") - - # Act - country = lastfm_user.get_country() - - # Assert - self.assertIsNone(country) - - @handle_lastfm_exceptions - def test_love(self): - # Arrange - artist = "Test Artist" - title = "test title" - track = self.network.get_track(artist, title) - lastfm_user = self.network.get_user(self.username) - - # Act - track.love() - - # Assert - 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") - - @handle_lastfm_exceptions - def test_unlove(self): - # Arrange - artist = pylast.Artist("Test Artist", self.network) - title = "test title" - track = pylast.Track(artist, title, self.network) - lastfm_user = self.network.get_user(self.username) - track.love() - - # Act - track.unlove() - - # Assert - 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") - - @handle_lastfm_exceptions - def test_get_100_albums(self): - # Arrange - library = pylast.Library(user=self.username, network=self.network) - - # Act - albums = library.get_albums(limit=100) - - # Assert - self.assertGreaterEqual(len(albums), 0) - - @handle_lastfm_exceptions - def test_get_limitless_albums(self): - # Arrange - library = pylast.Library(user=self.username, network=self.network) - - # Act - albums = library.get_albums(limit=None) - - # Assert - self.assertGreaterEqual(len(albums), 0) - - @handle_lastfm_exceptions - def test_user_equals_none(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act - value = (lastfm_user is None) - - # Assert - self.assertFalse(value) - - @handle_lastfm_exceptions - def test_user_not_equal_to_none(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act - value = (lastfm_user is not None) - - # Assert - self.assertTrue(value) - - @handle_lastfm_exceptions - def test_now_playing_user_with_no_scrobbles(self): - # Arrange - # Currently test-account has no scrobbles: - user = self.network.get_user('test-account') - - # Act - current_track = user.get_now_playing() - - # Assert - self.assertIsNone(current_track) - - @handle_lastfm_exceptions - def test_love_limits(self): - # Arrange - # 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) - self.assertLessEqual(len(user.get_loved_tracks(limit=100)), 100) - self.assertGreaterEqual(len(user.get_loved_tracks(limit=None)), 23) - self.assertGreaterEqual(len(user.get_loved_tracks(limit=0)), 23) - - @handle_lastfm_exceptions - def test_update_now_playing(self): - # Arrange - artist = "Test Artist" - title = "test title" - album = "Test Album" - track_number = 1 - lastfm_user = self.network.get_user(self.username) - - # Act - self.network.update_now_playing( - artist=artist, title=title, album=album, track_number=track_number) - - # Assert - current_track = lastfm_user.get_now_playing() - self.assertIsNotNone(current_track) - self.assertEqual(str(current_track.title), "test title") - self.assertEqual(str(current_track.artist), "Test Artist") - - @handle_lastfm_exceptions - 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) - - # Assert - self.assertGreater(len(tags), 0) - self.assertIsInstance(tags[0], pylast.TopItem) - def helper_is_thing_hashable(self, thing): # Arrange things = set() @@ -398,433 +63,6 @@ class TestPyLast(unittest.TestCase): self.assertIsNotNone(thing) self.assertEqual(len(things), 1) - @handle_lastfm_exceptions - def test_album_is_hashable(self): - # Arrange - album = self.network.get_album("Test Artist", "Test Album") - - # Act/Assert - self.helper_is_thing_hashable(album) - - @handle_lastfm_exceptions - def test_artist_is_hashable(self): - # Arrange - test_artist = self.network.get_artist("Test Artist") - artist = test_artist.get_similar(limit=2)[0].item - self.assertIsInstance(artist, pylast.Artist) - - # Act/Assert - self.helper_is_thing_hashable(artist) - - @handle_lastfm_exceptions - def test_country_is_hashable(self): - # Arrange - country = self.network.get_country("Italy") - - # Act/Assert - self.helper_is_thing_hashable(country) - - @handle_lastfm_exceptions - def test_metro_is_hashable(self): - # Arrange - metro = self.network.get_metro("Helsinki", "Finland") - - # Act/Assert - self.helper_is_thing_hashable(metro) - - @handle_lastfm_exceptions - def test_event_is_hashable(self): - # Arrange - user = self.network.get_user("RJ") - event = user.get_past_events(limit=1)[0] - - # Act/Assert - self.helper_is_thing_hashable(event) - - @handle_lastfm_exceptions - def test_group_is_hashable(self): - # Arrange - group = self.network.get_group("Audioscrobbler Beta") - - # Act/Assert - self.helper_is_thing_hashable(group) - - @handle_lastfm_exceptions - def test_library_is_hashable(self): - # Arrange - library = pylast.Library(user=self.username, network=self.network) - - # Act/Assert - self.helper_is_thing_hashable(library) - - @handle_lastfm_exceptions - def test_playlist_is_hashable(self): - # Arrange - playlist = pylast.Playlist( - user="RJ", playlist_id="1k1qp_doglist", network=self.network) - - # Act/Assert - self.helper_is_thing_hashable(playlist) - - @handle_lastfm_exceptions - def test_tag_is_hashable(self): - # Arrange - tag = self.network.get_top_tags(limit=1)[0] - - # Act/Assert - self.helper_is_thing_hashable(tag) - - @handle_lastfm_exceptions - def test_track_is_hashable(self): - # Arrange - artist = self.network.get_artist("Test Artist") - track = artist.get_top_tracks()[0].item - self.assertIsInstance(track, pylast.Track) - - # Act/Assert - self.helper_is_thing_hashable(track) - - @handle_lastfm_exceptions - def test_user_is_hashable(self): - # Arrange - artist = self.network.get_artist("Test Artist") - user = artist.get_top_fans(limit=1)[0].item - self.assertIsInstance(user, pylast.User) - - # Act/Assert - self.helper_is_thing_hashable(user) - - @handle_lastfm_exceptions - def test_venue_is_hashable(self): - # Arrange - venue_id = "8778225" # Last.fm office - venue = pylast.Venue(venue_id, self.network) - - # Act/Assert - self.helper_is_thing_hashable(venue) - - @handle_lastfm_exceptions - def test_xspf_is_hashable(self): - # Arrange - xspf = pylast.XSPF( - uri="lastfm://playlist/1k1qp_doglist", network=self.network) - - # Act/Assert - self.helper_is_thing_hashable(xspf) - - @handle_lastfm_exceptions - def test_invalid_xml(self): - # Arrange - # Currently causes PCDATA invalid Char value 25 - artist = "Blind Willie Johnson" - title = "It's nobody's fault but mine" - - # Act - search = self.network.search_for_track(artist, title) - total = search.get_total_result_count() - - # Assert - self.skip_if_lastfm_api_broken(total) - self.assertGreaterEqual(int(total), 0) - - @handle_lastfm_exceptions - 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) - - # Act - count = track.get_userplaycount() - - # Assert - self.assertGreaterEqual(count, 0) - - @handle_lastfm_exceptions - 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) - - # Act - loved = track.get_userloved() - - # Assert - self.assertIsNotNone(loved) - self.assertIsInstance(loved, bool) - self.assertNotIsInstance(loved, str) - - @handle_lastfm_exceptions - def test_album_in_recent_tracks(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act - # limit=2 to ignore now-playing: - track = lastfm_user.get_recent_tracks(limit=2)[0] - - # Assert - self.assertTrue(hasattr(track, 'album')) - - @handle_lastfm_exceptions - 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] - - # Assert - self.assertTrue(hasattr(track, 'album')) - - @handle_lastfm_exceptions - def test_enable_rate_limiting(self): - # Arrange - self.assertFalse(self.network.is_rate_limited()) - - # Act - self.network.enable_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.assertTrue(self.network.is_rate_limited()) - self.assertGreaterEqual(now - then, 0.2) - - @handle_lastfm_exceptions - def test_disable_rate_limiting(self): - # Arrange - self.network.enable_rate_limit() - self.assertTrue(self.network.is_rate_limited()) - - # Act - self.network.disable_rate_limit() - # 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() - - # 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. - # 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) - - # # Assert - # self.assertGreaterEqual(len(tracks), 0) - - def helper_past_events_have_valid_ids(self, thing): - # Act - events = thing.get_past_events() - - # Assert - self.helper_assert_events_have_valid_ids(events) - - def helper_upcoming_events_have_valid_ids(self, thing): - # Act - events = thing.get_upcoming_events() - - # Assert - self.helper_assert_events_have_valid_ids(events) - - def helper_assert_events_have_valid_ids(self, events): - # Assert - # 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) - - @handle_lastfm_exceptions - def test_artist_upcoming_events_returns_valid_ids(self): - # Arrange - artist = pylast.Artist("Test Artist", self.network) - - # Act/Assert - self.helper_upcoming_events_have_valid_ids(artist) - - @handle_lastfm_exceptions - def test_user_past_events_returns_valid_ids(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act/Assert - self.helper_past_events_have_valid_ids(lastfm_user) - - @handle_lastfm_exceptions - def test_user_recommended_events_returns_valid_ids(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act - events = lastfm_user.get_upcoming_events() - - # Assert - self.helper_assert_events_have_valid_ids(events) - - @handle_lastfm_exceptions - def test_user_upcoming_events_returns_valid_ids(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act/Assert - self.helper_upcoming_events_have_valid_ids(lastfm_user) - - @handle_lastfm_exceptions - def test_venue_past_events_returns_valid_ids(self): - # Arrange - venue_id = "8778225" # Last.fm office - venue = pylast.Venue(venue_id, self.network) - - # Act/Assert - self.helper_past_events_have_valid_ids(venue) - - @handle_lastfm_exceptions - def test_venue_upcoming_events_returns_valid_ids(self): - # Arrange - venue_id = "8778225" # Last.fm office - venue = pylast.Venue(venue_id, self.network) - - # Act/Assert - self.helper_upcoming_events_have_valid_ids(venue) - - @handle_lastfm_exceptions - def test_pickle(self): - # Arrange - import pickle - lastfm_user = self.network.get_user(self.username) - filename = str(self.unix_timestamp()) + ".pkl" - - # Act - with open(filename, "wb") as f: - pickle.dump(lastfm_user, f) - with open(filename, "rb") as f: - loaded_user = pickle.load(f) - os.remove(filename) - - # Assert - self.assertEqual(lastfm_user, loaded_user) - - @handle_lastfm_exceptions - def test_bio_published_date(self): - # Arrange - artist = pylast.Artist("Test Artist", self.network) - - # Act - bio = artist.get_bio_published_date() - - # Assert - self.assertIsNotNone(bio) - self.assertGreaterEqual(len(bio), 1) - - @handle_lastfm_exceptions - def test_bio_content(self): - # Arrange - artist = pylast.Artist("Test Artist", self.network) - - # Act - bio = artist.get_bio_content(language="en") - - # Assert - self.assertIsNotNone(bio) - self.assertGreaterEqual(len(bio), 1) - - @handle_lastfm_exceptions - def test_bio_summary(self): - # Arrange - artist = pylast.Artist("Test Artist", self.network) - - # Act - bio = artist.get_bio_summary(language="en") - - # Assert - self.assertIsNotNone(bio) - self.assertGreaterEqual(len(bio), 1) - - @handle_lastfm_exceptions - def test_album_wiki_content(self): - # Arrange - album = pylast.Album("Test Artist", "Test Album", self.network) - - # Act - wiki = album.get_wiki_content() - - # Assert - self.assertIsNotNone(wiki) - self.assertGreaterEqual(len(wiki), 1) - - @handle_lastfm_exceptions - def test_album_wiki_published_date(self): - # Arrange - album = pylast.Album("Test Artist", "Test Album", self.network) - - # Act - wiki = album.get_wiki_published_date() - - # Assert - self.assertIsNotNone(wiki) - self.assertGreaterEqual(len(wiki), 1) - - @handle_lastfm_exceptions - def test_album_wiki_summary(self): - # Arrange - album = pylast.Album("Test Artist", "Test Album", self.network) - - # Act - wiki = album.get_wiki_summary() - - # Assert - self.assertIsNotNone(wiki) - self.assertGreaterEqual(len(wiki), 1) - - @handle_lastfm_exceptions - def test_track_wiki_content(self): - # Arrange - track = pylast.Track("Test Artist", "test title", self.network) - - # Act - wiki = track.get_wiki_content() - - # Assert - self.assertIsNotNone(wiki) - self.assertGreaterEqual(len(wiki), 1) - - @handle_lastfm_exceptions - def test_track_wiki_summary(self): - # Arrange - track = pylast.Track("Test Artist", "test title", self.network) - - # Act - wiki = track.get_wiki_summary() - - # Assert - self.assertIsNotNone(wiki) - self.assertGreaterEqual(len(wiki), 1) - - @handle_lastfm_exceptions - def test_lastfm_network_name(self): - # Act - name = str(self.network) - - # Assert - self.assertEqual(name, "Last.fm Network") - def helper_validate_results(self, a, b, c): # Assert self.assertIsNotNone(a) @@ -849,278 +87,6 @@ class TestPyLast(unittest.TestCase): # Assert self.helper_validate_results(result1, result2, result3) - @handle_lastfm_exceptions - def test_cacheable_artist_get_shouts(self): - # Arrange - artist = self.network.get_artist("Test Artist") - - # Act/Assert - self.helper_validate_cacheable(artist, "get_shouts") - - @handle_lastfm_exceptions - def test_cacheable_event_get_shouts(self): - # Arrange - user = self.network.get_user("RJ") - event = user.get_past_events(limit=1)[0] - - # Act/Assert - self.helper_validate_cacheable(event, "get_shouts") - - @handle_lastfm_exceptions - def test_cacheable_track_get_shouts(self): - # Arrange - track = self.network.get_top_tracks()[0].item - - # Act/Assert - self.helper_validate_cacheable(track, "get_shouts") - - @handle_lastfm_exceptions - def test_cacheable_group_get_members(self): - # Arrange - group = self.network.get_group("Audioscrobbler Beta") - - # Act/Assert - self.helper_validate_cacheable(group, "get_members") - - @handle_lastfm_exceptions - def test_cacheable_library(self): - # Arrange - library = pylast.Library(self.username, self.network) - - # Act/Assert - self.helper_validate_cacheable(library, "get_albums") - self.helper_validate_cacheable(library, "get_artists") - self.helper_validate_cacheable(library, "get_tracks") - - @handle_lastfm_exceptions - def test_cacheable_user_artist_tracks(self): - # Arrange - lastfm_user = self.network.get_authenticated_user() - - # Act - 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) - - @handle_lastfm_exceptions - def test_cacheable_user(self): - # Arrange - lastfm_user = self.network.get_authenticated_user() - - # Act/Assert - # Skip the first one because Last.fm API is broken - # self.helper_validate_cacheable(lastfm_user, "get_friends") - self.helper_validate_cacheable(lastfm_user, "get_loved_tracks") - self.helper_validate_cacheable(lastfm_user, "get_neighbours") - self.helper_validate_cacheable(lastfm_user, "get_past_events") - self.helper_validate_cacheable(lastfm_user, "get_recent_tracks") - self.helper_validate_cacheable(lastfm_user, "get_recommended_artists") - self.helper_validate_cacheable(lastfm_user, "get_recommended_events") - self.helper_validate_cacheable(lastfm_user, "get_shouts") - - @handle_lastfm_exceptions - def test_geo_get_events_in_location(self): - # Arrange - # Act - events = self.network.get_geo_events( - location="London", tag="blues", limit=1) - - # Assert - self.assertEqual(len(events), 1) - event = events[0] - self.assertIsInstance(event, pylast.Event) - self.assertIn(event.get_venue().location['city'], - ["London", "Camden"]) - - @handle_lastfm_exceptions - def test_geo_get_events_in_latlong(self): - # Arrange - # Act - events = self.network.get_geo_events( - latitude=53.466667, longitude=-2.233333, distance=5, limit=1) - - # Assert - self.assertEqual(len(events), 1) - event = events[0] - self.assertIsInstance(event, pylast.Event) - self.assertEqual(event.get_venue().location['city'], "Manchester") - - @handle_lastfm_exceptions - def test_geo_get_events_festival(self): - # Arrange - # Act - events = self.network.get_geo_events( - location="Reading", festivalsonly=True, limit=1) - - # Assert - self.assertEqual(len(events), 1) - event = events[0] - self.assertIsInstance(event, pylast.Event) - self.assertEqual(event.get_venue().location['city'], "Reading") - - def helper_dates_valid(self, dates): - # Assert - self.assertGreaterEqual(len(dates), 1) - self.assertIsInstance(dates[0], tuple) - (start, end) = dates[0] - self.assertLess(start, end) - - @handle_lastfm_exceptions - def test_get_metro_weekly_chart_dates(self): - # Arrange - # Act - dates = self.network.get_metro_weekly_chart_dates() - - # Assert - self.helper_dates_valid(dates) - - 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() - (from_date, to_date) = dates[0] - - # get metro.function_name() - func = getattr(metro, function_name, None) - - # Act - chart = func(from_date=from_date, to_date=to_date, limit=1) - - # Assert - self.assertEqual(len(chart), 1) - self.assertIsInstance(chart[0], pylast.TopItem) - self.assertIsInstance(chart[0].item, expected_type) - - @handle_lastfm_exceptions - def test_get_metro_artist_chart(self): - # Arrange/Act/Assert - self.helper_geo_chart("get_artist_chart") - - @handle_lastfm_exceptions - def test_get_metro_hype_artist_chart(self): - # Arrange/Act/Assert - self.helper_geo_chart("get_hype_artist_chart") - - @handle_lastfm_exceptions - def test_get_metro_unique_artist_chart(self): - # Arrange/Act/Assert - self.helper_geo_chart("get_unique_artist_chart") - - @handle_lastfm_exceptions - def test_get_metro_track_chart(self): - # Arrange/Act/Assert - self.helper_geo_chart("get_track_chart", expected_type=pylast.Track) - - @handle_lastfm_exceptions - def test_get_metro_hype_track_chart(self): - # Arrange/Act/Assert - self.helper_geo_chart( - "get_hype_track_chart", expected_type=pylast.Track) - - @handle_lastfm_exceptions - def test_get_metro_unique_track_chart(self): - # Arrange/Act/Assert - self.helper_geo_chart( - "get_unique_track_chart", expected_type=pylast.Track) - - @handle_lastfm_exceptions - def test_geo_get_metros(self): - # Arrange - # Act - metros = self.network.get_metros(country="Poland") - - # Assert - self.assertGreaterEqual(len(metros), 1) - self.assertIsInstance(metros[0], pylast.Metro) - self.assertEqual(metros[0].get_country(), "Poland") - - @handle_lastfm_exceptions - def test_geo_get_top_artists(self): - # Arrange - # Act - artists = self.network.get_geo_top_artists( - country="United Kingdom", limit=1) - - # Assert - self.assertEqual(len(artists), 1) - self.assertIsInstance(artists[0], pylast.TopItem) - self.assertIsInstance(artists[0].item, pylast.Artist) - - @handle_lastfm_exceptions - def test_geo_get_top_tracks(self): - # Arrange - # Act - tracks = self.network.get_geo_top_tracks( - country="United Kingdom", location="Manchester", limit=1) - - # Assert - self.assertEqual(len(tracks), 1) - self.assertIsInstance(tracks[0], pylast.TopItem) - self.assertIsInstance(tracks[0].item, pylast.Track) - - @handle_lastfm_exceptions - def test_metro_class(self): - # Arrange - # Act - metro = self.network.get_metro("Bergen", "Norway") - - # Assert - self.assertEqual(metro.get_name(), "Bergen") - 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)) - - @handle_lastfm_exceptions - def test_get_album_play_links(self): - # Arrange - album1 = self.network.get_album("Portishead", "Dummy") - album2 = self.network.get_album("Radiohead", "OK Computer") - albums = [album1, album2] - - # Act - links = self.network.get_album_play_links(albums) - - # Assert - self.assertIsInstance(links, list) - self.assertEqual(len(links), 2) - self.assertIn("spotify:album:", links[0]) - self.assertIn("spotify:album:", links[1]) - - @handle_lastfm_exceptions - def test_get_artist_play_links(self): - # Arrange - artists = ["Portishead", "Radiohead"] - # Act - links = self.network.get_artist_play_links(artists) - - # Assert - self.assertIsInstance(links, list) - self.assertEqual(len(links), 2) - self.assertIn("spotify:artist:", links[0]) - self.assertIn("spotify:artist:", links[1]) - - @handle_lastfm_exceptions - 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") - tracks = [track1, track2] - - # Act - links = self.network.get_track_play_links(tracks) - - # Assert - self.assertIsInstance(links, list) - self.assertEqual(len(links), 2) - self.assertIn("spotify:track:", links[0]) - self.assertIn("spotify:track:", links[1]) - def helper_at_least_one_thing_in_top_list(self, things, expected_type): # Assert self.assertGreater(len(things), 1) @@ -1152,1049 +118,6 @@ class TestPyLast(unittest.TestCase): self.assertIsInstance(thing2.item, expected_type) self.assertNotEqual(thing1, thing2) - def helper_two_things_in_list(self, things, expected_type): - # Assert - self.assertEqual(len(things), 2) - self.assertIsInstance(things, list) - thing1 = things[0] - thing2 = things[1] - self.assertIsInstance(thing1, expected_type) - self.assertIsInstance(thing2, expected_type) - - @handle_lastfm_exceptions - def test_user_get_top_tags_with_limit(self): - # Arrange - user = self.network.get_user("RJ") - - # Act - tags = user.get_top_tags(limit=1) - - # Assert - self.skip_if_lastfm_api_broken(tags) - self.helper_only_one_thing_in_top_list(tags, pylast.Tag) - - @handle_lastfm_exceptions - def test_network_get_top_artists_with_limit(self): - # Arrange - # Act - artists = self.network.get_top_artists(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - - @handle_lastfm_exceptions - def test_network_get_top_tags_with_limit(self): - # Arrange - # Act - tags = self.network.get_top_tags(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(tags, pylast.Tag) - - @handle_lastfm_exceptions - def test_network_get_top_tags_with_no_limit(self): - # Arrange - # Act - tags = self.network.get_top_tags() - - # Assert - self.helper_at_least_one_thing_in_top_list(tags, pylast.Tag) - - @handle_lastfm_exceptions - def test_network_get_top_tracks_with_limit(self): - # Arrange - # Act - tracks = self.network.get_top_tracks(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(tracks, pylast.Track) - - @handle_lastfm_exceptions - def test_artist_top_tracks(self): - # Arrange - # Pick an artist with plenty of plays - artist = self.network.get_top_artists(limit=1)[0].item - - # Act - things = artist.get_top_tracks(limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.Track) - - @handle_lastfm_exceptions - def test_artist_top_albums(self): - # Arrange - # Pick an artist with plenty of plays - artist = self.network.get_top_artists(limit=1)[0].item - - # Act - things = artist.get_top_albums(limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.Album) - - @handle_lastfm_exceptions - def test_artist_top_fans(self): - # Arrange - # Pick an artist with plenty of plays - artist = self.network.get_top_artists(limit=1)[0].item - - # Act - things = artist.get_top_fans(limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.User) - - @handle_lastfm_exceptions - def test_country_top_tracks(self): - # Arrange - country = self.network.get_country("Croatia") - - # Act - things = country.get_top_tracks(limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.Track) - - @handle_lastfm_exceptions - def test_country_network_top_tracks(self): - # Arrange - # Act - things = self.network.get_geo_top_tracks("Croatia", limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.Track) - - @handle_lastfm_exceptions - def test_tag_top_tracks(self): - # Arrange - tag = self.network.get_tag("blues") - - # Act - things = tag.get_top_tracks(limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.Track) - - @handle_lastfm_exceptions - def test_user_top_tracks(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act - things = lastfm_user.get_top_tracks(limit=2) - - # Assert - self.helper_two_different_things_in_top_list(things, pylast.Track) - - def helper_assert_chart(self, chart, expected_type): - # Assert - self.assertIsNotNone(chart) - self.assertGreater(len(chart), 0) - self.assertIsInstance(chart[0], pylast.TopItem) - self.assertIsInstance(chart[0].item, expected_type) - - def helper_get_assert_charts(self, thing, date): - # Arrange - (from_date, to_date) = date - - # Act - artist_chart = thing.get_weekly_artist_charts(from_date, to_date) - if type(thing) is not pylast.Tag: - album_chart = thing.get_weekly_album_charts(from_date, to_date) - track_chart = thing.get_weekly_track_charts(from_date, to_date) - - # Assert - self.helper_assert_chart(artist_chart, pylast.Artist) - if type(thing) is not pylast.Tag: - self.helper_assert_chart(album_chart, pylast.Album) - self.helper_assert_chart(track_chart, pylast.Track) - - @handle_lastfm_exceptions - def test_group_charts(self): - # Arrange - group = self.network.get_group("mnml") - dates = group.get_weekly_chart_dates() - self.helper_dates_valid(dates) - - # Act/Assert - self.helper_get_assert_charts(group, dates[-2]) - - @handle_lastfm_exceptions - def test_tag_charts(self): - # Arrange - tag = self.network.get_tag("rock") - dates = tag.get_weekly_chart_dates() - self.helper_dates_valid(dates) - - # Act/Assert - self.helper_get_assert_charts(tag, dates[-2]) - - @handle_lastfm_exceptions - def test_user_charts(self): - # Arrange - lastfm_user = self.network.get_user("RJ") - dates = lastfm_user.get_weekly_chart_dates() - self.helper_dates_valid(dates) - - # Act/Assert - self.helper_get_assert_charts(lastfm_user, dates[0]) - - @handle_lastfm_exceptions - def test_track_top_fans(self): - # Arrange - track = self.network.get_track("The Cinematic Orchestra", "Postlude") - - # Act - fans = track.get_top_fans() - - # Assert - self.helper_at_least_one_thing_in_top_list(fans, pylast.User) - - # Commented out to avoid spamming - # def test_share_spam(self): - # # Arrange - # users_to_spam = [TODO_ENTER_SPAMEES_HERE] - # spam_message = "Dig the krazee sound!" - # artist = self.network.get_top_artists(limit=1)[0].item - # track = artist.get_top_tracks(limit=1)[0].item - # event = artist.get_upcoming_events()[0] - - # # Act - # artist.share(users_to_spam, spam_message) - # track.share(users_to_spam, spam_message) - # event.share(users_to_spam, spam_message) - - # Assert - # Check inbox for spam! - - # album/artist/event/track/user - - @handle_lastfm_exceptions - def test_album_shouts(self): - # Arrange - # Pick an artist with plenty of plays - artist = self.network.get_top_artists(limit=1)[0].item - album = artist.get_top_albums(limit=1)[0].item - - # Act - shouts = album.get_shouts(limit=2) - - # Assert - self.helper_two_things_in_list(shouts, pylast.Shout) - - @handle_lastfm_exceptions - def test_artist_shouts(self): - # Arrange - # Pick an artist with plenty of plays - artist = self.network.get_top_artists(limit=1)[0].item - - # Act - shouts = artist.get_shouts(limit=2) - - # Assert - self.helper_two_things_in_list(shouts, pylast.Shout) - - @handle_lastfm_exceptions - def test_event_shouts(self): - # Arrange - event_id = 3478520 # Glasto 2014 - event = pylast.Event(event_id, self.network) - - # Act - shouts = event.get_shouts(limit=2) - - # Assert - self.helper_two_things_in_list(shouts, pylast.Shout) - - @handle_lastfm_exceptions - def test_track_shouts(self): - # Arrange - track = self.network.get_track("The Cinematic Orchestra", "Postlude") - - # Act - shouts = track.get_shouts(limit=2) - - # Assert - self.helper_two_things_in_list(shouts, pylast.Shout) - - @handle_lastfm_exceptions - def test_user_shouts(self): - # Arrange - user = self.network.get_user("RJ") - - # Act - shouts = user.get_shouts(limit=2) - - # Assert - self.helper_two_things_in_list(shouts, pylast.Shout) - - @handle_lastfm_exceptions - def test_album_data(self): - # Arrange - thing = self.network.get_album("Test Artist", "Test Album") - - # Act - stringed = str(thing) - repr = thing.__repr__() - title = thing.get_title() - name = thing.get_name() - playcount = thing.get_playcount() - url = thing.get_url() - - # Assert - self.assertEqual(stringed, "Test Artist - Test Album") - self.assertIn("pylast.Album('Test Artist', 'Test Album',", repr) - self.assertEqual(title, name) - self.assertIsInstance(playcount, int) - self.assertGreater(playcount, 1) - self.assertEqual( - "https://www.last.fm/music/test%2bartist/test%2balbum", url) - - @handle_lastfm_exceptions - def test_track_data(self): - # Arrange - thing = self.network.get_track("Test Artist", "test title") - - # Act - stringed = str(thing) - repr = thing.__repr__() - title = thing.get_title() - name = thing.get_name() - playcount = thing.get_playcount() - url = thing.get_url(pylast.DOMAIN_FRENCH) - - # Assert - self.assertEqual(stringed, "Test Artist - test title") - self.assertIn("pylast.Track('Test Artist', 'test title',", repr) - self.assertEqual(title, "test title") - self.assertEqual(title, name) - self.assertIsInstance(playcount, int) - self.assertGreater(playcount, 1) - self.assertEqual( - "https://www.last.fm/fr/music/test%2bartist/_/test%2btitle", url) - - @handle_lastfm_exceptions - def test_tag_top_artists(self): - # Arrange - tag = self.network.get_tag("blues") - - # Act - artists = tag.get_top_artists(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - - @handle_lastfm_exceptions - def test_country_top_artists(self): - # Arrange - country = self.network.get_country("Ukraine") - - # Act - artists = country.get_top_artists(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - - @handle_lastfm_exceptions - def test_user_top_artists(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - - # Act - artists = lastfm_user.get_top_artists(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - - @handle_lastfm_exceptions - def test_tag_top_albums(self): - # Arrange - tag = self.network.get_tag("blues") - - # Act - albums = tag.get_top_albums(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(albums, pylast.Album) - - @handle_lastfm_exceptions - def test_user_top_albums(self): - # Arrange - user = self.network.get_user("RJ") - - # Act - albums = user.get_top_albums(limit=1) - - # Assert - self.helper_only_one_thing_in_top_list(albums, pylast.Album) - - @handle_lastfm_exceptions - def test_user_tagged_artists(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - tags = ["artisttagola"] - artist = self.network.get_artist("Test Artist") - artist.add_tags(tags) - - # Act - artists = lastfm_user.get_tagged_artists('artisttagola', limit=1) - - # Assert - self.helper_only_one_thing_in_list(artists, pylast.Artist) - - @handle_lastfm_exceptions - def test_user_tagged_albums(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - tags = ["albumtagola"] - album = self.network.get_album("Test Artist", "Test Album") - album.add_tags(tags) - - # Act - albums = lastfm_user.get_tagged_albums('albumtagola', limit=1) - - # Assert - self.helper_only_one_thing_in_list(albums, pylast.Album) - - @handle_lastfm_exceptions - def test_user_tagged_tracks(self): - # Arrange - lastfm_user = self.network.get_user(self.username) - tags = ["tracktagola"] - track = self.network.get_track("Test Artist", "test title") - track.add_tags(tags) - # Act - tracks = lastfm_user.get_tagged_tracks('tracktagola', limit=1) - - # Assert - self.helper_only_one_thing_in_list(tracks, pylast.Track) - - @handle_lastfm_exceptions - def test_caching(self): - # Arrange - user = self.network.get_user("RJ") - - # Act - self.network.enable_caching() - shouts1 = user.get_shouts(limit=1, cacheable=True) - shouts2 = user.get_shouts(limit=1, cacheable=True) - - # Assert - self.assertTrue(self.network.is_caching_enabled()) - self.assertEqual(shouts1, shouts2) - self.network.disable_caching() - self.assertFalse(self.network.is_caching_enabled()) - - @handle_lastfm_exceptions - def test_create_playlist(self): - # Arrange - title = "Test playlist" - description = "Testing" - lastfm_user = self.network.get_user(self.username) - - # Act - playlist = self.network.create_new_playlist(title, description) - - # Assert - self.assertIsInstance(playlist, pylast.Playlist) - self.assertEqual(playlist.get_title(), "Test playlist") - self.assertEqual(playlist.get_description(), "Testing") - self.assertEqual(playlist.get_user(), lastfm_user) - - @handle_lastfm_exceptions - def test_empty_playlist_unstreamable(self): - # Arrange - title = "Empty playlist" - description = "Unstreamable" - - # Act - playlist = self.network.create_new_playlist(title, description) - - # Assert - self.assertEqual(playlist.get_size(), 0) - self.assertEqual(playlist.get_duration(), 0) - self.assertFalse(playlist.is_streamable()) - - @handle_lastfm_exceptions - def test_big_playlist_is_streamable(self): - # Arrange - # Find a big playlist on Last.fm, eg "top 100 classick rock songs" - user = "kaxior" - id = 10417943 - playlist = pylast.Playlist(user, id, self.network) - self.assertEqual( - playlist.get_url(), - "https://www.last.fm/user/kaxior/library/" - "playlists/67ajb_top_100_classick_rock_songs") - - # Act - # Nothing - - # Assert - self.assertIsInstance(playlist, pylast.Playlist) - self.assertGreaterEqual(playlist.get_size(), 45) - self.assertGreater(playlist.get_duration(), 0) - self.assertTrue(playlist.is_streamable()) - - @handle_lastfm_exceptions - def test_add_track_to_playlist(self): - # Arrange - title = "One track playlist" - description = "Testing" - playlist = self.network.create_new_playlist(title, description) - track = pylast.Track("Test Artist", "test title", self.network) - - # Act - playlist.add_track(track) - - # Assert - self.assertEqual(playlist.get_size(), 1) - self.assertEqual(len(playlist.get_tracks()), 1) - self.assertTrue(playlist.has_track(track)) - - @handle_lastfm_exceptions - def test_album_mbid(self): - # Arrange - mbid = "a6a265bf-9f81-4055-8224-f7ac0aa6b937" - - # Act - album = self.network.get_album_by_mbid(mbid) - album_mbid = album.get_mbid() - - # Assert - self.assertIsInstance(album, pylast.Album) - self.assertEqual(album.title.lower(), "test") - self.assertEqual(album_mbid, mbid) - - @handle_lastfm_exceptions - def test_artist_mbid(self): - # Arrange - mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55" - - # Act - artist = self.network.get_artist_by_mbid(mbid) - - # Assert - self.assertIsInstance(artist, pylast.Artist) - self.assertEqual(artist.name, "MusicBrainz Test Artist") - - @handle_lastfm_exceptions - def test_track_mbid(self): - # Arrange - mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" - - # Act - track = self.network.get_track_by_mbid(mbid) - track_mbid = track.get_mbid() - - # Assert - self.assertIsInstance(track, pylast.Track) - self.assertEqual(track.title, "first") - self.assertEqual(track_mbid, mbid) - - @handle_lastfm_exceptions - def test_artist_listener_count(self): - # Arrange - artist = self.network.get_artist("Test Artist") - - # Act - count = artist.get_listener_count() - - # Assert - self.assertIsInstance(count, int) - self.assertGreater(count, 0) - - @handle_lastfm_exceptions - def test_event_attendees(self): - # Arrange - user = self.network.get_user("RJ") - event = user.get_past_events(limit=1)[0] - - # Act - users = event.get_attendees() - - # Assert - self.assertIsInstance(users, list) - self.assertIsInstance(users[0], pylast.User) - - @handle_lastfm_exceptions - def test_tag_artist(self): - # Arrange - artist = self.network.get_artist("Test Artist") -# artist.clear_tags() - - # Act - artist.add_tag("testing") - - # Assert - tags = artist.get_tags() - self.assertGreater(len(tags), 0) - found = False - for tag in tags: - if tag.name == "testing": - found = True - break - self.assertTrue(found) - - @handle_lastfm_exceptions - def test_remove_tag_of_type_text(self): - # Arrange - tag = "testing" # text - artist = self.network.get_artist("Test Artist") - artist.add_tag(tag) - - # Act - artist.remove_tag(tag) - - # Assert - tags = artist.get_tags() - found = False - for tag in tags: - if tag.name == "testing": - found = True - break - self.assertFalse(found) - - @handle_lastfm_exceptions - def test_remove_tag_of_type_tag(self): - # Arrange - tag = pylast.Tag("testing", self.network) # Tag - artist = self.network.get_artist("Test Artist") - artist.add_tag(tag) - - # Act - artist.remove_tag(tag) - - # Assert - tags = artist.get_tags() - found = False - for tag in tags: - if tag.name == "testing": - found = True - break - self.assertFalse(found) - - @handle_lastfm_exceptions - def test_remove_tags(self): - # Arrange - tags = ["removetag1", "removetag2"] - artist = self.network.get_artist("Test Artist") - artist.add_tags(tags) - artist.add_tags("1more") - tags_before = artist.get_tags() - - # Act - artist.remove_tags(tags) - - # Assert - tags_after = artist.get_tags() - self.assertEqual(len(tags_after), len(tags_before) - 2) - found1, found2 = False, False - for tag in tags_after: - if tag.name == "removetag1": - found1 = True - elif tag.name == "removetag2": - found2 = True - self.assertFalse(found1) - self.assertFalse(found2) - - @handle_lastfm_exceptions - def test_set_tags(self): - # Arrange - tags = ["sometag1", "sometag2"] - artist = self.network.get_artist("Test Artist") - artist.add_tags(tags) - tags_before = artist.get_tags() - new_tags = ["settag1", "settag2"] - - # Act - artist.set_tags(new_tags) - - # Assert - tags_after = artist.get_tags() - self.assertNotEqual(tags_before, tags_after) - self.assertEqual(len(tags_after), 2) - found1, found2 = False, False - for tag in tags_after: - if tag.name == "settag1": - found1 = True - elif tag.name == "settag2": - found2 = True - self.assertTrue(found1) - self.assertTrue(found2) - - @handle_lastfm_exceptions - def test_tracks_notequal(self): - # Arrange - track1 = pylast.Track("Test Artist", "test title", self.network) - track2 = pylast.Track("Test Artist", "Test Track", self.network) - - # Act - # Assert - self.assertNotEqual(track1, track2) - - @handle_lastfm_exceptions - def test_track_id(self): - # Arrange - track = pylast.Track("Test Artist", "test title", self.network) - - # Act - id = track.get_id() - - # Assert - self.skip_if_lastfm_api_broken(id) - self.assertEqual(id, "14053327") - - @handle_lastfm_exceptions - def test_track_title_prop_caps(self): - # Arrange - track = pylast.Track("test artist", "test title", self.network) - - # Act - title = track.get_title(properly_capitalized=True) - - # Assert - self.assertEqual(title, "test title") - - @handle_lastfm_exceptions - def test_track_listener_count(self): - # Arrange - track = pylast.Track("test artist", "test title", self.network) - - # Act - count = track.get_listener_count() - - # Assert - self.assertGreater(count, 21) - - @handle_lastfm_exceptions - def test_album_rel_date(self): - # Arrange - album = pylast.Album("Test Artist", "Test Release", self.network) - - # Act - date = album.get_release_date() - - # Assert - self.skip_if_lastfm_api_broken(date) - self.assertIn("2011", date) - - @handle_lastfm_exceptions - def test_album_tracks(self): - # Arrange - album = pylast.Album("Test Artist", "Test Release", self.network) - - # Act - tracks = album.get_tracks() - - # Assert - self.assertIsInstance(tracks, list) - self.assertIsInstance(tracks[0], pylast.Track) - self.assertEqual(len(tracks), 4) - - @handle_lastfm_exceptions - def test_tags(self): - # Arrange - tag1 = self.network.get_tag("blues") - tag2 = self.network.get_tag("rock") - - # Act - tag_repr = repr(tag1) - tag_str = str(tag1) - name = tag1.get_name(properly_capitalized=True) - url = tag1.get_url() - - # Assert - self.assertEqual("blues", tag_str) - self.assertIn("pylast.Tag", tag_repr) - self.assertIn("blues", tag_repr) - self.assertEqual("blues", name) - self.assertTrue(tag1 == tag1) - self.assertTrue(tag1 != tag2) - self.assertEqual(url, "https://www.last.fm/tag/blues") - - @handle_lastfm_exceptions - def test_tags_similar(self): - # Arrange - tag = self.network.get_tag("blues") - - # Act - similar = tag.get_similar() - - # Assert - self.skip_if_lastfm_api_broken(similar) - found = False - for tag in similar: - if tag.name == "delta blues": - found = True - break - self.assertTrue(found) - - @handle_lastfm_exceptions - def test_artists(self): - # Arrange - artist1 = self.network.get_artist("Radiohead") - artist2 = self.network.get_artist("Portishead") - - # Act - url = artist1.get_url() - mbid = artist1.get_mbid() - image = artist1.get_cover_image() - playcount = artist1.get_playcount() - streamable = artist1.is_streamable() - name = artist1.get_name(properly_capitalized=False) - name_cap = artist1.get_name(properly_capitalized=True) - - # Assert - self.assertIn("http", image) - self.assertGreater(playcount, 1) - self.assertTrue(artist1 != artist2) - self.assertEqual(name.lower(), name_cap.lower()) - self.assertEqual(url, "https://www.last.fm/music/radiohead") - self.assertEqual(mbid, "a74b1b7f-71a5-4011-9441-d0b5e4122711") - self.assertIsInstance(streamable, bool) - - @handle_lastfm_exceptions - def test_events(self): - # Arrange - event_id_1 = 3162700 # Glasto 2013 - event_id_2 = 3478520 # Glasto 2014 - event1 = pylast.Event(event_id_1, self.network) - event2 = pylast.Event(event_id_2, self.network) - - # Act - text = str(event1) - rep = repr(event1) - title = event1.get_title() - artists = event1.get_artists() - start = event1.get_start_date() - description = event1.get_description() - review_count = event1.get_review_count() - attendance_count = event1.get_attendance_count() - - # Assert - self.assertIn("3162700", rep) - self.assertIn("pylast.Event", rep) - self.assertEqual(text, "Event #3162700") - self.assertTrue(event1 != event2) - self.assertIn("Glastonbury", title) - found = False - for artist in artists: - if artist.name == "The Rolling Stones": - found = True - break - self.assertTrue(found) - self.assertIn("Wed, 26 Jun 2013", start) - self.assertIn("astonishing bundle", description) - self.assertGreater(review_count, 0) - self.assertGreater(attendance_count, 100) - - @handle_lastfm_exceptions - def test_countries(self): - # Arrange - country1 = pylast.Country("Italy", self.network) - country2 = pylast.Country("Finland", self.network) - - # Act - text = str(country1) - rep = repr(country1) - url = country1.get_url() - - # Assert - self.assertIn("Italy", rep) - self.assertIn("pylast.Country", rep) - self.assertEqual(text, "Italy") - self.assertTrue(country1 == country1) - self.assertTrue(country1 != country2) - self.assertEqual(url, "https://www.last.fm/place/italy") - - @handle_lastfm_exceptions - def test_track_eq_none_is_false(self): - # Arrange - track1 = None - track2 = pylast.Track("Test Artist", "test title", self.network) - - # Act / Assert - self.assertFalse(track1 == track2) - - @handle_lastfm_exceptions - def test_track_ne_none_is_true(self): - # Arrange - track1 = None - track2 = pylast.Track("Test Artist", "test title", self.network) - - # Act / Assert - self.assertTrue(track1 != track2) - - @handle_lastfm_exceptions - def test_artist_eq_none_is_false(self): - # Arrange - artist1 = None - artist2 = pylast.Artist("Test Artist", self.network) - - # Act / Assert - self.assertFalse(artist1 == artist2) - - @handle_lastfm_exceptions - def test_artist_ne_none_is_true(self): - # Arrange - artist1 = None - artist2 = pylast.Artist("Test Artist", self.network) - - # Act / Assert - self.assertTrue(artist1 != artist2) - - @handle_lastfm_exceptions - def test_album_eq_none_is_false(self): - # Arrange - album1 = None - album2 = pylast.Album("Test Artist", "Test Album", self.network) - - # Act / Assert - self.assertFalse(album1 == album2) - - @handle_lastfm_exceptions - def test_album_ne_none_is_true(self): - # Arrange - album1 = None - album2 = pylast.Album("Test Artist", "Test Album", self.network) - - # Act / Assert - self.assertTrue(album1 != album2) - - @handle_lastfm_exceptions - def test_event_eq_none_is_false(self): - # Arrange - event1 = None - event_id = 3478520 # Glasto 2014 - event2 = pylast.Event(event_id, self.network) - - # Act / Assert - self.assertFalse(event1 == event2) - - @handle_lastfm_exceptions - def test_event_ne_none_is_true(self): - # Arrange - event1 = None - event_id = 3478520 # Glasto 2014 - event2 = pylast.Event(event_id, self.network) - - # Act / Assert - self.assertTrue(event1 != event2) - - @handle_lastfm_exceptions - def test_band_members(self): - # Arrange - artist = pylast.Artist("The Beatles", self.network) - - # Act - band_members = artist.get_band_members() - - # Assert - self.skip_if_lastfm_api_broken(band_members) - self.assertGreaterEqual(len(band_members), 4) - - @handle_lastfm_exceptions - def test_no_band_members(self): - # Arrange - artist = pylast.Artist("John Lennon", self.network) - - # Act - band_members = artist.get_band_members() - - # Assert - self.assertIsNone(band_members) - - @handle_lastfm_exceptions - def test_get_recent_tracks_from_to(self): - # Arrange - lastfm_user = self.network.get_user("RJ") - - from datetime import datetime - start = datetime(2011, 7, 21, 15, 10) - end = datetime(2011, 7, 21, 15, 15) - import calendar - utc_start = calendar.timegm(start.utctimetuple()) - utc_end = calendar.timegm(end.utctimetuple()) - - # Act - tracks = lastfm_user.get_recent_tracks(time_from=utc_start, - time_to=utc_end) - - # Assert - self.assertEqual(len(tracks), 1) - self.assertEqual(str(tracks[0].track.artist), "Johnny Cash") - self.assertEqual(str(tracks[0].track.title), "Ring of Fire") - - @handle_lastfm_exceptions - def test_artist_get_correction(self): - # Arrange - artist = pylast.Artist("guns and roses", self.network) - - # Act - corrected_artist_name = artist.get_correction() - - # Assert - self.assertEqual(corrected_artist_name, "Guns N' Roses") - - @handle_lastfm_exceptions - def test_track_get_correction(self): - # Arrange - track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network) - - # Act - corrected_track_name = track.get_correction() - - # Assert - self.assertEqual(corrected_track_name, "Mr. Brownstone") - - @handle_lastfm_exceptions - def test_track_with_no_mbid(self): - # Arrange - track = pylast.Track("Static-X", "Set It Off", self.network) - - # Act - mbid = track.get_mbid() - - # Assert - self.assertEqual(mbid, None) - - def test_init_with_token(self): - # Arrange/Act - try: - pylast.LastFMNetwork( - api_key=self.__class__.secrets["api_key"], - api_secret=self.__class__.secrets["api_secret"], - token="invalid", - ) - except pylast.WSError as exc: - msg = str(exc) - - # Assert - self.assertEqual(msg, - "Unauthorized Token - This token has not been issued") - - -@flaky(max_runs=5, min_passes=1) -class TestPyLastWithLibreFm(unittest.TestCase): - """Own class for Libre.fm because we don't need the Last.fm setUp""" - - def test_libre_fm(self): - # Arrange - secrets = load_secrets() - username = secrets["username"] - password_hash = secrets["password_hash"] - - # Act - network = pylast.LibreFMNetwork( - password_hash=password_hash, username=username) - artist = network.get_artist("Radiohead") - name = artist.get_name() - - # Assert - self.assertEqual(name, "Radiohead") - if __name__ == '__main__': unittest.main(failfast=True) diff --git a/tests/test_pylast_album.py b/tests/test_pylast_album.py new file mode 100755 index 0000000..53581a5 --- /dev/null +++ b/tests/test_pylast_album.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastAlbum(PyLastTestCase): + + 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) + + # Assert + self.assertGreater(len(tags), 0) + self.assertIsInstance(tags[0], pylast.TopItem) + + def test_album_is_hashable(self): + # Arrange + album = self.network.get_album("Test Artist", "Test Album") + + # Act/Assert + self.helper_is_thing_hashable(album) + + def test_album_in_recent_tracks(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + + # Act + # 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] + + # Assert + self.assertTrue(hasattr(track, 'album')) + + def test_album_wiki_content(self): + # Arrange + album = pylast.Album("Test Artist", "Test Album", self.network) + + # Act + wiki = album.get_wiki_content() + + # Assert + self.assertIsNotNone(wiki) + self.assertGreaterEqual(len(wiki), 1) + + def test_album_wiki_published_date(self): + # Arrange + album = pylast.Album("Test Artist", "Test Album", self.network) + + # Act + wiki = album.get_wiki_published_date() + + # Assert + self.assertIsNotNone(wiki) + self.assertGreaterEqual(len(wiki), 1) + + def test_album_wiki_summary(self): + # Arrange + album = pylast.Album("Test Artist", "Test Album", self.network) + + # Act + wiki = album.get_wiki_summary() + + # Assert + self.assertIsNotNone(wiki) + self.assertGreaterEqual(len(wiki), 1) + + def test_album_eq_none_is_false(self): + # Arrange + album1 = None + album2 = pylast.Album("Test Artist", "Test Album", self.network) + + # Act / Assert + self.assertFalse(album1 == album2) + + def test_album_ne_none_is_true(self): + # Arrange + album1 = None + album2 = pylast.Album("Test Artist", "Test Album", self.network) + + # Act / Assert + self.assertTrue(album1 != album2) + + def test_get_cover_image(self): + # Arrange + album = self.network.get_album("Test Artist", "Test Album") + + # Act + image = album.get_cover_image() + + # Assert + self.assertTrue(image.startswith("https://")) + self.assertTrue(image.endswith(".png")) + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_artist.py b/tests/test_pylast_artist.py new file mode 100755 index 0000000..66dbb49 --- /dev/null +++ b/tests/test_pylast_artist.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastArtist(PyLastTestCase): + + def test_repr(self): + # Arrange + artist = pylast.Artist("Test Artist", self.network) + + # Act + representation = repr(artist) + + # Assert + self.assertTrue( + representation.startswith("pylast.Artist('Test Artist',")) + + def test_artist_is_hashable(self): + # Arrange + test_artist = self.network.get_artist("Test Artist") + artist = test_artist.get_similar(limit=2)[0].item + self.assertIsInstance(artist, pylast.Artist) + + # Act/Assert + self.helper_is_thing_hashable(artist) + + def test_bio_published_date(self): + # Arrange + artist = pylast.Artist("Test Artist", self.network) + + # Act + bio = artist.get_bio_published_date() + + # Assert + self.assertIsNotNone(bio) + self.assertGreaterEqual(len(bio), 1) + + def test_bio_content(self): + # Arrange + artist = pylast.Artist("Test Artist", self.network) + + # Act + 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") + + # Assert + self.assertIsNotNone(bio) + self.assertGreaterEqual(len(bio), 1) + + def test_artist_top_tracks(self): + # Arrange + # Pick an artist with plenty of plays + artist = self.network.get_top_artists(limit=1)[0].item + + # Act + things = artist.get_top_tracks(limit=2) + + # Assert + self.helper_two_different_things_in_top_list(things, pylast.Track) + + def test_artist_top_albums(self): + # Arrange + # Pick an artist with plenty of plays + artist = self.network.get_top_artists(limit=1)[0].item + + # Act + things = artist.get_top_albums(limit=2) + + # Assert + self.helper_two_different_things_in_top_list(things, pylast.Album) + + def test_artist_listener_count(self): + # Arrange + artist = self.network.get_artist("Test Artist") + + # Act + count = artist.get_listener_count() + + # Assert + self.assertIsInstance(count, int) + self.assertGreater(count, 0) + + def test_tag_artist(self): + # Arrange + artist = self.network.get_artist("Test Artist") +# artist.clear_tags() + + # Act + artist.add_tag("testing") + + # Assert + tags = artist.get_tags() + self.assertGreater(len(tags), 0) + found = False + for tag in tags: + if tag.name == "testing": + found = True + break + self.assertTrue(found) + + def test_remove_tag_of_type_text(self): + # Arrange + tag = "testing" # text + artist = self.network.get_artist("Test Artist") + artist.add_tag(tag) + + # Act + artist.remove_tag(tag) + + # Assert + tags = artist.get_tags() + found = False + for tag in tags: + if tag.name == "testing": + found = True + break + self.assertFalse(found) + + def test_remove_tag_of_type_tag(self): + # Arrange + tag = pylast.Tag("testing", self.network) # Tag + artist = self.network.get_artist("Test Artist") + artist.add_tag(tag) + + # Act + artist.remove_tag(tag) + + # Assert + tags = artist.get_tags() + found = False + for tag in tags: + if tag.name == "testing": + found = True + break + self.assertFalse(found) + + def test_remove_tags(self): + # Arrange + tags = ["removetag1", "removetag2"] + artist = self.network.get_artist("Test Artist") + artist.add_tags(tags) + artist.add_tags("1more") + tags_before = artist.get_tags() + + # Act + artist.remove_tags(tags) + + # Assert + tags_after = artist.get_tags() + self.assertEqual(len(tags_after), len(tags_before) - 2) + found1, found2 = False, False + for tag in tags_after: + if tag.name == "removetag1": + found1 = True + elif tag.name == "removetag2": + found2 = True + self.assertFalse(found1) + self.assertFalse(found2) + + def test_set_tags(self): + # Arrange + tags = ["sometag1", "sometag2"] + artist = self.network.get_artist("Test Artist 2") + artist.add_tags(tags) + tags_before = artist.get_tags() + new_tags = ["settag1", "settag2"] + + # Act + artist.set_tags(new_tags) + + # Assert + tags_after = artist.get_tags() + self.assertNotEqual(tags_before, tags_after) + self.assertEqual(len(tags_after), 2) + found1, found2 = False, False + for tag in tags_after: + if tag.name == "settag1": + found1 = True + elif tag.name == "settag2": + found2 = True + self.assertTrue(found1) + self.assertTrue(found2) + + def test_artists(self): + # Arrange + artist1 = self.network.get_artist("Radiohead") + artist2 = self.network.get_artist("Portishead") + + # Act + url = artist1.get_url() + mbid = artist1.get_mbid() + image = artist1.get_cover_image() + playcount = artist1.get_playcount() + streamable = artist1.is_streamable() + name = artist1.get_name(properly_capitalized=False) + name_cap = artist1.get_name(properly_capitalized=True) + + # Assert + self.assertIn("https", image) + self.assertGreater(playcount, 1) + self.assertTrue(artist1 != artist2) + self.assertEqual(name.lower(), name_cap.lower()) + self.assertEqual(url, "https://www.last.fm/music/radiohead") + self.assertEqual(mbid, "a74b1b7f-71a5-4011-9441-d0b5e4122711") + self.assertIsInstance(streamable, bool) + + def test_artist_eq_none_is_false(self): + # Arrange + artist1 = None + artist2 = pylast.Artist("Test Artist", self.network) + + # Act / Assert + self.assertFalse(artist1 == artist2) + + def test_artist_ne_none_is_true(self): + # Arrange + artist1 = None + artist2 = pylast.Artist("Test Artist", self.network) + + # Act / Assert + self.assertTrue(artist1 != artist2) + + def test_artist_get_correction(self): + # Arrange + artist = pylast.Artist("guns and roses", self.network) + + # Act + corrected_artist_name = artist.get_correction() + + # Assert + self.assertEqual(corrected_artist_name, "Guns N' Roses") + + def test_get_userplaycount(self): + # Arrange + artist = pylast.Artist("John Lennon", self.network, + username=self.username) + + # Act + playcount = artist.get_userplaycount() + + # Assert + self.assertGreaterEqual(playcount, 0) + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_country.py b/tests/test_pylast_country.py new file mode 100755 index 0000000..7d9554e --- /dev/null +++ b/tests/test_pylast_country.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastCountry(PyLastTestCase): + + def test_country_is_hashable(self): + # Arrange + country = self.network.get_country("Italy") + + # Act/Assert + self.helper_is_thing_hashable(country) + + def test_countries(self): + # Arrange + country1 = pylast.Country("Italy", self.network) + country2 = pylast.Country("Finland", self.network) + + # Act + text = str(country1) + rep = repr(country1) + url = country1.get_url() + + # Assert + self.assertIn("Italy", rep) + self.assertIn("pylast.Country", rep) + self.assertEqual(text, "Italy") + self.assertTrue(country1 == country1) + self.assertTrue(country1 != country2) + self.assertEqual(url, "https://www.last.fm/place/italy") + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_library.py b/tests/test_pylast_library.py new file mode 100755 index 0000000..1e437f1 --- /dev/null +++ b/tests/test_pylast_library.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastLibrary(PyLastTestCase): + + def test_repr(self): + # Arrange + library = pylast.Library(user=self.username, network=self.network) + + # Act + representation = repr(library) + + # Assert + self.assertTrue(representation.startswith("pylast.Library(")) + + def test_str(self): + # Arrange + library = pylast.Library(user=self.username, network=self.network) + + # Act + string = str(library) + + # Assert + self.assertTrue(string.endswith("'s Library")) + + def test_library_is_hashable(self): + # Arrange + library = pylast.Library(user=self.username, network=self.network) + + # Act/Assert + self.helper_is_thing_hashable(library) + + def test_cacheable_library(self): + # Arrange + library = pylast.Library(self.username, self.network) + + # Act/Assert + self.helper_validate_cacheable(library, "get_artists") + + def test_get_user(self): + # Arrange + library = pylast.Library(user=self.username, network=self.network) + user_to_get = self.network.get_user(self.username) + + # Act + library_user = library.get_user() + + # Assert + self.assertEqual(library_user, user_to_get) + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_librefm.py b/tests/test_pylast_librefm.py new file mode 100755 index 0000000..7c958cb --- /dev/null +++ b/tests/test_pylast_librefm.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +from flaky import flaky + +import pylast + +from .test_pylast import load_secrets + + +@flaky(max_runs=5, min_passes=1) +class TestPyLastWithLibreFm(unittest.TestCase): + """Own class for Libre.fm because we don't need the Last.fm setUp""" + + def test_libre_fm(self): + # Arrange + secrets = load_secrets() + username = secrets["username"] + password_hash = secrets["password_hash"] + + # Act + network = pylast.LibreFMNetwork( + password_hash=password_hash, username=username) + artist = network.get_artist("Radiohead") + name = artist.get_name() + + # Assert + self.assertEqual(name, "Radiohead") + + def test_repr(self): + # Arrange + secrets = load_secrets() + username = secrets["username"] + password_hash = secrets["password_hash"] + network = pylast.LibreFMNetwork( + password_hash=password_hash, username=username) + + # Act + representation = repr(network) + + # Assert + self.assertTrue(representation.startswith("pylast.LibreFMNetwork(")) + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_network.py b/tests/test_pylast_network.py new file mode 100755 index 0000000..23d2f20 --- /dev/null +++ b/tests/test_pylast_network.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import time +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastNetwork(PyLastTestCase): + + def test_scrobble(self): + # Arrange + artist = "Test Artist" + title = "test title" + timestamp = self.unix_timestamp() + lastfm_user = self.network.get_user(self.username) + + # Act + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) + + # Assert + # 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_update_now_playing(self): + # Arrange + artist = "Test Artist" + title = "test title" + album = "Test Album" + track_number = 1 + lastfm_user = self.network.get_user(self.username) + + # Act + self.network.update_now_playing( + artist=artist, title=title, album=album, track_number=track_number) + + # Assert + current_track = lastfm_user.get_now_playing() + self.assertIsNotNone(current_track) + self.assertEqual(str(current_track.title), "test title") + self.assertEqual(str(current_track.artist), "Test Artist") + + def test_enable_rate_limiting(self): + # Arrange + self.assertFalse(self.network.is_rate_limited()) + + # Act + self.network.enable_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.assertTrue(self.network.is_rate_limited()) + self.assertGreaterEqual(now - then, 0.2) + + def test_disable_rate_limiting(self): + # Arrange + self.network.enable_rate_limit() + self.assertTrue(self.network.is_rate_limited()) + + # Act + self.network.disable_rate_limit() + # 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() + + # Assert + self.assertFalse(self.network.is_rate_limited()) + + def test_lastfm_network_name(self): + # Act + name = str(self.network) + + # Assert + self.assertEqual(name, "Last.fm Network") + + def test_geo_get_top_artists(self): + # Arrange + # Act + artists = self.network.get_geo_top_artists( + country="United Kingdom", limit=1) + + # Assert + self.assertEqual(len(artists), 1) + self.assertIsInstance(artists[0], pylast.TopItem) + self.assertIsInstance(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) + + # Assert + self.assertEqual(len(tracks), 1) + self.assertIsInstance(tracks[0], pylast.TopItem) + self.assertIsInstance(tracks[0].item, pylast.Track) + + def test_network_get_top_artists_with_limit(self): + # Arrange + # Act + 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) + + # Assert + self.helper_only_one_thing_in_top_list(tags, pylast.Tag) + + def test_network_get_top_tags_with_no_limit(self): + # Arrange + # Act + tags = self.network.get_top_tags() + + # Assert + self.helper_at_least_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) + + # Assert + self.helper_only_one_thing_in_top_list(tracks, pylast.Track) + + def test_country_top_tracks(self): + # Arrange + country = self.network.get_country("Croatia") + + # Act + things = country.get_top_tracks(limit=2) + + # Assert + self.helper_two_different_things_in_top_list(things, pylast.Track) + + def test_country_network_top_tracks(self): + # Arrange + # Act + things = self.network.get_geo_top_tracks("Croatia", limit=2) + + # Assert + self.helper_two_different_things_in_top_list(things, pylast.Track) + + def test_tag_top_tracks(self): + # Arrange + tag = self.network.get_tag("blues") + + # Act + things = tag.get_top_tracks(limit=2) + + # Assert + self.helper_two_different_things_in_top_list(things, pylast.Track) + + def test_album_data(self): + # Arrange + thing = self.network.get_album("Test Artist", "Test Album") + + # Act + stringed = str(thing) + rep = thing.__repr__() + title = thing.get_title() + name = thing.get_name() + playcount = thing.get_playcount() + url = thing.get_url() + + # Assert + self.assertEqual(stringed, "Test Artist - Test Album") + self.assertIn("pylast.Album('Test Artist', 'Test Album',", rep) + self.assertEqual(title, name) + self.assertIsInstance(playcount, int) + self.assertGreater(playcount, 1) + self.assertEqual( + "https://www.last.fm/music/test%2bartist/test%2balbum", url) + + def test_track_data(self): + # Arrange + thing = self.network.get_track("Test Artist", "test title") + + # Act + stringed = str(thing) + rep = thing.__repr__() + title = thing.get_title() + name = thing.get_name() + playcount = thing.get_playcount() + url = thing.get_url(pylast.DOMAIN_FRENCH) + + # Assert + self.assertEqual(stringed, "Test Artist - test title") + self.assertIn("pylast.Track('Test Artist', 'test title',", rep) + self.assertEqual(title, "test title") + self.assertEqual(title, name) + self.assertIsInstance(playcount, int) + self.assertGreater(playcount, 1) + self.assertEqual( + "https://www.last.fm/fr/music/test%2bartist/_/test%2btitle", url) + + def test_country_top_artists(self): + # Arrange + country = self.network.get_country("Ukraine") + + # Act + artists = country.get_top_artists(limit=1) + + # Assert + self.helper_only_one_thing_in_top_list(artists, pylast.Artist) + + def test_caching(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + self.network.enable_caching() + tags1 = user.get_top_tags(limit=1, cacheable=True) + tags2 = user.get_top_tags(limit=1, cacheable=True) + + # Assert + self.assertTrue(self.network.is_caching_enabled()) + self.assertEqual(tags1, tags2) + self.network.disable_caching() + self.assertFalse(self.network.is_caching_enabled()) + + def test_album_mbid(self): + # Arrange + mbid = "a6a265bf-9f81-4055-8224-f7ac0aa6b937" + + # Act + album = self.network.get_album_by_mbid(mbid) + album_mbid = album.get_mbid() + + # Assert + self.assertIsInstance(album, pylast.Album) + self.assertEqual(album.title.lower(), "test") + self.assertEqual(album_mbid, mbid) + + def test_artist_mbid(self): + # Arrange + mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55" + + # Act + artist = self.network.get_artist_by_mbid(mbid) + + # Assert + self.assertIsInstance(artist, pylast.Artist) + self.assertEqual(artist.name, "MusicBrainz Test Artist") + + def test_track_mbid(self): + # Arrange + mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" + + # Act + track = self.network.get_track_by_mbid(mbid) + track_mbid = track.get_mbid() + + # Assert + self.assertIsInstance(track, pylast.Track) + self.assertEqual(track.title, "first") + self.assertEqual(track_mbid, mbid) + + def test_init_with_token(self): + # Arrange/Act + msg = None + try: + pylast.LastFMNetwork( + api_key=self.__class__.secrets["api_key"], + api_secret=self.__class__.secrets["api_secret"], + token="invalid", + ) + except pylast.WSError as exc: + msg = str(exc) + + # Assert + self.assertEqual(msg, + "Unauthorized Token - This token has not been issued") + + def test_proxy(self): + # Arrange + host = "https://example.com" + port = 1234 + + # Act / Assert + self.network.enable_proxy(host, port) + self.assertTrue(self.network.is_proxy_enabled()) + self.assertEqual(self.network._get_proxy(), + ["https://example.com", 1234]) + + self.network.disable_proxy() + self.assertFalse(self.network.is_proxy_enabled()) + + def test_album_search(self): + # Arrange + album = "Nevermind" + + # Act + search = self.network.search_for_album(album) + results = search.get_next_page() + + # Assert + self.assertIsInstance(results, list) + self.assertIsInstance(results[0], pylast.Album) + + def test_artist_search(self): + # Arrange + artist = "Nirvana" + + # Act + search = self.network.search_for_artist(artist) + results = search.get_next_page() + + # Assert + self.assertIsInstance(results, list) + self.assertIsInstance(results[0], pylast.Artist) + + def test_track_search(self): + # Arrange + artist = "Nirvana" + track = "Smells Like Teen Spirit" + + # Act + search = self.network.search_for_track(artist, track) + results = search.get_next_page() + + # Assert + self.assertIsInstance(results, list) + self.assertIsInstance(results[0], pylast.Track) + + def test_search_get_total_result_count(self): + # Arrange + artist = "Nirvana" + track = "Smells Like Teen Spirit" + search = self.network.search_for_track(artist, track) + + # Act + total = search.get_total_result_count() + + # Assert + self.assertGreater(int(total), 10000) + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_tag.py b/tests/test_pylast_tag.py new file mode 100755 index 0000000..8d5440e --- /dev/null +++ b/tests/test_pylast_tag.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastTag(PyLastTestCase): + + def test_tag_is_hashable(self): + # Arrange + tag = self.network.get_top_tags(limit=1)[0] + + # Act/Assert + self.helper_is_thing_hashable(tag) + + def test_tag_top_artists(self): + # Arrange + tag = self.network.get_tag("blues") + + # Act + artists = tag.get_top_artists(limit=1) + + # Assert + self.helper_only_one_thing_in_top_list(artists, pylast.Artist) + + def test_tag_top_albums(self): + # Arrange + tag = self.network.get_tag("blues") + + # Act + albums = tag.get_top_albums(limit=1) + + # Assert + self.helper_only_one_thing_in_top_list(albums, pylast.Album) + + def test_tags(self): + # Arrange + tag1 = self.network.get_tag("blues") + tag2 = self.network.get_tag("rock") + + # Act + tag_repr = repr(tag1) + tag_str = str(tag1) + name = tag1.get_name(properly_capitalized=True) + url = tag1.get_url() + + # Assert + self.assertEqual("blues", tag_str) + self.assertIn("pylast.Tag", tag_repr) + self.assertIn("blues", tag_repr) + self.assertEqual("blues", name) + self.assertTrue(tag1 == tag1) + self.assertTrue(tag1 != tag2) + self.assertEqual(url, "https://www.last.fm/tag/blues") + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_track.py b/tests/test_pylast_track.py new file mode 100755 index 0000000..61ef132 --- /dev/null +++ b/tests/test_pylast_track.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastTrack(PyLastTestCase): + + def test_love(self): + # Arrange + artist = "Test Artist" + title = "test title" + track = self.network.get_track(artist, title) + lastfm_user = self.network.get_user(self.username) + + # Act + track.love() + + # Assert + 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 = pylast.Artist("Test Artist", self.network) + title = "test title" + track = pylast.Track(artist, title, self.network) + lastfm_user = self.network.get_user(self.username) + track.love() + + # Act + track.unlove() + + # Assert + 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_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) + + # Act + count = track.get_userplaycount() + + # 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) + + # Act + loved = track.get_userloved() + + # Assert + self.assertIsNotNone(loved) + self.assertIsInstance(loved, bool) + self.assertNotIsInstance(loved, str) + + def test_track_is_hashable(self): + # Arrange + artist = self.network.get_artist("Test Artist") + track = artist.get_top_tracks()[0].item + self.assertIsInstance(track, pylast.Track) + + # Act/Assert + self.helper_is_thing_hashable(track) + + def test_track_wiki_content(self): + # Arrange + track = pylast.Track("Test Artist", "test title", self.network) + + # Act + wiki = track.get_wiki_content() + + # Assert + self.assertIsNotNone(wiki) + self.assertGreaterEqual(len(wiki), 1) + + def test_track_wiki_summary(self): + # Arrange + track = pylast.Track("Test Artist", "test title", self.network) + + # Act + wiki = track.get_wiki_summary() + + # Assert + self.assertIsNotNone(wiki) + self.assertGreaterEqual(len(wiki), 1) + + def test_track_get_duration(self): + # Arrange + track = pylast.Track("Nirvana", "Lithium", self.network) + + # Act + duration = track.get_duration() + + # Assert + self.assertGreaterEqual(duration, 200000) + + def test_track_is_streamable(self): + # Arrange + track = pylast.Track("Nirvana", "Lithium", self.network) + + # Act + streamable = track.is_streamable() + + # Assert + self.assertFalse(streamable) + + def test_track_is_fulltrack_available(self): + # Arrange + track = pylast.Track("Nirvana", "Lithium", self.network) + + # Act + fulltrack_available = track.is_fulltrack_available() + + # Assert + self.assertFalse(fulltrack_available) + + def test_track_get_album(self): + # Arrange + track = pylast.Track("Nirvana", "Lithium", self.network) + + # Act + album = track.get_album() + print(album) + + # Assert + self.assertEqual(str(album), "Nirvana - Nevermind") + + def test_track_get_similar(self): + # Arrange + track = pylast.Track("Cher", "Believe", self.network) + + # Act + similar = track.get_similar() + + # Assert + found = False + for track in similar: + if str(track.item) == "Madonna - Vogue": + found = True + break + self.assertTrue(found) + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tests/test_pylast_user.py b/tests/test_pylast_user.py new file mode 100755 index 0000000..1169f41 --- /dev/null +++ b/tests/test_pylast_user.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python +""" +Integration (not unit) tests for pylast.py +""" +import os +import unittest + +import pylast + +from .test_pylast import PyLastTestCase + + +class TestPyLastUser(PyLastTestCase): + + def test_repr(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + representation = repr(user) + + # Assert + self.assertTrue(representation.startswith("pylast.User('RJ',")) + + def test_str(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + string = str(user) + + # Assert + self.assertEqual(string, "RJ") + + def test_equality(self): + # Arrange + user_1a = self.network.get_user("RJ") + user_1b = self.network.get_user("RJ") + user_2 = self.network.get_user("Test User") + not_a_user = self.network + + # Act / Assert + self.assertEqual(user_1a, user_1b) + self.assertTrue(user_1a == user_1b) + self.assertFalse(user_1a != user_1b) + + self.assertNotEqual(user_1a, user_2) + self.assertTrue(user_1a != user_2) + self.assertFalse(user_1a == user_2) + + self.assertNotEqual(user_1a, not_a_user) + self.assertTrue(user_1a != not_a_user) + self.assertFalse(user_1a == not_a_user) + + def test_get_name(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + name = user.get_name(properly_capitalized=True) + + # Assert + self.assertEqual(name, "RJ") + + def test_get_user_registration(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + registered = user.get_registered() + + # Assert + if int(registered): + # Last.fm API broken? Used to be yyyy-mm-dd not Unix timestamp + self.assertEqual(registered, "1037793040") + else: + # Old way + # Just check date because of timezones + self.assertIn(u"2002-11-20 ", registered) + + def test_get_user_unixtime_registration(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + unixtime_registered = user.get_unixtime_registered() + + # Assert + # Just check date because of timezones + self.assertEqual(unixtime_registered, u"1037793040") + + def test_get_countryless_user(self): + # Arrange + # Currently test_user has no country set: + lastfm_user = self.network.get_user("test_user") + + # Act + country = lastfm_user.get_country() + + # Assert + self.assertIsNone(country) + + def test_user_get_country(self): + # Arrange + lastfm_user = self.network.get_user("RJ") + + # Act + country = lastfm_user.get_country() + + # Assert + self.assertEqual(str(country), "United Kingdom") + + def test_user_equals_none(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + + # Act + 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 is not None) + + # Assert + self.assertTrue(value) + + def test_now_playing_user_with_no_scrobbles(self): + # Arrange + # Currently test-account has no scrobbles: + user = self.network.get_user('test-account') + + # Act + current_track = user.get_now_playing() + + # Assert + self.assertIsNone(current_track) + + def test_love_limits(self): + # Arrange + # 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) + self.assertLessEqual(len(user.get_loved_tracks(limit=100)), 100) + self.assertGreaterEqual(len(user.get_loved_tracks(limit=None)), 23) + self.assertGreaterEqual(len(user.get_loved_tracks(limit=0)), 23) + + def test_user_is_hashable(self): + # Arrange + user = self.network.get_user(self.username) + + # Act/Assert + self.helper_is_thing_hashable(user) + + # 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) + + # # Assert + # self.assertGreaterEqual(len(tracks), 0) + + def test_pickle(self): + # Arrange + import pickle + lastfm_user = self.network.get_user(self.username) + filename = str(self.unix_timestamp()) + ".pkl" + + # Act + with open(filename, "wb") as f: + pickle.dump(lastfm_user, f) + with open(filename, "rb") as f: + loaded_user = pickle.load(f) + os.remove(filename) + + # Assert + self.assertEqual(lastfm_user, loaded_user) + + def test_cacheable_user_artist_tracks(self): + # Arrange + lastfm_user = self.network.get_authenticated_user() + + # Act + 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() + + # Act/Assert + self.helper_validate_cacheable(lastfm_user, "get_friends") + self.helper_validate_cacheable(lastfm_user, "get_loved_tracks") + self.helper_validate_cacheable(lastfm_user, "get_recent_tracks") + + def test_user_get_top_tags_with_limit(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + tags = user.get_top_tags(limit=1) + + # Assert + self.helper_only_one_thing_in_top_list(tags, pylast.Tag) + + def test_user_top_tracks(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + + # Act + things = lastfm_user.get_top_tracks(limit=2) + + # Assert + self.helper_two_different_things_in_top_list(things, pylast.Track) + + def helper_assert_chart(self, chart, expected_type): + # Assert + self.assertIsNotNone(chart) + self.assertGreater(len(chart), 0) + self.assertIsInstance(chart[0], pylast.TopItem) + self.assertIsInstance(chart[0].item, expected_type) + + def helper_get_assert_charts(self, thing, date): + # Arrange + album_chart, track_chart = None, None + (from_date, to_date) = date + + # Act + artist_chart = thing.get_weekly_artist_charts(from_date, to_date) + if type(thing) is not pylast.Tag: + album_chart = thing.get_weekly_album_charts(from_date, to_date) + track_chart = thing.get_weekly_track_charts(from_date, to_date) + + # Assert + self.helper_assert_chart(artist_chart, pylast.Artist) + if type(thing) is not pylast.Tag: + self.helper_assert_chart(album_chart, pylast.Album) + self.helper_assert_chart(track_chart, pylast.Track) + + def helper_dates_valid(self, dates): + # Assert + self.assertGreaterEqual(len(dates), 1) + self.assertIsInstance(dates[0], tuple) + (start, end) = dates[0] + self.assertLess(start, end) + + def test_user_charts(self): + # Arrange + lastfm_user = self.network.get_user("RJ") + dates = lastfm_user.get_weekly_chart_dates() + self.helper_dates_valid(dates) + + # Act/Assert + self.helper_get_assert_charts(lastfm_user, dates[0]) + + def test_user_top_artists(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + + # Act + artists = lastfm_user.get_top_artists(limit=1) + + # Assert + self.helper_only_one_thing_in_top_list(artists, pylast.Artist) + + def test_user_top_albums(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + albums = user.get_top_albums(limit=1) + + # Assert + self.helper_only_one_thing_in_top_list(albums, pylast.Album) + + def test_user_tagged_artists(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + tags = ["artisttagola"] + artist = self.network.get_artist("Test Artist") + artist.add_tags(tags) + + # Act + artists = lastfm_user.get_tagged_artists('artisttagola', limit=1) + + # Assert + self.helper_only_one_thing_in_list(artists, pylast.Artist) + + def test_user_tagged_albums(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + tags = ["albumtagola"] + album = self.network.get_album("Test Artist", "Test Album") + album.add_tags(tags) + + # Act + albums = lastfm_user.get_tagged_albums('albumtagola', limit=1) + + # Assert + self.helper_only_one_thing_in_list(albums, pylast.Album) + + def test_user_tagged_tracks(self): + # Arrange + lastfm_user = self.network.get_user(self.username) + tags = ["tracktagola"] + track = self.network.get_track("Test Artist", "test title") + track.add_tags(tags) + + # Act + tracks = lastfm_user.get_tagged_tracks('tracktagola', limit=1) + + # Assert + self.helper_only_one_thing_in_list(tracks, pylast.Track) + + def test_user_subscriber(self): + # Arrange + subscriber = self.network.get_user("RJ") + non_subscriber = self.network.get_user("Test User") + + # Act + subscriber_is_subscriber = subscriber.is_subscriber() + non_subscriber_is_subscriber = non_subscriber.is_subscriber() + + # Assert + self.assertTrue(subscriber_is_subscriber) + self.assertFalse(non_subscriber_is_subscriber) + + def test_user_get_image(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + url = user.get_image() + + # Assert + self.assertTrue(url.startswith("https://")) + + def test_user_get_library(self): + # Arrange + user = self.network.get_user(self.username) + + # Act + library = user.get_library() + + # Assert + self.assertIsInstance(library, pylast.Library) + + def test_get_recent_tracks_from_to(self): + # Arrange + lastfm_user = self.network.get_user("RJ") + + from datetime import datetime + start = datetime(2011, 7, 21, 15, 10) + end = datetime(2011, 7, 21, 15, 15) + import calendar + utc_start = calendar.timegm(start.utctimetuple()) + utc_end = calendar.timegm(end.utctimetuple()) + + # Act + tracks = lastfm_user.get_recent_tracks(time_from=utc_start, + time_to=utc_end) + + # Assert + self.assertEqual(len(tracks), 1) + self.assertEqual(str(tracks[0].track.artist), "Johnny Cash") + self.assertEqual(str(tracks[0].track.title), "Ring of Fire") + + def test_tracks_notequal(self): + # Arrange + track1 = pylast.Track("Test Artist", "test title", self.network) + track2 = pylast.Track("Test Artist", "Test Track", self.network) + + # Act + # Assert + self.assertNotEqual(track1, track2) + + def test_track_title_prop_caps(self): + # Arrange + track = pylast.Track("test artist", "test title", self.network) + + # Act + title = track.get_title(properly_capitalized=True) + + # Assert + self.assertEqual(title, "test title") + + def test_track_listener_count(self): + # Arrange + track = pylast.Track("test artist", "test title", self.network) + + # Act + count = track.get_listener_count() + + # Assert + self.assertGreater(count, 21) + + def test_album_tracks(self): + # Arrange + album = pylast.Album("Test Artist", "Test Release", self.network) + + # Act + tracks = album.get_tracks() + url = tracks[0].get_url() + + # Assert + self.assertIsInstance(tracks, list) + self.assertIsInstance(tracks[0], pylast.Track) + self.assertEqual(len(tracks), 4) + self.assertTrue(url.startswith("https://www.last.fm/music/test")) + + def test_track_eq_none_is_false(self): + # Arrange + track1 = None + track2 = pylast.Track("Test Artist", "test title", self.network) + + # Act / Assert + self.assertFalse(track1 == track2) + + def test_track_ne_none_is_true(self): + # Arrange + track1 = None + track2 = pylast.Track("Test Artist", "test title", self.network) + + # Act / Assert + self.assertTrue(track1 != track2) + + def test_track_get_correction(self): + # Arrange + track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network) + + # Act + corrected_track_name = track.get_correction() + + # Assert + self.assertEqual(corrected_track_name, "Mr. Brownstone") + + def test_track_with_no_mbid(self): + # Arrange + track = pylast.Track("Static-X", "Set It Off", self.network) + + # Act + mbid = track.get_mbid() + + # Assert + self.assertEqual(mbid, None) + + def test_get_playcount(self): + # Arrange + user = self.network.get_user("RJ") + + # Act + playcount = user.get_playcount() + + # Assert + self.assertGreaterEqual(playcount, 128387) + + def test_get_image(self): + # Arrange + user = self.network.get_user("RJ") + + # Act / Assert + image = user.get_image() + + self.assertTrue(image.startswith("https://")) + self.assertTrue(image.endswith(".png")) + + def test_get_url(self): + # Arrange + user = self.network.get_user("RJ") + + # Act / Assert + url = user.get_url() + + self.assertEqual(url, "https://www.last.fm/user/rj") + + +if __name__ == '__main__': + unittest.main(failfast=True) diff --git a/tox.ini b/tox.ini index 42f8433..6347575 100644 --- a/tox.ini +++ b/tox.ini @@ -15,22 +15,30 @@ deps = ipdb pytest-cov flaky -commands = py.test -v --cov pylast --cov-report term-missing {posargs} +commands = pytest -v -s -W all --cov pylast --cov-report term-missing {posargs} [testenv:venv] deps = ipdb commands = {posargs} -[testenv:lint] +[testenv:py2lint] deps = - coverage - pep8 - pyyaml + pycodestyle pyflakes clonedigger commands = pyflakes pylast pyflakes tests - pep8 pylast - pep8 tests + pycodestyle pylast + pycodestyle tests ./clonedigger.sh + +[testenv:py3lint] +deps = + pycodestyle + pyflakes +commands = + pyflakes pylast + pyflakes tests + pycodestyle pylast + pycodestyle tests