summaryrefslogtreecommitdiff
path: root/src
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 /src
parent74eac1d449ac2ac25b88e307c0a513e44cc29d96 (diff)
input/qobuz: new input plugin to receive Qobuz streams
Diffstat (limited to 'src')
-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
13 files changed, 1206 insertions, 0 deletions
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