summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Kellermann <max@musicpd.org>2018-01-10 20:57:50 +0100
committerMax Kellermann <max@musicpd.org>2018-01-12 14:33:22 +0100
commit93b51d56aa61aae9a5096d40cd5a36de0e658f49 (patch)
tree70ecddebd3490af59b55630becb3d3054ebbc153 /src
parent86c50574d2056563d8e506cb0ccdfafa628070f1 (diff)
input/tidal: new input plugin to receive Tidal streams
Diffstat (limited to 'src')
-rw-r--r--src/input/Registry.cxx4
-rw-r--r--src/input/plugins/TidalInputPlugin.cxx174
-rw-r--r--src/input/plugins/TidalInputPlugin.hxx25
-rw-r--r--src/input/plugins/TidalLoginRequest.cxx138
-rw-r--r--src/input/plugins/TidalLoginRequest.hxx81
-rw-r--r--src/input/plugins/TidalSessionManager.cxx106
-rw-r--r--src/input/plugins/TidalSessionManager.hxx144
-rw-r--r--src/input/plugins/TidalTrackRequest.cxx141
-rw-r--r--src/input/plugins/TidalTrackRequest.hxx84
-rw-r--r--src/ls.cxx3
10 files changed, 900 insertions, 0 deletions
diff --git a/src/input/Registry.cxx b/src/input/Registry.cxx
index bbadaf82d..9df6e8e30 100644
--- a/src/input/Registry.cxx
+++ b/src/input/Registry.cxx
@@ -21,6 +21,7 @@
#include "Registry.hxx"
#include "util/Macros.hxx"
#include "plugins/FileInputPlugin.hxx"
+#include "plugins/TidalInputPlugin.hxx"
#ifdef ENABLE_ALSA
#include "plugins/AlsaInputPlugin.hxx"
@@ -62,6 +63,9 @@ const InputPlugin *const input_plugins[] = {
#ifdef ENABLE_ARCHIVE
&input_plugin_archive,
#endif
+#ifdef ENABLE_TIDAL
+ &tidal_input_plugin,
+#endif
#ifdef ENABLE_CURL
&input_plugin_curl,
#endif
diff --git a/src/input/plugins/TidalInputPlugin.cxx b/src/input/plugins/TidalInputPlugin.cxx
new file mode 100644
index 000000000..07dc84b36
--- /dev/null
+++ b/src/input/plugins/TidalInputPlugin.cxx
@@ -0,0 +1,174 @@
+/*
+ * 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 "TidalInputPlugin.hxx"
+#include "TidalSessionManager.hxx"
+#include "TidalTrackRequest.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>
+
+static TidalSessionManager *tidal_session;
+
+class TidalInputStream final
+ : public ProxyInputStream, TidalSessionHandler, TidalTrackHandler {
+
+ const std::string track_id;
+
+ std::unique_ptr<TidalTrackRequest> track_request;
+
+ std::exception_ptr error;
+
+public:
+ TidalInputStream(const char *_uri, const char *_track_id,
+ Mutex &_mutex, Cond &_cond) noexcept
+ :ProxyInputStream(_uri, _mutex, _cond),
+ track_id(_track_id)
+ {
+ tidal_session->AddLoginHandler(*this);
+ }
+
+ ~TidalInputStream() {
+ tidal_session->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 TidalSessionHandler */
+ void OnTidalSession() noexcept override;
+
+ /* virtual methods from TidalTrackHandler */
+ void OnTidalTrackSuccess(std::string &&url) noexcept override;
+ void OnTidalTrackError(std::exception_ptr error) noexcept override;
+};
+
+void
+TidalInputStream::OnTidalSession() noexcept
+{
+ const std::lock_guard<Mutex> protect(mutex);
+
+ try {
+ TidalTrackHandler &handler = *this;
+ track_request = std::make_unique<TidalTrackRequest>(tidal_session->GetCurl(),
+ tidal_session->GetBaseUrl(),
+ tidal_session->GetToken(),
+ tidal_session->GetSession().c_str(),
+ track_id.c_str(),
+ handler);
+ } catch (...) {
+ Failed(std::current_exception());
+ }
+}
+
+void
+TidalInputStream::OnTidalTrackSuccess(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
+TidalInputStream::OnTidalTrackError(std::exception_ptr e) noexcept
+{
+ const std::lock_guard<Mutex> protect(mutex);
+
+ Failed(e);
+}
+
+static void
+InitTidalInput(EventLoop &event_loop, const ConfigBlock &block)
+{
+ const char *base_url = block.GetBlockValue("base_url",
+ "https://api.tidal.com/v1");
+
+ const char *token = block.GetBlockValue("token");
+ if (token == nullptr)
+ throw PluginUnavailable("No Tidal application token configured");
+
+ const char *username = block.GetBlockValue("username");
+ if (username == nullptr)
+ throw PluginUnavailable("No Tidal username configured");
+
+ const char *password = block.GetBlockValue("password");
+ if (password == nullptr)
+ throw PluginUnavailable("No Tidal password configured");
+
+ // TODO: "audioquality" setting
+
+ tidal_session = new TidalSessionManager(event_loop, base_url, token,
+ username, password);
+}
+
+static void
+FinishTidalInput()
+{
+ delete tidal_session;
+}
+
+static InputStreamPtr
+OpenTidalInput(const char *uri, Mutex &mutex, Cond &cond)
+{
+ assert(tidal_session != nullptr);
+
+ const char *track_id;
+
+ track_id = StringAfterPrefix(uri, "tidal://track/");
+ if (track_id == nullptr)
+ track_id = StringAfterPrefix(uri, "https://listen.tidal.com/track/");
+
+ if (track_id == nullptr || *track_id == 0)
+ return nullptr;
+
+ // TODO: validate track_id
+
+ return std::make_unique<TidalInputStream>(uri, track_id, mutex, cond);
+}
+
+const InputPlugin tidal_input_plugin = {
+ "tidal",
+ InitTidalInput,
+ FinishTidalInput,
+ OpenTidalInput,
+};
diff --git a/src/input/plugins/TidalInputPlugin.hxx b/src/input/plugins/TidalInputPlugin.hxx
new file mode 100644
index 000000000..a1847c4f8
--- /dev/null
+++ b/src/input/plugins/TidalInputPlugin.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_TIDAL_HXX
+#define INPUT_TIDAL_HXX
+
+extern const struct InputPlugin tidal_input_plugin;
+
+#endif
diff --git a/src/input/plugins/TidalLoginRequest.cxx b/src/input/plugins/TidalLoginRequest.cxx
new file mode 100644
index 000000000..419143325
--- /dev/null
+++ b/src/input/plugins/TidalLoginRequest.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 "TidalLoginRequest.hxx"
+#include "lib/curl/Form.hxx"
+#include "lib/yajl/Callbacks.hxx"
+#include "util/RuntimeError.hxx"
+
+using Wrapper = Yajl::CallbacksWrapper<TidalLoginRequest>;
+static constexpr yajl_callbacks parse_callbacks = {
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ Wrapper::String,
+ nullptr,
+ Wrapper::MapKey,
+ Wrapper::EndMap,
+ nullptr,
+ nullptr,
+};
+
+static std::string
+MakeLoginUrl(const char *base_url)
+{
+ return std::string(base_url) + "/login/username";
+}
+
+TidalLoginRequest::TidalLoginRequest(CurlGlobal &curl,
+ const char *base_url, const char *token,
+ const char *username, const char *password,
+ TidalLoginHandler &_handler) noexcept
+ :request(curl, MakeLoginUrl(base_url).c_str(), *this),
+ parser(&parse_callbacks, nullptr, this),
+ handler(_handler)
+{
+ request_headers.Append((std::string("X-Tidal-Token:")
+ + token).c_str());
+ request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
+
+ request.SetOption(CURLOPT_COPYPOSTFIELDS,
+ EncodeForm(request.Get(),
+ {{"username", username}, {"password", password}}).c_str());
+
+ request.StartIndirect();
+}
+
+TidalLoginRequest::~TidalLoginRequest() noexcept
+{
+ request.StopIndirect();
+}
+
+void
+TidalLoginRequest::OnHeaders(unsigned status,
+ std::multimap<std::string, std::string> &&headers)
+{
+ if (status != 200)
+ throw FormatRuntimeError("Status %u from Tidal", 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 Tidal");
+}
+
+void
+TidalLoginRequest::OnData(ConstBuffer<void> data)
+{
+ parser.Parse((const unsigned char *)data.data, data.size);
+}
+
+void
+TidalLoginRequest::OnEnd()
+{
+ parser.CompleteParse();
+
+ if (session.empty())
+ throw std::runtime_error("No sessionId in login response");
+
+ handler.OnTidalLoginSuccess(std::move(session));
+}
+
+void
+TidalLoginRequest::OnError(std::exception_ptr e) noexcept
+{
+ handler.OnTidalLoginError(e);
+}
+
+inline bool
+TidalLoginRequest::String(StringView value) noexcept
+{
+ switch (state) {
+ case State::NONE:
+ break;
+
+ case State::SESSION_ID:
+ session.assign(value.data, value.size);
+ break;
+ }
+
+ return true;
+}
+
+inline bool
+TidalLoginRequest::MapKey(StringView value) noexcept
+{
+ if (value.Equals("sessionId"))
+ state = State::SESSION_ID;
+ else
+ state = State::NONE;
+
+ return true;
+}
+
+inline bool
+TidalLoginRequest::EndMap() noexcept
+{
+ state = State::NONE;
+
+ return true;
+}
diff --git a/src/input/plugins/TidalLoginRequest.hxx b/src/input/plugins/TidalLoginRequest.hxx
new file mode 100644
index 000000000..c0562c742
--- /dev/null
+++ b/src/input/plugins/TidalLoginRequest.hxx
@@ -0,0 +1,81 @@
+/*
+ * 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 TIDAL_LOGIN_REQUEST_HXX
+#define TIDAL_LOGIN_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 CurlRequest;
+
+class TidalLoginHandler {
+public:
+ virtual void OnTidalLoginSuccess(std::string &&session) noexcept = 0;
+ virtual void OnTidalLoginError(std::exception_ptr error) noexcept = 0;
+};
+
+class TidalLoginRequest final : CurlResponseHandler {
+ CurlSlist request_headers;
+
+ CurlRequest request;
+
+ Yajl::Handle parser;
+
+ enum class State {
+ NONE,
+ SESSION_ID,
+ } state = State::NONE;
+
+ std::string session;
+
+ std::exception_ptr error;
+
+ TidalLoginHandler &handler;
+
+public:
+ TidalLoginRequest(CurlGlobal &curl,
+ const char *base_url, const char *token,
+ const char *username, const char *password,
+ TidalLoginHandler &_handler) noexcept;
+
+ ~TidalLoginRequest() noexcept;
+
+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/input/plugins/TidalSessionManager.cxx b/src/input/plugins/TidalSessionManager.cxx
new file mode 100644
index 000000000..48c00ed4c
--- /dev/null
+++ b/src/input/plugins/TidalSessionManager.cxx
@@ -0,0 +1,106 @@
+/*
+ * 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 "TidalSessionManager.hxx"
+#include "lib/curl/Global.hxx"
+
+TidalSessionManager::TidalSessionManager(EventLoop &event_loop,
+ const char *_base_url, const char *_token,
+ const char *_username,
+ const char *_password) noexcept
+ :base_url(_base_url), token(_token),
+ username(_username), password(_password),
+ curl(event_loop),
+ defer_invoke_handlers(event_loop,
+ BIND_THIS_METHOD(InvokeHandlers))
+{
+}
+
+TidalSessionManager::~TidalSessionManager() noexcept
+{
+ assert(handlers.empty());
+}
+
+void
+TidalSessionManager::AddLoginHandler(TidalSessionHandler &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 && session.empty() && !login_request) {
+ // TODO: throttle login attempts?
+
+ std::string login_uri(base_url);
+ login_uri += "/login/username";
+
+ try {
+ TidalLoginHandler &handler = *this;
+ login_request =
+ std::make_unique<TidalLoginRequest>(*curl, base_url,
+ token,
+ username, password,
+ handler);
+ } catch (...) {
+ error = std::current_exception();
+ ScheduleInvokeHandlers();
+ return;
+ }
+ }
+}
+
+void
+TidalSessionManager::OnTidalLoginSuccess(std::string &&_session) noexcept
+{
+ {
+ const std::lock_guard<Mutex> protect(mutex);
+ session = std::move(_session);
+ }
+
+ ScheduleInvokeHandlers();
+}
+
+void
+TidalSessionManager::OnTidalLoginError(std::exception_ptr e) noexcept
+{
+ {
+ const std::lock_guard<Mutex> protect(mutex);
+ error = e;
+ }
+
+ ScheduleInvokeHandlers();
+}
+
+void
+TidalSessionManager::InvokeHandlers() noexcept
+{
+ const std::lock_guard<Mutex> protect(mutex);
+ while (!handlers.empty()) {
+ auto &h = handlers.front();
+ handlers.pop_front();
+
+ const ScopeUnlock unlock(mutex);
+ h.OnTidalSession();
+ }
+
+ login_request.reset();
+}
diff --git a/src/input/plugins/TidalSessionManager.hxx b/src/input/plugins/TidalSessionManager.hxx
new file mode 100644
index 000000000..906aa0f2e
--- /dev/null
+++ b/src/input/plugins/TidalSessionManager.hxx
@@ -0,0 +1,144 @@
+/*
+ * 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 TIDAL_SESSION_MANAGER_HXX
+#define TIDAL_SESSION_MANAGER_HXX
+
+#include "check.h"
+#include "TidalLoginRequest.hxx"
+#include "lib/curl/Init.hxx"
+#include "thread/Mutex.hxx"
+#include "event/DeferEvent.hxx"
+
+#include <boost/intrusive/list.hpp>
+
+#include <memory>
+#include <string>
+
+class CurlRequest;
+class TidalLoginRequest;
+
+class TidalSessionHandler
+ : public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::safe_link>>
+{
+public:
+ virtual void OnTidalSession() noexcept = 0;
+};
+
+class TidalSessionManager final : TidalLoginHandler {
+ /**
+ * The Tidal API base URL.
+ */
+ const char *const base_url;
+
+ /**
+ * The configured Tidal application token.
+ */
+ const char *const token;
+
+ /**
+ * The configured Tidal user name.
+ */
+ const char *const username;
+
+ /**
+ * The configured Tidal password.
+ */
+ const char *const password;
+
+ CurlInit curl;
+
+ DeferEvent defer_invoke_handlers;
+
+ /**
+ * Protects #session, #error and #handlers.
+ */
+ mutable Mutex mutex;
+
+ std::exception_ptr error;
+
+ /**
+ * The current Tidal session id, empty if none.
+ */
+ std::string session;
+
+ typedef boost::intrusive::list<TidalSessionHandler,
+ boost::intrusive::constant_time_size<false>> LoginHandlerList;
+
+ LoginHandlerList handlers;
+
+ std::unique_ptr<TidalLoginRequest> login_request;
+
+public:
+ TidalSessionManager(EventLoop &event_loop,
+ const char *_base_url, const char *_token,
+ const char *_username,
+ const char *_password) noexcept;
+
+ ~TidalSessionManager() noexcept;
+
+ EventLoop &GetEventLoop() noexcept {
+ return defer_invoke_handlers.GetEventLoop();
+ }
+
+ CurlGlobal &GetCurl() noexcept {
+ return *curl;
+ }
+
+ const char *GetBaseUrl() const noexcept {
+ return base_url;
+ }
+
+ void AddLoginHandler(TidalSessionHandler &h) noexcept;
+
+ void RemoveLoginHandler(TidalSessionHandler &h) noexcept {
+ const std::lock_guard<Mutex> protect(mutex);
+ if (h.is_linked())
+ handlers.erase(handlers.iterator_to(h));
+ }
+
+ const char *GetToken() const noexcept {
+ return token;
+ }
+
+ std::string GetSession() const {
+ const std::lock_guard<Mutex> protect(mutex);
+
+ if (error)
+ std::rethrow_exception(error);
+
+ if (session.empty())
+ throw std::runtime_error("No session");
+
+ return session;
+ }
+
+private:
+ void InvokeHandlers() noexcept;
+
+ void ScheduleInvokeHandlers() noexcept {
+ defer_invoke_handlers.Schedule();
+ }
+
+ /* virtual methods from TidalLoginHandler */
+ void OnTidalLoginSuccess(std::string &&session) noexcept override;
+ void OnTidalLoginError(std::exception_ptr error) noexcept override;
+};
+
+#endif
diff --git a/src/input/plugins/TidalTrackRequest.cxx b/src/input/plugins/TidalTrackRequest.cxx
new file mode 100644
index 000000000..b5f25a44f
--- /dev/null
+++ b/src/input/plugins/TidalTrackRequest.cxx
@@ -0,0 +1,141 @@
+/*
+ * 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 "TidalTrackRequest.hxx"
+#include "lib/yajl/Callbacks.hxx"
+#include "util/RuntimeError.hxx"
+
+using Wrapper = Yajl::CallbacksWrapper<TidalTrackRequest>;
+static constexpr yajl_callbacks parse_callbacks = {
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ Wrapper::String,
+ nullptr,
+ Wrapper::MapKey,
+ Wrapper::EndMap,
+ nullptr,
+ nullptr,
+};
+
+static std::string
+MakeTrackUrl(const char *base_url, const char *track_id)
+{
+ // TODO: add "audioquality" parameter to this function
+ return std::string(base_url)
+ + "/tracks/"
+ + track_id
+ + "/urlpostpaywall?assetpresentation=FULL&audioquality=LOW&urlusagemode=STREAM";
+}
+
+TidalTrackRequest::TidalTrackRequest(CurlGlobal &curl,
+ const char *base_url, const char *token,
+ const char *session,
+ const char *track_id,
+ TidalTrackHandler &_handler) noexcept
+ :request(curl, MakeTrackUrl(base_url, track_id).c_str(), *this),
+ parser(&parse_callbacks, nullptr, this),
+ handler(_handler)
+{
+ request_headers.Append((std::string("X-Tidal-Token:")
+ + token).c_str());
+ request_headers.Append((std::string("X-Tidal-SessionId:")
+ + session).c_str());
+ request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
+
+ request.StartIndirect();
+}
+
+TidalTrackRequest::~TidalTrackRequest() noexcept
+{
+ request.StopIndirect();
+}
+
+void
+TidalTrackRequest::OnHeaders(unsigned status,
+ std::multimap<std::string, std::string> &&headers)
+{
+ if (status != 200)
+ throw FormatRuntimeError("Status %u from Tidal", 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 Tidal");
+}
+
+void
+TidalTrackRequest::OnData(ConstBuffer<void> data)
+{
+ parser.Parse((const unsigned char *)data.data, data.size);
+}
+
+void
+TidalTrackRequest::OnEnd()
+{
+ parser.CompleteParse();
+
+ if (url.empty())
+ throw std::runtime_error("No url in track response");
+
+ handler.OnTidalTrackSuccess(std::move(url));
+}
+
+void
+TidalTrackRequest::OnError(std::exception_ptr e) noexcept
+{
+ handler.OnTidalTrackError(e);
+}
+
+inline bool
+TidalTrackRequest::String(StringView value) noexcept
+{
+ switch (state) {
+ case State::NONE:
+ break;
+
+ case State::URLS:
+ if (url.empty())
+ url.assign(value.data, value.size);
+ break;
+ }
+
+ return true;
+}
+
+inline bool
+TidalTrackRequest::MapKey(StringView value) noexcept
+{
+ if (value.Equals("urls"))
+ state = State::URLS;
+ else
+ state = State::NONE;
+
+ return true;
+}
+
+inline bool
+TidalTrackRequest::EndMap() noexcept
+{
+ state = State::NONE;
+
+ return true;
+}
diff --git a/src/input/plugins/TidalTrackRequest.hxx b/src/input/plugins/TidalTrackRequest.hxx
new file mode 100644
index 000000000..79fdd718d
--- /dev/null
+++ b/src/input/plugins/TidalTrackRequest.hxx
@@ -0,0 +1,84 @@
+/*
+ * 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 TIDAL_TRACK_REQUEST_HXX
+#define TIDAL_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 CurlRequest;
+
+class TidalTrackHandler
+ : public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::safe_link>>
+{
+public:
+ virtual void OnTidalTrackSuccess(std::string &&url) noexcept = 0;
+ virtual void OnTidalTrackError(std::exception_ptr error) noexcept = 0;
+};
+
+class TidalTrackRequest final : CurlResponseHandler {
+ CurlSlist request_headers;
+
+ CurlRequest request;
+
+ Yajl::Handle parser;
+
+ enum class State {
+ NONE,
+ URLS,
+ } state = State::NONE;
+
+ std::string url;
+
+ std::exception_ptr error;
+
+ TidalTrackHandler &handler;
+
+public:
+ TidalTrackRequest(CurlGlobal &curl,
+ const char *base_url, const char *token,
+ const char *session,
+ const char *track_id,
+ TidalTrackHandler &_handler) noexcept;
+
+ ~TidalTrackRequest() noexcept;
+
+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/ls.cxx b/src/ls.cxx
index 72754dd7f..b8b9ad049 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_TIDAL
+ "tidal://",
+#endif
NULL
};