#include "mpdas.h"
#define APIKEY "a0ed2629d3d28606f67d7214c916788d"
#define SECRET "295f31c5d28215215b1503fb0327cc01"
#define CURL_MAX_RETRIES 3
#define CURL_RETRY_DELAY 3 // Seconds
CAudioScrobbler* AudioScrobbler = 0;
#define CLEANUP() _response.clear()
size_t writecb(void* ptr, size_t size, size_t nmemb, void *stream)
{
AudioScrobbler->ReportResponse((char*)ptr, size*nmemb);
return size*nmemb;
}
CAudioScrobbler::CAudioScrobbler(CConfig *cfg)
{
_cfg = cfg;
_failcount = 0;
_authed = false;
_response = "";
_handle = curl_easy_init();
if(!_handle) {
eprintf("%s", "Could not initialize CURL.");
exit(EXIT_FAILURE);
}
}
CAudioScrobbler::~CAudioScrobbler()
{
curl_easy_cleanup(_handle);
curl_global_cleanup();
}
std::string CAudioScrobbler::GetServiceURL()
{
if(_cfg->getService() == LibreFm) {
return "https://libre.fm/2.0/";
}
return "https://ws.audioscrobbler.com/2.0/";
}
void CAudioScrobbler::OpenURL(std::string url, const char* postfields = 0, char* errbuf = 0)
{
curl_easy_setopt(_handle, CURLOPT_DNS_CACHE_TIMEOUT, 0);
curl_easy_setopt(_handle, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt(_handle, CURLOPT_WRITEFUNCTION, writecb);
curl_easy_setopt(_handle, CURLOPT_TIMEOUT, 10);
if(postfields) {
curl_easy_setopt(_handle, CURLOPT_POST, 1);
curl_easy_setopt(_handle, CURLOPT_POSTFIELDS, postfields);
}
else
curl_easy_setopt(_handle, CURLOPT_POST, 0);
if(errbuf)
curl_easy_setopt(_handle, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(_handle, CURLOPT_URL, url.c_str());
CURLcode res = curl_easy_perform(_handle);
// Sometimes last.fm likes to just timeout for no reason, leaving us hanging.
// If this happens, retry a few times with a small delay.
if (res != CURLE_OK) {
eprintf("libcurl error (%d): %s", res, curl_easy_strerror(res));
eprintf("Will retry %d times with a %d second delay.", CURL_MAX_RETRIES, CURL_RETRY_DELAY);
int retries = 0;
do {
sleep(CURL_RETRY_DELAY);
retries++;
eprintf("Retry %d/%d", retries, CURL_MAX_RETRIES);
res = curl_easy_perform(_handle);
if (res != CURLE_OK) {
eprintf("Failed: %s", curl_easy_strerror(res));
}
} while (res != CURLE_OK && retries < CURL_MAX_RETRIES);
}
}
void CAudioScrobbler::ReportResponse(char* buf, size_t size)
{
_response.append(buf);
}
std::string CAudioScrobbler::CreateScrobbleMessage(int index, const CacheEntry& entry)
{
const Song& song = entry.getSong();
CLastFMMessage msg(_handle);
msg.AddField("method", "track.Scrobble");
msg.AddField("artist", song.getArtist());
msg.AddField("track", song.getTitle());
msg.AddField("duration", song.getDuration());
msg.AddField("timestamp", entry.getStartTime());
msg.AddField("sk", _sessionid);
msg.AddField("api_key", APIKEY);
if(!song.getAlbum().empty()) {
msg.AddField("album", song.getAlbum());
}
if(!song.getAlbumArtist().empty()) {
msg.AddField("albumArtist", song.getAlbumArtist());
}
return msg.GetMessage();
}
void CAudioScrobbler::Failure()
{
_failcount += 1;
if(_failcount >= 3) {
eprintf("%s", "Re-Handshaking!");
_failcount = 0;
Handshake();
}
}
bool CAudioScrobbler::CheckFailure(std::string response)
{
bool retval = false;
size_t start, end;
start = _response.find("<error code=\"")+13;
end = _response.find(">", start)-1;
std::string errorcode = _response.substr(start, end-start);
int code = strtol(errorcode.c_str(), 0, 10);
eprintf("%s%i", "Code: ", code);
switch(code) {
case 3:
eprintf("Invalid Method. This should not happen.");
retval = true;
break;
case 4:
eprintf("Authentication failed. Please check your login data.");
exit(EXIT_FAILURE);
case 9:
eprintf("Invalid session key. Re-authenticating.");
retval = true;
_failcount = 3;
break;
case 10:
eprintf("Invalid API-Key. Let's bugger off.");
exit(EXIT_FAILURE);
case 13:
eprintf("Invalid method signature.");
exit(EXIT_FAILURE);
case 16:
eprintf("The service is temporarily unavailable, we will try again later..");
retval = true;
break;
case 26:
eprintf("Uh oh. Suspended API key - Access for your account has been suspended, please contact Last.fm");
exit(EXIT_FAILURE);
}
return retval;
}
bool CAudioScrobbler::Scrobble(const CacheEntry& entry)
{
bool retval = false;
if(!_authed) {
eprintf("Handshake hasn't been done yet.");
Handshake();
return retval;
}
iprintf("Scrobbling: %s - %s", entry.getSong().getArtist().c_str(), entry.getSong().getTitle().c_str());
OpenURL(GetServiceURL(), CreateScrobbleMessage(0, entry).c_str());
if(_response.find("<lfm status=\"ok\">") != std::string::npos) {
iprintf("%s", "Scrobbled successfully.");
retval = true;
}
else if(_response.find("<lfm status=\"failed\">") != std::string::npos) {
eprintf("%s%s", "Last.fm returned an error while scrobbling:\n", _response.c_str());
if(CheckFailure(_response))
Failure();
}
CLEANUP();
return retval;
}
bool CAudioScrobbler::LoveTrack(const Song& song, bool unlove)
{
bool retval = false;
CLastFMMessage msg(_handle);
msg.AddField("method", unlove ? "track.unlove" : "track.love");
msg.AddField("artist", song.getArtist());
msg.AddField("track", song.getTitle());
msg.AddField("api_key", APIKEY);
msg.AddField("sk", _sessionid);
OpenURL(GetServiceURL(), msg.GetMessage().c_str());
if(_response.find("<lfm status=\"ok\">") != std::string::npos) {
iprintf("%s", "(Un)loved track successfully.");
retval = true;
}
else if(_response.find("<lfm status=\"failed\">") != std::string::npos) {
eprintf("%s%s", "Last.fm returned an error while (un)loving the currently playing track:\n", _response.c_str());
if(CheckFailure(_response))
Failure();
}
CLEANUP();
return retval;
}
bool CAudioScrobbler::SendNowPlaying(const Song& song)
{
bool retval = false;
CLastFMMessage msg(_handle);
msg.AddField("method", "track.updateNowPlaying");
msg.AddField("artist", song.getArtist());
msg.AddField("track", song.getTitle());
msg.AddField("duration", song.getDuration());
msg.AddField("sk", _sessionid);
msg.AddField("api_key", APIKEY);
if(!song.getAlbum().empty()) {
msg.AddField("album", song.getAlbum());
}
if(!song.getAlbumArtist().empty()) {
msg.AddField("albumArtist", song.getAlbumArtist());
}
OpenURL(GetServiceURL(), msg.GetMessage().c_str());
if(_response.find("<lfm status=\"ok\">") != std::string::npos) {
iprintf("%s", "Updated \"Now Playing\" status successfully.");
retval = true;
}
else if(_response.find("<lfm status=\"failed\">") != std::string::npos) {
eprintf("%s%s", "Last.fm returned an error while updating the currently playing track:\n", _response.c_str());
if(CheckFailure(_response))
Failure();
}
CLEANUP();
return retval;
}
void CAudioScrobbler::Handshake()
{
std::string username = "";
for(unsigned int i = 0; i < _cfg->Get("username").length(); i++) {
username.append(1, tolower(_cfg->Get("username").c_str()[i]));
}
std::string password = _cfg->Get("password");
CLastFMMessage msg(_handle);
msg.AddField("method", "auth.getMobileSession");
msg.AddField("username", username);
if(_cfg->getService() == LastFm) {
msg.AddField("password", password);
}
else {
std::string password_hashed(md5sum((char*)"%s", password.c_str()));
std::string authtoken(md5sum((char*)"%s%s", username.c_str(), password_hashed.c_str()));
msg.AddField("authToken", authtoken);
msg.AddField("password", password_hashed);
}
msg.AddField("api_key", APIKEY);
OpenURL(GetServiceURL(), msg.GetMessage().c_str());
if(_response.find("<lfm status=\"ok\">") != std::string::npos) {
size_t start, end;
start = _response.find("<key>") + 5;
end = _response.find("</key>");
_sessionid = _response.substr(start, end-start);
iprintf("%s%s", "Last.fm handshake successful. SessionID: ", _sessionid.c_str());
_authed = true;
}
else if(_response.find("<lfm status=\"failed\">") != std::string::npos) {
CheckFailure(_response);
exit(EXIT_FAILURE);
}
CLEANUP();
}
std::string CLastFMMessage::GetMessage()
{
std::ostringstream strstream;
for(std::map<std::string, std::string>::iterator it = valueMap.begin(); it != valueMap.end(); ++it) {
if(it != valueMap.begin()) {
strstream << "&";
}
char* escaped = curl_easy_escape(curl_handle, it->second.c_str(), it->second.length());
strstream << it->first << "=" << escaped;
curl_free(escaped);
}
// append signature hash
strstream << "&api_sig=" << GetSignatureHash();
return strstream.str();
}
std::string CLastFMMessage::GetSignatureHash()
{
std::ostringstream strstream;
for(std::map<std::string, std::string>::iterator it = valueMap.begin(); it != valueMap.end(); ++it) {
strstream << it->first << it->second;
}
// append secret key
strstream << SECRET;
return std::string(md5sum((char*)"%s", strstream.str().c_str()));
}