From a282d4abc5ac988653327a9f2f6edfe0286eaf64 Mon Sep 17 00:00:00 2001 From: Marvin Steadfast Date: Fri, 26 Aug 2016 10:21:46 +0200 Subject: [PATCH] embyupdate: Fix authentication header problem There was a problem with the authentication header in the latest versions. The header creation function changed to fix that. Username and passwort authentication should work again. The `host` config variable takes now a full hostname. For example `http://localhost` instead of just `localhost`. This makes it easier to use https hosts. --- beetsplug/embyupdate.py | 82 +++++++++++++++++++++++++++++-------- docs/changelog.rst | 2 + docs/plugins/embyupdate.rst | 6 +-- test/test_embyupdate.py | 50 ++++++++++++---------- 4 files changed, 99 insertions(+), 41 deletions(-) diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py index 2a2f792f5..4da124dbb 100644 --- a/beetsplug/embyupdate.py +++ b/beetsplug/embyupdate.py @@ -3,25 +3,38 @@ """Updates the Emby Library whenever the beets library is changed. emby: - host: localhost + host: http://localhost port: 8096 username: user password: password """ from __future__ import division, absolute_import, print_function -from beets import config -from beets.plugins import BeetsPlugin -from six.moves.urllib.parse import urlencode -from six.moves.urllib.parse import urljoin, parse_qs, urlsplit, urlunsplit import hashlib import requests +from six.moves.urllib.parse import urlencode +from six.moves.urllib.parse import urljoin, parse_qs, urlsplit, urlunsplit + +from beets import config +from beets.plugins import BeetsPlugin + def api_url(host, port, endpoint): """Returns a joined url. + + Takes host, port and endpoint and generates a valid emby API url. + + :param host: Hostname of the emby server + :param port: Portnumber of the emby server + :param endpoint: API endpoint + :type host: str + :type port: int + :type endpoint: str + :returns: Full API url + :rtype: str """ - joined = urljoin('http://{0}:{1}'.format(host, port), endpoint) + joined = urljoin('{0}:{1}'.format(host, port), endpoint) scheme, netloc, path, query_string, fragment = urlsplit(joined) query_params = parse_qs(query_string) @@ -33,6 +46,13 @@ def api_url(host, port, endpoint): def password_data(username, password): """Returns a dict with username and its encoded password. + + :param username: Emby username + :param password: Emby password + :type username: str + :type password: str + :returns: Dictionary with username and encoded password + :rtype: dict """ return { 'username': username, @@ -43,24 +63,45 @@ def password_data(username, password): def create_headers(user_id, token=None): """Return header dict that is needed to talk to the Emby API. + + :param user_id: Emby user ID + :param token: Authentication token for Emby + :type user_id: str + :type token: str + :returns: Headers for requests + :rtype: dict """ - headers = { - 'Authorization': 'MediaBrowser', - 'UserId': user_id, - 'Client': 'other', - 'Device': 'empy', - 'DeviceId': 'beets', - 'Version': '0.0.0' - } + headers = {} + + authorization = ( + 'MediaBrowser UserId="{user_id}", ' + 'Client="other", ' + 'Device="beets", ' + 'DeviceId="beets", ' + 'Version="0.0.0"' + ).format(user_id=user_id) + + headers['x-emby-authorization'] = authorization if token: - headers['X-MediaBrowser-Token'] = token + headers['x-mediabrowser-token'] = token return headers def get_token(host, port, headers, auth_data): """Return token for a user. + + :param host: Emby host + :param port: Emby port + :param headers: Headers for requests + :param auth_data: Username and encoded password for authentication + :type host: str + :type port: int + :type headers: dict + :type auth_data: dict + :returns: Access Token + :rtype: str """ url = api_url(host, port, '/Users/AuthenticateByName') r = requests.post(url, headers=headers, data=auth_data) @@ -70,6 +111,15 @@ def get_token(host, port, headers, auth_data): def get_user(host, port, username): """Return user dict from server or None if there is no user. + + :param host: Emby host + :param port: Emby port + :username: Username + :type host: str + :type port: int + :type username: str + :returns: Matched Users + :rtype: list """ url = api_url(host, port, '/Users/Public') r = requests.get(url) @@ -84,7 +134,7 @@ class EmbyUpdate(BeetsPlugin): # Adding defaults. config['emby'].add({ - u'host': u'localhost', + u'host': u'http://localhost', u'port': 8096 }) diff --git a/docs/changelog.rst b/docs/changelog.rst index c3f5ba1bb..a3a392d53 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -48,6 +48,8 @@ And there are a few bug fixes too: omitted. * With :ref:`ignore_hidden` enabled, non-UTF-8 filenames would cause a crash. This is fixed. :bug:`2168` +* :doc:`/plugins/embyupdate`: Fixes authentication header problem that caused + a problem that it was not possible to get tokens from the Emby API. The last release, 1.3.19, also erroneously reported its version as "1.3.18" when you typed ``beet version``. This has been corrected. diff --git a/docs/plugins/embyupdate.rst b/docs/plugins/embyupdate.rst index a902e6076..339f0c818 100644 --- a/docs/plugins/embyupdate.rst +++ b/docs/plugins/embyupdate.rst @@ -6,7 +6,7 @@ EmbyUpdate Plugin To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll probably want to configure the specifics of your Emby server. You can do that using an ``emby:`` section in your ``config.yaml``, which looks like this:: emby: - host: localhost + host: http://localhost port: 8096 username: user apikey: apikey @@ -25,8 +25,8 @@ Configuration The available options under the ``emby:`` section are: -- **host**: The Emby server name. - Default: ``localhost`` +- **host**: The Emby server host. You have to include ``http://`` or ``https://``. + Default: ``http://localhost`` - **port**: The Emby server port. Default: 8096 - **username**: A username of a Emby user that is allowed to refresh the library. diff --git a/test/test_embyupdate.py b/test/test_embyupdate.py index dc4cad23c..905766f7a 100644 --- a/test/test_embyupdate.py +++ b/test/test_embyupdate.py @@ -14,7 +14,7 @@ class EmbyUpdateTest(unittest.TestCase, TestHelper): self.load_plugins('embyupdate') self.config['emby'] = { - u'host': u'localhost', + u'host': u'http://localhost', u'port': 8096, u'username': u'username', u'password': u'password' @@ -47,12 +47,14 @@ class EmbyUpdateTest(unittest.TestCase, TestHelper): self.assertEqual( embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721'), { - 'Authorization': 'MediaBrowser', - 'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721', - 'Client': 'other', - 'Device': 'empy', - 'DeviceId': 'beets', - 'Version': '0.0.0' + 'x-emby-authorization': ( + 'MediaBrowser ' + 'UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", ' + 'Client="other", ' + 'Device="beets", ' + 'DeviceId="beets", ' + 'Version="0.0.0"' + ) } ) @@ -61,13 +63,15 @@ class EmbyUpdateTest(unittest.TestCase, TestHelper): embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721', token='abc123'), { - 'Authorization': 'MediaBrowser', - 'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721', - 'Client': 'other', - 'Device': 'empy', - 'DeviceId': 'beets', - 'Version': '0.0.0', - 'X-MediaBrowser-Token': 'abc123' + 'x-emby-authorization': ( + 'MediaBrowser ' + 'UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", ' + 'Client="other", ' + 'Device="beets", ' + 'DeviceId="beets", ' + 'Version="0.0.0"' + ), + 'x-mediabrowser-token': 'abc123' } ) @@ -132,12 +136,14 @@ class EmbyUpdateTest(unittest.TestCase, TestHelper): content_type='application/json') headers = { - 'Authorization': 'MediaBrowser', - 'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721', - 'Client': 'other', - 'Device': 'empy', - 'DeviceId': 'beets', - 'Version': '0.0.0' + 'x-emby-authorization': ( + 'MediaBrowser ' + 'UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", ' + 'Client="other", ' + 'Device="beets", ' + 'DeviceId="beets", ' + 'Version="0.0.0"' + ) } auth_data = { @@ -147,7 +153,7 @@ class EmbyUpdateTest(unittest.TestCase, TestHelper): } self.assertEqual( - embyupdate.get_token('localhost', 8096, headers, auth_data), + embyupdate.get_token('http://localhost', 8096, headers, auth_data), '4b19180cf02748f7b95c7e8e76562fc8') @responses.activate @@ -196,7 +202,7 @@ class EmbyUpdateTest(unittest.TestCase, TestHelper): status=200, content_type='application/json') - response = embyupdate.get_user('localhost', 8096, 'username') + response = embyupdate.get_user('http://localhost', 8096, 'username') self.assertEqual(response[0]['Id'], '2ec276a2642e54a19b612b9418a8bd3b')