From 1690c08e77092257659dda0bfe4124fc47d3f795 Mon Sep 17 00:00:00 2001 From: Jef LeCompte Date: Thu, 5 Dec 2019 22:19:09 -0500 Subject: [PATCH 1/4] Added URL to config and deprecated old configuration --- beetsplug/subsonicupdate.py | 107 +++++++++++++++++++++----------- docs/plugins/subsonicupdate.rst | 14 +++++ 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/beetsplug/subsonicupdate.py b/beetsplug/subsonicupdate.py index bb9e8a952..9cb8758d9 100644 --- a/beetsplug/subsonicupdate.py +++ b/beetsplug/subsonicupdate.py @@ -25,16 +25,68 @@ a "subsonic" section like the following: """ from __future__ import division, absolute_import, print_function -from beets.plugins import BeetsPlugin -from beets import config -import requests -import string import hashlib import random +import string + +import requests + +from beets import config +from beets.plugins import BeetsPlugin __author__ = 'https://github.com/maffo999' +def build_payload(): + """ To avoid sending plaintext passwords, authentication will be + performed via username, a token, and a 6 random + letters/numbers sequence. + The token is the concatenation of your password and the 6 random + letters/numbers (the salt) which is hashed with MD5. + """ + + user = config['subsonic']['user'].as_str() + password = config['subsonic']['pass'].as_str() + + # Pick the random sequence and salt the password + r = string.ascii_letters + string.digits + salt = "".join([random.choice(r) for n in range(6)]) + t = password + salt + token = hashlib.md5() + token.update(t.encode('utf-8')) + + # Put together the payload of the request to the server and the URL + return { + 'u': user, + 't': token.hexdigest(), + 's': salt, + 'v': '1.15.0', # Subsonic 6.1 and newer. + 'c': 'beets' + } + + +def formal_url(): + """ Formats URL to send request to Subsonic + DEPRECATED schema, host, port, contextpath; use ${url} + """ + + host = config['subsonic']['host'].as_str() + port = config['subsonic']['port'].get(int) + + context_path = config['subsonic']['contextpath'].as_str() + if context_path == '/': + context_path = '' + + url = config['subsonic']['url'].as_str() + if url and url.endsWith('/'): + url = url[:-1] + + if not url: + url = "http://{}:{}{}".format(host, port, context_path) + + return url + '/rest/startScan' + + class SubsonicUpdate(BeetsPlugin): def __init__(self): super(SubsonicUpdate, self).__init__() @@ -46,42 +98,23 @@ class SubsonicUpdate(BeetsPlugin): 'user': 'admin', 'pass': 'admin', 'contextpath': '/', + 'url': 'http://localhost:4040' }) + config['subsonic']['pass'].redact = True - self.register_listener('import', self.loaded) + self.register_listener('import', self.start_scan) - def loaded(self): - host = config['subsonic']['host'].as_str() - port = config['subsonic']['port'].get(int) - user = config['subsonic']['user'].as_str() - passw = config['subsonic']['pass'].as_str() - contextpath = config['subsonic']['contextpath'].as_str() + def start_scan(self): + url = formal_url() + payload = build_payload() - # To avoid sending plaintext passwords, authentication will be - # performed via username, a token, and a 6 random - # letters/numbers sequence. - # The token is the concatenation of your password and the 6 random - # letters/numbers (the salt) which is hashed with MD5. - - # Pick the random sequence and salt the password - r = string.ascii_letters + string.digits - salt = "".join([random.choice(r) for n in range(6)]) - t = passw + salt - token = hashlib.md5() - token.update(t.encode('utf-8')) - - # Put together the payload of the request to the server and the URL - payload = { - 'u': user, - 't': token.hexdigest(), - 's': salt, - 'v': '1.15.0', # Subsonic 6.1 and newer. - 'c': 'beets' - } - if contextpath == '/': - contextpath = '' - url = "http://{}:{}{}/rest/startScan".format(host, port, contextpath) response = requests.post(url, params=payload) - if response.status_code != 200: - self._log.error(u'Generic error, please try again later.') + if response.status_code == 403: + self._log.error(u'Server authentication failed') + elif response.status_code == 200: + self._log.debug(u'Updating Subsonic') + else: + self._log.error( + u'Generic error, please try again later [Status Code: {}]' + .format(response.status_code)) diff --git a/docs/plugins/subsonicupdate.rst b/docs/plugins/subsonicupdate.rst index 2d9331b7c..667bceb3e 100644 --- a/docs/plugins/subsonicupdate.rst +++ b/docs/plugins/subsonicupdate.rst @@ -12,6 +12,12 @@ Then, you'll probably want to configure the specifics of your Subsonic server. You can do that using a ``subsonic:`` section in your ``config.yaml``, which looks like this:: + subsonic: + url: https://mydomain.com:443/subsonic + user: username + pass: password + + # DEPRECATED subsonic: host: X.X.X.X port: 4040 @@ -30,6 +36,14 @@ Configuration The available options under the ``subsonic:`` section are: +- **url**: The Subsonic server resource. Default: ``http://localhost:4040`` + +Example: ``https://mydomain.com:443/subsonic`` + +\* Note: context path is optional + +DEPRECATED: + - **host**: The Subsonic server name/IP. Default: ``localhost`` - **port**: The Subsonic server port. Default: ``4040`` - **user**: The Subsonic user. Default: ``admin`` From 5a38e1b35c98c93f0c952929ab7de4fc7deb7325 Mon Sep 17 00:00:00 2001 From: Jef LeCompte Date: Fri, 6 Dec 2019 18:30:52 -0500 Subject: [PATCH 2/4] Refactored token generation and updated comments based on suggestions. Also updated documentation to note the password options. --- beetsplug/subsonicupdate.py | 64 ++++++++++++++++----------------- docs/plugins/subsonicupdate.rst | 22 ++---------- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/beetsplug/subsonicupdate.py b/beetsplug/subsonicupdate.py index 9cb8758d9..24d430839 100644 --- a/beetsplug/subsonicupdate.py +++ b/beetsplug/subsonicupdate.py @@ -37,51 +37,42 @@ from beets.plugins import BeetsPlugin __author__ = 'https://github.com/maffo999' -def build_payload(): - """ To avoid sending plaintext passwords, authentication will be - performed via username, a token, and a 6 random - letters/numbers sequence. - The token is the concatenation of your password and the 6 random - letters/numbers (the salt) which is hashed with MD5. - """ +def create_token(): + """ Creates salt and token from given password. - user = config['subsonic']['user'].as_str() + :return: The generated salt and hashed token + """ password = config['subsonic']['pass'].as_str() # Pick the random sequence and salt the password r = string.ascii_letters + string.digits - salt = "".join([random.choice(r) for n in range(6)]) - t = password + salt - token = hashlib.md5() - token.update(t.encode('utf-8')) + salt = "".join([random.choice(r) for _ in range(6)]) + salted_password = password + salt + token = hashlib.md5().update(salted_password.encode('utf-8')).hexdigest() # Put together the payload of the request to the server and the URL - return { - 'u': user, - 't': token.hexdigest(), - 's': salt, - 'v': '1.15.0', # Subsonic 6.1 and newer. - 'c': 'beets' - } + return salt, token -def formal_url(): - """ Formats URL to send request to Subsonic - DEPRECATED schema, host, port, contextpath; use ${url} +def format_url(): + """ Get the Subsonic URL to trigger a scan. Uses either the url + config option or the deprecated host, port, and context_path config + options together. + + :return: Endpoint for updating Subsonic """ - host = config['subsonic']['host'].as_str() - port = config['subsonic']['port'].get(int) - - context_path = config['subsonic']['contextpath'].as_str() - if context_path == '/': - context_path = '' - url = config['subsonic']['url'].as_str() if url and url.endsWith('/'): url = url[:-1] + # @deprecated("Use url config option instead") if not url: + host = config['subsonic']['host'].as_str() + port = config['subsonic']['port'].get(int) + context_path = config['subsonic']['contextpath'].as_str() + if context_path == '/': + context_path = '' url = "http://{}:{}{}".format(host, port, context_path) return url + '/rest/startScan' @@ -105,8 +96,17 @@ class SubsonicUpdate(BeetsPlugin): self.register_listener('import', self.start_scan) def start_scan(self): - url = formal_url() - payload = build_payload() + user = config['subsonic']['user'].as_str() + url = format_url() + salt, token = create_token() + + payload = { + 'u': user, + 't': token, + 's': salt, + 'v': '1.15.0', # Subsonic 6.1 and newer. + 'c': 'beets' + } response = requests.post(url, params=payload) @@ -117,4 +117,4 @@ class SubsonicUpdate(BeetsPlugin): else: self._log.error( u'Generic error, please try again later [Status Code: {}]' - .format(response.status_code)) + .format(response.status_code)) diff --git a/docs/plugins/subsonicupdate.rst b/docs/plugins/subsonicupdate.rst index 667bceb3e..68496c4e3 100644 --- a/docs/plugins/subsonicupdate.rst +++ b/docs/plugins/subsonicupdate.rst @@ -13,17 +13,11 @@ You can do that using a ``subsonic:`` section in your ``config.yaml``, which looks like this:: subsonic: - url: https://mydomain.com:443/subsonic + url: https://example.com:443/subsonic user: username pass: password - # DEPRECATED - subsonic: - host: X.X.X.X - port: 4040 - user: username - pass: password - contextpath: /subsonic +\* NOTE: The pass config option can either be clear text or hex-encoded with a "enc:" prefix. With that all in place, beets will send a Rest API to your Subsonic server every time you import new music. @@ -38,14 +32,4 @@ The available options under the ``subsonic:`` section are: - **url**: The Subsonic server resource. Default: ``http://localhost:4040`` -Example: ``https://mydomain.com:443/subsonic`` - -\* Note: context path is optional - -DEPRECATED: - -- **host**: The Subsonic server name/IP. Default: ``localhost`` -- **port**: The Subsonic server port. Default: ``4040`` -- **user**: The Subsonic user. Default: ``admin`` -- **pass**: The Subsonic user password. Default: ``admin`` -- **contextpath**: The Subsonic context path. Default: ``/`` +Example: ``https://mydomain.com:443/subsonic`` \ No newline at end of file From e18b91da2673619548daff275787ee2c9b3120ac Mon Sep 17 00:00:00 2001 From: Jef LeCompte Date: Fri, 6 Dec 2019 18:32:35 -0500 Subject: [PATCH 3/4] Remove example --- docs/plugins/subsonicupdate.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/plugins/subsonicupdate.rst b/docs/plugins/subsonicupdate.rst index 68496c4e3..5508e103c 100644 --- a/docs/plugins/subsonicupdate.rst +++ b/docs/plugins/subsonicupdate.rst @@ -30,6 +30,4 @@ Configuration The available options under the ``subsonic:`` section are: -- **url**: The Subsonic server resource. Default: ``http://localhost:4040`` - -Example: ``https://mydomain.com:443/subsonic`` \ No newline at end of file +- **url**: The Subsonic server resource. Default: ``http://localhost:4040`` \ No newline at end of file From 89f21d960198884488ead88805a1f09bd1f048cb Mon Sep 17 00:00:00 2001 From: Jef LeCompte Date: Mon, 9 Dec 2019 07:13:25 -0500 Subject: [PATCH 4/4] Updated documentation --- beetsplug/subsonicupdate.py | 6 +++--- docs/plugins/subsonicupdate.rst | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/beetsplug/subsonicupdate.py b/beetsplug/subsonicupdate.py index 24d430839..b3d05e245 100644 --- a/beetsplug/subsonicupdate.py +++ b/beetsplug/subsonicupdate.py @@ -38,7 +38,7 @@ __author__ = 'https://github.com/maffo999' def create_token(): - """ Creates salt and token from given password. + """Creates salt and token from given password. :return: The generated salt and hashed token """ @@ -55,7 +55,7 @@ def create_token(): def format_url(): - """ Get the Subsonic URL to trigger a scan. Uses either the url + """Get the Subsonic URL to trigger a scan. Uses either the url config option or the deprecated host, port, and context_path config options together. @@ -89,7 +89,7 @@ class SubsonicUpdate(BeetsPlugin): 'user': 'admin', 'pass': 'admin', 'contextpath': '/', - 'url': 'http://localhost:4040' + 'url': 'http://localhost:4040', }) config['subsonic']['pass'].redact = True diff --git a/docs/plugins/subsonicupdate.rst b/docs/plugins/subsonicupdate.rst index 5508e103c..bdd33593b 100644 --- a/docs/plugins/subsonicupdate.rst +++ b/docs/plugins/subsonicupdate.rst @@ -17,8 +17,6 @@ which looks like this:: user: username pass: password -\* NOTE: The pass config option can either be clear text or hex-encoded with a "enc:" prefix. - With that all in place, beets will send a Rest API to your Subsonic server every time you import new music. Due to a current limitation of the API, all libraries visible to that user will be scanned. @@ -30,4 +28,8 @@ Configuration The available options under the ``subsonic:`` section are: -- **url**: The Subsonic server resource. Default: ``http://localhost:4040`` \ No newline at end of file +- **url**: The Subsonic server resource. Default: ``http://localhost:4040`` +- **user**: The Subsonic user. Default: ``admin`` +- **pass**: The Subsonic user password. Default: ``admin`` + +\* NOTE: The pass config option can either be clear text or hex-encoded with a "enc:" prefix. \ No newline at end of file