input/tidal: new input plugin to receive Tidal streams

This commit is contained in:
Max Kellermann 2018-01-10 20:57:50 +01:00
parent 86c50574d2
commit 93b51d56aa
14 changed files with 980 additions and 1 deletions

View File

@ -1366,6 +1366,16 @@ libinput_a_SOURCES += \
src/IcyMetaDataParser.cxx src/IcyMetaDataParser.hxx src/IcyMetaDataParser.cxx src/IcyMetaDataParser.hxx
endif 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 if ENABLE_SMBCLIENT
libinput_a_SOURCES += \ libinput_a_SOURCES += \
$(SMBCLIENT_SOURCES) \ $(SMBCLIENT_SOURCES) \

2
NEWS
View File

@ -5,6 +5,8 @@ ver 0.21 (not yet released)
- "outputs" prints the plugin name - "outputs" prints the plugin name
- "outputset" sets runtime attributes - "outputset" sets runtime attributes
- close connection when client sends HTTP request - close connection when client sends HTTP request
* input
- tidal: new plugin to play Tidal streams
* tags * tags
- new tags "OriginalDate", "MUSICBRAINZ_WORKID" - new tags "OriginalDate", "MUSICBRAINZ_WORKID"
* decoder * decoder

View File

@ -367,6 +367,11 @@ AC_ARG_ENABLE(soundcloud,
[enable support for soundcloud.com]),, [enable support for soundcloud.com]),,
[enable_soundcloud=auto]) [enable_soundcloud=auto])
AC_ARG_ENABLE(tidal,
AS_HELP_STRING([--enable-tidal],
[enable support for Tidal streaming]),,
[enable_tidal=auto])
AC_ARG_ENABLE([libwrap], AC_ARG_ENABLE([libwrap],
AS_HELP_STRING([--enable-libwrap], [use libwrap]),, AS_HELP_STRING([--enable-libwrap], [use libwrap]),,
[enable_libwrap=auto]) [enable_libwrap=auto])
@ -562,7 +567,7 @@ MPD_ENABLE_AUTO_PKG(expat, EXPAT, [expat],
dnl -------------------------------- yajl ------------------------------------- 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], PKG_CHECK_MODULES([YAJL], [yajl >= 2.0],
[found_yajl=yes], [found_yajl=yes],
[found_yajl=no]) [found_yajl=no])
@ -700,6 +705,7 @@ dnl Input Plugins
dnl --------------------------------------------------------------------------- dnl ---------------------------------------------------------------------------
dnl ----------------------------------- CURL ---------------------------------- dnl ----------------------------------- CURL ----------------------------------
MPD_ENABLE_AUTO_PKG(curl, CURL, [libcurl >= 7.18], MPD_ENABLE_AUTO_PKG(curl, CURL, [libcurl >= 7.18],
[libcurl HTTP streaming], [libcurl not found]) [libcurl HTTP streaming], [libcurl not found])
@ -718,6 +724,12 @@ MPD_DEPENDS([enable_soundcloud], [found_yajl],
MPD_DEFINE_CONDITIONAL(enable_soundcloud, ENABLE_SOUNDCLOUD, MPD_DEFINE_CONDITIONAL(enable_soundcloud, ENABLE_SOUNDCLOUD,
[soundcloud.com support]) [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 --------------------------------- dnl ---------------------------------- cdio ---------------------------------
MPD_ENABLE_AUTO_PKG(cdio_paranoia, CDIO_PARANOIA, [libcdio_paranoia], MPD_ENABLE_AUTO_PKG(cdio_paranoia, CDIO_PARANOIA, [libcdio_paranoia],
[libcdio_paranoia input plugin], [libcdio_paranoia not found]) [libcdio_paranoia input plugin], [libcdio_paranoia not found])
@ -1515,6 +1527,7 @@ results(cdio_paranoia, [CDIO_PARANOIA])
results(curl,[CURL]) results(curl,[CURL])
results(smbclient,[SMBCLIENT]) results(smbclient,[SMBCLIENT])
results(soundcloud,[Soundcloud]) results(soundcloud,[Soundcloud])
results(tidal,[Tidal])
printf '\n\t' printf '\n\t'
results(mms,[MMS]) results(mms,[MMS])

View File

@ -2369,6 +2369,60 @@ run</programlisting>
<filename>mpc add smb://servername/sharename/filename.ogg</filename> <filename>mpc add smb://servername/sharename/filename.ogg</filename>
</para> </para>
</section> </section>
<section id="tidal_input">
<title><varname>tidal</varname></title>
<para>
Play songs from the commercial streaming service <ulink
url="http://tidal.com/">Tidal</ulink>. It plays URLs in the
form <filename>tidal://track/ID</filename>, e.g.:
</para>
<programlisting>mpc add tidal://track/59727857</programlisting>
<informaltable>
<tgroup cols="2">
<thead>
<row>
<entry>Setting</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>
<varname>token</varname>
<parameter>TOKEN</parameter>
</entry>
<entry>
The Tidal application token.
</entry>
</row>
<row>
<entry>
<varname>username</varname>
<parameter>USERNAME</parameter>
</entry>
<entry>
The Tidal user name.
</entry>
</row>
<row>
<entry>
<varname>password</varname>
<parameter>PASSWORD</parameter>
</entry>
<entry>
The Tidal password.
</entry>
</row>
</tbody>
</tgroup>
</informaltable>
</section>
</section> </section>
<section id="decoder_plugins"> <section id="decoder_plugins">

View File

@ -21,6 +21,7 @@
#include "Registry.hxx" #include "Registry.hxx"
#include "util/Macros.hxx" #include "util/Macros.hxx"
#include "plugins/FileInputPlugin.hxx" #include "plugins/FileInputPlugin.hxx"
#include "plugins/TidalInputPlugin.hxx"
#ifdef ENABLE_ALSA #ifdef ENABLE_ALSA
#include "plugins/AlsaInputPlugin.hxx" #include "plugins/AlsaInputPlugin.hxx"
@ -62,6 +63,9 @@ const InputPlugin *const input_plugins[] = {
#ifdef ENABLE_ARCHIVE #ifdef ENABLE_ARCHIVE
&input_plugin_archive, &input_plugin_archive,
#endif #endif
#ifdef ENABLE_TIDAL
&tidal_input_plugin,
#endif
#ifdef ENABLE_CURL #ifdef ENABLE_CURL
&input_plugin_curl, &input_plugin_curl,
#endif #endif

View File

@ -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,
};

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -60,6 +60,9 @@ static const char *const remoteUrlPrefixes[] = {
#endif #endif
#ifdef ENABLE_ALSA #ifdef ENABLE_ALSA
"alsa://", "alsa://",
#endif
#ifdef ENABLE_TIDAL
"tidal://",
#endif #endif
NULL NULL
}; };