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.");}