rendered paste bodyIndex: xbmc/Application.cpp===================================================================--- xbmc/Application.cpp (revision 20107)+++ xbmc/Application.cpp (working copy)@@ -3006,7 +3006,6 @@ if (IsPlaying() && action.fAmount1 && (action.wID == ACTION_ANALOG_SEEK_FORWARD || action.wID == ACTION_ANALOG_SEEK_BACK)) { if (!m_pPlayer->CanSeek()) return false;- CScrobbler::GetInstance()->SetSubmitSong(false); // Do not submit songs to Audioscrobbler when seeking, see CheckAudioScrobblerStatus() m_guiDialogSeekBar.OnAction(action); return true; }@@ -4394,6 +4393,8 @@ if (m_pXbmcHttp && g_stSettings.m_HttpApiBroadcastLevel>=1) getApplicationMessenger().HttpApi("broadcastlevel; OnPlayBackEnded;1"); #endif+ if (IsPlayingAudio())+ CScrobbler::GetInstance()->SubmitQueue(); CLog::Log(LOGDEBUG, "Playback has finished"); @@ -4422,6 +4423,16 @@ CGUIMessage msg(GUI_MSG_PLAYBACK_STARTED, 0, 0); m_gWindowManager.SendThreadMessage(msg);+/* + if (IsPlayingAudio())+ {+ const CMusicInfoTag* tag=g_infoManager.GetCurrentSongTag();+ if (tag)+ CScrobbler::GetInstance()->AddSong(*tag);+ else+ CLog::Log(LOGDEBUG, "tag not ready.");+ }+*/ } void CApplication::OnQueueNextItem()@@ -4439,6 +4450,9 @@ #endif CLog::Log(LOGDEBUG, "Player has asked for the next item"); + if(IsPlayingAudio())+ CScrobbler::GetInstance()->SubmitQueue();+ CGUIMessage msg(GUI_MSG_QUEUE_NEXT_ITEM, 0, 0); m_gWindowManager.SendThreadMessage(msg); }@@ -4459,6 +4473,7 @@ if (m_pXbmcHttp && g_stSettings.m_HttpApiBroadcastLevel>=1) getApplicationMessenger().HttpApi("broadcastlevel; OnPlayBackStopped;1"); #endif+ CScrobbler::GetInstance()->SubmitQueue(); CLog::Log(LOGDEBUG, "Playback was stopped\n"); @@ -4908,14 +4923,12 @@ m_pKaraokeMgr->Start(m_itemCurrentFile->m_strPath); } #endif- // Activate audio scrobbler- if (CLastFmManager::GetInstance()->CanScrobble(*m_itemCurrentFile))- {- CScrobbler::GetInstance()->SetSongStartTime();- CScrobbler::GetInstance()->SetSubmitSong(true);- }+ // Let scrobbler know about the track+ const CMusicInfoTag* tag=g_infoManager.GetCurrentSongTag();+ if (tag)+ CScrobbler::GetInstance()->AddSong(*tag); else- CScrobbler::GetInstance()->SetSubmitSong(false);+ CLog::Log(LOGDEBUG, "tag not ready."); } return true; }@@ -4983,9 +4996,6 @@ g_infoManager.ResetCurrentItem(); m_currentStack->Clear(); - // Reset audioscrobbler submit status- CScrobbler::GetInstance()->SetSubmitSong(false);- // stop lastfm if (CLastFmManager::GetInstance()->IsRadioEnabled()) CLastFmManager::GetInstance()->StopRadio();@@ -5164,7 +5174,7 @@ if (IsPlayingAudio()) {- CheckAudioScrobblerStatus();+ CScrobbler::GetInstance()->UpdateStatus(); // Update audio file state every 0.5 second UpdateAudioFileState(); }@@ -5660,54 +5670,6 @@ } } -void CApplication::CheckAudioScrobblerStatus()-{- if (IsPlayingAudio() && CLastFmManager::GetInstance()->CanScrobble(*m_itemCurrentFile) &&- !CScrobbler::GetInstance()->ShouldSubmit() && GetTime()==0.0)- {- // We seeked to the beginning of the file- // reinit audio scrobbler- CScrobbler::GetInstance()->SetSongStartTime();- CScrobbler::GetInstance()->SetSubmitSong(true);- return;- }-- if (!IsPlayingAudio() || !CScrobbler::GetInstance()->ShouldSubmit())- return;-- // Don't submit songs to audioscrobber when the user seeks.- // Rule from audioscrobbler:- // http://www.audioscrobbler.com/development/protocol.php- if (GetPlaySpeed()!=1)- {- CScrobbler::GetInstance()->SetSubmitSong(false);- return;- }-- // Submit the song if 50% or 240 seconds are played- double dTime=(double)g_infoManager.GetPlayTime()/1000.0;- const CMusicInfoTag* tag=g_infoManager.GetCurrentSongTag();- double dLength=0.f;- if (tag)- dLength=(tag->GetDuration()>0) ? (tag->GetDuration()/2.0f) : (GetTotalTime()/2.0f);-- if (!tag || !tag->Loaded() || dLength==0.0f)- {- CScrobbler::GetInstance()->SetSubmitSong(false);- return;- }- if ((dLength)>240.0f)- dLength=240.0f;- int iTimeTillSubmit=(int)(dLength-dTime);- CScrobbler::GetInstance()->SetSecsTillSubmit(iTimeTillSubmit);-- if (dTime>dLength)- {- CScrobbler::GetInstance()->AddSong(*tag);- CScrobbler::GetInstance()->SetSubmitSong(false);- }-}- bool CApplication::ProcessAndStartPlaylist(const CStdString& strPlayList, CPlayList& playlist, int iPlaylist) { CLog::Log(LOGDEBUG,"CApplication::ProcessAndStartPlaylist(%s, %i)",strPlayList.c_str(), iPlaylist);Index: xbmc/GUIWindowSettingsCategory.cpp===================================================================--- xbmc/GUIWindowSettingsCategory.cpp (revision 20107)+++ xbmc/GUIWindowSettingsCategory.cpp (working copy)@@ -886,7 +886,7 @@ { for (unsigned int i = 0; i < m_vecSettings.size(); i++) {- CBaseSettingControl *pSettingControl = m_vecSettings[i];+ CBaseSettingControl *pSettingControl = m_vecSettings[i]; pSettingControl->Update(); CStdString strSetting = pSettingControl->GetSetting()->GetSetting(); if (strSetting.Equals("videoscreen.testresolution"))@@ -1676,14 +1676,15 @@ { JumpToSection(WINDOW_SETTINGS_APPEARANCE, "locale"); }- else if (strSetting.Equals("lastfm.enable") || strSetting.Equals("lastfm.username") || strSetting.Equals("lastfm.password"))+ else if (strSetting.Equals("lastfm.enable") || strSetting.Equals("lastfm.recordtoprofile") || strSetting.Equals("lastfm.username") || strSetting.Equals("lastfm.password")) {- if (g_guiSettings.GetBool("lastfm.enable") || g_guiSettings.GetBool("lastfm.recordtoprofile"))+ CStdString strPassword=g_guiSettings.GetString("lastfm.password");+ CStdString strUserName=g_guiSettings.GetString("lastfm.username");+ if ((g_guiSettings.GetBool("lastfm.enable") || + g_guiSettings.GetBool("lastfm.recordtoprofile")) &&+ !strUserName.IsEmpty() && !strPassword.IsEmpty()) {- CStdString strPassword=g_guiSettings.GetString("lastfm.password");- CStdString strUserName=g_guiSettings.GetString("lastfm.username");- if (!strUserName.IsEmpty() || !strPassword.IsEmpty())- CScrobbler::GetInstance()->Init();+ CScrobbler::GetInstance()->Init(); } else {Index: xbmc/lib/libscrobbler/scrobbler.h===================================================================--- xbmc/lib/libscrobbler/scrobbler.h (revision 20107)+++ xbmc/lib/libscrobbler/scrobbler.h (working copy)@@ -1,268 +1,145 @@-/*- This file is part of libscrobbler. Modified for XBMC by Bobbin007+#ifndef LIBSCROBBLER_H__+#define LIBSCROBBLER_H__ - libscrobbler 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 of the License, or- (at your option) any later version.-- libscrobbler 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 libscrobbler; if not, write to the Free Software- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA-- Copyright 2003 Russell Garrett (russ-scrobbler@garrett.co.uk)-*/-#ifndef _SCROBBLER_H-#define _SCROBBLER_H- #include <vector>+#include "StdString.h" #include "Thread.h"+#include "FileSystem/FileCurl.h"+#include "CriticalSection.h" namespace MUSIC_INFO { class CMusicInfoTag; } -/**- An enumeration of the status messages so that clients can- do things with them, and not have to parse the text- to work out what's going on.- If you don't care about these, then you can continue to- just override the char* only version of statusUpdate-*/-enum ScrobbleStatus+/* The following structure describes an entry in the scrobbler submission+ * journal. Declare members added purely for convenience at the top and+ * members which will actually be read/written from/to disk under a+ * version number comment. Also, remember to bump the version macro in+ * scrobbler.cpp.+ */+typedef struct SubmissionJournalEntry_s {- S_SUBMITTING = 0,-- S_NOT_SUBMITTING,-- S_CONNECT_ERROR,- - S_HANDSHAKE_SUCCESS,- S_HANDSHAKE_UP_TO_DATE,- S_HANDSHAKE_OLD_CLIENT,- S_HANDSHAKE_INVALID_RESPONSE,- S_HANDSHAKE_BAD_USERNAME,- S_HANDSHAKE_ERROR,- S_HANDHAKE_NOTREADY,-- S_SUBMIT_SUCCESS,- S_SUBMIT_INVALID_RESPONSE,- S_SUBMIT_INTERVAL,- S_SUBMIT_BAD_PASSWORD,- S_SUBMIT_FAILED,- S_SUBMIT_BADAUTH,-- S_DEBUG--};--typedef struct SubmissionJournalEntry_s {+ int length;+ // v0 CStdString strArtist; CStdString strAlbum; CStdString strTitle;- CStdString strLength;+ CStdString strLength; // Required if strSource below is "P" CStdString strStartTime; CStdString strMusicBrainzID;- SubmissionJournalEntry_s() {};- SubmissionJournalEntry_s(const struct SubmissionJournalEntry_s& j) {- strArtist = j.strArtist;- strAlbum = j.strAlbum;- strTitle = j.strTitle;- strLength = j.strLength;- strStartTime = j.strStartTime;- strMusicBrainzID = j.strMusicBrainzID;- };+ // v1+ CStdString strTrackNum;+ CStdString strSource;+ /* strSource must be one of the following.+ * P: Chosen by the user (the most common value, unless you have a reason+ * for choosing otherwise, use this).+ * R: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1).+ * E: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast).+ * L: Last.fm (any mode). In this case, the 5-digit Last.fm recommendation+ * key must be appended to this source ID to prove the validity of the+ * submission (for example, "o[0]=L1b48a").+ * U: Source unknown.+ */+ CStdString strRating;+ /* strRating must be one of the following or empty.+ * L: Love (on any mode if the user has manually loved the track). This+ * implies a listen.+ * B: Ban (only if source=L). This implies a skip, and the client should+ * skip to the next track when a ban happens.+ * S: Skip (only if source=L)+ *+ * NOTE: This will eventually replace the love/ban web service.+ */+ SubmissionJournalEntry_s() {}+ SubmissionJournalEntry_s(const struct SubmissionJournalEntry_s& j)+ {+ strArtist = j.strArtist;+ strAlbum = j.strAlbum;+ strTitle = j.strTitle;+ strLength = j.strLength;+ strStartTime = j.strStartTime;+ strMusicBrainzID = j.strMusicBrainzID;+ strTrackNum = j.strTrackNum;+ strSource = j.strSource;+ strRating = j.strRating;+ length = j.length;+ }+ void Clear()+ {+ strArtist.clear();+ strAlbum.clear();+ strTitle.clear();+ strLength.clear();+ strStartTime.clear();+ strMusicBrainzID.clear();+ strTrackNum.clear();+ strSource = "P";+ strRating.clear();+ length = 0;+ } } SubmissionJournalEntry; -/**- Audioscrobbler client class.- $Id$+typedef std::vector<SubmissionJournalEntry>::iterator SCROBBLERJOURNALITERATOR; - @version 1.1- @author Russ Garrett (russ-scrobbler@garrett.co.uk)-*/- class CScrobbler : public CThread { private:- /**- Use this to initially set up the username and password. This sends a - handshake request to the server to determine the submit URL and - initial submit interval, and submissions will only be sent when - it recieves a response to this.- - @param user The user's Audioscrobbler username.- @param pass The user's Audioscrobbler password.- @see setPassword()- @see setUsername()- */- CScrobbler();+ static long m_instanceLock;+ static CScrobbler *m_pInstance;+ bool m_bRunThread;+ bool m_bNotified;+ bool m_bSubmitted;+ bool m_bBanned;+ bool m_bBadAuth;+ int m_submissionTimer;+ int m_hardErrorCount;+ int m_failedHandshakeDelay;+ int m_action;+ time_t m_lastFailedHandshake;+ CStdString m_strUsername;+ CStdString m_strPasswordHash;+ CStdString m_strSessionID;+ CStdString m_strNowPlayingURL;+ CStdString m_strSubmissionURL;+ CStdString m_strHandshakeTimeStamp;+ SubmissionJournalEntry m_CurrentTrack;+ HANDLE m_hEvent;+ XFILE::CFileCurl m_http;+ CCriticalSection m_journalLock;+ CCriticalSection m_actionLock;+ std::vector<SubmissionJournalEntry> m_vecSubmissionQueue;++private:+ void ResetState();+ void ClearErrorState();+ void ClearSubmissionState();+ void ClearSession();+ void HandleHardError();+ bool CanScrobble();+ bool LoadJournal();+ bool SaveJournal();+ bool DoHandshake(time_t now);+ bool HandleHandshake(CStdString &strResponse);+ bool DoNowPlayingNotification();+ bool HandleNowPlayingNotification(CStdString &strResponse);+ bool DoSubmission();+ bool HandleSubmission(CStdString &strResponse);+ CStdString GetJournalFileName(); virtual void Process();- virtual void OnStartup();- virtual void OnExit(); public:+ CScrobbler(); virtual ~CScrobbler();-+ static CScrobbler *GetInstance(); static void RemoveInstance();- static CScrobbler* GetInstance();- /**- Call this to add a song to the submission queue. The submission will get- sent immediately unless the server has told it to cache, in which case it- will get sent with the first song submission after the cache period has - expired. - - @note Submission is not synchronous, so song submission status is only available through the statusUpdate callback.- @note There's no way to tell the class to submit explicitly, it'll try to submit when you addSong.-- @param artist The artist name.- @param title The title of the song.- @param length The length of the song in seconds.- @param ltime The time (unix time) in seconds when playback of the song started.- @param trackid The MusicBrainz TrackID of the song (in guid format).- @see statusUpdate()- @retval 0 Failure. This could be because the song submitted was too short.- @retval 1 This tripped song submission, Scrobbler is now connecting.- @retval 2 The submission was cached.- */- int AddSong(const MUSIC_INFO::CMusicInfoTag& tag);-- /**- Set the user's password if it's changed since the constructor.-- @param pass The user's new password.- @see Scrobbler()- @see setUsername()- */- void SetPassword(const CStdString& strPass);-- // should call just after contruction void Init();-- // should call juts before deletion void Term();--- /**- Set the user's user name if it's changed since the constructor.-- @param user The user's new user name.- @see Scrobbler()- @see setUsername()- */- void SetUsername(const CStdString& strUser);-- void SetSubmitSong(bool bSubmit);- bool ShouldSubmit();- void SetSongStartTime();-- CStdString GetConnectionState();- CStdString GetSubmitInterval();- CStdString GetFilesCached();- void SetSecsTillSubmit(int iSecs);- CStdString GetSubmitState();-private:-- /**- Override this to receive updates on the status of the scrobbler - client to display to the user. You don't have to override this,- but it helps.- - If you do ovveride it, the old text only version won't- work any more.-- @param status The status code.- @param text The text of the status update.- */- virtual void StatusUpdate(ScrobbleStatus status, const CStdString& strText);-- /**- old status update - text only- - Override this to receive updates on the status of the scrobbler - client to display to the user. You don't have to override this,- but it helps.-- @param text The text of the status update.- */- virtual void StatusUpdate(const CStdString& strText);-- virtual void SetInterval(int in);-- /**- Override this to save the cache - called from the destructor.-- @see LoadJournal()- @retval 1 You saved some cache.- @retval 0 You didn't save cache, or can't.- */- virtual int SaveJournal();- - /**- This is called when you should load the cache. Use setCache.-- @see SaveJournal()- @retval 1 You loaded some cache.- @retval 0 There was no cache to load, or you can't.- */- virtual int LoadCache();- virtual int LoadJournal();-- virtual void GenSessionKey();-- void DoSubmit();- void DoHandshake();-- // call back functions from curl- void HandleHandshake(char *handshake);- void HandleSubmit(char *data);-- CStdString GetTempFileName();- CStdString GetJournalFileName();-- CStdString m_strUserName;- CStdString m_strPassword; // MD5 hash- CStdString m_strChallenge;- CStdString m_strSessionKey;- CStdString m_strHsString;- CStdString m_strSubmitUrl;- CStdString m_strSubmit;- CStdString m_strClientId;- CStdString m_strClientVer;- CStdString m_strPostString;- - bool m_bReadyToSubmit;-- bool m_bSubmitInProgress;-- bool m_bCloseThread;- HANDLE m_hWorkerEvent;-- int m_iSongNum;-- time_t m_Interval;- time_t m_LastConnect;-- time_t m_SongStartTime;-- bool m_bUpdateWarningDone;- bool m_bConnectionWarningDone;- bool m_bAuthWarningDone;- bool m_bBadPassWarningDone;-- int m_iSecsTillSubmit;- bool m_bShouldSubmit;- bool m_bReHandShaking;- - std::vector<SubmissionJournalEntry> m_vecSubmissionJournal;-- static CScrobbler* m_pInstance;+ void AddSong(const MUSIC_INFO::CMusicInfoTag &tag);+ void UpdateStatus();+ void SubmitQueue();+ void SetUsername(const CStdString &strUser);+ void SetPassword(const CStdString &strPass); }; -#endif+#endif // LIBSCROBBLER_H__Index: xbmc/lib/libscrobbler/scrobbler.cpp===================================================================--- xbmc/lib/libscrobbler/scrobbler.cpp (revision 20107)+++ xbmc/lib/libscrobbler/scrobbler.cpp (working copy)@@ -1,90 +1,57 @@-/*- This file is part of libscrobbler. Modified for XBMC by Bobbin007-- libscrobbler 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 of the License, or- (at your option) any later version.-- libscrobbler 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 libscrobbler; if not, write to the Free Software- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA-- Copyright 2003 Russell Garrett (russ-scrobbler@garrett.co.uk)-*/-#include "stdafx.h" #include "scrobbler.h"-#include "errors.h"+#include "tinyXML/tinyxml.h"+#include "stdafx.h" #include "utils/md5.h"-#include "utils/CharsetConverter.h" #include "utils/log.h" #include "Util.h"-#include "Settings.h"-#include "Application.h" #include "MusicInfoTag.h"-#include "FileSystem/File.h"-#include "FileSystem/FileCurl.h"-#include "tinyXML/tinyxml.h"+#include "errors.h"+#include "utils/Atomics.h"+#include "Settings.h" #include "XMLUtils.h"-#include "RegExp.h"+#include "Application.h"+#include "SingleLock.h" -using namespace std;-using namespace MUSIC_INFO;-using namespace XFILE;+#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 2 -#define LS_VER "1.4"-#define MINLENGTH 30-#define MAXLENGTH 10800-#define HS_FAIL_WAIT 100000-#define MAXSUBMISSIONS 50+long CScrobbler::m_instanceLock = 0;+CScrobbler *CScrobbler::m_pInstance = NULL; -//#define CLIENT_ID "tst"-#define CLIENT_ID "xbm"-#define CLIENT_VERSION "0.1"+CScrobbler::CScrobbler() : CThread()+{ + m_bRunThread = true;+ m_bBanned = false;+ m_bBadAuth = false;+ ResetState(); -CScrobbler* CScrobbler::m_pInstance=NULL;--CScrobbler::CScrobbler()-{- m_bShouldSubmit=false;- m_bUpdateWarningDone=false;- m_bConnectionWarningDone=false;- m_bAuthWarningDone=false;- m_bBadPassWarningDone=false;- m_bReHandShaking=false;- m_strClientId = CLIENT_ID;- m_strClientVer = CLIENT_VERSION;- m_bCloseThread = false;- m_Interval = m_LastConnect = m_SongStartTime = 0;- m_iSecsTillSubmit=0;- m_bSubmitInProgress = false;- m_bReadyToSubmit=false;- m_iSongNum=0;- m_hWorkerEvent = CreateEvent(NULL, false, false, NULL);- if (!m_hWorkerEvent)+ if (!(m_hEvent = CreateEvent(NULL, false, false, NULL))) throw EOutOfMemory();- Create(); } CScrobbler::~CScrobbler() {- m_bCloseThread = true;- SetEvent(m_hWorkerEvent);- WaitForThreadExit(INFINITE);- CloseHandle(m_hWorkerEvent);- Sleep(0);+ Term();+ CloseHandle(m_hEvent); } -CScrobbler* CScrobbler::GetInstance()+CScrobbler *CScrobbler::GetInstance() {- if (!m_pInstance)- m_pInstance=new CScrobbler;-+ if (!m_pInstance) // Avoid spinning aimlessly+ {+ CAtomicSpinLock lock(m_instanceLock);+ if (!m_pInstance)+ {+ m_pInstance = new CScrobbler;+ }+ } return m_pInstance; } @@ -92,717 +59,546 @@ { if (m_pInstance) {- m_pInstance->Term();+ CAtomicSpinLock lock(m_instanceLock); delete m_pInstance;- m_pInstance=NULL;+ m_pInstance = NULL; } } void CScrobbler::Init() {- if ((!g_guiSettings.GetBool("lastfm.enable") && !g_guiSettings.GetBool("lastfm.recordtoprofile")) || !g_guiSettings.GetBool("network.enableinternet"))+ if (!CanScrobble()) return;-- CStdString strPassword=g_guiSettings.GetString("lastfm.password");- CStdString strUserName=g_guiSettings.GetString("lastfm.username");-- if (strPassword.IsEmpty() || strUserName.IsEmpty())- return;-- // show all warnings each time we init the scrobbler- m_bAuthWarningDone=false;- m_bUpdateWarningDone=false;- m_bConnectionWarningDone=false;- m_bBadPassWarningDone=false;-- SetPassword(strPassword);- SetUsername(strUserName);-+ ResetState();+ ClearSession(); // Revoke our session+ SetUsername(g_guiSettings.GetString("lastfm.username"));+ SetPassword(g_guiSettings.GetString("lastfm.password")); LoadJournal();- DoHandshake();+ if (!ThreadId())+ Create(); } void CScrobbler::Term() {- m_bReadyToSubmit=false;+ m_bRunThread = false;+ SetEvent(m_hEvent);+ StopThread(); SaveJournal(); } -void CScrobbler::SetPassword(const CStdString& strPass)+void CScrobbler::AddSong(const MUSIC_INFO::CMusicInfoTag &tag) {- if (strPass.IsEmpty())+ ClearSubmissionState();++ if (!CanScrobble() || !tag.Loaded()) return;- XBMC::MD5 md5state;- unsigned char md5pword[16];- md5state.append(strPass);- md5state.getDigest(md5pword);- char tmp[33];- strncpy(tmp, "\0", sizeof(tmp));- for (int j = 0;j < 16;j++)- {- char a[3];- sprintf(a, "%02x", md5pword[j]);- tmp[2*j] = a[0];- tmp[2*j+1] = a[1];- }- m_strPassword = tmp;- GenSessionKey();-} -void CScrobbler::SetUsername(const CStdString& strUser)-{- if (strUser.IsEmpty())+ 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_strUserName=strUser;-- CUtil::URLEncode(m_strUserName);-- m_strHsString = "http://post.audioscrobbler.com/?hs=true&p=1.1&c=" + m_strClientId + "&v=" + m_strClientVer + "&u=" + m_strUserName;+ m_bNotified = false;+ if (m_CurrentTrack.length > SCROBBLER_MIN_DURATION)+ m_bSubmitted = false; } -int CScrobbler::AddSong(const CMusicInfoTag& tag)+void CScrobbler::UpdateStatus() {- if ((!g_guiSettings.GetBool("lastfm.enable") && !g_guiSettings.GetBool("lastfm.recordtoprofile")) || !g_guiSettings.GetBool("network.enableinternet"))- return 0;+ // Called from CApp::ProcessSlow() every ~500ms.+ if (!CanScrobble())+ return;+ if (g_application.IsPaused())+ return; - if (tag.GetDuration() <= MINLENGTH || tag.GetDuration() > MAXLENGTH) // made <= to minlength to stop iTMS previews being submitted in iTunes- return 0;+ m_submissionTimer++; - if (!tag.Loaded() || tag.GetArtist().IsEmpty() || tag.GetTitle().IsEmpty())- return 0;-- if(m_bSubmitInProgress)+ // 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) {- StatusUpdate(S_NOT_SUBMITTING,"Previous submission still in progress");- return 0;- }-- char ti[20];- struct tm *today = gmtime(&m_SongStartTime);- strftime(ti, sizeof(ti), "%Y-%m-%d %H:%M:%S", today);-- SubmissionJournalEntry entry;- // our tags are stored as UTF-8, so no conversion needed- entry.strArtist = tag.GetArtist();- entry.strAlbum = tag.GetAlbum();- entry.strTitle = tag.GetTitle();- entry.strStartTime = ti;- entry.strLength.Format("%d",tag.GetDuration());- entry.strMusicBrainzID = tag.GetMusicBrainzTrackID();- m_vecSubmissionJournal.push_back(entry);- SaveJournal();-- while ((unsigned)m_iSongNum < m_vecSubmissionJournal.size() && m_iSongNum < MAXSUBMISSIONS)- {- entry = m_vecSubmissionJournal[m_iSongNum];- CStdString strStartTime = entry.strStartTime;- CUtil::URLEncode(entry.strArtist); - CUtil::URLEncode(entry.strTitle);- CUtil::URLEncode(entry.strAlbum);- CUtil::URLEncode(entry.strMusicBrainzID);- CUtil::URLEncode(entry.strLength);- CUtil::URLEncode(entry.strStartTime);- CStdString strSubmitStr;- strSubmitStr.Format("a[%i]=%s&t[%i]=%s&b[%i]=%s&m[%i]=%s&l[%i]=%s&i[%i]=%s&", - m_iSongNum, - entry.strArtist, - m_iSongNum, - entry.strTitle,- m_iSongNum,- entry.strAlbum,- m_iSongNum, - entry.strMusicBrainzID,- m_iSongNum, - entry.strLength,- m_iSongNum, - entry.strStartTime);-- if(m_strPostString.find(strStartTime) != m_strPostString.npos)+ m_bNotified = true; // Only try once {- // we have already tried to add a song at this time stamp- // I have no idea how this could happen but apparently it does so- // we stop it now-- StatusUpdate(S_NOT_SUBMITTING,strSubmitStr);- StatusUpdate(S_NOT_SUBMITTING,m_strPostString);- StatusUpdate(S_NOT_SUBMITTING,"Submission error, duplicate subbmission time found");- - m_vecSubmissionJournal.erase(m_vecSubmissionJournal.begin() + m_iSongNum);- SaveJournal();-- return 3;+ CSingleLock lock(m_actionLock);+ m_action = SCROBBLER_ACTION_NOWPLAYING; }-- m_strPostString += strSubmitStr;- m_iSongNum++;+ SetEvent(m_hEvent);+ return; } - time_t now;- time (&now);- if ((m_Interval + m_LastConnect) < now)+ // 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)) {- DoSubmit();- return 1;+ CSingleLock lock(m_journalLock);+ m_bSubmitted = true;+ m_vecSubmissionQueue.push_back(m_CurrentTrack);+ SaveJournal();+ CLog::Log(LOGDEBUG, "CScrobbler: Queued track for submission"); }- else- {- CStdString strMsg;- strMsg.Format("Not submitting, caching for %i more seconds. Cache is %i entries.", (int)(m_Interval + m_LastConnect - now), m_vecSubmissionJournal.size());- StatusUpdate(S_NOT_SUBMITTING,strMsg);- return 2;- } } -void CScrobbler::DoHandshake()+void CScrobbler::SubmitQueue() {- if (m_strUserName.IsEmpty() || m_strPassword.IsEmpty() || - (!g_guiSettings.GetBool("lastfm.enable") && - !g_guiSettings.GetBool("lastfm.recordtoprofile")) ||- !g_guiSettings.GetBool("network.enableinternet"))- return;-- m_bReadyToSubmit = false;- time_t now;- time (&now);- m_LastConnect = now;- m_strHsString = "http://post.audioscrobbler.com/?hs=true&p=1.1&c=" + m_strClientId + "&v=" + m_strClientVer + "&u=" + m_strUserName;- SetEvent(m_hWorkerEvent);+ if (CanScrobble()) {+ {+ CSingleLock lock(m_actionLock);+ m_action = SCROBBLER_ACTION_SUBMIT;+ }+ SetEvent(m_hEvent);+ } } -void CScrobbler::DoSubmit()+void CScrobbler::SetUsername(const CStdString& strUser) {- if(m_bSubmitInProgress)- {- StatusUpdate(S_NOT_SUBMITTING,"Previous submission still in progress");+ if (strUser.IsEmpty()) return;- } - if (m_strUserName.IsEmpty() || m_strPassword.IsEmpty() || !m_bReadyToSubmit)- return;-- m_bSubmitInProgress = true;-- StatusUpdate(S_SUBMITTING,"Submitting cache...");- time_t now;- time (&now);- m_LastConnect = now;- m_strSubmit.Format("u=%s&s=%s&", m_strUserName.c_str(), m_strSessionKey.c_str());- m_strSubmit += m_strPostString;- StatusUpdate(S_SUBMITTING,m_strSubmit);- SetEvent(m_hWorkerEvent);+ m_strUsername=strUser;+ CUtil::URLEncode(m_strUsername);+ m_bBadAuth = false;+ ClearSession(); } -void CScrobbler::HandleHandshake(char *handshake)+void CScrobbler::SetPassword(const CStdString& strPass) {- // Doesn't take into account multiple-packet returns (not that I've seen one yet)...-- // Ian says: strtok() is not re-entrant, but since it's only being called- // in only one function at a time, it's ok so far.-- char seps[] = " \n\r";- char *response = strtok(handshake, seps);- if (!response)- {- StatusUpdate(S_HANDSHAKE_INVALID_RESPONSE,"Handshake failed: Response invalid");+ if (strPass.IsEmpty()) return;- }- do- {- if (stricmp("UPTODATE", response) == 0)- {- StatusUpdate(S_HANDSHAKE_UP_TO_DATE,"Handshaking: Client up to date.");- }- else if (stricmp("UPDATE", response) == 0)- {- char *updateurl = strtok(NULL, seps);- if (!updateurl)- break;- string msg = "Handshaking: Please update your client at: ";- msg += updateurl;- StatusUpdate(S_HANDSHAKE_OLD_CLIENT,msg.c_str());- }- else if (stricmp("BADUSER", response) == 0)- {- StatusUpdate(S_HANDSHAKE_BAD_USERNAME,"Handshake failed: Bad username");- return;- }- else- {- break;- }- m_strChallenge = strtok(NULL, seps);- m_strSubmitUrl = strtok(NULL, seps);- char *inttext = strtok(NULL, seps);- if (!inttext || !(stricmp("INTERVAL", inttext) == 0))- break;- SetInterval(atoi(strtok(NULL, seps)));- GenSessionKey();- m_bReadyToSubmit = true;- StatusUpdate( S_HANDSHAKE_SUCCESS,"Handshake successful.");- return;- } while (0);- CStdString strBuf;- strBuf.Format("Handshake failed: %s", strtok(NULL, "\n"));- StatusUpdate(S_HANDSHAKE_ERROR, strBuf);+ XBMC::MD5 md5state;+ md5state.append(strPass);+ md5state.getDigest(m_strPasswordHash);+ m_bBadAuth = false;+ ClearSession(); } -void CScrobbler::HandleSubmit(char *data)+void CScrobbler::ResetState() {- // submit returned- m_bSubmitInProgress = false; //- this should already have been cancelled by the header callback+ ClearSession();+ ClearSubmissionState();+ ClearErrorState();+} - StatusUpdate(S_DEBUG,data);-- // Doesn't take into account multiple-packet returns (not that I've seen one yet)...- char seps[] = " \n\r";- char * response = strtok(data, seps);- if (!response)- {- StatusUpdate(S_SUBMIT_INVALID_RESPONSE,"Submission failed: Response invalid");- return;- }- if (stricmp("OK", response) == 0)- {- StatusUpdate(S_SUBMIT_SUCCESS,"Submission succeeded.");-- char *inttext = strtok(NULL, seps);- if (inttext && (stricmp("INTERVAL", inttext) == 0))- {- m_Interval = atoi(strtok(NULL, seps));- CStdString strBuf;- strBuf.Format("Submit interval set to %i seconds.", m_Interval);- StatusUpdate(S_SUBMIT_INTERVAL, strBuf);- }- - // Remove successfully submitted songs from journal and clear POST data- std::vector<SubmissionJournalEntry>::iterator it;- it = m_vecSubmissionJournal.begin();- m_vecSubmissionJournal.erase(it, it + m_iSongNum);- m_strPostString = "";- m_iSongNum = 0;- SaveJournal();- }- else if (stricmp("BADPASS", response) == 0)- {- StatusUpdate(S_SUBMIT_BAD_PASSWORD,"Submission failed: bad password.");- }- else if (stricmp("FAILED", response) == 0)- {- CStdString strBuf;- strBuf.Format("Submission failed: %s", strtok(NULL, "\n"));- StatusUpdate(S_SUBMIT_FAILED, strBuf);- char *inttext = strtok(NULL, seps);- if (inttext && (stricmp("INTERVAL", inttext) == 0))- {- m_Interval = atoi(strtok(NULL, seps));- strBuf.Format("Submit interval set to %i seconds.", m_Interval);- StatusUpdate(S_SUBMIT_INTERVAL, strBuf);- }- }- else if (stricmp("BADAUTH",response) == 0)- {- StatusUpdate(S_SUBMIT_BADAUTH,"Submission failed: bad authorization.");- char *inttext = strtok(NULL, seps);- if (inttext && (stricmp("INTERVAL", inttext) == 0))- {- m_Interval = atoi(strtok(NULL, seps));- CStdString strBuf;- strBuf.Format("Submit interval set to %i seconds.", m_Interval);- StatusUpdate(S_SUBMIT_INTERVAL, strBuf);- }- //re-handshake- m_bReHandShaking = true;- DoHandshake();- }- else- {- CStdString strBuf;- strBuf.Format("Submission failed: %s", strtok(NULL, "\n"));- StatusUpdate(S_SUBMIT_FAILED, strBuf);- }+void CScrobbler::ClearErrorState()+{+ m_hardErrorCount = 0;+ m_lastFailedHandshake = 0;+ m_failedHandshakeDelay = 0; } -void CScrobbler::SetInterval(int in)+void CScrobbler::ClearSubmissionState() {- m_Interval = in;- CStdString ret;- ret.Format("Submit interval set to %d seconds.", in);- StatusUpdate(S_SUBMIT_INTERVAL, ret);+ m_bNotified = true; // Explicitly clear these when necessary+ m_bSubmitted = true;+ m_submissionTimer = 0;+ CSingleLock lock(m_actionLock);+ m_action = 0; } -void CScrobbler::GenSessionKey()+void CScrobbler::ClearSession() {- CStdString clear = m_strPassword + m_strChallenge;- unsigned char md5pword[16];- XBMC::MD5 md5state;- md5state.append(clear);- md5state.getDigest(md5pword);+ m_strSessionID.clear();+} - char key[33];- strncpy(key, "\0", sizeof(key));- for (int j = 0;j < 16;j++)+void CScrobbler::HandleHardError()+{+ CLog::Log(LOGDEBUG, "CScrobbler: A hard error has occurred.");+ if (++m_hardErrorCount == 3) {- char a[3];- sprintf(a, "%02x", md5pword[j]);- key[2*j] = a[0];- key[2*j+1] = a[1];+ CLog::Log(LOGDEBUG, "CScrobbler: Three consecuetive hard errors have "\+ "occured. Forcing new handshake.");+ ClearSession(); }- m_strSessionKey = key; } -int CScrobbler::LoadCache()+bool CScrobbler::CanScrobble() {- CFile file;- if (file.Open(GetTempFileName()))- {- CArchive ar(&file, CArchive::load);- ar >> m_iSongNum;- ar >> m_strPostString;- ar.Close();- file.Close();- return 1;- }- return 0;+ return (!g_guiSettings.GetString("lastfm.username").IsEmpty() &&+ !g_guiSettings.GetString("lastfm.password").IsEmpty() &&+ (g_guiSettings.GetBool("lastfm.enable") ||+ g_guiSettings.GetBool("lastfm.recordtoprofile"))); } -int CScrobbler::LoadJournal()+bool CScrobbler::LoadJournal() {- SubmissionJournalEntry entry;- m_vecSubmissionJournal.clear();- - if (LoadCache()) - {- // load old style cache- CLog::Log(LOGDEBUG, "Audioscrobbler: Found old submission cache, converting to submission journal.");- for (int i = 0; i < m_iSongNum; i++)- {- CStdString strRegEx;- strRegEx.Format("a\\[%i\\]=(.*?)&t\\[%i\\]=(.*?)&b\\[%i\\]=(.*?)&m\\[%i\\]=(.*?)&l\\[%i\\]=(.*?)&i\\[%i\\]=(.*?)&", i, i, i, i, i, i);- CRegExp re;- if (!re.RegComp(strRegEx.c_str()))- break;- if (re.RegFind(m_strPostString) < 0)- continue;- - char *s;- s = re.GetReplaceString("\\1"); - entry.strArtist = s;- free(s);- s = re.GetReplaceString("\\2"); - entry.strTitle = s;- free(s);- s = re.GetReplaceString("\\3"); - entry.strAlbum = s;- free(s);- s = re.GetReplaceString("\\4"); - entry.strMusicBrainzID = s;- free(s);- s = re.GetReplaceString("\\5"); - entry.strLength = s;- free(s);- s = re.GetReplaceString("\\6"); - entry.strStartTime = s;- free(s);- CUtil::UrlDecode(entry.strArtist);- CUtil::UrlDecode(entry.strTitle);- CUtil::UrlDecode(entry.strAlbum);- CUtil::UrlDecode(entry.strMusicBrainzID);- CUtil::UrlDecode(entry.strLength);- CUtil::UrlDecode(entry.strStartTime);- m_vecSubmissionJournal.push_back(entry);- }- m_iSongNum = 0;- m_strPostString = "";- CFile::Delete(GetTempFileName());- CLog::Log(LOGDEBUG, "Audioscrobbler: Added %d entries from old cache file (%s) to journal.", m_vecSubmissionJournal.size(), GetTempFileName().c_str());- }+ int journalVersion = 0;+ SubmissionJournalEntry entry;+ TiXmlDocument xmlDoc;+ CStdString JournalFileName = GetJournalFileName();+ CSingleLock lock(m_journalLock); - TiXmlDocument xmlDoc;- CStdString JournalFileName = GetJournalFileName();+ m_vecSubmissionQueue.clear();+ if (!xmlDoc.LoadFile(JournalFileName)) {- CLog::Log(LOGDEBUG, "Audioscrobbler: %s, Line %d (%s)", JournalFileName.c_str(), xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());- return 0;+ 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, "Audioscrobbler: %s missing <asjournal>", JournalFileName.c_str());- return 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");- while (pNode)+ 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);- m_vecSubmissionJournal.push_back(entry);- pNode = pNode->NextSibling("entry");++ 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, "Audioscrobbler: Journal loaded with %d entries.", m_vecSubmissionJournal.size());- return (m_vecSubmissionJournal.size() > 0) ? 1 : 0;+ CLog::Log(LOGDEBUG, "CScrobbler: Journal loaded with %d entries.",+ m_vecSubmissionQueue.size());+ return !m_vecSubmissionQueue.empty(); } -int CScrobbler::SaveJournal()+bool CScrobbler::SaveJournal() {- TiXmlDocument xmlDoc;- TiXmlDeclaration decl("1.0", "utf-8", "yes");+ CSingleLock lock(m_journalLock);++ CStdString strJournalVersion;+ TiXmlDocument xmlDoc;+ TiXmlDeclaration decl("1.0", "utf-8", "yes");+ TiXmlElement xmlRootElement("asjournal"); xmlDoc.InsertEndChild(decl);- TiXmlElement xmlRootElement("asjournal");+ strJournalVersion.Format("%d", SCROBBLER_JOURNAL_VERSION);+ xmlRootElement.SetAttribute("version", strJournalVersion.c_str()); TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement); if (!pRoot)- return 0;- std::vector<SubmissionJournalEntry>::iterator it = m_vecSubmissionJournal.begin();- for (; it != m_vecSubmissionJournal.end(); it++)+ 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 0;+ 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();- return (xmlDoc.SaveFile(FileName)) ? 1 : 0;+ CLog::Log(LOGDEBUG, "CScrobbler: Journal with %d entries saved to %s", i,+ FileName.c_str());+ return xmlDoc.SaveFile(FileName); } -void CScrobbler::StatusUpdate(ScrobbleStatus status, const CStdString& strText)+bool CScrobbler::DoHandshake(time_t now) {- switch (status)- {- case S_HANDSHAKE_ERROR:- case S_SUBMIT_FAILED:- case S_HANDSHAKE_INVALID_RESPONSE:- case S_SUBMIT_INVALID_RESPONSE:- case S_CONNECT_ERROR:- // these are the bad ones just log- CLog::Log(LOGERROR, "AudioScrobbler: %s", strText.c_str());- break;- case S_HANDSHAKE_SUCCESS:- CLog::Log(LOGNOTICE, "AudioScrobbler: %s", strText.c_str());- break;- case S_HANDSHAKE_BAD_USERNAME:- case S_SUBMIT_BADAUTH:- CLog::Log(LOGERROR, "AudioScrobbler: %s", strText.c_str());- if(!m_bAuthWarningDone)- {- m_bAuthWarningDone=true;- m_bConnectionWarningDone=true; // eat "handshake not ready" message in this case- CStdString strMsg=g_localizeStrings.Get(15206); // Bad authorization: Check username and password- StatusUpdate(strMsg);- }- break;- case S_SUBMIT_BAD_PASSWORD:- CLog::Log(LOGERROR, "AudioScrobbler: %s", strText.c_str());- if(!m_bBadPassWarningDone)- {- m_bBadPassWarningDone=true;- CStdString strMsg=g_localizeStrings.Get(15206); // Bad authorization: Check username and password- StatusUpdate(strMsg);- }- break;- case S_HANDHAKE_NOTREADY:- CLog::Log(LOGNOTICE, "AudioScrobbler: %s", strText.c_str());- if(!m_bConnectionWarningDone)- {- m_bConnectionWarningDone=true;- CStdString strMsg=g_localizeStrings.Get(15204); // Unable to handshake: sleeping...- StatusUpdate(strMsg);- }- break;- case S_HANDSHAKE_OLD_CLIENT:- CLog::Log(LOGNOTICE, "AudioScrobbler: %s", strText.c_str());- if(!m_bUpdateWarningDone)- {- m_bUpdateWarningDone=true;- CStdString strMsg=g_localizeStrings.Get(15205); // Please update xbmc- StatusUpdate(strMsg);- }- break;- default:- CLog::Log(LOGDEBUG, "AudioScrobbler: %s", strText.c_str());- break;- }-}+ XBMC::MD5 authToken;+ CStdString strAuthToken;+ CStdString strTimeStamp;+ CStdString strResponse;+ CStdString strHandshakeRequest; -void CScrobbler::StatusUpdate(const CStdString& strText)-{- CStdString strAudioScrobbler=g_localizeStrings.Get(15200); // AudioScrobbler- g_application.m_guiDialogKaiToast.QueueNotification("", strAudioScrobbler, strText, 10000);+ // 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; } -void CScrobbler::OnStartup()+bool CScrobbler::HandleHandshake(CStdString &strResponse) {- CLog::Log(LOGDEBUG, "Audioscrobbler worker thread starting");-}+ // TODO toast messages+ if (strResponse.IsEmpty())+ return false;+ + std::vector<CStdString> vecTokens;+ CUtil::Tokenize(strResponse, vecTokens, " \n\r"); -void CScrobbler::OnExit()-{- CLog::Log(LOGDEBUG, "Audioscrobbler worker thread stopping");-}--void CScrobbler::Process()-{- while (1) + if (vecTokens[0] == "OK") {- WaitForSingleObject(m_hWorkerEvent, INFINITE);- if (m_bCloseThread)- break;-- CFileCurl http;- CStdString strHtml;- bool bSuccess;- if (!m_bReadyToSubmit)+ if (vecTokens.size() >= 4) {- bSuccess=http.Get(m_strHsString, strHtml);- if (bSuccess)- {- LPSTR lphtml=strHtml.GetBuffer();- HandleHandshake(lphtml);- strHtml.ReleaseBuffer();- if (m_bReHandShaking)- m_bReHandShaking = false;- }+ 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; }- else- {- bSuccess=http.Post(m_strSubmitUrl, m_strSubmit, strHtml);- if (bSuccess)- {- LPSTR lphtml=strHtml.GetBuffer();- HandleSubmit(lphtml);- strHtml.ReleaseBuffer();- if (m_bReHandShaking) continue;- }- }-- if(!bSuccess)- {- if(m_bReadyToSubmit)- m_bSubmitInProgress = false; // failed to post, means post is over-- StatusUpdate(S_CONNECT_ERROR,"Could not connect to server.");- }-- // OK, if this was a handshake, it failed since m_bReadyToSubmit isn't true. Submissions get cached.- while (!m_bReadyToSubmit)- {- StatusUpdate(S_HANDHAKE_NOTREADY,"Unable to handshake: sleeping...");- // sleep for HS_FAIL_WAIT, or until we have cancelled our thread- WaitForSingleObject(m_hWorkerEvent, HS_FAIL_WAIT);- if (m_bCloseThread)- return;- // and try again.- bSuccess=http.Get(m_strHsString, strHtml);- if (bSuccess)- {- LPSTR lphtml=strHtml.GetBuffer();- HandleHandshake(lphtml);- strHtml.ReleaseBuffer();- }- }+ 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; } -void CScrobbler::SetSongStartTime()+bool CScrobbler::DoNowPlayingNotification() {- time(&m_SongStartTime);-}+ CStdString strNowPlayingRequest;+ CStdString strResponse; -CStdString CScrobbler::GetConnectionState()-{- if (!g_guiSettings.GetBool("lastfm.enable") && !g_guiSettings.GetBool("lastfm.recordtoprofile"))- return "";+ // 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()); - return (m_bReadyToSubmit ? g_localizeStrings.Get(15207) : g_localizeStrings.Get(15208)); // Connected : Not Connected+ // Make and handle request+ if (m_http.Post(m_strNowPlayingURL, strNowPlayingRequest, strResponse) &&+ HandleNowPlayingNotification(strResponse))+ return true;+ + HandleHardError();+ return false; } -CStdString CScrobbler::GetSubmitInterval()+bool CScrobbler::HandleNowPlayingNotification(CStdString &strResponse) {- CStdString strInterval;+ if (strResponse.IsEmpty())+ return false;+ + std::vector<CStdString> vecTokens;+ CUtil::Tokenize(strResponse, vecTokens, " \n\r"); - if (!g_guiSettings.GetBool("lastfm.enable") && !g_guiSettings.GetBool("lastfm.recordtoprofile"))- return strInterval;+ 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."); - CStdString strFormat=g_localizeStrings.Get(15209); // Submit Interval %i- strInterval.Format(strFormat.c_str(), m_Interval);-- return strInterval;+ return false; } -CStdString CScrobbler::GetFilesCached()+bool CScrobbler::DoSubmission() {- CStdString strCachedFiles;+ int i;+ int numSubmissions;+ CStdString strSubmissionRequest;+ CStdString strSubmission;+ CStdString strResponse; - if (!g_guiSettings.GetBool("lastfm.enable") && !g_guiSettings.GetBool("lastfm.recordtoprofile"))- return strCachedFiles;+ // 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;+ } - CStdString strFormat=g_localizeStrings.Get(15210); // Cached %i Songs- strCachedFiles.Format(strFormat.c_str(), m_vecSubmissionJournal.size());+ // 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;+ } - return strCachedFiles;+ HandleHardError();+ return false; } -void CScrobbler::SetSecsTillSubmit(int iSecs)+bool CScrobbler::HandleSubmission(CStdString &strResponse) {- m_iSecsTillSubmit=iSecs;-}+ if (strResponse.IsEmpty())+ return false;+ + std::vector<CStdString> vecTokens;+ CUtil::Tokenize(strResponse, vecTokens, " \n\r"); -CStdString CScrobbler::GetSubmitState()-{- CStdString strText;-- if (!g_guiSettings.GetBool("lastfm.enable") && !g_guiSettings.GetBool("lastfm.recordtoprofile"))- return strText;-- if (m_bSubmitInProgress)+ if (vecTokens[0] == "OK") {- strText=g_localizeStrings.Get(15211); // Submitting...- return strText;+ CLog::Log(LOGDEBUG, "CScrobbler: Submission succeeded!");+ ClearErrorState();+ return true; }-- if (m_bReadyToSubmit && m_bShouldSubmit)+ else if (vecTokens[0] == "BADSESSION") {- CStdString strFormat=g_localizeStrings.Get(15212); // Submitting in %i secs- strText.Format(strFormat.c_str(), m_iSecsTillSubmit);- return strText;+ 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 strText;+ return false; } -void CScrobbler::SetSubmitSong(bool bSubmit)+CStdString CScrobbler::GetJournalFileName() {- m_bShouldSubmit=bSubmit;- m_iSecsTillSubmit=0;-}--bool CScrobbler::ShouldSubmit()-{- return m_bShouldSubmit;-}--CStdString CScrobbler::GetTempFileName()-{ CStdString strFileName = g_settings.GetProfileUserDataFolder();- return CUtil::AddFileToFolder(strFileName, "Scrobbler.tmp");+ return CUtil::AddFileToFolder(strFileName, "Scrobbler.xml"); } -CStdString CScrobbler::GetJournalFileName()+void CScrobbler::Process() {- CStdString strFileName = g_settings.GetProfileUserDataFolder();- return CUtil::AddFileToFolder(strFileName, "Scrobbler.xml");+ 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."); } Index: xbmc/utils/md5.h===================================================================--- xbmc/utils/md5.h (revision 20107)+++ xbmc/utils/md5.h (working copy)@@ -62,6 +62,7 @@ void append(unsigned char *inBuf, unsigned int inLen); void append(const CStdString& str); void getDigest(unsigned char digest[16]);+ void getDigest(CStdString& digest); private: MD5_CTX m_ctx;Index: xbmc/utils/GUIInfoManager.cpp===================================================================--- xbmc/utils/GUIInfoManager.cpp (revision 20107)+++ xbmc/utils/GUIInfoManager.cpp (working copy)@@ -3281,6 +3281,7 @@ CStdString CGUIInfoManager::GetAudioScrobblerLabel(int item) {+ /* switch (item) { case AUDIOSCROBBLER_CONN_STATE:@@ -3296,7 +3297,7 @@ return CScrobbler::GetInstance()->GetSubmitState(); break; }-+*/ return ""; } Index: xbmc/utils/md5.cpp===================================================================--- xbmc/utils/md5.cpp (revision 20107)+++ xbmc/utils/md5.cpp (working copy)@@ -45,6 +45,17 @@ MD5Final(digest, &m_ctx); } +void XBMC::MD5::getDigest(CStdString& digest)+{+ unsigned char szBuf[16] = {'\0'};+ getDigest(szBuf);+ digest.Format("%02x%02x%02x%02x%02x%02x%02x%02x"\+ "%02x%02x%02x%02x%02x%02x%02x%02x", szBuf[0], szBuf[1], szBuf[2],+ szBuf[3], szBuf[4], szBuf[5], szBuf[6], szBuf[7], szBuf[8],+ szBuf[9], szBuf[10], szBuf[11], szBuf[12], szBuf[13], szBuf[14],+ szBuf[15]);+}+ /* ********************************************************************** ** md5.c **