diff --git a/Makefile.am b/Makefile.am
index d5f9aa780..9339e9029 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1376,6 +1376,18 @@ libinput_a_SOURCES += \
 INPUT_LIBS += $(YAJL_LIBS)
 endif
 
+if ENABLE_QOBUZ
+libinput_a_SOURCES += \
+	$(YAJL_SOURCES) \
+	src/lib/gcrypt/MD5.cxx src/lib/gcrypt/MD5.hxx \
+	src/input/plugins/QobuzSession.hxx \
+	src/input/plugins/QobuzClient.cxx src/input/plugins/QobuzClient.hxx \
+	src/input/plugins/QobuzLoginRequest.cxx src/input/plugins/QobuzLoginRequest.hxx \
+	src/input/plugins/QobuzTrackRequest.cxx src/input/plugins/QobuzTrackRequest.hxx \
+	src/input/plugins/QobuzInputPlugin.cxx src/input/plugins/QobuzInputPlugin.hxx
+INPUT_LIBS += $(YAJL_LIBS) $(LIBGCRYPT_LIBS)
+endif
+
 if ENABLE_SMBCLIENT
 libinput_a_SOURCES += \
 	$(SMBCLIENT_SOURCES) \
diff --git a/NEWS b/NEWS
index c89f8ef92..d27149c30 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,7 @@ ver 0.21 (not yet released)
   - "outputset" sets runtime attributes
   - close connection when client sends HTTP request
 * input
+  - qobuz: new plugin to play Qobuz streams
   - tidal: new plugin to play Tidal streams
 * tags
   - new tags "OriginalDate", "MUSICBRAINZ_WORKID"
diff --git a/configure.ac b/configure.ac
index 6216a4f90..325eaa77e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -362,6 +362,11 @@ AC_ARG_ENABLE(ipv6,
 
 AC_SYS_LARGEFILE
 
+AC_ARG_ENABLE(qobuz,
+	AS_HELP_STRING([--enable-qobuz],
+		[enable support for Qobuz streaming]),,
+	[enable_qobuz=auto])
+
 AC_ARG_ENABLE(soundcloud,
 	AS_HELP_STRING([--enable-soundcloud],
 		[enable support for soundcloud.com]),,
@@ -565,9 +570,15 @@ dnl -------------------------------- expat --------------------------------
 MPD_ENABLE_AUTO_PKG(expat, EXPAT, [expat],
 	[expat XML parser], [expat not found])
 
+dnl -------------------------------- libgcrypt --------------------------------
+
+if test x$enable_qobuz != xno; then
+	AM_PATH_LIBGCRYPT([1], [found_gcrypt=yes], [found_gcrypt=no])
+fi
+
 dnl -------------------------------- yajl -------------------------------------
 
-if test x$enable_soundcloud != xno || test x$enable_tidal != xno; then
+if test x$enable_qobuz != xno || x$enable_soundcloud != xno || test x$enable_tidal != xno; then
 	PKG_CHECK_MODULES([YAJL], [yajl >= 2.0],
 		[found_yajl=yes],
 		[found_yajl=no])
@@ -718,6 +729,13 @@ dnl ----------------------------------- NFS -----------------------------
 MPD_ENABLE_AUTO_PKG(nfs, NFS, [libnfs],
 	[NFS input plugin], [libnfs not found])
 
+dnl --------------------------------- Qobuz -----------------------------------
+MPD_DEPENDS([enable_qobuz], [found_yajl], [Qobuz streaming], [libyajl not found])
+MPD_DEPENDS([enable_qobuz], [found_gcrypt], [Qobuz streaming], [libgcrypt not found])
+MPD_DEPENDS([enable_qobuz], [enable_curl], [Qobuz streaming], [libcurl not found])
+MPD_AUTO(qobuz, [Qobuz streaming], [Qobuz not available], [found_qobuz=yes])
+MPD_DEFINE_CONDITIONAL(enable_qobuz, ENABLE_QOBUZ, [Qobuz streaming])
+
 dnl --------------------------------- Soundcloud ------------------------------
 MPD_DEPENDS([enable_soundcloud], [found_yajl],
 	[soundcloud.com support], [libyajl not found])
@@ -1525,6 +1543,7 @@ fi
 printf '\nStreaming support:\n\t'
 results(cdio_paranoia, [CDIO_PARANOIA])
 results(curl,[CURL])
+results(qobuz,[Qobuz])
 results(smbclient,[SMBCLIENT])
 results(soundcloud,[Soundcloud])
 results(tidal,[Tidal])
diff --git a/doc/user.xml b/doc/user.xml
index 86fa0b49e..70466984f 100644
--- a/doc/user.xml
+++ b/doc/user.xml
@@ -2370,6 +2370,70 @@ run</programlisting>
         </para>
       </section>
 
+      <section id="qobuz_input">
+        <title><varname>qobuz</varname></title>
+
+        <para>
+          Play songs from the commercial streaming service <ulink
+          url="https://www.qobuz.com/">Qobuz</ulink>.  It plays URLs in the
+          form <filename>qobuz://track/ID</filename>, e.g.:
+        </para>
+
+        <programlisting>mpc add qobuz://track/23601296</programlisting>
+
+        <informaltable>
+          <tgroup cols="2">
+            <thead>
+              <row>
+                <entry>Setting</entry>
+                <entry>Description</entry>
+              </row>
+            </thead>
+            <tbody>
+              <row>
+                <entry>
+                  <varname>app_id</varname>
+                  <parameter>ID</parameter>
+                </entry>
+                <entry>
+                  The Qobuz application id.
+                </entry>
+              </row>
+
+              <row>
+                <entry>
+                  <varname>app_secret</varname>
+                  <parameter>SECRET</parameter>
+                </entry>
+                <entry>
+                  The Qobuz application secret.
+                </entry>
+              </row>
+
+              <row>
+                <entry>
+                  <varname>username</varname>
+                  <parameter>USERNAME</parameter>
+                </entry>
+                <entry>
+                  The Qobuz user name.
+                </entry>
+              </row>
+
+              <row>
+                <entry>
+                  <varname>password</varname>
+                  <parameter>PASSWORD</parameter>
+                </entry>
+                <entry>
+                  The Qobuz password.
+                </entry>
+              </row>
+            </tbody>
+          </tgroup>
+        </informaltable>
+      </section>
+
       <section id="tidal_input">
         <title><varname>tidal</varname></title>
 
diff --git a/m4/libgcrypt.m4 b/m4/libgcrypt.m4
new file mode 100644
index 000000000..c67cfecef
--- /dev/null
+++ b/m4/libgcrypt.m4
@@ -0,0 +1,143 @@
+# libgcrypt.m4 - Autoconf macros to detect libgcrypt
+# Copyright (C) 2002, 2003, 2004, 2011, 2014 g10 Code GmbH
+#
+# This file is free software; as a special exception the author gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+#
+# This file is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the
+# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# Last-changed: 2014-10-02
+
+
+dnl AM_PATH_LIBGCRYPT([MINIMUM-VERSION,
+dnl                   [ACTION-IF-FOUND [, ACTION-IF-NOT-FOUND ]]])
+dnl Test for libgcrypt and define LIBGCRYPT_CFLAGS and LIBGCRYPT_LIBS.
+dnl MINIMUN-VERSION is a string with the version number optionalliy prefixed
+dnl with the API version to also check the API compatibility. Example:
+dnl a MINIMUN-VERSION of 1:1.2.5 won't pass the test unless the installed
+dnl version of libgcrypt is at least 1.2.5 *and* the API number is 1.  Using
+dnl this features allows to prevent build against newer versions of libgcrypt
+dnl with a changed API.
+dnl
+dnl If a prefix option is not used, the config script is first
+dnl searched in $SYSROOT/bin and then along $PATH.  If the used
+dnl config script does not match the host specification the script
+dnl is added to the gpg_config_script_warn variable.
+dnl
+AC_DEFUN([AM_PATH_LIBGCRYPT],
+[ AC_REQUIRE([AC_CANONICAL_HOST])
+  AC_ARG_WITH(libgcrypt-prefix,
+            AC_HELP_STRING([--with-libgcrypt-prefix=PFX],
+                           [prefix where LIBGCRYPT is installed (optional)]),
+     libgcrypt_config_prefix="$withval", libgcrypt_config_prefix="")
+  if test x"${LIBGCRYPT_CONFIG}" = x ; then
+     if test x"${libgcrypt_config_prefix}" != x ; then
+        LIBGCRYPT_CONFIG="${libgcrypt_config_prefix}/bin/libgcrypt-config"
+     else
+       case "${SYSROOT}" in
+         /*)
+           if test -x "${SYSROOT}/bin/libgcrypt-config" ; then
+             LIBGCRYPT_CONFIG="${SYSROOT}/bin/libgcrypt-config"
+           fi
+           ;;
+         '')
+           ;;
+          *)
+           AC_MSG_WARN([Ignoring \$SYSROOT as it is not an absolute path.])
+           ;;
+       esac
+     fi
+  fi
+
+  AC_PATH_PROG(LIBGCRYPT_CONFIG, libgcrypt-config, no)
+  tmp=ifelse([$1], ,1:1.2.0,$1)
+  if echo "$tmp" | grep ':' >/dev/null 2>/dev/null ; then
+     req_libgcrypt_api=`echo "$tmp"     | sed 's/\(.*\):\(.*\)/\1/'`
+     min_libgcrypt_version=`echo "$tmp" | sed 's/\(.*\):\(.*\)/\2/'`
+  else
+     req_libgcrypt_api=0
+     min_libgcrypt_version="$tmp"
+  fi
+
+  AC_MSG_CHECKING(for LIBGCRYPT - version >= $min_libgcrypt_version)
+  ok=no
+  if test "$LIBGCRYPT_CONFIG" != "no" ; then
+    req_major=`echo $min_libgcrypt_version | \
+               sed 's/\([[0-9]]*\)\.\([[0-9]]*\)\.\([[0-9]]*\)/\1/'`
+    req_minor=`echo $min_libgcrypt_version | \
+               sed 's/\([[0-9]]*\)\.\([[0-9]]*\)\.\([[0-9]]*\)/\2/'`
+    req_micro=`echo $min_libgcrypt_version | \
+               sed 's/\([[0-9]]*\)\.\([[0-9]]*\)\.\([[0-9]]*\)/\3/'`
+    libgcrypt_config_version=`$LIBGCRYPT_CONFIG --version`
+    major=`echo $libgcrypt_config_version | \
+               sed 's/\([[0-9]]*\)\.\([[0-9]]*\)\.\([[0-9]]*\).*/\1/'`
+    minor=`echo $libgcrypt_config_version | \
+               sed 's/\([[0-9]]*\)\.\([[0-9]]*\)\.\([[0-9]]*\).*/\2/'`
+    micro=`echo $libgcrypt_config_version | \
+               sed 's/\([[0-9]]*\)\.\([[0-9]]*\)\.\([[0-9]]*\).*/\3/'`
+    if test "$major" -gt "$req_major"; then
+        ok=yes
+    else
+        if test "$major" -eq "$req_major"; then
+            if test "$minor" -gt "$req_minor"; then
+               ok=yes
+            else
+               if test "$minor" -eq "$req_minor"; then
+                   if test "$micro" -ge "$req_micro"; then
+                     ok=yes
+                   fi
+               fi
+            fi
+        fi
+    fi
+  fi
+  if test $ok = yes; then
+    AC_MSG_RESULT([yes ($libgcrypt_config_version)])
+  else
+    AC_MSG_RESULT(no)
+  fi
+  if test $ok = yes; then
+     # If we have a recent libgcrypt, we should also check that the
+     # API is compatible
+     if test "$req_libgcrypt_api" -gt 0 ; then
+        tmp=`$LIBGCRYPT_CONFIG --api-version 2>/dev/null || echo 0`
+        if test "$tmp" -gt 0 ; then
+           AC_MSG_CHECKING([LIBGCRYPT API version])
+           if test "$req_libgcrypt_api" -eq "$tmp" ; then
+             AC_MSG_RESULT([okay])
+           else
+             ok=no
+             AC_MSG_RESULT([does not match. want=$req_libgcrypt_api got=$tmp])
+           fi
+        fi
+     fi
+  fi
+  if test $ok = yes; then
+    LIBGCRYPT_CFLAGS=`$LIBGCRYPT_CONFIG --cflags`
+    LIBGCRYPT_LIBS=`$LIBGCRYPT_CONFIG --libs`
+    ifelse([$2], , :, [$2])
+    libgcrypt_config_host=`$LIBGCRYPT_CONFIG --host 2>/dev/null || echo none`
+    if test x"$libgcrypt_config_host" != xnone ; then
+      if test x"$libgcrypt_config_host" != x"$host" ; then
+  AC_MSG_WARN([[
+***
+*** The config script $LIBGCRYPT_CONFIG was
+*** built for $libgcrypt_config_host and thus may not match the
+*** used host $host.
+*** You may want to use the configure option --with-libgcrypt-prefix
+*** to specify a matching config script or use \$SYSROOT.
+***]])
+        gpg_config_script_warn="$gpg_config_script_warn libgcrypt"
+      fi
+    fi
+  else
+    LIBGCRYPT_CFLAGS=""
+    LIBGCRYPT_LIBS=""
+    ifelse([$3], , :, [$3])
+  fi
+  AC_SUBST(LIBGCRYPT_CFLAGS)
+  AC_SUBST(LIBGCRYPT_LIBS)
+])
diff --git a/src/input/Registry.cxx b/src/input/Registry.cxx
index 9df6e8e30..6d43e3d7c 100644
--- a/src/input/Registry.cxx
+++ b/src/input/Registry.cxx
@@ -22,6 +22,7 @@
 #include "util/Macros.hxx"
 #include "plugins/FileInputPlugin.hxx"
 #include "plugins/TidalInputPlugin.hxx"
+#include "plugins/QobuzInputPlugin.hxx"
 
 #ifdef ENABLE_ALSA
 #include "plugins/AlsaInputPlugin.hxx"
@@ -66,6 +67,9 @@ const InputPlugin *const input_plugins[] = {
 #ifdef ENABLE_TIDAL
 	&tidal_input_plugin,
 #endif
+#ifdef ENABLE_QOBUZ
+	&qobuz_input_plugin,
+#endif
 #ifdef ENABLE_CURL
 	&input_plugin_curl,
 #endif
diff --git a/src/input/plugins/QobuzClient.cxx b/src/input/plugins/QobuzClient.cxx
new file mode 100644
index 000000000..1858ec0c6
--- /dev/null
+++ b/src/input/plugins/QobuzClient.cxx
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "QobuzClient.hxx"
+#include "lib/gcrypt/MD5.hxx"
+#include "util/ConstBuffer.hxx"
+
+#include <stdexcept>
+
+#include <assert.h>
+
+namespace {
+
+class QueryStringBuilder {
+	bool first = true;
+
+public:
+	QueryStringBuilder &operator()(std::string &dest, const char *name,
+				       const char *value) noexcept {
+		dest.push_back(first ? '?' : '&');
+		first = false;
+
+		dest += name;
+		dest.push_back('=');
+		dest += value; // TODO: escape
+
+		return *this;
+	}
+};
+
+}
+
+QobuzClient::QobuzClient(EventLoop &event_loop,
+			 const char *_base_url,
+			 const char *_app_id, const char *_app_secret,
+			 const char *_device_manufacturer_id,
+			 const char *_username, const char *_email,
+			 const char *_password)
+	:base_url(_base_url), app_id(_app_id), app_secret(_app_secret),
+	 device_manufacturer_id(_device_manufacturer_id),
+	 username(_username), email(_email), password(_password),
+	 curl(event_loop),
+	 defer_invoke_handlers(event_loop, BIND_THIS_METHOD(InvokeHandlers))
+{
+}
+
+CurlGlobal &
+QobuzClient::GetCurl() noexcept
+{
+	return *curl;
+}
+
+void
+QobuzClient::StartLogin() noexcept
+{
+	assert(!session.IsDefined());
+	assert(!login_request);
+	assert(!handlers.empty());
+
+	QobuzLoginHandler &handler = *this;
+	login_request = std::make_unique<QobuzLoginRequest>(*curl, base_url,
+							    app_id,
+							    username, email,
+							    password,
+							    device_manufacturer_id,
+							    handler);
+	login_request->Start();
+}
+
+void
+QobuzClient::AddLoginHandler(QobuzSessionHandler &h) noexcept
+{
+	const std::lock_guard<Mutex> protect(mutex);
+	assert(!h.is_linked());
+
+	const bool was_empty = handlers.empty();
+	handlers.push_front(h);
+
+	if (!was_empty || login_request)
+		return;
+
+	if (session.IsDefined()) {
+		ScheduleInvokeHandlers();
+	} else {
+		// TODO: throttle login attempts?
+
+		std::string login_uri(base_url);
+		login_uri += "/login/username";
+
+		try {
+			StartLogin();
+		} catch (...) {
+			error = std::current_exception();
+			ScheduleInvokeHandlers();
+			return;
+		}
+	}
+}
+
+QobuzSession
+QobuzClient::GetSession() const
+{
+	const std::lock_guard<Mutex> protect(mutex);
+
+	if (error)
+		std::rethrow_exception(error);
+
+	if (!session.IsDefined())
+		throw std::runtime_error("No session");
+
+	return session;
+}
+
+void
+QobuzClient::OnQobuzLoginSuccess(QobuzSession &&_session) noexcept
+{
+	{
+		const std::lock_guard<Mutex> protect(mutex);
+		session = std::move(_session);
+	}
+
+	ScheduleInvokeHandlers();
+}
+
+void
+QobuzClient::OnQobuzLoginError(std::exception_ptr _error) noexcept
+{
+	{
+		const std::lock_guard<Mutex> protect(mutex);
+		error = std::move(_error);
+	}
+
+	ScheduleInvokeHandlers();
+}
+
+void
+QobuzClient::InvokeHandlers() noexcept
+{
+	const std::lock_guard<Mutex> protect(mutex);
+	while (!handlers.empty()) {
+		auto &h = handlers.front();
+		handlers.pop_front();
+
+		const ScopeUnlock unlock(mutex);
+		h.OnQobuzSession();
+	}
+
+	login_request.reset();
+}
+
+std::string
+QobuzClient::MakeSignedUrl(const char *object, const char *method,
+			   const std::multimap<std::string, std::string> &query) const noexcept
+{
+	assert(!query.empty());
+
+	std::string uri(base_url);
+	uri += object;
+	uri.push_back('/');
+	uri += method;
+
+	QueryStringBuilder q;
+	std::string concatenated_query(object);
+	concatenated_query += method;
+	for (const auto &i : query) {
+		q(uri, i.first.c_str(), i.second.c_str());
+
+		concatenated_query += i.first;
+		concatenated_query += i.second;
+	}
+
+	q(uri, "app_id", app_id);
+
+	const auto request_ts = std::to_string(time(nullptr));
+	q(uri, "request_ts", request_ts.c_str());
+	concatenated_query += request_ts;
+
+	concatenated_query += app_secret;
+
+	const auto md5_hex = MD5Hex({concatenated_query.data(), concatenated_query.size()});
+	q(uri, "request_sig", &md5_hex.front());
+
+	return uri;
+}
diff --git a/src/input/plugins/QobuzClient.hxx b/src/input/plugins/QobuzClient.hxx
new file mode 100644
index 000000000..fe1fa6ed9
--- /dev/null
+++ b/src/input/plugins/QobuzClient.hxx
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef QOBUZ_CLIENT_HXX
+#define QOBUZ_CLIENT_HXX
+
+#include "check.h"
+#include "QobuzSession.hxx"
+#include "QobuzLoginRequest.hxx"
+#include "lib/curl/Init.hxx"
+#include "thread/Mutex.hxx"
+#include "event/DeferEvent.hxx"
+
+#include <boost/intrusive/list.hpp>
+
+#include <memory>
+#include <map>
+#include <string>
+
+class QobuzSessionHandler
+	: public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::safe_link>>
+{
+public:
+	virtual void OnQobuzSession() noexcept = 0;
+};
+
+class QobuzClient final : QobuzLoginHandler {
+	const char *const base_url;
+	const char *const app_id, *const app_secret;
+	const char *const device_manufacturer_id;
+	const char *const username, *const email, *const password;
+
+	CurlInit curl;
+
+	DeferEvent defer_invoke_handlers;
+
+	/**
+	 * Protects #session, #error, #login_request, #handlers.
+	 */
+	mutable Mutex mutex;
+
+	QobuzSession session;
+
+	std::exception_ptr error;
+
+	typedef boost::intrusive::list<QobuzSessionHandler,
+				       boost::intrusive::constant_time_size<false>> LoginHandlerList;
+
+	LoginHandlerList handlers;
+
+	std::unique_ptr<QobuzLoginRequest> login_request;
+
+public:
+	QobuzClient(EventLoop &event_loop,
+		    const char *_base_url,
+		    const char *_app_id, const char *_app_secret,
+		    const char *_device_manufacturer_id,
+		    const char *_username, const char *_email,
+		    const char *_password);
+
+	gcc_pure
+	CurlGlobal &GetCurl() noexcept;
+
+	void AddLoginHandler(QobuzSessionHandler &h) noexcept;
+
+	void RemoveLoginHandler(QobuzSessionHandler &h) noexcept {
+		const std::lock_guard<Mutex> protect(mutex);
+		if (h.is_linked())
+			handlers.erase(handlers.iterator_to(h));
+	}
+
+	/**
+	 * Throws on error.
+	 */
+	QobuzSession GetSession() const;
+
+	std::string MakeSignedUrl(const char *object, const char *method,
+				  const std::multimap<std::string, std::string> &query) const noexcept;
+
+private:
+	void StartLogin() noexcept;
+
+	void InvokeHandlers() noexcept;
+
+	void ScheduleInvokeHandlers() noexcept {
+		defer_invoke_handlers.Schedule();
+	}
+
+	/* virtual methods from QobuzLoginHandler */
+	void OnQobuzLoginSuccess(QobuzSession &&session) noexcept override;
+	void OnQobuzLoginError(std::exception_ptr error) noexcept override;
+};
+
+#endif
diff --git a/src/input/plugins/QobuzInputPlugin.cxx b/src/input/plugins/QobuzInputPlugin.cxx
new file mode 100644
index 000000000..7ff9ba593
--- /dev/null
+++ b/src/input/plugins/QobuzInputPlugin.cxx
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "QobuzInputPlugin.hxx"
+#include "QobuzClient.hxx"
+#include "QobuzTrackRequest.hxx"
+#include "CurlInputPlugin.hxx"
+#include "PluginUnavailable.hxx"
+#include "input/ProxyInputStream.hxx"
+#include "input/FailingInputStream.hxx"
+#include "input/InputPlugin.hxx"
+#include "config/Block.hxx"
+#include "thread/Mutex.hxx"
+#include "util/StringCompare.hxx"
+
+#include <stdexcept>
+#include <memory>
+
+#include <time.h>
+
+static QobuzClient *qobuz_client;
+
+class QobuzInputStream final
+	: public ProxyInputStream, QobuzSessionHandler, QobuzTrackHandler {
+
+	const std::string track_id;
+
+	std::unique_ptr<QobuzTrackRequest> track_request;
+
+	std::exception_ptr error;
+
+public:
+	QobuzInputStream(const char *_uri, const char *_track_id,
+			 Mutex &_mutex, Cond &_cond) noexcept
+		:ProxyInputStream(_uri, _mutex, _cond),
+		 track_id(_track_id)
+	{
+		qobuz_client->AddLoginHandler(*this);
+	}
+
+	~QobuzInputStream() {
+		qobuz_client->RemoveLoginHandler(*this);
+	}
+
+	/* virtual methods from InputStream */
+
+	void Check() override {
+		if (error)
+			std::rethrow_exception(error);
+	}
+
+private:
+	void Failed(std::exception_ptr e) {
+		SetInput(std::make_unique<FailingInputStream>(GetURI(), e,
+							      mutex, cond));
+	}
+
+	/* virtual methods from QobuzSessionHandler */
+	void OnQobuzSession() noexcept override;
+
+	/* virtual methods from QobuzTrackHandler */
+	void OnQobuzTrackSuccess(std::string &&url) noexcept override;
+	void OnQobuzTrackError(std::exception_ptr error) noexcept override;
+};
+
+void
+QobuzInputStream::OnQobuzSession() noexcept
+{
+	const std::lock_guard<Mutex> protect(mutex);
+
+	try {
+		const auto session = qobuz_client->GetSession();
+
+		QobuzTrackHandler &handler = *this;
+		track_request = std::make_unique<QobuzTrackRequest>(*qobuz_client,
+								    session,
+								    track_id.c_str(),
+								    handler);
+		track_request->Start();
+	} catch (...) {
+		Failed(std::current_exception());
+	}
+}
+
+void
+QobuzInputStream::OnQobuzTrackSuccess(std::string &&url) noexcept
+{
+	const std::lock_guard<Mutex> protect(mutex);
+
+	try {
+		SetInput(OpenCurlInputStream(url.c_str(), {},
+					     mutex, cond));
+	} catch (...) {
+		Failed(std::current_exception());
+	}
+}
+
+void
+QobuzInputStream::OnQobuzTrackError(std::exception_ptr e) noexcept
+{
+	const std::lock_guard<Mutex> protect(mutex);
+
+	Failed(e);
+}
+
+static void
+InitQobuzInput(EventLoop &event_loop, const ConfigBlock &block)
+{
+	const char *base_url = block.GetBlockValue("base_url",
+						   "http://www.qobuz.com/api.json/0.2/");
+
+	const char *app_id = block.GetBlockValue("app_id");
+	if (app_id == nullptr)
+		throw PluginUnavailable("No Qobuz app_id configured");
+
+	const char *app_secret = block.GetBlockValue("app_secret");
+	if (app_secret == nullptr)
+		throw PluginUnavailable("No Qobuz app_secret configured");
+
+	const char *device_manufacturer_id = block.GetBlockValue("device_manufacturer_id",
+								 "df691fdc-fa36-11e7-9718-635337d7df8f");
+
+	const char *username = block.GetBlockValue("username");
+	const char *email = block.GetBlockValue("email");
+	if (username == nullptr && email == nullptr)
+		throw PluginUnavailable("No Qobuz username configured");
+
+	const char *password = block.GetBlockValue("password");
+	if (password == nullptr)
+		throw PluginUnavailable("No Qobuz password configured");
+
+	qobuz_client = new QobuzClient(event_loop, base_url,
+				       app_id, app_secret,
+				       device_manufacturer_id,
+				       username, email, password);
+}
+
+static void
+FinishQobuzInput()
+{
+	delete qobuz_client;
+}
+
+static InputStreamPtr
+OpenQobuzInput(const char *uri, Mutex &mutex, Cond &cond)
+{
+	assert(qobuz_client != nullptr);
+
+	const char *track_id;
+
+	// TODO: what's the standard "qobuz://" URI syntax?
+
+	track_id = StringAfterPrefix(uri, "qobuz://track/");
+
+	if (track_id == nullptr || *track_id == 0)
+		return nullptr;
+
+	// TODO: validate track_id
+
+	return std::make_unique<QobuzInputStream>(uri, track_id, mutex, cond);
+}
+
+const InputPlugin qobuz_input_plugin = {
+	"qobuz",
+	InitQobuzInput,
+	FinishQobuzInput,
+	OpenQobuzInput,
+};
diff --git a/src/input/plugins/QobuzInputPlugin.hxx b/src/input/plugins/QobuzInputPlugin.hxx
new file mode 100644
index 000000000..320f24651
--- /dev/null
+++ b/src/input/plugins/QobuzInputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef INPUT_QOBUZ_HXX
+#define INPUT_QOBUZ_HXX
+
+extern const struct InputPlugin qobuz_input_plugin;
+
+#endif
diff --git a/src/input/plugins/QobuzLoginRequest.cxx b/src/input/plugins/QobuzLoginRequest.cxx
new file mode 100644
index 000000000..38238aaf3
--- /dev/null
+++ b/src/input/plugins/QobuzLoginRequest.cxx
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "QobuzLoginRequest.hxx"
+#include "lib/curl/Form.hxx"
+#include "lib/yajl/Callbacks.hxx"
+#include "util/RuntimeError.hxx"
+
+using Wrapper = Yajl::CallbacksWrapper<QobuzLoginRequest>;
+static constexpr yajl_callbacks parse_callbacks = {
+	nullptr,
+	nullptr,
+	nullptr,
+	nullptr,
+	nullptr,
+	Wrapper::String,
+	Wrapper::StartMap,
+	Wrapper::MapKey,
+	Wrapper::EndMap,
+	nullptr,
+	nullptr,
+};
+
+static std::multimap<std::string, std::string>
+MakeLoginForm(const char *app_id,
+	      const char *username, const char *email,
+	      const char *password,
+	      const char *device_manufacturer_id)
+{
+	assert(username != nullptr || email != nullptr);
+
+	std::multimap<std::string, std::string> form{
+		{"app_id", app_id},
+		{"password", password},
+		{"device_manufacturer_id", device_manufacturer_id},
+	};
+
+	if (username != nullptr)
+		form.emplace("username", username);
+	else
+		form.emplace("email", email);
+
+	return form;
+}
+
+static std::string
+MakeLoginUrl(CURL *curl,
+	     const char *base_url, const char *app_id,
+	     const char *username, const char *email,
+	     const char *password,
+	     const char *device_manufacturer_id)
+{
+	std::string url(base_url);
+	url += "user/login?";
+	url += EncodeForm(curl,
+			  MakeLoginForm(app_id, username, email, password,
+					device_manufacturer_id));
+	return url;
+}
+
+QobuzLoginRequest::QobuzLoginRequest(CurlGlobal &curl,
+				     const char *base_url, const char *app_id,
+				     const char *username, const char *email,
+				     const char *password,
+				     const char *device_manufacturer_id,
+				     QobuzLoginHandler &_handler) noexcept
+	:request(curl, *this),
+	 parser(&parse_callbacks, nullptr, this),
+	 handler(_handler)
+{
+	request.SetUrl(MakeLoginUrl(request.Get(), base_url, app_id,
+				    username, email, password,
+				    device_manufacturer_id).c_str());
+}
+
+QobuzLoginRequest::~QobuzLoginRequest() noexcept
+{
+	request.StopIndirect();
+}
+
+void
+QobuzLoginRequest::OnHeaders(unsigned status,
+			     std::multimap<std::string, std::string> &&headers)
+{
+	if (status != 200)
+		throw FormatRuntimeError("Status %u from Qobuz", status);
+
+	auto i = headers.find("content-type");
+	if (i == headers.end() || i->second.find("/json") == i->second.npos)
+		throw std::runtime_error("Not a JSON response from Qobuz");
+}
+
+void
+QobuzLoginRequest::OnData(ConstBuffer<void> data)
+{
+	parser.Parse((const unsigned char *)data.data, data.size);
+}
+
+void
+QobuzLoginRequest::OnEnd()
+{
+	parser.CompleteParse();
+
+	if (session.user_auth_token.empty())
+		throw std::runtime_error("No user_auth_token in login response");
+
+	if (session.device_id.empty())
+		throw std::runtime_error("No device id in login response");
+
+	handler.OnQobuzLoginSuccess(std::move(session));
+}
+
+void
+QobuzLoginRequest::OnError(std::exception_ptr e) noexcept
+{
+	handler.OnQobuzLoginError(e);
+}
+
+inline bool
+QobuzLoginRequest::String(StringView value) noexcept
+{
+	switch (state) {
+	case State::NONE:
+	case State::DEVICE:
+		break;
+
+	case State::DEVICE_ID:
+		session.device_id.assign(value.data, value.size);
+		break;
+
+	case State::USER_AUTH_TOKEN:
+		session.user_auth_token.assign(value.data, value.size);
+		break;
+	}
+
+	return true;
+}
+
+inline bool
+QobuzLoginRequest::StartMap() noexcept
+{
+	switch (state) {
+	case State::NONE:
+		break;
+
+	case State::DEVICE:
+	case State::DEVICE_ID:
+		++map_depth;
+		break;
+
+	case State::USER_AUTH_TOKEN:
+		break;
+	}
+
+	return true;
+}
+
+inline bool
+QobuzLoginRequest::MapKey(StringView value) noexcept
+{
+	switch (state) {
+	case State::NONE:
+		if (value.Equals("user_auth_token"))
+			state = State::USER_AUTH_TOKEN;
+		else if (value.Equals("device")) {
+			state = State::DEVICE;
+			map_depth = 0;
+		}
+
+		break;
+
+	case State::DEVICE:
+		if (value.Equals("id"))
+			state = State::DEVICE_ID;
+		break;
+
+	case State::DEVICE_ID:
+		break;
+
+	case State::USER_AUTH_TOKEN:
+		break;
+	}
+
+
+	return true;
+}
+
+inline bool
+QobuzLoginRequest::EndMap() noexcept
+{
+	switch (state) {
+	case State::NONE:
+		break;
+
+	case State::DEVICE_ID:
+		state = State::DEVICE;
+		break;
+
+	case State::DEVICE:
+	case State::USER_AUTH_TOKEN:
+		break;
+	}
+
+	switch (state) {
+	case State::NONE:
+	case State::DEVICE_ID:
+		break;
+
+	case State::DEVICE:
+		assert(map_depth > 0);
+		if (--map_depth == 0)
+			state = State::NONE;
+		break;
+
+	case State::USER_AUTH_TOKEN:
+		break;
+	}
+
+	return true;
+}
diff --git a/src/input/plugins/QobuzLoginRequest.hxx b/src/input/plugins/QobuzLoginRequest.hxx
new file mode 100644
index 000000000..e13bd2b30
--- /dev/null
+++ b/src/input/plugins/QobuzLoginRequest.hxx
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef QOBUZ_LOGIN_REQUEST_HXX
+#define QOBUZ_LOGIN_REQUEST_HXX
+
+#include "check.h"
+#include "QobuzSession.hxx"
+#include "lib/curl/Handler.hxx"
+#include "lib/curl/Request.hxx"
+#include "lib/yajl/Handle.hxx"
+
+#include <exception>
+#include <string>
+
+class CurlRequest;
+
+class QobuzLoginHandler {
+public:
+	virtual void OnQobuzLoginSuccess(QobuzSession &&session) noexcept = 0;
+	virtual void OnQobuzLoginError(std::exception_ptr error) noexcept = 0;
+};
+
+class QobuzLoginRequest final : CurlResponseHandler {
+	CurlRequest request;
+
+	Yajl::Handle parser;
+
+	enum class State {
+		NONE,
+		DEVICE,
+		DEVICE_ID,
+		USER_AUTH_TOKEN,
+	} state = State::NONE;
+
+	unsigned map_depth = 0;
+
+	QobuzSession session;
+
+	std::exception_ptr error;
+
+	QobuzLoginHandler &handler;
+
+public:
+	QobuzLoginRequest(CurlGlobal &curl,
+			  const char *base_url, const char *app_id,
+			  const char *username, const char *email,
+			  const char *password,
+			  const char *device_manufacturer_id,
+			  QobuzLoginHandler &_handler) noexcept;
+
+	~QobuzLoginRequest() noexcept;
+
+	void Start() noexcept {
+		request.StartIndirect();
+	}
+
+private:
+	/* virtual methods from CurlResponseHandler */
+	void OnHeaders(unsigned status,
+		       std::multimap<std::string, std::string> &&headers) override;
+	void OnData(ConstBuffer<void> data) override;
+	void OnEnd() override;
+	void OnError(std::exception_ptr e) noexcept override;
+
+public:
+	/* yajl callbacks */
+	bool String(StringView value) noexcept;
+	bool StartMap() noexcept;
+	bool MapKey(StringView value) noexcept;
+	bool EndMap() noexcept;
+};
+
+#endif
diff --git a/src/input/plugins/QobuzSession.hxx b/src/input/plugins/QobuzSession.hxx
new file mode 100644
index 000000000..b69f3d77b
--- /dev/null
+++ b/src/input/plugins/QobuzSession.hxx
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef QOBUZ_SESSION_HXX
+#define QOBUZ_SESSION_HXX
+
+#include <string>
+
+class CurlRequest;
+
+struct QobuzSession {
+	std::string user_auth_token, device_id;
+
+	bool IsDefined() const noexcept {
+		return !user_auth_token.empty();
+	}
+
+	void Clear() {
+		user_auth_token.clear();
+		device_id.clear();
+	}
+};
+
+#endif
diff --git a/src/input/plugins/QobuzTrackRequest.cxx b/src/input/plugins/QobuzTrackRequest.cxx
new file mode 100644
index 000000000..81d533fe2
--- /dev/null
+++ b/src/input/plugins/QobuzTrackRequest.cxx
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "QobuzTrackRequest.hxx"
+#include "QobuzClient.hxx"
+#include "lib/yajl/Callbacks.hxx"
+#include "util/RuntimeError.hxx"
+
+using Wrapper = Yajl::CallbacksWrapper<QobuzTrackRequest>;
+static constexpr yajl_callbacks parse_callbacks = {
+	nullptr,
+	nullptr,
+	nullptr,
+	nullptr,
+	nullptr,
+	Wrapper::String,
+	nullptr,
+	Wrapper::MapKey,
+	Wrapper::EndMap,
+	nullptr,
+	nullptr,
+};
+
+static std::string
+MakeTrackUrl(QobuzClient &client, const char *track_id)
+{
+	return client.MakeSignedUrl("track", "getFileUrl",
+				    {
+					    {"track_id", track_id},
+					    {"format_id", "5"},
+				    });
+}
+
+QobuzTrackRequest::QobuzTrackRequest(QobuzClient &client,
+				     const QobuzSession &session,
+				     const char *track_id,
+				     QobuzTrackHandler &_handler) noexcept
+	:request(client.GetCurl(),
+		 MakeTrackUrl(client, track_id).c_str(),
+		 *this),
+	 parser(&parse_callbacks, nullptr, this),
+	 handler(_handler)
+{
+	request_headers.Append(("X-User-Auth-Token:"
+				+ session.user_auth_token).c_str());
+	request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
+}
+
+QobuzTrackRequest::~QobuzTrackRequest() noexcept
+{
+	request.StopIndirect();
+}
+
+void
+QobuzTrackRequest::OnHeaders(unsigned status,
+			     std::multimap<std::string, std::string> &&headers)
+{
+	if (status != 200)
+		throw FormatRuntimeError("Status %u from Qobuz", status);
+
+	auto i = headers.find("content-type");
+	if (i == headers.end() || i->second.find("/json") == i->second.npos)
+		throw std::runtime_error("Not a JSON response from Qobuz");
+}
+
+void
+QobuzTrackRequest::OnData(ConstBuffer<void> data)
+{
+	parser.Parse((const unsigned char *)data.data, data.size);
+}
+
+void
+QobuzTrackRequest::OnEnd()
+{
+	parser.CompleteParse();
+
+	if (url.empty())
+		throw std::runtime_error("No url in track response");
+
+	handler.OnQobuzTrackSuccess(std::move(url));
+}
+
+void
+QobuzTrackRequest::OnError(std::exception_ptr e) noexcept
+{
+	handler.OnQobuzTrackError(e);
+}
+
+inline bool
+QobuzTrackRequest::String(StringView value) noexcept
+{
+	switch (state) {
+	case State::NONE:
+		break;
+
+	case State::URL:
+		url.assign(value.data, value.size);
+		break;
+	}
+
+	return true;
+}
+
+inline bool
+QobuzTrackRequest::MapKey(StringView value) noexcept
+{
+	if (value.Equals("url"))
+		state = State::URL;
+	else
+		state = State::NONE;
+
+	return true;
+}
+
+inline bool
+QobuzTrackRequest::EndMap() noexcept
+{
+	state = State::NONE;
+
+	return true;
+}
diff --git a/src/input/plugins/QobuzTrackRequest.hxx b/src/input/plugins/QobuzTrackRequest.hxx
new file mode 100644
index 000000000..69a3ff97e
--- /dev/null
+++ b/src/input/plugins/QobuzTrackRequest.hxx
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef QOBUZ_TRACK_REQUEST_HXX
+#define QOBUZ_TRACK_REQUEST_HXX
+
+#include "check.h"
+#include "lib/curl/Handler.hxx"
+#include "lib/curl/Slist.hxx"
+#include "lib/curl/Request.hxx"
+#include "lib/yajl/Handle.hxx"
+
+#include <exception>
+#include <string>
+
+class QobuzClient;
+struct QobuzSession;
+
+class QobuzTrackHandler
+	: public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::safe_link>>
+{
+public:
+	virtual void OnQobuzTrackSuccess(std::string &&url) noexcept = 0;
+	virtual void OnQobuzTrackError(std::exception_ptr error) noexcept = 0;
+};
+
+class QobuzTrackRequest final : CurlResponseHandler {
+	CurlSlist request_headers;
+
+	CurlRequest request;
+
+	Yajl::Handle parser;
+
+	enum class State {
+		NONE,
+		URL,
+	} state = State::NONE;
+
+	std::string url;
+
+	std::exception_ptr error;
+
+	QobuzTrackHandler &handler;
+
+public:
+	QobuzTrackRequest(QobuzClient &client, const QobuzSession &session,
+			  const char *track_id,
+			  QobuzTrackHandler &_handler) noexcept;
+
+	~QobuzTrackRequest() noexcept;
+
+	void Start() noexcept {
+		request.StartIndirect();
+	}
+
+private:
+	/* virtual methods from CurlResponseHandler */
+	void OnHeaders(unsigned status,
+		       std::multimap<std::string, std::string> &&headers) override;
+	void OnData(ConstBuffer<void> data) override;
+	void OnEnd() override;
+	void OnError(std::exception_ptr e) noexcept override;
+
+public:
+	/* yajl callbacks */
+	bool String(StringView value) noexcept;
+	bool MapKey(StringView value) noexcept;
+	bool EndMap() noexcept;
+};
+
+#endif
diff --git a/src/lib/gcrypt/MD5.cxx b/src/lib/gcrypt/MD5.cxx
new file mode 100644
index 000000000..3d74f1a8c
--- /dev/null
+++ b/src/lib/gcrypt/MD5.cxx
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "MD5.hxx"
+#include "util/ConstBuffer.hxx"
+
+#include <gcrypt.h>
+
+#include <stdio.h>
+
+std::array<uint8_t, 16>
+MD5(ConstBuffer<void> input) noexcept
+{
+	std::array<uint8_t, 16> result;
+	gcry_md_hash_buffer(GCRY_MD_MD5, &result.front(),
+			    input.data, input.size);
+	return result;
+}
+
+std::array<char, 33>
+MD5Hex(ConstBuffer<void> input) noexcept
+{
+	const auto raw = MD5(input);
+	std::array<char, 33> result;
+
+	char *p = &result.front();
+	for (const auto i : raw) {
+		sprintf(p, "%02x", i);
+		p += 2;
+	}
+
+	return result;
+}
diff --git a/src/lib/gcrypt/MD5.hxx b/src/lib/gcrypt/MD5.hxx
new file mode 100644
index 000000000..417884d6f
--- /dev/null
+++ b/src/lib/gcrypt/MD5.hxx
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GCRYPT_MD5_HXX
+#define GCRYPT_MD5_HXX
+
+#include "Compiler.h"
+
+#include <array>
+
+template<typename T> struct ConstBuffer;
+
+gcc_pure
+std::array<uint8_t, 16>
+MD5(ConstBuffer<void> input) noexcept;
+
+gcc_pure
+std::array<char, 33>
+MD5Hex(ConstBuffer<void> input) noexcept;
+
+#endif
diff --git a/src/ls.cxx b/src/ls.cxx
index b8b9ad049..77543492a 100644
--- a/src/ls.cxx
+++ b/src/ls.cxx
@@ -61,6 +61,9 @@ static const char *const remoteUrlPrefixes[] = {
 #ifdef ENABLE_ALSA
 	"alsa://",
 #endif
+#ifdef ENABLE_QOBUZ
+	"qobuz://",
+#endif
 #ifdef ENABLE_TIDAL
 	"tidal://",
 #endif