All pastes #1418035 Raw Edit

Anonymous

public cpp v1 · immutable
#1418035 ·published 2009-05-10 05:35 UTC
rendered paste body
#include "scrobbler.h"#include "tinyXML/tinyxml.h"#include "stdafx.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_PROTOCOL            "1.2.1"//#define SCROBBLER_CLIENT              "tst"     // For testing ONLY!#define SCROBBLER_CLIENT              "xbm"#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   2long CScrobbler::m_instanceLock = 0;CScrobbler *CScrobbler::m_pInstance = NULL;CScrobbler::CScrobbler() : CThread(){   m_bRunThread  = true;  m_bBanned     = false;  m_bBadAuth    = false;  ResetState();  if (!(m_hEvent = CreateEvent(NULL, false, false, NULL)))    throw EOutOfMemory();}CScrobbler::~CScrobbler(){  Term();  CloseHandle(m_hEvent);}CScrobbler *CScrobbler::GetInstance(){  if (!m_pInstance) // Avoid spinning aimlessly  {    CAtomicSpinLock lock(m_instanceLock);    if (!m_pInstance)    {      m_pInstance = new CScrobbler;    }  }  return m_pInstance;}void CScrobbler::RemoveInstance(){  if (m_pInstance)  {    CAtomicSpinLock lock(m_instanceLock);    delete m_pInstance;    m_pInstance = NULL;  }}void CScrobbler::Init(){  if (!CanScrobble())    return;  ResetState();  ClearSession();  // Revoke our session  SetUsername(g_guiSettings.GetString("lastfm.username"));  SetPassword(g_guiSettings.GetString("lastfm.password"));  LoadJournal();  if (!ThreadId())    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 libdary.  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_journalLock);    m_bSubmitted = true;    m_vecSubmissionQueue.push_back(m_CurrentTrack);    SaveJournal();    CLog::Log(LOGDEBUG, "CScrobbler: Queued track for submission");  }}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;  ClearSession();}void CScrobbler::SetPassword(const CStdString& strPass){  if (strPass.IsEmpty())    return;  XBMC::MD5 md5state;  md5state.append(strPass);  md5state.getDigest(m_strPasswordHash);  m_bBadAuth = false;  ClearSession();}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_bSubmitted            = true;  m_submissionTimer       = 0;  CSingleLock lock(m_actionLock);  m_action                = 0;}void CScrobbler::ClearSession(){  m_strSessionID.clear();}void CScrobbler::HandleHardError(){  CLog::Log(LOGDEBUG, "CScrobbler: A hard error has occurred.");  if (++m_hardErrorCount == 3)  {    CLog::Log(LOGDEBUG, "CScrobbler: Three consecuetive hard errors have "\        "occured. Forcing new handshake.");    ClearSession();  }}bool CScrobbler::CanScrobble(){  return (!g_guiSettings.GetString("lastfm.username").IsEmpty()  &&          !g_guiSettings.GetString("lastfm.password").IsEmpty()  &&         (g_guiSettings.GetBool("lastfm.enable") ||          g_guiSettings.GetBool("lastfm.recordtoprofile")));}bool CScrobbler::LoadJournal(){  int                     journalVersion  = 0;  SubmissionJournalEntry  entry;  TiXmlDocument           xmlDoc;  CStdString              JournalFileName = GetJournalFileName();  CSingleLock             lock(m_journalLock);  m_vecSubmissionQueue.clear();    if (!xmlDoc.LoadFile(JournalFileName))  {    CLog::Log(LOGDEBUG, "CScrobbler: %s, Line %d (%s)",         JournalFileName.c_str(), xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());    return false;  }  TiXmlElement *pRoot = xmlDoc.RootElement();  if (strcmpi(pRoot->Value(), "asjournal") != 0)  {    CLog::Log(LOGDEBUG, "CScrobbler: %s missing <asjournal>",        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, "CScrobbler: Journal loaded with %d entries.",      m_vecSubmissionQueue.size());  return !m_vecSubmissionQueue.empty();}bool CScrobbler::SaveJournal(){  CSingleLock lock(m_journalLock);  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, "CScrobbler: Journal with %d entries saved to %s", 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://post.audioscrobbler.com/?hs=true"\      "&p=%s&c=%s&v=%s&u=%s&t=%d&a=%s", SCROBBLER_PROTOCOL,      SCROBBLER_CLIENT, SCROBBLER_CLIENT_VERSION, m_strUsername.c_str(),      now, strAuthToken.c_str());    // Make and handle request  if (m_http.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, "CScrobbler: A hard error has occurred during "\        "handshake. Sleeping for %d minutes.", m_failedHandshakeDelay/60);    return false;}bool CScrobbler::HandleHandshake(CStdString &strResponse){  // TODO toast messages  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,  "CScrobbler: Handshake succeeded!");      CLog::Log(LOGDEBUG, "CScrobbler: SessionID is %s",          m_strSessionID.c_str());      CLog::Log(LOGDEBUG, "CScrobbler: NP URL is %s",          m_strNowPlayingURL.c_str());      CLog::Log(LOGDEBUG, "CScrobbler: Submit URL is %s",          m_strSubmissionURL.c_str());      ClearErrorState();      return true;    }    CLog::Log(LOGERROR, "CScrobbler: Handshake failed! Received malformed "\        "reply.");  }  else if (vecTokens[0] == "BANNED")  {    CLog::Log(LOGERROR, "CScrobbler: Handshake failed! Client is banned! "\        "Disabling submissions. Subsequent scrobbles will be cached. "\        "Please update your client to the newest version. ");    m_bBanned = true;  }  else if (vecTokens[0] == "BADAUTH")  {    CLog::Log(LOGERROR, "CScrobbler: Handshake failed! Authentication failed! "\        "Disabling submissions. Subsequent scrobbles will be cached. "\        "Please enter the correct credentials to re-enable scrobbling. ");    m_bBadAuth = true;  }  else if (vecTokens[0] == "BADTIME")  {    CLog::Log(LOGDEBUG, "CScrobbler: Handshake failed! Timestamp is invalid! "\        "Disabling submissions. Subsequent scrobbles will be cached. "\        "Please correct the system time and restart the application. ");  }  else if (vecTokens[0] == "FAILED")  {    CLog::Log(LOGDEBUG, "CScrobbler: Handshake failed! REASON: %s! ",         vecTokens[1].c_str());  }  else    CLog::Log(LOGDEBUG, "CScrobbler: Handshake failed! REASON: Unspecified!");    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_http.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, "CScrobbler: Now playing notification succeeded!");    ClearErrorState();    return true;  }  else if (vecTokens[0] == "BADSESSION")  {    CLog::Log(LOGDEBUG, "CScrobbler: Now playing notification failed! "\        "REASON: Bad session ID. Forcing new handshake.");    ClearSession();  }  else    CLog::Log(LOGDEBUG, "CScrobbler: Now playing notification failed! "\        "REASON: Unspecified.");  return false;}bool CScrobbler::DoSubmission(){  int               i;  int               numSubmissions;  CStdString        strSubmissionRequest;  CStdString        strSubmission;  CStdString        strResponse;  // Construct submission URL.  numSubmissions =     std::min((size_t)SCROBBLER_MAX_SUBMISSIONS, m_vecSubmissionQueue.size());  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_http.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, "CScrobbler: Submission succeeded!");    ClearErrorState();    return true;  }  else if (vecTokens[0] == "BADSESSION")  {    CLog::Log(LOGDEBUG, "CScrobbler: Submission failed! "\        "REASON: Bad session. Forcing new handshake.");    ClearSession();  }  else if (vecTokens[0] == "FAILED")  {    CLog::Log(LOGDEBUG, "CScrobbler: Submission failed! "\        "REASON: %s", vecTokens[1].c_str());  }  else    CLog::Log(LOGDEBUG, "CScrobbler: Now submission failed! "\        "REASON: Unspecified.");  return false;}CStdString CScrobbler::GetJournalFileName(){  CStdString strFileName = g_settings.GetProfileUserDataFolder();  return CUtil::AddFileToFolder(strFileName, "Scrobbler.xml");}void CScrobbler::Process(){  CLog::Log(LOGDEBUG, "CScrobbler: Thread started.");  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)      DoSubmission();  }  CLog::Log(LOGDEBUG, "CScrobbler: Thread ended.");}