changed: Music thumb caching no longer has the 4096 limit.
[xbmc:xbmc-antiquated.git] / XBMC / xbmc / FileSystem / FileLastFM.cpp
1 #include "../stdafx.h"
2 #include "FileLastFM.h"
3 #include "../Util.h"
4 #include "../utils/GUIInfoManager.h"
5 #include "../utils/md5.h"
6 #include "../Application.h"
7
8 namespace XFILE
9 {
10
11 enum ACTION
12 {
13   ACTION_Idle,
14   ACTION_Handshaking,
15   ACTION_ChangingStation,
16   ACTION_RetreivingMetaData,
17   ACTION_SkipNext
18 };
19 typedef struct FileStateSt
20 {
21   ACTION Action;
22   bool bActionDone;
23   bool bHandshakeDone;
24   bool bError;
25 } FileState;
26
27 static FileState m_fileState;
28
29 CFileLastFM::CFileLastFM() : CThread()
30 {
31   m_fileState.bHandshakeDone = false;
32   m_fileState.bActionDone    = false;
33   m_fileState.bError         = false;
34   
35   m_pFile        = NULL;
36   m_pSyncBuffer  = new char[6];
37   m_bOpened      = false;
38   m_bDirectSkip  = false;
39   m_hWorkerEvent = CreateEvent(NULL, false, false, NULL);
40 }
41
42 CFileLastFM::~CFileLastFM()
43 {
44   Close();
45   CloseHandle(m_hWorkerEvent);
46   delete[] m_pSyncBuffer;
47   m_pSyncBuffer = NULL;
48 }
49
50 bool CFileLastFM::CanSeek()
51 {
52   return false;
53 }
54
55 __int64 CFileLastFM::GetPosition()
56 {
57   return 0;
58 }
59
60 __int64 CFileLastFM::GetLength()
61 {
62   return 0;
63 }
64
65 void CFileLastFM::Parameter(const CStdString& key, const CStdString& data, CStdString& value)
66 {
67   value = "";
68   vector<CStdString> params;
69   int iNumItems = StringUtils::SplitString(data, "\n", params);
70   for (int i = 0; i < (int)params.size(); i++)
71   {
72     CStdString tmp = params[i];
73     if (int pos = tmp.Find(key) >= 0)
74     {
75       tmp.Delete(pos - 1, key.GetLength() + 1);
76       value = tmp;
77       break;
78     }
79   }
80   CLog::Log(LOGDEBUG, "Parameter %s -> %s", key.c_str(), value.c_str());
81 }
82
83 bool CFileLastFM::HandShake()
84 {
85   m_Session = "";
86   m_fileState.bHandshakeDone = false;
87
88   CHTTP http;
89   CStdString html;
90
91   CStdString strPassword = g_guiSettings.GetString("lastfm.password");
92   CStdString strUserName = g_guiSettings.GetString("lastfm.username");
93   if (strUserName.IsEmpty() || strPassword.IsEmpty())
94   {
95     CLog::Log(LOGERROR, "Last.fm stream selected but no username or password set.");
96     return false;
97   }
98   MD5_CTX md5state;
99   unsigned char md5pword[16];
100   MD5Init(&md5state);
101   MD5Update(&md5state, (unsigned char *)strPassword.c_str(), (int)strPassword.size());
102   MD5Final(md5pword, &md5state);
103   char tmp[33];
104   strncpy(tmp, "\0", sizeof(tmp));
105   for (int j = 0;j < 16;j++) 
106   {
107     char a[3];
108     sprintf(a, "%02x", md5pword[j]);
109     tmp[2*j] = a[0];
110     tmp[2*j+1] = a[1];
111   }
112   CStdString passwordmd5 = tmp;
113
114   CStdString url;
115   CUtil::URLEncode(strUserName);
116   url.Format("http://ws.audioscrobbler.com/radio/handshake.php?version=%s&platform=%s&username=%s&passwordmd5=%s&debug=%i&partner=%s", "0.1", "xbmc", strUserName, passwordmd5, 0, "");
117   if (!http.Get(url, html))
118   {
119     CLog::Log(LOGERROR, "Connect to Last.fm failed.");
120     return false;
121   }
122   CLog::DebugLog("Handshake: %s", html.c_str());
123
124   Parameter("session",    html, m_Session);
125   Parameter("stream_url", html, m_StreamUrl);
126   Parameter("base_url",   html, m_BaseUrl);
127   Parameter("base_path",  html, m_BasePath);
128   Parameter("subscriber", html, m_Subscriber);
129   Parameter("banned",     html, m_Banned);
130
131   if (m_Session == "failed")
132   {
133     CLog::Log(LOGERROR, "Last.fm return failed response, possible bad username or password?");
134     m_Session = "";
135     m_fileState.bHandshakeDone = false;
136   }
137   else 
138   {
139     m_fileState.bHandshakeDone = true;
140   }
141   return m_fileState.bHandshakeDone;
142 }
143
144 bool CFileLastFM::RecordToProfile(bool enabled)
145 {
146   CHTTP http;
147   CStdString url;
148   CStdString html;
149   url.Format("http://" + m_BaseUrl + m_BasePath + "/control.php?session=%s&command=%s&debug=%i", m_Session, enabled?"rtp":"nortp", 0);
150   if (!http.Get(url, html)) return false;
151   CLog::DebugLog("RTP: %s", html.c_str());
152   CStdString value;
153   Parameter("response", html, value);
154   return value == "OK";
155 }
156
157 bool CFileLastFM::ChangeStation(const CURL& stationUrl)
158 {
159   CStdString strUrl;
160   stationUrl.GetURL(strUrl);
161
162   CHTTP http;
163   CStdString url;
164   CStdString html;
165   url.Format("http://" + m_BaseUrl + m_BasePath + "/adjust.php?session=%s&url=%s&debug=%i", m_Session, strUrl, 0);
166   if (!http.Get(url, html)) 
167   {
168     CLog::Log(LOGERROR, "Connect to Last.fm to change station failed.");
169     return false;
170   }
171   CLog::DebugLog("ChangeStation: %s", html.c_str());
172
173   CStdString strErrorCode;
174   Parameter("error", html,  strErrorCode);
175   if (strErrorCode != "")
176   {
177     //int errCode = 
178     //if ( errCode > 0 )
179     //{
180     //    errorCode( errCode );
181     //}
182     CLog::Log(LOGERROR, "Last.fm returned an error (%s) response for change station request.", strErrorCode.c_str());
183     return false;
184   }
185   return true;
186 }
187
188 bool CFileLastFM::Open(const CURL& url, bool bBinary)
189 {
190   Object = NULL;
191   CGUIDialogProgress* dlgProgress = (CGUIDialogProgress*)m_gWindowManager.GetWindow(WINDOW_DIALOG_PROGRESS);
192
193   url.GetURL(m_Url);
194   m_Url.Replace(" ", "%20");
195   CStdString strUrl = m_Url;
196   CUtil::UrlDecode(strUrl);
197
198   Create();
199   if (dlgProgress)
200   {
201     dlgProgress->SetHeading("Last.fm");
202     dlgProgress->SetLine(0, 259);
203     dlgProgress->SetLine(1, strUrl);
204     dlgProgress->SetLine(2, "");
205     if (!dlgProgress->IsDialogRunning())
206       dlgProgress->StartModal();
207   }
208   
209   if (!m_fileState.bHandshakeDone)
210   {
211     m_fileState.Action      = ACTION_Handshaking;
212     m_fileState.bActionDone = false;
213     SetEvent(m_hWorkerEvent);
214     dlgProgress->SetLine(2, 15251);//Connecting to Last.fm...
215     while (!m_fileState.bError && !m_fileState.bActionDone && !dlgProgress->IsCanceled())
216     {
217       dlgProgress->Progress();
218       Sleep(100);
219     }
220     if (dlgProgress->IsCanceled() || m_fileState.bError)
221     {
222       if (dlgProgress) dlgProgress->Close();
223       Close();
224       return false;
225     }
226   }
227   m_fileState.Action      = ACTION_ChangingStation;
228   m_fileState.bActionDone = false;
229   SetEvent(m_hWorkerEvent);
230   dlgProgress->SetLine(2, 15252); // Selecting station...
231   while (!m_fileState.bError && !m_fileState.bActionDone && !dlgProgress->IsCanceled())
232   {
233     dlgProgress->Progress();
234     Sleep(100);
235   }
236   if (dlgProgress->IsCanceled() || m_fileState.bError)
237   {
238     if (dlgProgress) dlgProgress->Close();
239     Close();
240     return false;
241   }
242
243   dlgProgress->SetLine(2, 15107); //Buffering...
244   if (!OpenStream())
245   {
246     CLog::Log(LOGERROR, "Last.fm could not open stream.");
247     if (dlgProgress) dlgProgress->Close();
248     Close();
249     return false;
250   }
251   if (dlgProgress->IsCanceled() || m_fileState.bError)
252   {
253     if (m_fileState.bError)
254       CLog::Log(LOGERROR, "Last.fm streaming failed.");
255     if (dlgProgress) dlgProgress->Close();
256     Close();
257     return false;
258   }
259   
260   if (dlgProgress)
261   {
262     dlgProgress->SetLine(2, 261);
263     dlgProgress->Progress();
264     dlgProgress->Close();
265   }
266   m_bOpened = true;
267   return true;
268 }
269
270 bool CFileLastFM::Exists(const CURL& url)
271 {
272   CURL url2(url);
273   url2.SetProtocol("http");
274   CStdString strURL;
275   url2.GetURL(strURL);
276   return CFile::Exists(strURL);
277 }
278
279 bool CFileLastFM::OpenStream()
280 {
281   m_pFile = new CFileCurl();
282   if (!m_pFile) 
283   {
284     CLog::Log(LOGERROR, "Last.fm could not create new CFileCurl.");
285     return false;
286   }
287   
288   m_pFile->SetUserAgent("");
289   m_pFile->SetBufferSize(8192);
290   
291   if (!m_pFile->Open(CURL(m_StreamUrl), true))
292   {
293     CLog::Log(LOGERROR, "Last.fm could not open url %s.", m_StreamUrl.c_str());
294     delete m_pFile;
295     m_pFile = NULL;
296     return false;
297   }
298   return true;
299 }
300
301 int CFileLastFM::SyncReceived(const char* data, unsigned int size)
302 {
303   if (size < 4) return false;
304
305   unsigned int i = 0;
306   const char* pSync = data;
307   while (i <= size - 4)
308   {
309     if ( 
310       (pSync[0] == 'S') &&
311       (pSync[1] == 'Y') && 
312       (pSync[2] == 'N') &&
313       (pSync[3] == 'C')
314       )
315     {
316       CLog::Log(LOGDEBUG, "Last.fm SYNC found.");
317       return i;
318     }
319     pSync++;
320     i++;
321   }
322   return -1;
323 }
324
325 unsigned int CFileLastFM::Read(void* lpBuf, __int64 uiBufSize)
326 {
327   if (!m_bOpened) return 0;
328   unsigned int read = 0;
329   unsigned int tms = 0;
330   read = m_pFile->Read(lpBuf, uiBufSize);
331   if (read == 0) return 0;
332   char* data = (char*)lpBuf;
333   //copy first 3 chars to syncbuffer, might be "YNC"
334   memcpy(m_pSyncBuffer + 3, (char*)lpBuf, 3);
335   int iSyncPos = -1;
336   if ((iSyncPos = SyncReceived(m_pSyncBuffer, 6)) != -1)
337   {
338     m_bDirectSkip = false;
339     memmove(data, data + iSyncPos + 1, read - (iSyncPos + 1));
340     read -= (iSyncPos + 1);
341     m_fileState.Action = ACTION_RetreivingMetaData;
342     SetEvent(m_hWorkerEvent);
343   }
344   else if ((iSyncPos = SyncReceived(data, read)) != -1)
345   {
346     if (m_bDirectSkip)
347     {
348       read -= (iSyncPos + 4);
349       memmove(data, data + iSyncPos + 4, read);
350       //clear whatever is in the buffer and continue with the new track after a skip.
351       if (Object != NULL)
352       {
353         CRingHoldBuffer* ringHoldBuffer = (CRingHoldBuffer*)Object;
354         ringHoldBuffer->Clear();
355       }
356       m_bDirectSkip = false;
357     }
358     else
359     {
360       memmove(data + iSyncPos, data + iSyncPos + 4, read - (iSyncPos + 4));
361       read -= 4;
362     }
363     m_fileState.Action = ACTION_RetreivingMetaData;
364     SetEvent(m_hWorkerEvent);
365   }
366   //copy last 3 chars of data to first three chars of syncbuffer, might be "SYN"
367   memcpy(m_pSyncBuffer, (char*)lpBuf + max(0, read - 3), 3);
368   return read;
369 }
370
371 __int64 CFileLastFM::Seek(__int64 iFilePosition, int iWhence)
372 {
373   return 0;
374 }
375
376
377 bool CFileLastFM::DoSkipNext()
378 {
379   m_bDirectSkip = true;
380   CHTTP http;
381   CStdString url;
382   CStdString html;
383   url.Format("http://" + m_BaseUrl + m_BasePath + "/control.php?session=%s&command=skip&debug=%i", m_Session, 0);
384   if (!http.Get(url, html)) return false;
385   CLog::DebugLog("Skip: %s", html.c_str());
386   CStdString value;
387   Parameter("response", html, value);
388   if (value == "OK")
389   {
390     return true;
391   }
392   m_bDirectSkip = false;
393   return false;
394 }
395
396 bool CFileLastFM::SkipNext()
397 {
398   if (m_bDirectSkip) return true; //already skipping
399   if (m_fileState.Action == ACTION_Idle)
400   {
401     m_fileState.Action = ACTION_SkipNext;
402     SetEvent(m_hWorkerEvent);
403   }
404   return true; //only to indicate we handle the skipnext
405 }
406
407 void CFileLastFM::Close()
408 {
409   Object = NULL;
410   if (m_ThreadHandle)
411   {
412     m_bStop = true;
413     SetEvent(m_hWorkerEvent);
414     StopThread();
415   }
416
417   if (m_pFile)
418   {
419     m_pFile->Close();
420     delete m_pFile;
421     m_pFile = NULL;
422   }
423   m_bOpened = false;
424
425   CLog::DebugLog("LastFM closed");
426 }
427
428 bool CFileLastFM::RetreiveMetaData()
429 {
430   DWORD m_dwTime = timeGetTime();
431
432   CHTTP http;
433   CMusicInfoTag tag;
434   
435   CStdString url;
436   CStdString html;
437   CStdString value;
438   //CStdString convertedvalue;
439   url.Format("http://" + m_BaseUrl + m_BasePath + "/np.php?session=%s&debug=%i", m_Session, 0 );
440   if (!http.Get(url, html)) return false;
441   CLog::DebugLog("MetaData: %s", html.c_str());
442   Parameter("artist", html, value);
443   tag.SetArtist(value);
444   //CStdString station;
445   //Parameter("station", html, station);
446   //tag.SetArtist(station + " - " + value);
447   Parameter("album", html, value);
448   tag.SetAlbum(value);
449   Parameter("track", html, value);
450   tag.SetTitle(value);
451   Parameter("trackduration", html, value);
452   tag.SetDuration(atoi(value.c_str()));
453   Parameter("albumcover_medium", html, value);
454   CStdString coverUrl = value;
455
456   const CMusicInfoTag* currenttag = g_infoManager.GetCurrentSongTag();
457   if (!currenttag ||
458     ((currenttag->GetAlbum() != tag.GetAlbum()) ||
459     (currenttag->GetArtist() != tag.GetArtist()) ||
460     (currenttag->GetTitle() != tag.GetTitle()))
461     )
462   {
463     CStdString cachedFile = "";
464     if ((coverUrl != "") && (coverUrl.Find("no_album") == -1) && (coverUrl.Find("noalbum") == -1) && (coverUrl.Right(1) != "/") && (coverUrl.Find(".gif") == -1))
465     {
466       Crc32 crc;
467       crc.ComputeFromLowerCase(coverUrl);
468       cachedFile.Format("%s\\%08x.tbn", g_settings.GetLastFMThumbFolder().c_str(), crc);
469       if (!CFile::Exists(cachedFile))
470       {
471         http.Download(coverUrl, cachedFile);
472       }
473     }
474     g_infoManager.SetCurrentAlbumThumb(cachedFile);
475     tag.SetLoaded();
476     g_infoManager.SetCurrentSongTag(tag);
477
478     //inform app a new track has started, and reset our playtime
479     CGUIMessage msg(GUI_MSG_PLAYBACK_STARTED, 0, 0, 0, 0, NULL);
480     m_gWindowManager.SendThreadMessage(msg);
481     g_application.ResetPlayTime();
482
483     //check recordtoprofile, only update if server has wrong setting
484     Parameter("recordtoprofile", html, value);
485     bool bRTP = g_guiSettings.GetBool("lastfm.recordtoprofile");
486     if ((value == "1" && !bRTP) || (value == "0" && bRTP))
487       RecordToProfile(bRTP);
488     return true;
489   }
490   return false;
491 }
492
493 void CFileLastFM::Process()
494 {
495   while (!m_bStop)
496   {
497     WaitForSingleObject(m_hWorkerEvent, INFINITE);
498     if (m_bStop)
499       break;
500     switch (m_fileState.Action)
501     {
502     case ACTION_Handshaking:
503       if (!HandShake())
504         m_fileState.bError = true;
505       else
506         m_fileState.bActionDone = true;
507       m_fileState.Action = ACTION_Idle;
508       break;
509     case ACTION_ChangingStation:
510       if (!ChangeStation(m_Url))
511         m_fileState.bError = true;
512       else
513         m_fileState.bActionDone = true;
514       m_fileState.Action = ACTION_Idle;
515       break;
516     case ACTION_RetreivingMetaData:
517       if (!RetreiveMetaData())
518         m_fileState.bError = true;
519       else
520         m_fileState.bActionDone = true;
521       m_fileState.Action = ACTION_Idle;
522       break;
523     case ACTION_SkipNext:
524       if (!DoSkipNext())
525         m_fileState.bError = true;
526       else
527         m_fileState.bActionDone = true;
528       m_fileState.Action = ACTION_Idle;
529       break;
530     }
531   }
532 }
533
534 }