Index: 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 +#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::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 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 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::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 ", JournalFileName.c_str()); - return 0; + CLog::Log(LOGDEBUG, "CScrobbler: %s missing ", + 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::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 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 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 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 **