From 93b51d56aa61aae9a5096d40cd5a36de0e658f49 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Wed, 10 Jan 2018 20:57:50 +0100 Subject: [PATCH] input/tidal: new input plugin to receive Tidal streams --- Makefile.am | 10 ++ NEWS | 2 + configure.ac | 15 +- doc/user.xml | 54 +++++++ src/input/Registry.cxx | 4 + src/input/plugins/TidalInputPlugin.cxx | 174 ++++++++++++++++++++++ src/input/plugins/TidalInputPlugin.hxx | 25 ++++ src/input/plugins/TidalLoginRequest.cxx | 138 +++++++++++++++++ src/input/plugins/TidalLoginRequest.hxx | 81 ++++++++++ src/input/plugins/TidalSessionManager.cxx | 106 +++++++++++++ src/input/plugins/TidalSessionManager.hxx | 144 ++++++++++++++++++ src/input/plugins/TidalTrackRequest.cxx | 141 ++++++++++++++++++ src/input/plugins/TidalTrackRequest.hxx | 84 +++++++++++ src/ls.cxx | 3 + 14 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 src/input/plugins/TidalInputPlugin.cxx create mode 100644 src/input/plugins/TidalInputPlugin.hxx create mode 100644 src/input/plugins/TidalLoginRequest.cxx create mode 100644 src/input/plugins/TidalLoginRequest.hxx create mode 100644 src/input/plugins/TidalSessionManager.cxx create mode 100644 src/input/plugins/TidalSessionManager.hxx create mode 100644 src/input/plugins/TidalTrackRequest.cxx create mode 100644 src/input/plugins/TidalTrackRequest.hxx diff --git a/Makefile.am b/Makefile.am index d19987fd0..d5f9aa780 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1366,6 +1366,16 @@ libinput_a_SOURCES += \ src/IcyMetaDataParser.cxx src/IcyMetaDataParser.hxx endif +if ENABLE_TIDAL +libinput_a_SOURCES += \ + $(YAJL_SOURCES) \ + src/input/plugins/TidalLoginRequest.cxx src/input/plugins/TidalLoginRequest.hxx \ + src/input/plugins/TidalSessionManager.cxx src/input/plugins/TidalSessionManager.hxx \ + src/input/plugins/TidalTrackRequest.cxx src/input/plugins/TidalTrackRequest.hxx \ + src/input/plugins/TidalInputPlugin.cxx src/input/plugins/TidalInputPlugin.hxx +INPUT_LIBS += $(YAJL_LIBS) +endif + if ENABLE_SMBCLIENT libinput_a_SOURCES += \ $(SMBCLIENT_SOURCES) \ diff --git a/NEWS b/NEWS index 44ea6b6be..bea7245cf 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,8 @@ ver 0.21 (not yet released) - "outputs" prints the plugin name - "outputset" sets runtime attributes - close connection when client sends HTTP request +* input + - tidal: new plugin to play Tidal streams * tags - new tags "OriginalDate", "MUSICBRAINZ_WORKID" * decoder diff --git a/configure.ac b/configure.ac index ca9ee1691..6216a4f90 100644 --- a/configure.ac +++ b/configure.ac @@ -367,6 +367,11 @@ AC_ARG_ENABLE(soundcloud, [enable support for soundcloud.com]),, [enable_soundcloud=auto]) +AC_ARG_ENABLE(tidal, + AS_HELP_STRING([--enable-tidal], + [enable support for Tidal streaming]),, + [enable_tidal=auto]) + AC_ARG_ENABLE([libwrap], AS_HELP_STRING([--enable-libwrap], [use libwrap]),, [enable_libwrap=auto]) @@ -562,7 +567,7 @@ MPD_ENABLE_AUTO_PKG(expat, EXPAT, [expat], dnl -------------------------------- yajl ------------------------------------- -if test x$enable_soundcloud != xno; then +if test x$enable_soundcloud != xno || test x$enable_tidal != xno; then PKG_CHECK_MODULES([YAJL], [yajl >= 2.0], [found_yajl=yes], [found_yajl=no]) @@ -700,6 +705,7 @@ dnl Input Plugins dnl --------------------------------------------------------------------------- dnl ----------------------------------- CURL ---------------------------------- + MPD_ENABLE_AUTO_PKG(curl, CURL, [libcurl >= 7.18], [libcurl HTTP streaming], [libcurl not found]) @@ -718,6 +724,12 @@ MPD_DEPENDS([enable_soundcloud], [found_yajl], MPD_DEFINE_CONDITIONAL(enable_soundcloud, ENABLE_SOUNDCLOUD, [soundcloud.com support]) +dnl --------------------------------- Tidal ----------------------------------- +MPD_DEPENDS([enable_tidal], [found_yajl], [Tidal streaming], [libyajl not found]) +MPD_DEPENDS([enable_tidal], [enable_curl], [Tidal streaming], [libcurl not found]) +MPD_AUTO(tidal, [Tidal streaming], [Tidal not available], [found_tidal=yes]) +MPD_DEFINE_CONDITIONAL(enable_tidal, ENABLE_TIDAL, [Tidal streaming]) + dnl ---------------------------------- cdio --------------------------------- MPD_ENABLE_AUTO_PKG(cdio_paranoia, CDIO_PARANOIA, [libcdio_paranoia], [libcdio_paranoia input plugin], [libcdio_paranoia not found]) @@ -1515,6 +1527,7 @@ results(cdio_paranoia, [CDIO_PARANOIA]) results(curl,[CURL]) results(smbclient,[SMBCLIENT]) results(soundcloud,[Soundcloud]) +results(tidal,[Tidal]) printf '\n\t' results(mms,[MMS]) diff --git a/doc/user.xml b/doc/user.xml index 74795b882..86fa0b49e 100644 --- a/doc/user.xml +++ b/doc/user.xml @@ -2369,6 +2369,60 @@ run mpc add smb://servername/sharename/filename.ogg + +
+ <varname>tidal</varname> + + + Play songs from the commercial streaming service Tidal. It plays URLs in the + form tidal://track/ID, e.g.: + + + mpc add tidal://track/59727857 + + + + + + Setting + Description + + + + + + token + TOKEN + + + The Tidal application token. + + + + + + username + USERNAME + + + The Tidal user name. + + + + + + password + PASSWORD + + + The Tidal password. + + + + + +
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 +#include + +static TidalSessionManager *tidal_session; + +class TidalInputStream final + : public ProxyInputStream, TidalSessionHandler, TidalTrackHandler { + + const std::string track_id; + + std::unique_ptr 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(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 protect(mutex); + + try { + TidalTrackHandler &handler = *this; + track_request = std::make_unique(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 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 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(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; +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 &&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 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 +#include + +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 &&headers) override; + void OnData(ConstBuffer 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 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(*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 protect(mutex); + session = std::move(_session); + } + + ScheduleInvokeHandlers(); +} + +void +TidalSessionManager::OnTidalLoginError(std::exception_ptr e) noexcept +{ + { + const std::lock_guard protect(mutex); + error = e; + } + + ScheduleInvokeHandlers(); +} + +void +TidalSessionManager::InvokeHandlers() noexcept +{ + const std::lock_guard 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 + +#include +#include + +class CurlRequest; +class TidalLoginRequest; + +class TidalSessionHandler + : public boost::intrusive::list_base_hook> +{ +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> LoginHandlerList; + + LoginHandlerList handlers; + + std::unique_ptr 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 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 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; +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 &&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 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 +#include + +class CurlRequest; + +class TidalTrackHandler + : public boost::intrusive::list_base_hook> +{ +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 &&headers) override; + void OnData(ConstBuffer 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 @@ -60,6 +60,9 @@ static const char *const remoteUrlPrefixes[] = { #endif #ifdef ENABLE_ALSA "alsa://", +#endif +#ifdef ENABLE_TIDAL + "tidal://", #endif NULL };