Implemented the track.scrobble() call. Posts to last.fm work now!

This commit is contained in:
casey langen 2018-05-02 21:32:16 -07:00
parent 8a75e7e8b3
commit c31514c315
8 changed files with 158 additions and 82 deletions

View File

@ -46,6 +46,7 @@ namespace musik { namespace core { namespace io {
class HttpClient: public std::enable_shared_from_this<HttpClient<T>> {
public:
enum class Thread { Current, Background };
enum class HttpMethod { Get, Post };
using HttpHeaders = std::unordered_map<std::string, std::string>;
using Callback = std::function<void(HttpClient<T>* caller, int, CURLcode)>;
@ -65,6 +66,8 @@ namespace musik { namespace core { namespace io {
HttpClient<T>& Decorator(DecoratorCallback decoratorCb);
HttpClient<T>& Canceled(CanceledCallback canceledCb);
HttpClient<T>& Mode(Thread mode);
HttpClient<T>& PostBody(const std::string& postBody);
HttpClient<T>& Method(HttpMethod mode);
const T& Stream() const { return this->ostream; }
const HttpHeaders& ResponseHeaders() const { return this->responseHeaders; }
@ -90,12 +93,14 @@ namespace musik { namespace core { namespace io {
T ostream;
std::string url;
std::string postBody;
HttpHeaders requestHeaders, responseHeaders;
HeaderCallback headersCb;
DecoratorCallback decoratorCb;
CanceledCallback canceledCallback;
bool cancel;
Thread mode{ Thread::Background };
HttpMethod method{ HttpMethod::Get };
CURL* curl;
static std::mutex instanceMutex;
@ -226,6 +231,11 @@ namespace musik { namespace core { namespace io {
curl_easy_setopt(curl, CURLOPT_HEADERDATA, this);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &CurlHeaderCallback);
#if 0
curl_easy_setopt(curl, CURLOPT_PROXY, "http://localhost");
curl_easy_setopt(curl, CURLOPT_PROXYPORT, 8080);
#endif
if (this->requestHeaders.size()) {
struct curl_slist* slist = nullptr;
for (auto it : this->requestHeaders) {
@ -235,6 +245,14 @@ namespace musik { namespace core { namespace io {
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
}
if (this->method == HttpMethod::Post) {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
if (this->postBody.size()) {
curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, this->postBody.c_str());
}
}
if (mode == Thread::Background) {
std::unique_lock<std::mutex> lock(instanceMutex);
instances.insert(this->shared_from_this());
@ -293,12 +311,24 @@ namespace musik { namespace core { namespace io {
return *this;
}
template <typename T>
HttpClient<T>& HttpClient<T>::PostBody(const std::string& postBody) {
this->postBody = postBody;
return *this;
}
template <typename T>
HttpClient<T>& HttpClient<T>::Mode(Thread mode) {
this->mode = mode;
return *this;
}
template <typename T>
HttpClient<T>& HttpClient<T>::Method(HttpMethod method) {
this->method = method;
return *this;
}
template <typename T>
HttpClient<T>& HttpClient<T>::Header(const std::string& key, const std::string& value) {
this->requestHeaders[key] = value;

View File

@ -46,36 +46,13 @@ using namespace musik;
using namespace cursespp;
static std::map<LastFmOverlay::State, std::string> stateToText = {
{
LastFmOverlay::State::Unregistered,
"no last.fm account linked.\n\n"
"press ENTER to begin the linking process."
},
{
LastFmOverlay::State::ObtainingToken,
"contacting last.fm for an account link token..."
},
{
LastFmOverlay::State::WaitingForUser,
"press 'o' to open the account link page in your default browser, "
"or manually navigate to the following url:"
"\n\n{{link}}\n\n"
"after granting permission, press 'ENTER' to continue.\n\n"
"(note: you may be asked to sign into your last.fm account.)"
},
{
LastFmOverlay::State::RegisteringSession,
"getting a session token, please wait..."
},
{
LastFmOverlay::State::Registered,
"registered with account: '{{username}}'."
},
{
LastFmOverlay::State::Error,
"failed to link your last.fm account.\n\n"
"please try again later."
}
{ LastFmOverlay::State::Unregistered, "settings_last_fm_dialog_message_unregistered" },
{ LastFmOverlay::State::ObtainingToken, "settings_last_fm_dialog_message_obtaining_token" },
{ LastFmOverlay::State::WaitingForUser, "settings_last_fm_dialog_message_waiting_for_user" },
{ LastFmOverlay::State::RegisteringSession, "settings_last_fm_dialog_message_registering_session" },
{ LastFmOverlay::State::Registered, "settings_last_fm_dialog_message_registered" },
{ LastFmOverlay::State::LinkError, "settings_last_fm_dialog_message_link_error" },
{ LastFmOverlay::State::RegisterError, "settings_last_fm_dialog_message_register_error" }
};
void LastFmOverlay::Show() {
@ -110,7 +87,7 @@ void LastFmOverlay::GetLinkToken() {
this->PostState(State::WaitingForUser);
}
else {
this->PostState(State::Error);
this->PostState(State::LinkError);
}
});
}
@ -123,7 +100,7 @@ void LastFmOverlay::CreateSession() {
this->PostState(State::Registered);
}
else {
this->PostState(State::Error);
this->PostState(State::RegisterError);
}
});
}
@ -139,7 +116,7 @@ void LastFmOverlay::SetState(State state) {
}
void LastFmOverlay::UpdateMessage() {
std::string message = stateToText[state];
std::string message = _TSTR(stateToText[state]);
switch (this->state) {
case State::Registered: {
@ -148,7 +125,8 @@ void LastFmOverlay::UpdateMessage() {
break;
}
case State::WaitingForUser: {
case State::WaitingForUser:
case State::RegisterError: {
std::string url = lastfm::CreateLinkUrl(this->linkToken);
core::ReplaceAll(message, "{{link}}", url);
break;
@ -164,66 +142,71 @@ void LastFmOverlay::UpdateButtons() {
switch (this->state) {
case State::Unregistered: {
this->AddButton(
"KEY_ENTER",
"ENTER",
"start",
"KEY_ENTER", "ENTER", _TSTR("button_start"),
[this](std::string key) {
this->GetLinkToken();
});
this->AddButton(
"^[", "ESC", _TSTR("button_cancel"),
[this](std::string key) {
this->Dismiss();
});
break;
}
case State::Registered: {
this->AddButton(
"u",
"u",
"unregister",
"u", "u", "unregister",
[this](std::string key) {
lastfm::ClearSession();
this->LoadDefaultState();
});
break;
}
case State::WaitingForUser: {
this->AddButton(
"o",
"o",
"open url",
[this](std::string key) {
core::OpenFile(lastfm::CreateLinkUrl(this->linkToken));
});
this->AddButton(
"KEY_ENTER",
"ENTER",
"start",
[this](std::string key) {
this->CreateSession();
});
break;
}
case State::Error: {
this->AddButton(
"KEY_ENTER",
"ENTER",
_TSTR("button_ok"),
"KEY_ENTER", "ENTER", _TSTR("button_close"),
[this](std::string key) {
this->Dismiss();
});
break;
}
}
if (this->state != State::Error) {
this->AddButton(
"^[",
"ESC",
_TSTR("button_close"),
[this](std::string key) {
this->Dismiss();
});
case State::WaitingForUser:
case State::RegisterError: {
this->AddButton(
"o", "o", "open url",
[this](std::string key) {
core::OpenFile(lastfm::CreateLinkUrl(this->linkToken));
});
std::string continueText = _TSTR(
(state == State::WaitingForUser)
? "button_continue"
: "button_retry");
this->AddButton(
"KEY_ENTER", "ENTER", continueText,
[this](std::string key) {
this->CreateSession();
});
this->AddButton(
"^[", "ESC", _TSTR("button_cancel"),
[this](std::string key) {
this->Dismiss();
});
break;
}
case State::LinkError: {
this->AddButton(
"KEY_ENTER", "ENTER", _TSTR("button_ok"),
[this](std::string key) {
this->Dismiss();
});
break;
}
}
}

View File

@ -52,7 +52,8 @@ namespace musik { namespace cube {
WaitingForUser = 2,
RegisteringSession = 3,
Registered = 4,
Error = 5
LinkError = 5,
RegisterError = 6
};
static void Show();

View File

@ -53,6 +53,7 @@ static const std::string API_SECRET = "6dc09da925fe5c115b90320213c53b46";
static const std::string URL_BASE = "http://ws.audioscrobbler.com/2.0/";
static const std::string GET_TOKEN = "auth.getToken";
static const std::string GET_SESSION = "auth.getSession";
static const std::string UPDATE_NOW_PLAYING = "track.scrobble";
static const std::string ACCOUNT_LINK_URL_BASE = "http://www.last.fm/api/auth/?api_key=" + API_KEY + "&token=";
using namespace musik;
@ -73,20 +74,32 @@ static void validate(musik::cube::lastfm::Session& session) {
session.token.size();
}
static std::string generateSignedUrl(
static std::string encode(std::string value) {
static CURL* curl = curl_easy_init();
if (curl && value.c_str()) {
char* encoded = curl_easy_escape(curl, value.c_str(), value.size());
if (encoded) {
value = encoded;
curl_free(encoded);
}
}
return value;
}
static std::string gernateSignedUrlParams(
const std::string& method,
std::map<std::string, std::string>&& params = { })
std::map<std::string, std::string>&& params = {})
{
params["method"] = method;
params["api_key"] = API_KEY;
std::string toHash;
std::string url = URL_BASE;
std::string urlParams;
bool first = true;
for (auto it : params) {
toHash += it.first + it.second;
url += (first ? "?" : "&") + it.first + "=" + it.second;
urlParams += (first ? "" : "&") + it.first + "=" + encode(it.second);
first = false;
}
@ -103,9 +116,15 @@ static std::string generateSignedUrl(
}
hexDigest[32] = 0;
url += "&format=json&api_sig=" + std::string(hexDigest);
urlParams += "&format=json&api_sig=" + std::string(hexDigest);
return urlParams;
}
return url;
static std::string generateSignedUrl(
const std::string& method,
std::map<std::string, std::string>&& params = { })
{
return URL_BASE + "?" + gernateSignedUrlParams(method, std::move(params));
}
static inline Prefs settings() {
@ -190,4 +209,28 @@ namespace musik { namespace cube { namespace lastfm {
SaveSession(session);
}
void Scrobble(musik::core::TrackPtr track) {
if (track) {
auto session = LoadSession();
if (session.valid) {
std::string postBody = gernateSignedUrlParams(UPDATE_NOW_PLAYING, {
{ "track", track->GetString("title") },
{ "album", track->GetString("album") },
{ "artist", track->GetString("artist") },
{ "albumArtist", track->GetString("album_artist") },
{ "trackNumber", track->GetString("track") },
{ "timestamp", std::to_string(std::time(0)) },
{ "sk", session.sessionId }
});
createClient()->Url(URL_BASE)
.Mode(LastFmClient::Thread::Background)
.Header("Content-Type", "application/x-www-form-urlencoded")
.Method(LastFmClient::HttpMethod::Post)
.PostBody(postBody)
.Run();
}
}
}
} } }

View File

@ -35,6 +35,7 @@
#pragma once
#include "stdafx.h"
#include <core/library/track/Track.h>
namespace musik { namespace cube { namespace lastfm {
struct Session {
@ -48,6 +49,7 @@ namespace musik { namespace cube { namespace lastfm {
extern void CreateAccountLinkToken(TokenCallback callback);
extern const std::string CreateLinkUrl(const std::string& token);
extern void CreateSession(const std::string& token, SessionCallback session);
extern void Scrobble(musik::core::TrackPtr track);
extern Session LoadSession();
extern void SaveSession(const Session& session);

View File

@ -50,6 +50,7 @@
#include <app/util/Hotkeys.h>
#include <app/util/Messages.h>
#include <app/util/LastFm.h>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>
@ -426,6 +427,11 @@ void TransportWindow::OnPlaybackServiceTrackChanged(size_t index, TrackPtr track
this->currentTrack = track;
this->lastTime = DEFAULT_TIME;
this->UpdateReplayGainState();
if (playback.GetPlaybackState() == PlaybackPlaying) {
lastfm::Scrobble(track);
}
DEBOUNCE_REFRESH(TimeSync, 0);
}

View File

@ -105,6 +105,7 @@ DialogOverlay& DialogOverlay::SetAutoDismiss(bool dismiss) {
DialogOverlay& DialogOverlay::ClearButtons() {
this->shortcuts->RemoveAll();
this->buttons.clear();
return *this;
}

View File

@ -9,6 +9,9 @@
"button_save": "save",
"button_cancel": "cancel",
"button_close": "close",
"button_start": "start",
"button_retry": "retry",
"button_continue": "continue",
"button_dont_remind_me": "don't remind me again",
"button_remind_me_later": "remind me later",
@ -62,7 +65,14 @@
"settings_auto_update_check": "check for updates on startup",
"settings_save_session_on_exit": "save session on exit",
"settings_check_for_updates": "check for updates now",
"settings_last_fm": "last fm",
"settings_last_fm": "last.fm",
"settings_last_fm_dialog_message_unregistered": "no last.fm account registered.\n\npress ENTER to begin the linking process.",
"settings_last_fm_dialog_message_obtaining_token": "contacting last.fm for an account link token...",
"settings_last_fm_dialog_message_waiting_for_user": "press 'o' to open the account link page in your default browser, or manually navigate to the following url:\n\n{{link}}\n\nafter granting permission, press 'ENTER' to continue.\n\n(note: you may be asked to sign into your last.fm account.)",
"settings_last_fm_dialog_message_registering_session": "getting a session token, please wait...",
"settings_last_fm_dialog_message_registered": "registered with account: '{{username}}'.\n\ngo listen to some music!",
"settings_last_fm_dialog_message_link_error": "failed to get an account link token from last.fm.\n\nplease try again later.",
"settings_last_fm_dialog_message_register_error": "failed link your last.fm account.\n\nplease make sure you open the following url and grant permission:\n\n{{link}}\n\npress ENTER to try again.",
"settings_server_enable_websockets": "metadata server enabled",
"settings_server_enable_http": "audio streaming enabled",