diff --git a/NEWS b/NEWS index a9cae45d1..810379466 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ ver 0.22.10 (not yet released) - simple: fix crash bug * input - curl: fix crash bug after stream with Icy metadata was closed by peer + - tidal: remove defunct unmaintained plugin ver 0.22.9 (2021/06/23) * database diff --git a/doc/plugins.rst b/doc/plugins.rst index ee637f20a..bac76f106 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -295,37 +295,6 @@ in the form ``qobuz://track/ID``, e.g.: * - **format_id N** - The `Qobuz format identifier `_, i.e. a number which chooses the format and quality to be requested from Qobuz. The default is "5" (320 kbit/s MP3). -tidal ------ - -Play songs from the commercial streaming service `Tidal -`_. It plays URLs in the form ``tidal://track/ID``, -e.g.: - -.. warning:: - - This plugin is currently defunct because Tidal has changed the - protocol and decided not to share documentation. - -.. code-block:: none - - mpc add tidal://track/59727857 - -.. list-table:: - :widths: 20 80 - :header-rows: 1 - - * - Setting - - Description - * - **token TOKEN** - - The Tidal application token. Since Tidal is unwilling to assign a token to MPD, this needs to be reverse-engineered from another (approved) Tidal client. - * - **username USERNAME** - - The Tidal user name. - * - **password PASSWORD** - - The Tidal password. - * - **audioquality Q** - - The Tidal "audioquality" parameter. Possible values: HI_RES, LOSSLESS, HIGH, LOW. Default is HIGH. - .. _decoder_plugins: Decoder plugins diff --git a/meson_options.txt b/meson_options.txt index 36214692d..89f889b57 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -104,7 +104,6 @@ option('smbclient', type: 'feature', value: 'disabled', description: 'SMB suppor option('qobuz', type: 'feature', description: 'Qobuz client') option('soundcloud', type: 'feature', description: 'SoundCloud client') -option('tidal', type: 'feature', description: 'Tidal client') # # Archive plugins diff --git a/src/input/InputStream.cxx b/src/input/InputStream.cxx index 5af7d681b..322138914 100644 --- a/src/input/InputStream.cxx +++ b/src/input/InputStream.cxx @@ -57,7 +57,6 @@ static bool ExpensiveSeeking(const char *uri) noexcept { return StringStartsWithCaseASCII(uri, "http://") || - StringStartsWithCaseASCII(uri, "tidal://") || StringStartsWithCaseASCII(uri, "qobuz://") || StringStartsWithCaseASCII(uri, "https://"); } diff --git a/src/input/Registry.cxx b/src/input/Registry.cxx index 4a234a88c..d00a620cc 100644 --- a/src/input/Registry.cxx +++ b/src/input/Registry.cxx @@ -20,7 +20,6 @@ #include "Registry.hxx" #include "InputPlugin.hxx" #include "input/Features.h" -#include "plugins/TidalInputPlugin.hxx" #include "plugins/QobuzInputPlugin.hxx" #include "config.h" @@ -56,9 +55,6 @@ const InputPlugin *const input_plugins[] = { #ifdef ENABLE_ALSA &input_plugin_alsa, #endif -#ifdef ENABLE_TIDAL - &tidal_input_plugin, -#endif #ifdef ENABLE_QOBUZ &qobuz_input_plugin, #endif diff --git a/src/input/plugins/TidalError.hxx b/src/input/plugins/TidalError.hxx deleted file mode 100644 index 036f12998..000000000 --- a/src/input/plugins/TidalError.hxx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2003-2021 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_ERROR_HXX -#define TIDAL_ERROR_HXX - -#include - -/** - * An error condition reported by the server. - * - * See http://developer.tidal.com/technical/errors/ for details (login - * required). - */ -class TidalError : public std::runtime_error { - /** - * The HTTP status code. - */ - unsigned status; - - /** - * The Tidal-specific "subStatus". 0 if none was found in the - * JSON response. - */ - unsigned sub_status; - -public: - template - TidalError(unsigned _status, unsigned _sub_status, W &&_what) noexcept - :std::runtime_error(std::forward(_what)), - status(_status), sub_status(_sub_status) {} - - unsigned GetStatus() const noexcept { - return status; - } - - unsigned GetSubStatus() const noexcept { - return sub_status; - } - - bool IsInvalidSession() const noexcept { - return sub_status == 6001 || sub_status == 6002; - } -}; - -#endif diff --git a/src/input/plugins/TidalErrorParser.cxx b/src/input/plugins/TidalErrorParser.cxx deleted file mode 100644 index 8049113d4..000000000 --- a/src/input/plugins/TidalErrorParser.cxx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalErrorParser.hxx" -#include "TidalError.hxx" -#include "lib/yajl/Callbacks.hxx" -#include "util/RuntimeError.hxx" - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks tidal_error_parser_callbacks = { - nullptr, - nullptr, - Wrapper::Integer, - nullptr, - nullptr, - Wrapper::String, - nullptr, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -TidalErrorParser::TidalErrorParser(unsigned _status, - const std::multimap &headers) - :YajlResponseParser(&tidal_error_parser_callbacks, nullptr, this), - status(_status) -{ - auto i = headers.find("content-type"); - if (i == headers.end() || i->second.find("/json") == i->second.npos) - throw FormatRuntimeError("Status %u from Tidal", status); -} - -void -TidalErrorParser::OnEnd() -{ - YajlResponseParser::OnEnd(); - - char what[1024]; - - if (!message.empty()) - snprintf(what, sizeof(what), "Error from Tidal: %s", - message.c_str()); - else - snprintf(what, sizeof(what), "Status %u from Tidal", status); - - throw TidalError(status, sub_status, what); -} - -inline bool -TidalErrorParser::Integer(long long value) noexcept -{ - switch (state) { - case State::NONE: - case State::USER_MESSAGE: - break; - - case State::SUB_STATUS: - sub_status = value; - break; - } - - return true; -} - -inline bool -TidalErrorParser::String(StringView value) noexcept -{ - switch (state) { - case State::NONE: - case State::SUB_STATUS: - break; - - case State::USER_MESSAGE: - message.assign(value.data, value.size); - break; - } - - return true; -} - -inline bool -TidalErrorParser::MapKey(StringView value) noexcept -{ - if (value.Equals("userMessage")) - state = State::USER_MESSAGE; - else if (value.Equals("subStatus")) - state = State::SUB_STATUS; - else - state = State::NONE; - - return true; -} - -inline bool -TidalErrorParser::EndMap() noexcept -{ - state = State::NONE; - - return true; -} diff --git a/src/input/plugins/TidalErrorParser.hxx b/src/input/plugins/TidalErrorParser.hxx deleted file mode 100644 index 6291d130f..000000000 --- a/src/input/plugins/TidalErrorParser.hxx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2003-2021 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_ERROR_PARSER_HXX -#define TIDAL_ERROR_PARSER_HXX - -#include "lib/yajl/ResponseParser.hxx" - -#include -#include - -template struct ConstBuffer; -struct StringView; - -/** - * Parse an error JSON response and throw a #TidalError upon - * completion. - */ -class TidalErrorParser final : public YajlResponseParser { - const unsigned status; - - enum class State { - NONE, - USER_MESSAGE, - SUB_STATUS, - } state = State::NONE; - - unsigned sub_status = 0; - - std::string message; - -public: - /** - * May throw if there is a formal error in the response - * headers. - */ - TidalErrorParser(unsigned status, - const std::multimap &headers); - -protected: - /* virtual methods from CurlResponseParser */ - [[noreturn]] - void OnEnd() override; - -public: - /* yajl callbacks */ - bool Integer(long long value) noexcept; - bool String(StringView value) noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -#endif diff --git a/src/input/plugins/TidalInputPlugin.cxx b/src/input/plugins/TidalInputPlugin.cxx deleted file mode 100644 index 1781d16ae..000000000 --- a/src/input/plugins/TidalInputPlugin.cxx +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalInputPlugin.hxx" -#include "TidalSessionManager.hxx" -#include "TidalTrackRequest.hxx" -#include "TidalTagScanner.hxx" -#include "TidalError.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/Domain.hxx" -#include "util/Exception.hxx" -#include "util/StringCompare.hxx" -#include "Log.hxx" - -#include -#include - -static constexpr Domain tidal_domain("tidal"); - -static TidalSessionManager *tidal_session; -static const char *tidal_audioquality; - -class TidalInputStream final - : public ProxyInputStream, TidalSessionHandler, TidalTrackHandler { - - const std::string track_id; - - std::unique_ptr track_request; - - std::exception_ptr error; - - /** - * Retry to login if TidalError::IsInvalidSession() returns - * true? - */ - bool retry_login = true; - -public: - TidalInputStream(const char *_uri, const char *_track_id, - Mutex &_mutex) noexcept - :ProxyInputStream(_uri, _mutex), - track_id(_track_id) - { - tidal_session->AddLoginHandler(*this); - } - - ~TidalInputStream() override { - tidal_session->RemoveLoginHandler(*this); - } - - /* virtual methods from InputStream */ - - void Check() override { - if (error) - std::rethrow_exception(error); - } - -private: - void Failed(const std::exception_ptr& e) { - SetInput(std::make_unique(GetURI(), e, - mutex)); - } - - /* 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 &h = *this; - track_request = std::make_unique(tidal_session->GetCurl(), - tidal_session->GetBaseUrl(), - tidal_session->GetToken(), - tidal_session->GetSession().c_str(), - track_id.c_str(), - tidal_audioquality, - h); - track_request->Start(); - } catch (...) { - Failed(std::current_exception()); - } -} - -void -TidalInputStream::OnTidalTrackSuccess(std::string url) noexcept -{ - FormatDebug(tidal_domain, "Tidal track '%s' resolves to %s", - track_id.c_str(), url.c_str()); - - const std::lock_guard protect(mutex); - - track_request.reset(); - - try { - SetInput(OpenCurlInputStream(url.c_str(), {}, - mutex)); - } catch (...) { - Failed(std::current_exception()); - } -} - -gcc_pure -static bool -IsInvalidSession(std::exception_ptr e) noexcept -{ - try { - std::rethrow_exception(std::move(e)); - } catch (const TidalError &te) { - return te.IsInvalidSession(); - } catch (...) { - return false; - } -} - -void -TidalInputStream::OnTidalTrackError(std::exception_ptr e) noexcept -{ - const std::lock_guard protect(mutex); - - if (retry_login && IsInvalidSession(e)) { - /* the session has expired - obtain a new session id - by logging in again */ - - FormatInfo(tidal_domain, "Session expired ('%s'), retrying to log in", - GetFullMessage(e).c_str()); - - retry_login = false; - tidal_session->AddLoginHandler(*this); - return; - } - - 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 PluginUnconfigured("No Tidal application token configured"); - - const char *username = block.GetBlockValue("username"); - if (username == nullptr) - throw PluginUnconfigured("No Tidal username configured"); - - const char *password = block.GetBlockValue("password"); - if (password == nullptr) - throw PluginUnconfigured("No Tidal password configured"); - - FormatWarning(tidal_domain, "The Tidal input plugin is deprecated because Tidal has changed the protocol and doesn't share documentation"); - - tidal_audioquality = block.GetBlockValue("audioquality", "HIGH"); - - tidal_session = new TidalSessionManager(event_loop, base_url, token, - username, password); -} - -static void -FinishTidalInput() noexcept -{ - delete tidal_session; -} - -gcc_pure -static const char * -ExtractTidalTrackId(const char *uri) -{ - const char *track_id = StringAfterPrefix(uri, "tidal://track/"); - if (track_id == nullptr) { - track_id = StringAfterPrefix(uri, "https://listen.tidal.com/track/"); - if (track_id == nullptr) - return nullptr; - } - - if (*track_id == 0) - return nullptr; - - return track_id; -} - -static InputStreamPtr -OpenTidalInput(const char *uri, Mutex &mutex) -{ - assert(tidal_session != nullptr); - - const char *track_id = ExtractTidalTrackId(uri); - if (track_id == nullptr) - return nullptr; - - // TODO: validate track_id - - return std::make_unique(uri, track_id, mutex); -} - -static std::unique_ptr -ScanTidalTags(const char *uri, RemoteTagHandler &handler) -{ - assert(tidal_session != nullptr); - - const char *track_id = ExtractTidalTrackId(uri); - if (track_id == nullptr) - return nullptr; - - return std::make_unique(tidal_session->GetCurl(), - tidal_session->GetBaseUrl(), - tidal_session->GetToken(), - track_id, handler); -} - -static constexpr const char *tidal_prefixes[] = { - "tidal://", - nullptr -}; - -const InputPlugin tidal_input_plugin = { - "tidal", - tidal_prefixes, - InitTidalInput, - FinishTidalInput, - OpenTidalInput, - nullptr, - ScanTidalTags, -}; diff --git a/src/input/plugins/TidalInputPlugin.hxx b/src/input/plugins/TidalInputPlugin.hxx deleted file mode 100644 index ba88bf68b..000000000 --- a/src/input/plugins/TidalInputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2003-2021 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 deleted file mode 100644 index ae7133734..000000000 --- a/src/input/plugins/TidalLoginRequest.cxx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalLoginRequest.hxx" -#include "TidalErrorParser.hxx" -#include "lib/curl/Form.hxx" -#include "lib/yajl/Callbacks.hxx" -#include "lib/yajl/ResponseParser.hxx" - -#include - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks parse_callbacks = { - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - Wrapper::String, - nullptr, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -class TidalLoginRequest::ResponseParser final : public YajlResponseParser { - enum class State { - NONE, - SESSION_ID, - } state = State::NONE; - - std::string session; - -public: - explicit ResponseParser() noexcept - :YajlResponseParser(&parse_callbacks, nullptr, this) {} - - std::string &&GetSession() { - if (session.empty()) - throw std::runtime_error("No sessionId in login response"); - - return std::move(session); - } - - /* yajl callbacks */ - bool String(StringView value) noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -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) - :request(curl, MakeLoginUrl(base_url).c_str(), *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()); -} - -TidalLoginRequest::~TidalLoginRequest() noexcept -{ - request.StopIndirect(); -} - -std::unique_ptr -TidalLoginRequest::MakeParser(unsigned status, - std::multimap &&headers) -{ - if (status != 200) - return std::make_unique(status, headers); - - 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"); - - return std::make_unique(); -} - -void -TidalLoginRequest::FinishParser(std::unique_ptr p) -{ - assert(dynamic_cast(p.get()) != nullptr); - auto &rp = (ResponseParser &)*p; - handler.OnTidalLoginSuccess(rp.GetSession()); -} - -void -TidalLoginRequest::OnError(std::exception_ptr e) noexcept -{ - handler.OnTidalLoginError(e); -} - -inline bool -TidalLoginRequest::ResponseParser::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::ResponseParser::MapKey(StringView value) noexcept -{ - if (value.Equals("sessionId")) - state = State::SESSION_ID; - else - state = State::NONE; - - return true; -} - -inline bool -TidalLoginRequest::ResponseParser::EndMap() noexcept -{ - state = State::NONE; - - return true; -} diff --git a/src/input/plugins/TidalLoginRequest.hxx b/src/input/plugins/TidalLoginRequest.hxx deleted file mode 100644 index b830b2f2c..000000000 --- a/src/input/plugins/TidalLoginRequest.hxx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2003-2021 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 "lib/curl/Delegate.hxx" -#include "lib/curl/Slist.hxx" -#include "lib/curl/Request.hxx" - -/** - * Callback class for #TidalLoginRequest. - * - * Its methods must be thread-safe. - */ -class TidalLoginHandler { -public: - virtual void OnTidalLoginSuccess(std::string session) noexcept = 0; - virtual void OnTidalLoginError(std::exception_ptr error) noexcept = 0; -}; - -/** - * An asynchronous Tidal "login/username" request. - * - * After construction, call Start() to initiate the request. - */ -class TidalLoginRequest final : DelegateCurlResponseHandler { - CurlSlist request_headers; - - CurlRequest request; - - TidalLoginHandler &handler; - -public: - class ResponseParser; - - TidalLoginRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *username, const char *password, - TidalLoginHandler &_handler); - - ~TidalLoginRequest() noexcept; - - void Start() { - request.StartIndirect(); - } - -private: - /* virtual methods from DelegateCurlResponseHandler */ - std::unique_ptr MakeParser(unsigned status, - std::multimap &&headers) override; - void FinishParser(std::unique_ptr p) override; - - /* virtual methods from CurlResponseHandler */ - void OnError(std::exception_ptr e) noexcept override; -}; - -#endif diff --git a/src/input/plugins/TidalSessionManager.cxx b/src/input/plugins/TidalSessionManager.cxx deleted file mode 100644 index c454429eb..000000000 --- a/src/input/plugins/TidalSessionManager.cxx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalSessionManager.hxx" -#include "util/Domain.hxx" - -#include "Log.hxx" - -static constexpr Domain tidal_domain("tidal"); - -TidalSessionManager::TidalSessionManager(EventLoop &event_loop, - const char *_base_url, const char *_token, - const char *_username, - const char *_password) - :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 || login_request) - return; - - if (session.empty()) { - // TODO: throttle login attempts? - - LogDebug(tidal_domain, "Sending login request"); - - 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); - login_request->Start(); - } catch (...) { - error = std::current_exception(); - ScheduleInvokeHandlers(); - return; - } - } else - ScheduleInvokeHandlers(); -} - -void -TidalSessionManager::OnTidalLoginSuccess(std::string _session) noexcept -{ - FormatDebug(tidal_domain, "Login successful, session=%s", _session.c_str()); - - { - const std::lock_guard protect(mutex); - login_request.reset(); - session = std::move(_session); - } - - ScheduleInvokeHandlers(); -} - -void -TidalSessionManager::OnTidalLoginError(std::exception_ptr e) noexcept -{ - { - const std::lock_guard protect(mutex); - login_request.reset(); - 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(); - } -} diff --git a/src/input/plugins/TidalSessionManager.hxx b/src/input/plugins/TidalSessionManager.hxx deleted file mode 100644 index 2d4159b32..000000000 --- a/src/input/plugins/TidalSessionManager.hxx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalLoginRequest.hxx" -#include "lib/curl/Init.hxx" -#include "thread/Mutex.hxx" -#include "event/DeferEvent.hxx" - -#include - -#include -#include - -/** - * Callback class for #TidalSessionManager. - * - * Its methods must be thread-safe. - */ -class TidalSessionHandler - : public boost::intrusive::list_base_hook> -{ -public: - /** - * TidalSessionHandler::AddLoginHandler() has completed - * (successful or failed). This method may now call - * #TidalSessionHandler::GetSession(). - */ - 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); - - ~TidalSessionManager() noexcept; - - auto &GetEventLoop() const noexcept { - return defer_invoke_handlers.GetEventLoop(); - } - - CurlGlobal &GetCurl() noexcept { - return *curl; - } - - const char *GetBaseUrl() const noexcept { - return base_url; - } - - /** - * Ask the object to call back once the login to Tidal has - * completed. If no session exists currently, then one is - * created. Since the callback may occur in another thread, - * the it may have been completed already before this method - * returns. - */ - 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; - } - - /** - * Get the Tidal session id, or rethrows an exception if an - * error has occurred while logging in. - */ - 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/TidalTagScanner.cxx b/src/input/plugins/TidalTagScanner.cxx deleted file mode 100644 index 4b345dc35..000000000 --- a/src/input/plugins/TidalTagScanner.cxx +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalTagScanner.hxx" -#include "TidalErrorParser.hxx" -#include "lib/yajl/Callbacks.hxx" -#include "tag/Builder.hxx" -#include "tag/Tag.hxx" - -#include - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks parse_callbacks = { - nullptr, - nullptr, - Wrapper::Integer, - nullptr, - nullptr, - Wrapper::String, - Wrapper::StartMap, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -class TidalTagScanner::ResponseParser final : public YajlResponseParser { - enum class State { - NONE, - TITLE, - DURATION, - ARTIST, - ARTIST_NAME, - ALBUM, - ALBUM_TITLE, - } state = State::NONE; - - unsigned map_depth = 0; - - TagBuilder tag; - -public: - explicit ResponseParser() noexcept - :YajlResponseParser(&parse_callbacks, nullptr, this) {} - - Tag GetTag() { - return tag.Commit(); - } - - /* yajl callbacks */ - bool Integer(long long value) noexcept; - bool String(StringView value) noexcept; - bool StartMap() noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -static std::string -MakeTrackUrl(const char *base_url, const char *track_id) -{ - return std::string(base_url) - + "/tracks/" - + track_id - // TODO: configurable countryCode? - + "?countryCode=US"; -} - -TidalTagScanner::TidalTagScanner(CurlGlobal &curl, - const char *base_url, const char *token, - const char *track_id, - RemoteTagHandler &_handler) - :request(curl, MakeTrackUrl(base_url, track_id).c_str(), *this), - handler(_handler) -{ - request_headers.Append((std::string("X-Tidal-Token:") - + token).c_str()); - request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get()); -} - -TidalTagScanner::~TidalTagScanner() noexcept -{ - request.StopIndirect(); -} - -std::unique_ptr -TidalTagScanner::MakeParser(unsigned status, - std::multimap &&headers) -{ - if (status != 200) - return std::make_unique(status, headers); - - 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"); - - return std::make_unique(); -} - -void -TidalTagScanner::FinishParser(std::unique_ptr p) -{ - assert(dynamic_cast(p.get()) != nullptr); - auto &rp = (ResponseParser &)*p; - handler.OnRemoteTag(rp.GetTag()); -} - -void -TidalTagScanner::OnError(std::exception_ptr e) noexcept -{ - handler.OnRemoteTagError(e); -} - -inline bool -TidalTagScanner::ResponseParser::Integer(long long value) noexcept -{ - switch (state) { - case State::NONE: - case State::TITLE: - case State::ARTIST: - case State::ARTIST_NAME: - case State::ALBUM: - case State::ALBUM_TITLE: - break; - - case State::DURATION: - if (map_depth == 1 && value > 0) - tag.SetDuration(SignedSongTime::FromS((unsigned)value)); - break; - } - - return true; -} - -inline bool -TidalTagScanner::ResponseParser::String(StringView value) noexcept -{ - switch (state) { - case State::NONE: - case State::DURATION: - case State::ARTIST: - case State::ALBUM: - break; - - case State::TITLE: - if (map_depth == 1) - tag.AddItem(TAG_TITLE, value); - break; - - case State::ARTIST_NAME: - if (map_depth == 2) - tag.AddItem(TAG_ARTIST, value); - break; - - case State::ALBUM_TITLE: - if (map_depth == 2) - tag.AddItem(TAG_ALBUM, value); - break; - } - - return true; -} - -inline bool -TidalTagScanner::ResponseParser::StartMap() noexcept -{ - ++map_depth; - return true; -} - -inline bool -TidalTagScanner::ResponseParser::MapKey(StringView value) noexcept -{ - switch (map_depth) { - case 1: - if (value.Equals("title")) - state = State::TITLE; - else if (value.Equals("duration")) - state = State::DURATION; - else if (value.Equals("artist")) - state = State::ARTIST; - else if (value.Equals("album")) - state = State::ALBUM; - else - state = State::NONE; - break; - - case 2: - switch (state) { - case State::NONE: - case State::TITLE: - case State::DURATION: - break; - - case State::ARTIST: - case State::ARTIST_NAME: - if (value.Equals("name")) - state = State::ARTIST_NAME; - else - state = State::ARTIST; - break; - - case State::ALBUM: - case State::ALBUM_TITLE: - if (value.Equals("title")) - state = State::ALBUM_TITLE; - else - state = State::ALBUM; - break; - } - break; - } - - return true; -} - -inline bool -TidalTagScanner::ResponseParser::EndMap() noexcept -{ - switch (map_depth) { - case 2: - state = State::NONE; - break; - } - - --map_depth; - - return true; -} diff --git a/src/input/plugins/TidalTagScanner.hxx b/src/input/plugins/TidalTagScanner.hxx deleted file mode 100644 index b7c8cd3bc..000000000 --- a/src/input/plugins/TidalTagScanner.hxx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2003-2021 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_TAG_SCANNER_HXX -#define TIDAL_TAG_SCANNER_HXX - -#include "lib/curl/Delegate.hxx" -#include "lib/curl/Slist.hxx" -#include "lib/curl/Request.hxx" -#include "input/RemoteTagScanner.hxx" - -class TidalTagScanner final - : public RemoteTagScanner, DelegateCurlResponseHandler -{ - CurlSlist request_headers; - - CurlRequest request; - - RemoteTagHandler &handler; - -public: - class ResponseParser; - - TidalTagScanner(CurlGlobal &curl, - const char *base_url, const char *token, - const char *track_id, - RemoteTagHandler &_handler); - - ~TidalTagScanner() noexcept override; - - void Start() override { - request.StartIndirect(); - } - -private: - /* virtual methods from DelegateCurlResponseHandler */ - std::unique_ptr MakeParser(unsigned status, - std::multimap &&headers) override; - void FinishParser(std::unique_ptr p) override; - - /* virtual methods from CurlResponseHandler */ - void OnError(std::exception_ptr e) noexcept override; -}; - -#endif diff --git a/src/input/plugins/TidalTrackRequest.cxx b/src/input/plugins/TidalTrackRequest.cxx deleted file mode 100644 index bced95ef9..000000000 --- a/src/input/plugins/TidalTrackRequest.cxx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalTrackRequest.hxx" -#include "TidalErrorParser.hxx" -#include "lib/yajl/Callbacks.hxx" - -#include - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks parse_callbacks = { - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - Wrapper::String, - nullptr, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -class TidalTrackRequest::ResponseParser final : public YajlResponseParser { - enum class State { - NONE, - URLS, - } state = State::NONE; - - std::string url; - -public: - explicit ResponseParser() noexcept - :YajlResponseParser(&parse_callbacks, nullptr, this) {} - - std::string &&GetUrl() { - if (url.empty()) - throw std::runtime_error("No url in track response"); - - return std::move(url); - } - - /* yajl callbacks */ - bool String(StringView value) noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -static std::string -MakeTrackUrl(const char *base_url, const char *track_id, - const char *audioquality) noexcept -{ - return std::string(base_url) - + "/tracks/" - + track_id - + "/urlpostpaywall?assetpresentation=FULL&audioquality=" - + audioquality + "&urlusagemode=STREAM"; -} - -TidalTrackRequest::TidalTrackRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *session, - const char *track_id, - const char *audioquality, - TidalTrackHandler &_handler) - :request(curl, MakeTrackUrl(base_url, track_id, audioquality).c_str(), - *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()); -} - -TidalTrackRequest::~TidalTrackRequest() noexcept -{ - request.StopIndirect(); -} - -std::unique_ptr -TidalTrackRequest::MakeParser(unsigned status, - std::multimap &&headers) -{ - if (status != 200) - return std::make_unique(status, headers); - - 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"); - - return std::make_unique(); -} - -void -TidalTrackRequest::FinishParser(std::unique_ptr p) -{ - assert(dynamic_cast(p.get()) != nullptr); - auto &rp = (ResponseParser &)*p; - handler.OnTidalTrackSuccess(rp.GetUrl()); -} - -void -TidalTrackRequest::OnError(std::exception_ptr e) noexcept -{ - handler.OnTidalTrackError(e); -} - -inline bool -TidalTrackRequest::ResponseParser::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::ResponseParser::MapKey(StringView value) noexcept -{ - if (value.Equals("urls")) - state = State::URLS; - else - state = State::NONE; - - return true; -} - -inline bool -TidalTrackRequest::ResponseParser::EndMap() noexcept -{ - state = State::NONE; - - return true; -} diff --git a/src/input/plugins/TidalTrackRequest.hxx b/src/input/plugins/TidalTrackRequest.hxx deleted file mode 100644 index 44f3dad6b..000000000 --- a/src/input/plugins/TidalTrackRequest.hxx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2003-2021 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 "lib/curl/Delegate.hxx" -#include "lib/curl/Slist.hxx" -#include "lib/curl/Request.hxx" - -/** - * Callback class for #TidalTrackRequest. - * - * Its methods must be thread-safe. - */ -class TidalTrackHandler { -public: - virtual void OnTidalTrackSuccess(std::string url) noexcept = 0; - virtual void OnTidalTrackError(std::exception_ptr error) noexcept = 0; -}; - -/** - * An asynchronous request for the streaming URL of a Tidal track. - * - * After construction, call Start() to initiate the request. - */ -class TidalTrackRequest final : DelegateCurlResponseHandler { - CurlSlist request_headers; - - CurlRequest request; - - TidalTrackHandler &handler; - -public: - class ResponseParser; - - TidalTrackRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *session, - const char *track_id, - const char *audioquality, - TidalTrackHandler &_handler); - - ~TidalTrackRequest() noexcept; - - void Start() { - request.StartIndirect(); - } - -private: - /* virtual methods from DelegateCurlResponseHandler */ - std::unique_ptr MakeParser(unsigned status, - std::multimap &&headers) override; - void FinishParser(std::unique_ptr p) override; - - /* virtual methods from CurlResponseHandler */ - void OnError(std::exception_ptr e) noexcept override; -}; - -#endif diff --git a/src/input/plugins/meson.build b/src/input/plugins/meson.build index 73223654f..9802e942e 100644 --- a/src/input/plugins/meson.build +++ b/src/input/plugins/meson.build @@ -63,27 +63,6 @@ if enable_qobuz ] endif -tidal_feature = get_option('tidal') -if tidal_feature.disabled() - enable_tidal = false -else - enable_tidal = curl_dep.found() and yajl_dep.found() - if not enable_tidal and tidal_feature.enabled() - error('Tidal requires CURL and libyajl') - endif -endif -input_features.set('ENABLE_TIDAL', enable_tidal) -if enable_tidal - input_plugins_sources += [ - 'TidalErrorParser.cxx', - 'TidalLoginRequest.cxx', - 'TidalSessionManager.cxx', - 'TidalTrackRequest.cxx', - 'TidalTagScanner.cxx', - 'TidalInputPlugin.cxx', - ] -endif - input_plugins = static_library( 'input_plugins', input_plugins_sources,