2018-01-10 20:57:50 +01:00
|
|
|
/*
|
|
|
|
* 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 "TidalInputPlugin.hxx"
|
|
|
|
#include "TidalSessionManager.hxx"
|
|
|
|
#include "TidalTrackRequest.hxx"
|
2018-01-26 16:46:38 +01:00
|
|
|
#include "TidalTagScanner.hxx"
|
2018-01-24 15:18:19 +01:00
|
|
|
#include "TidalError.hxx"
|
2018-01-10 20:57:50 +01:00
|
|
|
#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"
|
2018-01-24 15:19:31 +01:00
|
|
|
#include "util/Domain.hxx"
|
2018-01-24 15:18:19 +01:00
|
|
|
#include "util/Exception.hxx"
|
2018-01-10 20:57:50 +01:00
|
|
|
#include "util/StringCompare.hxx"
|
2018-01-24 15:19:31 +01:00
|
|
|
#include "Log.hxx"
|
2018-01-10 20:57:50 +01:00
|
|
|
|
|
|
|
#include <stdexcept>
|
|
|
|
#include <memory>
|
|
|
|
|
2018-01-24 15:19:31 +01:00
|
|
|
static constexpr Domain tidal_domain("tidal");
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
static TidalSessionManager *tidal_session;
|
2018-01-26 19:18:13 +01:00
|
|
|
static const char *tidal_audioquality;
|
2018-01-10 20:57:50 +01:00
|
|
|
|
|
|
|
class TidalInputStream final
|
|
|
|
: public ProxyInputStream, TidalSessionHandler, TidalTrackHandler {
|
|
|
|
|
|
|
|
const std::string track_id;
|
|
|
|
|
|
|
|
std::unique_ptr<TidalTrackRequest> track_request;
|
|
|
|
|
|
|
|
std::exception_ptr error;
|
|
|
|
|
2018-01-24 15:18:19 +01:00
|
|
|
/**
|
|
|
|
* Retry to login if TidalError::IsInvalidSession() returns
|
|
|
|
* true?
|
|
|
|
*/
|
|
|
|
bool retry_login = true;
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
public:
|
|
|
|
TidalInputStream(const char *_uri, const char *_track_id,
|
2018-06-22 19:37:18 +02:00
|
|
|
Mutex &_mutex) noexcept
|
|
|
|
:ProxyInputStream(_uri, _mutex),
|
2018-01-10 20:57:50 +01:00
|
|
|
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,
|
2018-06-22 19:37:18 +02:00
|
|
|
mutex));
|
2018-01-10 20:57:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/* virtual methods from TidalSessionHandler */
|
|
|
|
void OnTidalSession() noexcept override;
|
|
|
|
|
|
|
|
/* virtual methods from TidalTrackHandler */
|
2018-01-20 00:33:01 +01:00
|
|
|
void OnTidalTrackSuccess(std::string url) noexcept override;
|
2018-01-10 20:57:50 +01:00
|
|
|
void OnTidalTrackError(std::exception_ptr error) noexcept override;
|
|
|
|
};
|
|
|
|
|
|
|
|
void
|
|
|
|
TidalInputStream::OnTidalSession() noexcept
|
|
|
|
{
|
|
|
|
const std::lock_guard<Mutex> protect(mutex);
|
|
|
|
|
|
|
|
try {
|
2018-06-22 19:37:18 +02:00
|
|
|
TidalTrackHandler &h = *this;
|
2018-01-10 20:57:50 +01:00
|
|
|
track_request = std::make_unique<TidalTrackRequest>(tidal_session->GetCurl(),
|
|
|
|
tidal_session->GetBaseUrl(),
|
|
|
|
tidal_session->GetToken(),
|
|
|
|
tidal_session->GetSession().c_str(),
|
|
|
|
track_id.c_str(),
|
2018-01-26 19:18:13 +01:00
|
|
|
tidal_audioquality,
|
2018-06-22 19:37:18 +02:00
|
|
|
h);
|
2018-01-19 11:09:18 +01:00
|
|
|
track_request->Start();
|
2018-01-10 20:57:50 +01:00
|
|
|
} catch (...) {
|
|
|
|
Failed(std::current_exception());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2018-01-20 00:33:01 +01:00
|
|
|
TidalInputStream::OnTidalTrackSuccess(std::string url) noexcept
|
2018-01-10 20:57:50 +01:00
|
|
|
{
|
2018-01-24 15:19:31 +01:00
|
|
|
FormatDebug(tidal_domain, "Tidal track '%s' resolves to %s",
|
|
|
|
track_id.c_str(), url.c_str());
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
const std::lock_guard<Mutex> protect(mutex);
|
|
|
|
|
2018-01-20 00:34:44 +01:00
|
|
|
track_request.reset();
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
try {
|
|
|
|
SetInput(OpenCurlInputStream(url.c_str(), {},
|
2018-06-22 19:37:18 +02:00
|
|
|
mutex));
|
2018-01-10 20:57:50 +01:00
|
|
|
} catch (...) {
|
|
|
|
Failed(std::current_exception());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-24 15:18:19 +01:00
|
|
|
gcc_pure
|
|
|
|
static bool
|
|
|
|
IsInvalidSession(std::exception_ptr e) noexcept
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
std::rethrow_exception(e);
|
|
|
|
} catch (const TidalError &te) {
|
|
|
|
return te.IsInvalidSession();
|
|
|
|
} catch (...) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
void
|
|
|
|
TidalInputStream::OnTidalTrackError(std::exception_ptr e) noexcept
|
|
|
|
{
|
|
|
|
const std::lock_guard<Mutex> protect(mutex);
|
|
|
|
|
2018-01-24 15:18:19 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
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");
|
|
|
|
|
2019-06-26 23:09:12 +02:00
|
|
|
FormatWarning(tidal_domain, "The Tidal input plugin is deprecated because Tidal has changed the protocol and doesn't share documentation");
|
|
|
|
|
2018-01-26 19:18:13 +01:00
|
|
|
tidal_audioquality = block.GetBlockValue("audioquality", "HIGH");
|
2018-01-10 20:57:50 +01:00
|
|
|
|
|
|
|
tidal_session = new TidalSessionManager(event_loop, base_url, token,
|
|
|
|
username, password);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
FinishTidalInput()
|
|
|
|
{
|
|
|
|
delete tidal_session;
|
|
|
|
}
|
|
|
|
|
2018-01-26 16:59:09 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
static InputStreamPtr
|
2018-06-22 19:37:18 +02:00
|
|
|
OpenTidalInput(const char *uri, Mutex &mutex)
|
2018-01-10 20:57:50 +01:00
|
|
|
{
|
|
|
|
assert(tidal_session != nullptr);
|
|
|
|
|
2018-01-26 16:59:09 +01:00
|
|
|
const char *track_id = ExtractTidalTrackId(uri);
|
2018-01-10 20:57:50 +01:00
|
|
|
if (track_id == nullptr)
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
// TODO: validate track_id
|
|
|
|
|
2018-06-22 19:37:18 +02:00
|
|
|
return std::make_unique<TidalInputStream>(uri, track_id, mutex);
|
2018-01-10 20:57:50 +01:00
|
|
|
}
|
|
|
|
|
2018-01-26 16:46:38 +01:00
|
|
|
static std::unique_ptr<RemoteTagScanner>
|
|
|
|
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<TidalTagScanner>(tidal_session->GetCurl(),
|
|
|
|
tidal_session->GetBaseUrl(),
|
|
|
|
tidal_session->GetToken(),
|
|
|
|
track_id, handler);
|
|
|
|
}
|
|
|
|
|
2018-10-24 20:25:32 +02:00
|
|
|
static constexpr const char *tidal_prefixes[] = {
|
|
|
|
"tidal://",
|
|
|
|
nullptr
|
|
|
|
};
|
|
|
|
|
2018-01-10 20:57:50 +01:00
|
|
|
const InputPlugin tidal_input_plugin = {
|
|
|
|
"tidal",
|
2018-10-24 20:25:32 +02:00
|
|
|
tidal_prefixes,
|
2018-01-10 20:57:50 +01:00
|
|
|
InitTidalInput,
|
|
|
|
FinishTidalInput,
|
|
|
|
OpenTidalInput,
|
2018-01-26 16:46:38 +01:00
|
|
|
ScanTidalTags,
|
2018-01-10 20:57:50 +01:00
|
|
|
};
|