All pastes #1418028 Raw Edit

Someone

public diff v1 · immutable
#1418028 ·published 2009-05-10 05:24 UTC
rendered paste body
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 <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                                                            **