summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Kellermann <max@musicpd.org>2018-01-13 18:03:26 +0100
committerMax Kellermann <max@musicpd.org>2018-01-15 21:50:20 +0100
commit9420066895a50f2354323e9746838037eac94e40 (patch)
tree6a6f50dd3c7d55837c8c274ace5bb11d3bdf7568
parent74eac1d449ac2ac25b88e307c0a513e44cc29d96 (diff)
input/qobuz: new input plugin to receive Qobuz streams
-rw-r--r--Makefile.am12
-rw-r--r--NEWS1
-rw-r--r--configure.ac21
-rw-r--r--doc/user.xml64
-rw-r--r--m4/libgcrypt.m4143
-rw-r--r--src/input/Registry.cxx4
-rw-r--r--src/input/plugins/QobuzClient.cxx201
-rw-r--r--src/input/plugins/QobuzClient.hxx110
-rw-r--r--src/input/plugins/QobuzInputPlugin.cxx185
-rw-r--r--src/input/plugins/QobuzInputPlugin.hxx25
-rw-r--r--src/input/plugins/QobuzLoginRequest.cxx237
-rw-r--r--src/input/plugins/QobuzLoginRequest.hxx90
-rw-r--r--src/input/plugins/QobuzSession.hxx40
-rw-r--r--src/input/plugins/QobuzTrackRequest.cxx138
-rw-r--r--src/input/plugins/QobuzTrackRequest.hxx87
-rw-r--r--src/lib/gcrypt/MD5.cxx49
-rw-r--r--src/lib/gcrypt/MD5.hxx37
-rw-r--r--src/ls.cxx3
18 files changed, 1446 insertions, 1 deletions
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