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
======
-[](https://travis-ci.org/pylast/pylast)
-[](https://pypi.python.org/pypi/pylast/)
-
+[](https://travis-ci.org/pylast/pylast)
+[](https://pypi.python.org/pypi/pylast/)
+[](https://pypi.python.org/pypi/pylast/)
[](https://codecov.io/gh/pylast/pylast)
[](https://coveralls.io/github/pylast/pylast?branch=develop)
[](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