diff options
author | Max Kellermann <max@musicpd.org> | 2018-01-10 20:57:50 +0100 |
---|---|---|
committer | Max Kellermann <max@musicpd.org> | 2018-01-12 14:33:22 +0100 |
commit | 93b51d56aa61aae9a5096d40cd5a36de0e658f49 (patch) | |
tree | 70ecddebd3490af59b55630becb3d3054ebbc153 /src | |
parent | 86c50574d2056563d8e506cb0ccdfafa628070f1 (diff) |
input/tidal: new input plugin to receive Tidal streams
Diffstat (limited to 'src')
-rw-r--r-- | src/input/Registry.cxx | 4 | ||||
-rw-r--r-- | src/input/plugins/TidalInputPlugin.cxx | 174 | ||||
-rw-r--r-- | src/input/plugins/TidalInputPlugin.hxx | 25 | ||||
-rw-r--r-- | src/input/plugins/TidalLoginRequest.cxx | 138 | ||||
-rw-r--r-- | src/input/plugins/TidalLoginRequest.hxx | 81 | ||||
-rw-r--r-- | src/input/plugins/TidalSessionManager.cxx | 106 | ||||
-rw-r--r-- | src/input/plugins/TidalSessionManager.hxx | 144 | ||||
-rw-r--r-- | src/input/plugins/TidalTrackRequest.cxx | 141 | ||||
-rw-r--r-- | src/input/plugins/TidalTrackRequest.hxx | 84 | ||||
-rw-r--r-- | src/ls.cxx | 3 |
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 }; |