commit
a3cf6644fa
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
# User Credentials
|
||||
test_pylast.yaml
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -57,3 +60,6 @@ docs/_build/
|
|||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# JetBrains
|
||||
.idea/
|
||||
|
|
40
.travis.yml
40
.travis.yml
|
@ -1,4 +1,5 @@
|
|||
language: python
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: ivg6II471E9HV8xyqnawLIuP/sZ0J63Y+BC0BQcRVKtLn/K3zmD1ozM3TFL9S549Nxd0FqDKHXJvXsgaTGIDpK8sxE2AMKV5IojyM0iAVuN7YjPK9vwSlRw1u0EysPMFqxOZVQnoDyHrSGIUrP/VMdnhBu6dbUX0FyEkvZshXhY=
|
||||
|
@ -9,24 +10,39 @@ env:
|
|||
- secure: VPARlWNg/0Nit7a924vJlDfv7yiuTDtrcGZNFrZ6yN3dl8ZjVPizQXQNKA3yq0y2jW25nwjRwZYj3eY5MdM9F7Sw51d+/8AjFtdCuRgDvwlQFR/pCoyzqgJATkXKo7mlejvnA+5EKUzAmu3drIbboFgbLgRTMrG7b/ot9tazTHs=
|
||||
- secure: CQYL7MH6tSVrCcluIfWfDSTo4E/p+9pF0eI7Vtf0oaZBzyulODHK8h/mzJp4HwezyfOu0RCedq6sloGQr1/29CvWWESaYyoGoGz9Mz2ZS+MpIcjGISfZa+x4vSp6QPFvd4i/1Z/1j2gJVVyswkrIVUwZIDJtfAKzZI5iHx2gH8Y=
|
||||
- secure: SsKJoJwtDVWrL5xxl9C/gTRy6FhfRQQNNAFOogl9mTs/WeI2t9QTYoKsxLPXOdoRdu4MvT3h/B2sjwggt7zP81fBVxQRTkg4nq0zSHlj0NqclbFa6I5lUYdGwH9gPk/HWJJwXhKRDsqn/iRw2v+qBDs/j3kIgPQ0yjM58LEPXic=
|
||||
|
||||
matrix:
|
||||
- TOXENV=lint
|
||||
- TOXENV=py27
|
||||
- TOXENV=py33
|
||||
- TOXENV=py34
|
||||
- TOXENV=pypy
|
||||
- TOXENV=pypy3
|
||||
include:
|
||||
- python: 2.7
|
||||
env: TOXENV=lint
|
||||
- python: 2.7
|
||||
env: TOXENV=py27
|
||||
- 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
|
||||
env: TOXENV=pypy
|
||||
allow_failures:
|
||||
- env: TOXENV=pypy
|
||||
- env: TOXENV=pypy3
|
||||
fast_finish: true
|
||||
|
||||
sudo: false
|
||||
|
||||
install:
|
||||
- travis_retry pip install tox==2.1.1
|
||||
- travis_retry pip install coveralls
|
||||
|
||||
script: tox
|
||||
|
||||
after_success:
|
||||
- travis_retry pip install coveralls && coveralls
|
||||
- travis_retry pip install scrutinizer-ocular && ocular
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: '3.4'
|
||||
- python: pypy
|
||||
- python: pypy3
|
||||
fast_finish: true
|
||||
|
||||
|
|
12
README.md
12
README.md
|
@ -4,7 +4,7 @@ pyLast
|
|||
[](https://travis-ci.org/pylast/pylast) [](https://pypi.python.org/pypi/pylast/) [](https://pypi.python.org/pypi/pylast/) [](https://coveralls.io/r/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](http://www.last.fm/) and other API-compatible websites such as [Libre.fm](http://libre.fm/).
|
||||
|
||||
Try using the pydoc utility for help on usage or see [test_pylast.py](tests/test_pylast.py) for examples.
|
||||
|
||||
|
@ -32,7 +32,7 @@ Features
|
|||
Getting Started
|
||||
---------------
|
||||
|
||||
Here's a simple code example to get you started. In order to create any object from pyLast, you need a Network object which represents a social music network that is Last.fm or any other API-compatible one. You can obtain a pre-configured one for Last.fm and use it as follows:
|
||||
Here's some simple code example to get you started. In order to create any object from pyLast, you need a `Network` object which represents a social music network that is Last.fm or any other API-compatible one. You can obtain a pre-configured one for Last.fm and use it as follows:
|
||||
|
||||
```python
|
||||
import pylast
|
||||
|
@ -49,7 +49,7 @@ password_hash = pylast.md5("your_password")
|
|||
network = pylast.LastFMNetwork(api_key = API_KEY, api_secret =
|
||||
API_SECRET, username = username, password_hash = password_hash)
|
||||
|
||||
# now you can use that object everywhere
|
||||
# Now you can use that object everywhere
|
||||
artist = network.get_artist("System of a Down")
|
||||
artist.shout("<3")
|
||||
|
||||
|
@ -58,7 +58,7 @@ track = network.get_track("Iron Maiden", "The Nomad")
|
|||
track.love()
|
||||
track.add_tags(("awesome", "favorite"))
|
||||
|
||||
# type help(pylast.LastFMNetwork) or help(pylast) in a Python interpreter to get more help
|
||||
# Type help(pylast.LastFMNetwork) or help(pylast) in a Python interpreter to get more help
|
||||
# about anything and see examples of how it works
|
||||
```
|
||||
|
||||
|
@ -69,7 +69,7 @@ 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.
|
||||
|
||||
For integration tests you need a test account at Last.fm that will be 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:
|
||||
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:
|
||||
|
||||
```sh
|
||||
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
|
||||
|
@ -80,7 +80,7 @@ export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE
|
|||
|
||||
To run all unit and integration tests:
|
||||
```sh
|
||||
pip install pytest flaky
|
||||
pip install pytest flaky mock
|
||||
py.test
|
||||
```
|
||||
|
||||
|
|
|
@ -42,7 +42,22 @@ __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
|
||||
|
@ -51,6 +66,9 @@ if sys.version_info[0] == 3:
|
|||
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
|
||||
|
@ -131,6 +149,59 @@ RE_XML_ILLEGAL = (u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' +
|
|||
|
||||
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()
|
||||
|
||||
|
||||
class _Network(object):
|
||||
"""
|
||||
|
@ -915,8 +986,8 @@ class LibreFMNetwork(_Network):
|
|||
_Network.__init__(
|
||||
self,
|
||||
name="Libre.fm",
|
||||
homepage="http://alpha.libre.fm",
|
||||
ws_server=("alpha.libre.fm", "/2.0/"),
|
||||
homepage="http://libre.fm",
|
||||
ws_server=("libre.fm", "/2.0/"),
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
session_key=session_key,
|
||||
|
@ -924,18 +995,18 @@ class LibreFMNetwork(_Network):
|
|||
username=username,
|
||||
password_hash=password_hash,
|
||||
domain_names={
|
||||
DOMAIN_ENGLISH: "alpha.libre.fm",
|
||||
DOMAIN_GERMAN: "alpha.libre.fm",
|
||||
DOMAIN_SPANISH: "alpha.libre.fm",
|
||||
DOMAIN_FRENCH: "alpha.libre.fm",
|
||||
DOMAIN_ITALIAN: "alpha.libre.fm",
|
||||
DOMAIN_POLISH: "alpha.libre.fm",
|
||||
DOMAIN_PORTUGUESE: "alpha.libre.fm",
|
||||
DOMAIN_SWEDISH: "alpha.libre.fm",
|
||||
DOMAIN_TURKISH: "alpha.libre.fm",
|
||||
DOMAIN_RUSSIAN: "alpha.libre.fm",
|
||||
DOMAIN_JAPANESE: "alpha.libre.fm",
|
||||
DOMAIN_CHINESE: "alpha.libre.fm",
|
||||
DOMAIN_ENGLISH: "libre.fm",
|
||||
DOMAIN_GERMAN: "libre.fm",
|
||||
DOMAIN_SPANISH: "libre.fm",
|
||||
DOMAIN_FRENCH: "libre.fm",
|
||||
DOMAIN_ITALIAN: "libre.fm",
|
||||
DOMAIN_POLISH: "libre.fm",
|
||||
DOMAIN_PORTUGUESE: "libre.fm",
|
||||
DOMAIN_SWEDISH: "libre.fm",
|
||||
DOMAIN_TURKISH: "libre.fm",
|
||||
DOMAIN_RUSSIAN: "libre.fm",
|
||||
DOMAIN_JAPANESE: "libre.fm",
|
||||
DOMAIN_CHINESE: "libre.fm",
|
||||
},
|
||||
urls={
|
||||
"album": "artist/%(artist)s/album/%(album)s",
|
||||
|
@ -1098,6 +1169,12 @@ 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])
|
||||
|
@ -1110,7 +1187,15 @@ class _Request(object):
|
|||
raise NetworkError(self.network, e)
|
||||
|
||||
else:
|
||||
conn = HTTPConnection(host=HOST_NAME)
|
||||
if _can_use_ssl_securely():
|
||||
conn = HTTPSConnection(
|
||||
context=SSL_CONTEXT,
|
||||
host=HOST_NAME
|
||||
)
|
||||
else:
|
||||
conn = HTTPConnection(
|
||||
host=HOST_NAME
|
||||
)
|
||||
|
||||
try:
|
||||
conn.request(
|
||||
|
@ -1681,15 +1766,15 @@ class WSError(Exception):
|
|||
|
||||
|
||||
class MalformedResponseError(Exception):
|
||||
"""Exception conveying a malformed response from Last.fm."""
|
||||
"""Exception conveying a malformed response from the music network."""
|
||||
|
||||
def __init__(self, network, underlying_error):
|
||||
self.network = network
|
||||
self.underlying_error = underlying_error
|
||||
|
||||
def __str__(self):
|
||||
return "Malformed response from Last.fm. Underlying error: %s" % str(
|
||||
self.underlying_error)
|
||||
return "Malformed response from {}. Underlying error: {}".format(
|
||||
self.network.name, str(self.underlying_error))
|
||||
|
||||
|
||||
class NetworkError(Exception):
|
||||
|
@ -4291,7 +4376,15 @@ class _ScrobblerRequest(object):
|
|||
def execute(self):
|
||||
"""Returns a string response of this request."""
|
||||
|
||||
connection = HTTPConnection(self.hostname)
|
||||
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():
|
||||
|
|
8
setup.py
8
setup.py
|
@ -7,6 +7,12 @@ setup(
|
|||
version="1.6.0",
|
||||
author="Amr Hassan <amr.hassan@gmail.com>",
|
||||
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"),
|
||||
author_email="amr.hassan@gmail.com",
|
||||
|
@ -22,6 +28,8 @@ setup(
|
|||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
],
|
||||
keywords=["Last.fm", "music", "scrobble", "scrobbling"],
|
||||
packages=find_packages(exclude=('tests*',)),
|
||||
|
|
|
@ -375,21 +375,6 @@ class TestPyLast(unittest.TestCase):
|
|||
self.assertEqual(str(current_track.title), "Test Title")
|
||||
self.assertEqual(str(current_track.artist), "Test Artist")
|
||||
|
||||
@handle_lastfm_exceptions
|
||||
def test_libre_fm(self):
|
||||
# Arrange
|
||||
username = self.__class__.secrets["username"]
|
||||
password_hash = self.__class__.secrets["password_hash"]
|
||||
|
||||
# Act
|
||||
network = pylast.LibreFMNetwork(
|
||||
password_hash=password_hash, username=username)
|
||||
tags = network.get_top_tags(limit=1)
|
||||
|
||||
# Assert
|
||||
self.assertGreater(len(tags), 0)
|
||||
self.assertIsInstance(tags[0], pylast.TopItem)
|
||||
|
||||
@handle_lastfm_exceptions
|
||||
def test_album_tags_are_topitems(self):
|
||||
# Arrange
|
||||
|
@ -2175,5 +2160,26 @@ class TestPyLast(unittest.TestCase):
|
|||
# Assert
|
||||
self.assertEqual(mbid, None)
|
||||
|
||||
|
||||
@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)
|
||||
|
|
Loading…
Reference in a new issue