All pastes #1421779 Raw Edit

scrobbler.cpp

public cpp v1 · immutable
#1421779 ·published 2009-05-13 18:01 UTC
rendered paste body
/* *      Copyright (C) 2005-2008 Team XBMC *      http://xbmc.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, 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 XBMC; see the file COPYING.  If not, write to *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. *  http://www.gnu.org/copyleft/gpl.html * */#include "stdafx.h"#include "PlatformDefs.h"#include "scrobbler.h"#include "tinyXML/tinyxml.h"#include "utils/md5.h"#include "utils/log.h"#include "Util.h"#include "MusicInfoTag.h"#include "errors.h"#include "utils/Atomics.h"#include "Settings.h"#include "XMLUtils.h"#include "Application.h"#include "SingleLock.h"#define SCROBBLER_CLIENT              "xbm"//#define SCROBBLER_CLIENT              "tst"     // For testing ONLY!#define SCROBBLER_PROTOCOL            "1.2.1"#define SCROBBLER_CLIENT_VERSION      "0.2"#define SCROBBLER_JOURNAL_VERSION     1#define SCROBBLER_MAX_SUBMISSIONS     50        // API rule#define SCROBBLER_MIN_DURATION        30        // seconds. API rule#define SCROBBLER_ACTION_SUBMIT       1#define SCROBBLER_ACTION_NOWPLAYING   2//#define SCROBBLER_HANDSHAKE_URL       "post.audioscrobbler.com"//#define SCROBBLER_HANDSHAKE_URL       "89.16.177.55" // libre.fmCScrobbler::CScrobbler(const CStdString &strHandshakeURL, const CStdString &strLogPrefix)  : CThread(){   m_bRunThread      = false;  m_bBanned         = false;  m_bBadAuth        = false;  m_pHttp           = NULL;  m_strHandshakeURL = strHandshakeURL;  m_strLogPrefix    = strLogPrefix;  ResetState();  if (!(m_hEvent = CreateEvent(NULL, false, false, NULL)))    throw EOutOfMemory();}CScrobbler::~CScrobbler(){  Term();  CloseHandle(m_hEvent);}void CScrobbler::Init(){  if (!CanScrobble())    return;  ResetState();  LoadCredentials();  LoadJournal();  if (!m_bRunThread)    Create();}void CScrobbler::Term(){  m_bRunThread = false;  SetEvent(m_hEvent);  StopThread();  SaveJournal();}void CScrobbler::AddSong(const MUSIC_INFO::CMusicInfoTag &tag){  ClearSubmissionState();  if (!CanScrobble() || !tag.Loaded())    return;  if (tag.GetArtist().IsEmpty() || tag.GetTitle().IsEmpty())    return;    // our tags are stored as UTF-8, so no conversion needed  m_CurrentTrack.length           = tag.GetDuration();  m_CurrentTrack.strArtist        = tag.GetArtist();  m_CurrentTrack.strAlbum         = tag.GetAlbum();  m_CurrentTrack.strTitle         = tag.GetTitle();  m_CurrentTrack.strMusicBrainzID = tag.GetMusicBrainzTrackID();  m_CurrentTrack.strSource        = "P";  // TODO Set source more appropriately  m_CurrentTrack.strRating        = "";  m_CurrentTrack.strLength.Format("%d", m_CurrentTrack.length);  m_CurrentTrack.strStartTime.Format("%d", time(NULL));  m_CurrentTrack.strTrackNum.Format("%d",tag.GetTrackNumber());    CUtil::URLEncode(m_CurrentTrack.strArtist);   CUtil::URLEncode(m_CurrentTrack.strTitle);  CUtil::URLEncode(m_CurrentTrack.strAlbum);  CUtil::URLEncode(m_CurrentTrack.strMusicBrainzID);  m_bNotified = false;  if (m_CurrentTrack.length > SCROBBLER_MIN_DURATION)    m_bSubmitted = false;}void CScrobbler::UpdateStatus(){  // Called from CApp::ProcessSlow() every ~500ms.  if (!CanScrobble())    return;  if (g_application.IsPaused())    return;  m_submissionTimer++;  // Try to notify Last.fm of our currently playing after ~5s of playback.  // Don't try too hard, this is optional and doesn't affect the users library.  if (!m_bNotified && m_submissionTimer >= 10)  {    m_bNotified = true; // Only try once    {      CSingleLock lock(m_actionLock);      m_action = SCROBBLER_ACTION_NOWPLAYING;    }    SetEvent(m_hEvent);    return;  }  // Scrobble the track after 50% playback or 240s, whichever occurs first.  // Just toss it in the queue here. We'll try to submit the queue at the  // end of playback.  if (!m_bSubmitted &&      (m_submissionTimer > m_CurrentTrack.length ||        m_submissionTimer >= 480))  {    CSingleLock lock(m_queueLock);    m_bSubmitted = true;    m_vecSubmissionQueue.push_back(m_CurrentTrack);    SaveJournal();    CLog::Log(LOGDEBUG, "%s: Queued track for submission", m_strLogPrefix.c_str());  }}void CScrobbler::SubmitQueue(){  if (CanScrobble())  {    {      CSingleLock lock(m_actionLock);      m_action = SCROBBLER_ACTION_SUBMIT;    }    SetEvent(m_hEvent);  }}void CScrobbler::SetUsername(const CStdString& strUser){  if (strUser.IsEmpty())    return;  m_strUsername=strUser;  CUtil::URLEncode(m_strUsername);  m_bBadAuth = false;}void CScrobbler::SetPassword(const CStdString& strPass){  if (strPass.IsEmpty())    return;  XBMC::MD5 md5state;  md5state.append(strPass);  md5state.getDigest(m_strPasswordHash);  m_bBadAuth = false;}void CScrobbler::ResetState(){  ClearSession();  ClearSubmissionState();  ClearErrorState();}void CScrobbler::ClearErrorState(){  m_hardErrorCount        = 0;  m_lastFailedHandshake   = 0;  m_failedHandshakeDelay  = 0;}void CScrobbler::ClearSubmissionState(){  m_bNotified             = true;  // Explicitly clear these when necessary  m_bSubmitting           = false;  m_bSubmitted            = true;  m_submissionTimer       = 0;  CSingleLock lock(m_actionLock);  m_action                = 0;}void CScrobbler::ClearSession(){  CLog::Log(LOGDEBUG, "%s: Clearing session.", m_strLogPrefix.c_str());  m_strSessionID.clear();}void CScrobbler::HandleHardError(){  CLog::Log(LOGDEBUG, "%s: A hard error has occurred.", m_strLogPrefix.c_str());  if (++m_hardErrorCount == 3)  {    CLog::Log(LOGDEBUG, "%s: Three consecuetive hard errors have "\        "occured. Forcing new handshake.", m_strLogPrefix.c_str());    ClearSession();  }}bool CScrobbler::LoadJournal(){  int                     journalVersion  = 0;  SubmissionJournalEntry  entry;  TiXmlDocument           xmlDoc;  CStdString              JournalFileName = GetJournalFileName();  CSingleLock             lock(m_queueLock);  m_vecSubmissionQueue.clear();    if (!xmlDoc.LoadFile(JournalFileName))  {    CLog::Log(LOGDEBUG, "%s: %s, Line %d (%s)", m_strLogPrefix.c_str(),         JournalFileName.c_str(), xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());    return false;  }  TiXmlElement *pRoot = xmlDoc.RootElement();  if (strcmpi(pRoot->Value(), "asjournal") != 0)  {    CLog::Log(LOGDEBUG, "%s: %s missing <asjournal>", m_strLogPrefix.c_str(),        JournalFileName.c_str());    return false;  }  if (pRoot->Attribute("version"))    journalVersion = atoi(pRoot->Attribute("version"));  TiXmlNode *pNode = pRoot->FirstChild("entry");  for (; pNode; pNode = pNode->NextSibling("entry"))  {    entry.Clear();    XMLUtils::GetString(pNode, "artist", entry.strArtist);    XMLUtils::GetString(pNode, "album", entry.strAlbum);    XMLUtils::GetString(pNode, "title", entry.strTitle);    XMLUtils::GetString(pNode, "length", entry.strLength);    entry.length = atoi(entry.strLength.c_str());    XMLUtils::GetString(pNode, "starttime", entry.strStartTime);    XMLUtils::GetString(pNode, "musicbrainzid", entry.strMusicBrainzID);    if (journalVersion > 0)    {      XMLUtils::GetString(pNode, "tracknum", entry.strTrackNum);      XMLUtils::GetString(pNode, "source", entry.strSource);      XMLUtils::GetString(pNode, "rating", entry.strRating);    }    else    {      // Update from journal v0      // Convert start time stamp      struct tm starttm;      time_t startt;      if (!strptime(entry.strStartTime.c_str(), "%Y-%m-%d %H:%M:%S", &starttm))        continue;      if ((startt = mktime(&starttm)) == -1)        continue;      entry.strStartTime.Format("%d", startt);      // url encode entries      CUtil::URLEncode(entry.strArtist);       CUtil::URLEncode(entry.strTitle);      CUtil::URLEncode(entry.strAlbum);      CUtil::URLEncode(entry.strMusicBrainzID);    }    m_vecSubmissionQueue.push_back(entry);  }  CLog::Log(LOGDEBUG, "%s: Journal loaded with %d entries.", m_strLogPrefix.c_str(),      m_vecSubmissionQueue.size());  return !m_vecSubmissionQueue.empty();}bool CScrobbler::SaveJournal(){  CSingleLock lock(m_queueLock);  CStdString        strJournalVersion;  TiXmlDocument     xmlDoc;  TiXmlDeclaration  decl("1.0", "utf-8", "yes");  TiXmlElement      xmlRootElement("asjournal");  xmlDoc.InsertEndChild(decl);  strJournalVersion.Format("%d", SCROBBLER_JOURNAL_VERSION);  xmlRootElement.SetAttribute("version", strJournalVersion.c_str());  TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);  if (!pRoot)    return false;  int i = 0;  SCROBBLERJOURNALITERATOR it = m_vecSubmissionQueue.begin();  for (; it != m_vecSubmissionQueue.end(); it++, i++)  {    TiXmlElement entryNode("entry");    TiXmlNode *pNode = pRoot->InsertEndChild(entryNode);    if (!pNode)      return false;    XMLUtils::SetString(pNode, "artist", it->strArtist);    XMLUtils::SetString(pNode, "album", it->strAlbum);    XMLUtils::SetString(pNode, "title", it->strTitle);    XMLUtils::SetString(pNode, "length", it->strLength);    XMLUtils::SetString(pNode, "starttime", it->strStartTime);    XMLUtils::SetString(pNode, "musicbrainzid", it->strMusicBrainzID);    XMLUtils::SetString(pNode, "tracknum", it->strTrackNum);    XMLUtils::SetString(pNode, "source", it->strSource);    XMLUtils::SetString(pNode, "rating", it->strRating);  }  CStdString FileName = GetJournalFileName();  CLog::Log(LOGDEBUG, "%s: Journal with %d entries saved to %s",      m_strLogPrefix.c_str(), i, FileName.c_str());  return xmlDoc.SaveFile(FileName);}bool CScrobbler::DoHandshake(time_t now){  XBMC::MD5         authToken;  CStdString        strAuthToken;  CStdString        strTimeStamp;  CStdString        strResponse;  CStdString        strHandshakeRequest;  // Create auth token. md5(md5(pass)+str(now))  strTimeStamp.Format("%d", now);  authToken.append(m_strPasswordHash + strTimeStamp);  authToken.getDigest(strAuthToken);    // Construct handshake URL.  strHandshakeRequest.Format("http://%s/?hs=true"\      "&p=%s&c=%s&v=%s&u=%s&t=%d&a=%s", m_strHandshakeURL.c_str(),      SCROBBLER_PROTOCOL, SCROBBLER_CLIENT, SCROBBLER_CLIENT_VERSION,      m_strUsername.c_str(), now, strAuthToken.c_str());    // Make and handle request  if (m_pHttp->Get(strHandshakeRequest, strResponse) &&      HandleHandshake(strResponse))    return true;      m_failedHandshakeDelay = // 60, 120, 240, ... 7200s    (m_failedHandshakeDelay) ? std::min(2*m_failedHandshakeDelay, 7200) : 60;  m_lastFailedHandshake = now;  if (!m_bBanned && !m_bBadAuth)    CLog::Log(LOGDEBUG, "%s: A hard error has occurred during "\        "handshake. Sleeping for %d minutes.",        m_strLogPrefix.c_str(), m_failedHandshakeDelay/60);    return false;}bool CScrobbler::HandleHandshake(CStdString &strResponse){  if (strResponse.IsEmpty())    return false;    std::vector<CStdString> vecTokens;  CUtil::Tokenize(strResponse, vecTokens, " \n\r");  if (vecTokens[0] == "OK")  {    if (vecTokens.size() >= 4)    {      m_strSessionID      = vecTokens[1];      m_strNowPlayingURL  = vecTokens[2];      m_strSubmissionURL  = vecTokens[3];      CLog::Log(LOGDEBUG, "%s: Handshake succeeded!", m_strLogPrefix.c_str());      CLog::Log(LOGDEBUG, "%s: SessionID is %s", m_strLogPrefix.c_str(),          m_strSessionID.c_str());      CLog::Log(LOGDEBUG, "%s: NP URL is %s", m_strLogPrefix.c_str(),          m_strNowPlayingURL.c_str());      CLog::Log(LOGDEBUG, "%s: Submit URL is %s", m_strLogPrefix.c_str(),          m_strSubmissionURL.c_str());      ClearErrorState();      return true;    }    CLog::Log(LOGERROR, "%s: Handshake failed! Received malformed "\        "reply.", m_strLogPrefix.c_str());  }  else if (vecTokens[0] == "BANNED")  {    CLog::Log(LOGERROR, "%s: Handshake failed! Client is banned! "\        "Disabling submissions. Subsequent scrobbles will be cached. "\        "Please update your client to the newest version. ", m_strLogPrefix.c_str());    if (m_failedHandshakeDelay == 0)    {      NotifyUser(SCROBBLER_USER_ERROR_BANNED);    }  }  else if (vecTokens[0] == "BADAUTH")  {    CLog::Log(LOGERROR, "%s: Handshake failed! Authentication failed! "\        "Disabling submissions. Subsequent scrobbles will be cached. "\        "Please enter the correct credentials to re-enable scrobbling. ",        m_strLogPrefix.c_str());    if (m_failedHandshakeDelay == 0)    {      NotifyUser(SCROBBLER_USER_ERROR_BADAUTH);    }  }  else if (vecTokens[0] == "BADTIME")  {    CLog::Log(LOGDEBUG, "%s: Handshake failed! Timestamp is invalid! "\        "Disabling submissions. Subsequent scrobbles will be cached. "\        "Please correct the system time and restart the application. ",        m_strLogPrefix.c_str());  }  else if (vecTokens[0] == "FAILED")  {    CLog::Log(LOGDEBUG, "%s: Handshake failed! REASON: %s! ", m_strLogPrefix.c_str(),         strResponse.c_str());  }  else    CLog::Log(LOGDEBUG, "%s: Handshake failed! REASON: Unspecified!", m_strLogPrefix.c_str());    return false;}bool CScrobbler::DoNowPlayingNotification(){  CStdString        strNowPlayingRequest;  CStdString        strResponse;  // Construct now playing notification URL.  strNowPlayingRequest.Format("s=%s&a=%s&t=%s&b=%s&l=%d&n=%s&m=%s",      m_strSessionID.c_str(), m_CurrentTrack.strArtist.c_str(),      m_CurrentTrack.strTitle.c_str(), m_CurrentTrack.strAlbum.c_str(),      m_CurrentTrack.length, m_CurrentTrack.strTrackNum.c_str(),      m_CurrentTrack.strMusicBrainzID.c_str());  // Make and handle request  if (m_pHttp->Post(m_strNowPlayingURL, strNowPlayingRequest, strResponse) &&      HandleNowPlayingNotification(strResponse))    return true;    HandleHardError();  return false;}bool CScrobbler::HandleNowPlayingNotification(CStdString &strResponse){  if (strResponse.IsEmpty())    return false;   std::vector<CStdString> vecTokens;  CUtil::Tokenize(strResponse, vecTokens, " \n\r");  if (vecTokens[0] == "OK")  {    CLog::Log(LOGDEBUG, "%s: Now playing notification succeeded!", m_strLogPrefix.c_str());    ClearErrorState();    return true;  }  else if (vecTokens[0] == "BADSESSION")  {    CLog::Log(LOGDEBUG, "%s: Now playing notification failed! "\        "REASON: Bad session ID. Forcing new handshake.", m_strLogPrefix.c_str());    ClearSession();  }  else    CLog::Log(LOGDEBUG, "%s: Now playing notification failed! "\        "REASON: Unspecified.", m_strLogPrefix.c_str());  return false;}bool CScrobbler::DoSubmission(){  int               i;  int               numSubmissions;  CStdString        strSubmissionRequest;  CStdString        strSubmission;  CStdString        strResponse;  CSingleLock lock(m_queueLock);  // Construct submission URL.  numSubmissions =     std::min((size_t)SCROBBLER_MAX_SUBMISSIONS, m_vecSubmissionQueue.size());  if (numSubmissions == 0)    return true;  strSubmissionRequest.Format("s=%s", m_strSessionID.c_str());  SCROBBLERJOURNALITERATOR it = m_vecSubmissionQueue.begin();  for (i = 0; it != m_vecSubmissionQueue.end() && i < numSubmissions; i++,it++)  {    strSubmission.Format("&a[%d]=%s&t[%d]=%s&i[%d]=%s&o[%d]=%s&r[%d]=%s",        i, it->strArtist.c_str(),     i, it->strTitle.c_str(),        i, it->strStartTime.c_str(),  i, it->strSource.c_str(),        i, it->strRating.c_str());    // Too many params, must be split (or hack CStdString)    strSubmission.Format("%s&l[%d]=%s&b[%d]=%s&n[%d]=%s&m[%d]=%s",        strSubmission.c_str(),        i, it->strLength.c_str(),        i, it->strAlbum.c_str(),      i, it->strTrackNum.c_str(),        i, it->strMusicBrainzID.c_str());    strSubmissionRequest += strSubmission;  }  // Make and handle request  if (m_pHttp->Post(m_strSubmissionURL, strSubmissionRequest, strResponse) &&      HandleSubmission(strResponse))  {    SCROBBLERJOURNALITERATOR it = m_vecSubmissionQueue.begin();    m_vecSubmissionQueue.erase(it, it + i); // Remove submitted entries    SaveJournal();    return true;  }  HandleHardError();  return false;}bool CScrobbler::HandleSubmission(CStdString &strResponse){  if (strResponse.IsEmpty())    return false;    std::vector<CStdString> vecTokens;  CUtil::Tokenize(strResponse, vecTokens, " \n\r");  if (vecTokens[0] == "OK")  {    CLog::Log(LOGDEBUG, "%s: Submission succeeded!", m_strLogPrefix.c_str());    ClearErrorState();    return true;  }  else if (vecTokens[0] == "BADSESSION")  {    CLog::Log(LOGDEBUG, "%s: Submission failed! "\        "REASON: Bad session. Forcing new handshake.", m_strLogPrefix.c_str());    ClearSession();  }  else if (vecTokens[0] == "FAILED")  {    CLog::Log(LOGDEBUG, "%s: Submission failed! "\        "REASON: %s", m_strLogPrefix.c_str(), strResponse.c_str());  }  else    CLog::Log(LOGDEBUG, "%s: Submission failed! "\        "REASON: Unspecified.", m_strLogPrefix.c_str());  return false;}void CScrobbler::Process(){  CLog::Log(LOGDEBUG, "%s: Thread started.", m_strLogPrefix.c_str());  if (!m_pHttp)  {    // Hack since CFileCurl isn't threadsafe    if (!(m_pHttp = new XFILE::CFileCurl))      return;  }  m_bRunThread = true;  while (1)  {    WaitForSingleObject(m_hEvent, INFINITE);    if (!m_bRunThread)      break;        if (m_strSessionID.IsEmpty())    {      time_t now = time(NULL);      // We need to handshake.      if (m_bBanned || m_bBadAuth ||          !g_guiSettings.GetBool("network.enableinternet") ||          ((now - m_lastFailedHandshake) < m_failedHandshakeDelay))        continue;      if (!DoHandshake(now))        continue;    }    int action = 0;    {      CSingleLock lock(m_actionLock);      action = m_action;      m_action = 0;    }    if (action == SCROBBLER_ACTION_NOWPLAYING)      DoNowPlayingNotification();    else if (action == SCROBBLER_ACTION_SUBMIT)    {      m_bSubmitting = true;      DoSubmission();      m_bSubmitting = false;    }  }  delete m_pHttp; // More of aforementioned hack   m_pHttp = NULL;  CLog::Log(LOGDEBUG, "%s: Thread ended.", m_strLogPrefix.c_str());}