xref: /MusicPlayer2/MusicPlayer2/LastFM.cpp (revision 45230f7761877b04341d3bd7a9e408b73e17af89)
1 #include "stdafx.h"
2 #include "LastFM.h"
3 #include "md5.h"
4 #include "InternetCommon.h"
5 #include "tinyxml2/tinyxml2.h"
6 #include "MusicPlayer2.h"
7 #include <inttypes.h>
8 
9 using namespace tinyxml2;
10 
11 class XMLHelper {
12 public:
13     tinyxml2::XMLDocument doc;
XMLHelper(std::wstring data)14     XMLHelper(std::wstring data) {
15         auto s = CCommon::UnicodeToStr(data, CodeType::UTF8);
16         doc.Parse(s.c_str(), s.size());
17     }
HasError()18     bool HasError() {
19         if (doc.Error() || !status()) return true;
20         return false;
21     }
PrintError()22     void PrintError() {
23         if (doc.Error()) {
24             theApp.WriteLog(CCommon::StrToUnicode(doc.ErrorStr()));
25         } else if (!status()) {
26             wchar_t msg[64];
27             swprintf(msg, 64, L"Last FM API returned code %i.", error_code());
28             theApp.WriteLog(msg);
29             auto emsg = error_msg();
30             theApp.WriteLog(emsg ? CCommon::StrToUnicode(emsg, CodeType::UTF8) : L"Failed to get error message.");
31         }
32     }
CorrectData(tinyxml2::XMLElement * parent,const char * tag_name,std::wstring & data)33     void CorrectData(tinyxml2::XMLElement* parent, const char* tag_name, std::wstring& data) {
34         auto ele = FindElement(parent, tag_name);
35         if (!ele) return;
36         auto attr = ele->FindAttribute("corrected");
37         if (!attr) return;
38         auto corrected = attr->IntValue();
39         if (corrected) {
40             auto text = ele->GetText();
41             if (text) {
42                 data = CCommon::StrToUnicode(text, CodeType::UTF8);
43             }
44         }
45     }
status()46     bool status() {
47         auto root = doc.RootElement();
48         if (!root) return false;
49         auto attr = root->FindAttribute("status");
50         if (!attr) return false;
51         auto status = attr->Value();
52         if (!status) return false;
53         return !strcmp(status, "ok") ? true : false;
54     }
FindElement(tinyxml2::XMLElement * parent,const char * tag_name)55     tinyxml2::XMLElement* FindElement(tinyxml2::XMLElement* parent, const char* tag_name) {
56         if (!parent) return nullptr;
57         auto child = parent->FirstChildElement();
58         if (!child) return nullptr;
59         do {
60             auto name = child->Name();
61             if (name && !strcmp(name, tag_name)) {
62                 return child;
63             }
64             child = child->NextSiblingElement();
65         } while (child != nullptr);
66         return nullptr;
67     }
error_code()68     int error_code() {
69         auto ele = FindElement(doc.RootElement(), "error");
70         if (!ele) return 0;
71         auto attr = ele->FindAttribute("code");
72         if (!attr) return 0;
73         return attr->IntValue();
74     }
error_msg()75     const char* error_msg() {
76         auto ele = FindElement(doc.RootElement(), "error");
77         if (!ele) return nullptr;
78         return ele->GetText();
79     }
token()80     const char* token() {
81         auto ele = FindElement(doc.RootElement(), "token");
82         if (!ele) return nullptr;
83         return ele->GetText();
84     }
session()85     tinyxml2::XMLElement* session() {
86         return FindElement(doc.RootElement(), "session");
87     }
session_key()88     const char* session_key() {
89         auto ele = FindElement(session(), "key");
90         if (!ele) return nullptr;
91         return ele->GetText();
92     }
session_name()93     const char* session_name() {
94         auto ele = FindElement(session(), "name");
95         if (!ele) return nullptr;
96         return ele->GetText();
97     }
nowplaying()98     tinyxml2::XMLElement* nowplaying() {
99         return FindElement(doc.RootElement(), "nowplaying");
100     }
scrobbles()101     tinyxml2::XMLElement* scrobbles() {
102         return FindElement(doc.RootElement(), "scrobbles");
103     }
PrintIgnoredMessage(tinyxml2::XMLElement * parent)104     void PrintIgnoredMessage(tinyxml2::XMLElement* parent) {
105         auto ele = FindElement(parent, "ignoredMessage");
106         if (!ele) return;
107         auto attr = ele->FindAttribute("code");
108         if (!attr) return;
109         auto code = attr->IntValue();
110         if (code) {
111             theApp.WriteLog(L"Last fm: Some data was ignored.");
112             const auto& msg = ele->GetText() ? CCommon::StrToUnicode(ele->GetText(), CodeType::UTF8) : L"";
113             theApp.WriteLog(msg);
114         }
115     }
116 };
117 
LastFM()118 LastFM::LastFM() {
119     api_key = LASTFM_API_KEY;
120     shared_secret = LASTFM_SHARED_SECRET;
121     mutex = CreateMutexA(NULL, FALSE, NULL);
122     if (!mutex) {
123         throw std::runtime_error("Failed to create mutex object.");
124     }
125 }
126 
~LastFM()127 LastFM::~LastFM() {
128     CloseHandle(mutex);
129 }
130 
GenerateApiSig(map<wstring,wstring> & params)131 void LastFM::GenerateApiSig(map<wstring, wstring>& params) {
132     MD5 md5;
133     for (const auto& param : params) {
134         if (param.first != L"api_sig") {
135             md5.Update(param.first);
136             md5.Update(param.second);
137         }
138     }
139     md5.Update(shared_secret);
140     md5.Finalize();
141     params[L"api_sig"] = CCommon::StrToUnicode(md5.HexDigest());
142 }
143 
GetToken()144 wstring LastFM::GetToken() {
145     map<wstring, wstring> params = { {L"api_key", api_key}, { L"method", L"auth.getToken" } };
146     GenerateApiSig(params);
147     wstring result;
148     if (!CInternetCommon::GetURL(GetUrl(params), result, true, true)) return L"";
149     OutputDebugStringW(result.c_str());
150     XMLHelper helper(result);
151     if (helper.HasError()) {
152         theApp.WriteLog(L"Error in LastFM::GetToken().");
153         helper.PrintError();
154         return L"";
155     }
156     auto token = helper.token();
157     return token ? CCommon::StrToUnicode(token, CodeType::UTF8) : L"";
158 }
159 
GetUrl(map<wstring,wstring> & params,wstring base)160 wstring LastFM::GetUrl(map<wstring, wstring>& params, wstring base) {
161     auto url(base);
162     bool first = true;
163     for (const auto& param : params) {
164         if (!first) {
165             url += L"&";
166         }
167         url += CCommon::EncodeURIComponent(param.first) + L"=" + CCommon::EncodeURIComponent(param.second);
168         first = false;
169     }
170     return url;
171 }
172 
GetRequestAuthorizationUrl(wstring token)173 wstring LastFM::GetRequestAuthorizationUrl(wstring token) {
174     if (token.empty()) {
175         return L"";
176     }
177     map<wstring, wstring> params = { { L"api_key", api_key }, { L"token", token } };
178     return GetUrl(params, L"https://www.last.fm/api/auth/?");
179 }
180 
HasSessionKey()181 bool LastFM::HasSessionKey() {
182     return !ar.session_key.empty();
183 }
184 
GetSession(wstring token)185 bool LastFM::GetSession(wstring token) {
186     map<wstring, wstring> params = {{L"api_key", api_key}, {L"method", L"auth.getSession"}, {L"token", token}};
187     GenerateApiSig(params);
188     wstring result;
189     if (!CInternetCommon::GetURL(GetUrl(params), result, true, true)) return L"";
190 #ifdef _DEBUG
191     OutputDebugStringW(result.c_str());
192 #endif
193     XMLHelper helper(result);
194     if (helper.HasError()) {
195         theApp.WriteLog(L"Error in LastFM::GetSession().");
196         helper.PrintError();
197         return false;
198     }
199     auto session_key = helper.session_key();
200     auto session_name = helper.session_name();
201     if (!session_key || !session_name) return false;
202     ar.session_key = CCommon::StrToUnicode(session_key, CodeType::UTF8);
203     ar.user_name = CCommon::StrToUnicode(session_name, CodeType::UTF8);
204     return true;
205 }
206 
UserName()207 wstring LastFM::UserName() {
208     return ar.user_name;
209 }
210 
UpdateNowPlaying(LastFMTrack track,LastFMTrack & corrected_track)211 bool LastFM::UpdateNowPlaying(LastFMTrack track, LastFMTrack& corrected_track) {
212     if (track.artist.empty() || track.track.empty() || ar.session_key.empty()) return false;
213     map <wstring, wstring> params = {{L"api_key", api_key}, {L"method", L"track.updateNowPlaying"}, {L"sk", ar.session_key}, {L"artist", track.artist}, {L"track", track.track}};
214     if (!track.album.empty()) {
215         params[L"album"] = track.album;
216     }
217     if (track.trackNumber) {
218         wchar_t tmp[64];
219         wsprintf(tmp, L"%" PRIu16, track.trackNumber);
220         params[L"trackNumber"] = wstring(tmp);
221     }
222     if (!track.mbid.empty()) {
223         params[L"mbid"] = track.mbid;
224     }
225     auto duration = track.duration.toInt() / 1000;
226     if (duration) {
227         wchar_t tmp[64];
228         wsprintf(tmp, L"%i", duration);
229         params[L"duration"] = wstring(tmp);
230     }
231     if (!track.albumArtist.empty()) {
232         params[L"albumArtist"] = track.albumArtist;
233     }
234     GenerateApiSig(params);
235     wstring result;
236     wstring ContentType(L"Content-Type: application/x-www-form-urlencoded\r\n");
237     if (CInternetCommon::HttpPost(GetDefaultBase(), result, GetUrl(params, L""), ContentType, true)) return false;
238     OutputDebugStringW(result.c_str());
239     XMLHelper helper(result);
240     if (helper.HasError()) {
241         theApp.WriteLog(L"Error in LastFM::UpdateNowPlaying().");
242         helper.PrintError();
243         return false;
244     }
245     auto nowplaying = helper.nowplaying();
246     if (!nowplaying) return false;
247     helper.CorrectData(nowplaying, "track", corrected_track.track);
248     helper.CorrectData(nowplaying, "artist", corrected_track.artist);
249     helper.CorrectData(nowplaying, "album", corrected_track.album);
250     helper.CorrectData(nowplaying, "albumArtist", corrected_track.albumArtist);
251     helper.PrintIgnoredMessage(nowplaying);
252     return true;
253 }
254 
UpdateNowPlaying()255 bool LastFM::UpdateNowPlaying() {
256     return UpdateNowPlaying(ar.current_track, ar.corrected_current_track);
257 }
258 
UpdateCurrentTrack(LastFMTrack track)259 void LastFM::UpdateCurrentTrack(LastFMTrack track) {
260     ar.current_track = LastFMTrack(track);
261     ar.corrected_current_track = LastFMTrack(track);
262     ar.current_played_time = 0;
263     ar.is_pushed = false;
264 }
265 
GetPostData(map<wstring,wstring> & params)266 wstring LastFM::GetPostData(map<wstring, wstring>& params) {
267     tinyxml2::XMLDocument doc;
268     auto root = doc.NewElement("methodCall");
269     if (!root) return L"";
270     doc.InsertFirstChild(root);
271     auto& method = params[L"method"];
272     if (method.empty()) return L"";
273     auto methodName = doc.NewElement("methodName");
274     if (!methodName) return L"";
275     methodName->SetText(CCommon::UnicodeToStr(method, CodeType::UTF8_NO_BOM).c_str());
276     root->InsertEndChild(methodName);
277     auto paramse = doc.NewElement("params");
278     if (!paramse) return L"";
279     root->InsertEndChild(paramse);
280     auto parame = doc.NewElement("param");
281     if (!parame) return L"";
282     paramse->InsertEndChild(parame);
283     auto value = doc.NewElement("value");
284     if (!value) return L"";
285     parame->InsertEndChild(value);
286     auto structe = doc.NewElement("struct");
287     if (!structe) return L"";
288     value->InsertEndChild(structe);
289     for (auto& param : params) {
290         if (param.first == L"method") continue;
291         auto member = doc.NewElement("member");
292         if (!member) return L"";
293         structe->InsertEndChild(member);
294         auto name = doc.NewElement("name");
295         if (!name) return L"";
296         name->SetText(CCommon::UnicodeToStr(param.first, CodeType::UTF8_NO_BOM).c_str());
297         member->InsertEndChild(name);
298         auto value = doc.NewElement("value");
299         if (!value) return L"";
300         member->InsertEndChild(value);
301         auto s = doc.NewElement("string");
302         if (!s) return L"";
303         s->SetText(CCommon::UnicodeToStr(param.second, CodeType::UTF8_NO_BOM).c_str());
304         value->InsertEndChild(s);
305     }
306     tinyxml2::XMLPrinter printer;
307     doc.Print(&printer);
308     string tmp(printer.CStr(), printer.CStrSize());
309     return CCommon::StrToUnicode(tmp, CodeType::UTF8_NO_BOM);
310 }
311 
CurrentTrack()312 const LastFMTrack& LastFM::CurrentTrack() {
313     return ar.current_track;
314 }
315 
CorrectedCurrentTrack()316 const LastFMTrack& LastFM::CorrectedCurrentTrack() {
317     return ar.corrected_current_track;
318 }
319 
Love(wstring track,wstring artist)320 bool LastFM::Love(wstring track, wstring artist) {
321     if (track.empty() || artist.empty() || ar.session_key.empty()) return false;
322     map <wstring, wstring> params = { {L"api_key", api_key}, {L"method", L"track.love"}, {L"sk", ar.session_key}, {L"artist", artist}, {L"track", track} };
323     GenerateApiSig(params);
324     wstring result;
325     wstring ContentType(L"Content-Type: application/x-www-form-urlencoded\r\n");
326     if (CInternetCommon::HttpPost(GetDefaultBase(), result, GetUrl(params, L""), ContentType, true)) return false;
327     OutputDebugStringW(result.c_str());
328     XMLHelper helper(result);
329     if (helper.HasError()) {
330         theApp.WriteLog(L"Error in LastFM::Love().");
331         helper.PrintError();
332         return false;
333     }
334     return true;
335 }
336 
Love()337 bool LastFM::Love() {
338     return Love(ar.corrected_current_track.track, ar.corrected_current_track.artist);
339 }
340 
Unlove(wstring track,wstring artist)341 bool LastFM::Unlove(wstring track, wstring artist) {
342     if (track.empty() || artist.empty() || ar.session_key.empty()) return false;
343     map <wstring, wstring> params = { {L"api_key", api_key}, {L"method", L"track.unlove"}, {L"sk", ar.session_key}, {L"artist", artist}, {L"track", track} };
344     GenerateApiSig(params);
345     wstring result;
346     wstring ContentType(L"Content-Type: application/x-www-form-urlencoded\r\n");
347     if (CInternetCommon::HttpPost(GetDefaultBase(), result, GetUrl(params, L""), ContentType, true)) return false;
348     OutputDebugStringW(result.c_str());
349     XMLHelper helper(result);
350     if (helper.HasError()) {
351         theApp.WriteLog(L"Error in LastFM::Unlove().");
352         helper.PrintError();
353         return false;
354     }
355     return true;
356 }
357 
Unlove()358 bool LastFM::Unlove() {
359     return Unlove(ar.corrected_current_track.track, ar.corrected_current_track.artist);
360 }
361 
362 #define RETURN_AND_RELEASE_MUTEX(value) return ReleaseMutex(mutex), value;
363 
Scrobble(std::list<LastFMTrack> & tracks)364 bool LastFM::Scrobble(std::list<LastFMTrack>& tracks) {
365     DWORD dw = WaitForSingleObject(mutex, 1000);
366     if (dw != WAIT_OBJECT_0) {
367         return false;
368     }
369     if (tracks.empty() || ar.session_key.empty()) RETURN_AND_RELEASE_MUTEX(false)
370     map <wstring, wstring> params = { {L"api_key", api_key}, {L"method", L"track.scrobble"}, {L"sk", ar.session_key} };
371     int i = 0;
372     for (auto& track: tracks) {
373         if (i >= 50) break;
374         wchar_t key[64], tmp[64];
375         if (track.artist.empty() || track.track.empty() || !track.timestamp) continue;
376         wsprintf(key, L"artist[%i]", i);
377         params[key] = track.artist;
378         wsprintf(key, L"track[%i]", i);
379         params[key] = track.track;
380         wsprintf(key, L"timestamp[%i]", i);
381         char tmp2[64];
382         // wsprintf �޷���ȷʶ��PRIu64
383         sprintf_s(tmp2, "%" PRIu64, track.timestamp);
384         params[key] = CCommon::StrToUnicode(tmp2);
385         if (!track.album.empty()) {
386             wsprintf(key, L"album[%i]", i);
387             params[key] = track.album;
388         }
389         if (!track.streamId.empty()) {
390             wsprintf(key, L"streamId[%i]", i);
391             params[key] = track.streamId;
392         }
393         wsprintf(key, L"chosenByUser[%i]", i);
394         params[key] = track.chosenByUser ? L"1" : L"0";
395         if (track.trackNumber) {
396             wsprintf(key, L"trackNumber[%i]", i);
397             wsprintf(tmp, L"%" PRIu16, track.trackNumber);
398             params[key] = tmp;
399         }
400         if (!track.mbid.empty()) {
401             wsprintf(key, L"mbid[%i]", i);
402             params[key] = track.mbid;
403         }
404         if (!track.albumArtist.empty()) {
405             wsprintf(key, L"albumArtist[%i]", i);
406             params[key] = track.albumArtist;
407         }
408         auto duration = track.duration.toInt();
409         if (duration) {
410             wsprintf(key, L"duration[%i]", i);
411             wsprintf(tmp, L"%i", duration / 1000);
412             params[key] = tmp;
413         }
414         i++;
415     }
416     if (i == 0) {
417         tracks.clear();
418         RETURN_AND_RELEASE_MUTEX(false)
419     }
420     GenerateApiSig(params);
421     wstring result;
422     wstring ContentType(L"Content-Type: application/x-www-form-urlencoded\r\n");
423     if (CInternetCommon::HttpPost(GetDefaultBase(), result, GetUrl(params, L""), ContentType, true)) RETURN_AND_RELEASE_MUTEX(false)
424     OutputDebugStringW(result.c_str());
425     XMLHelper helper(result);
426     if (helper.HasError()) {
427         theApp.WriteLog(L"Error in LastFM::Scrobble().");
428         helper.PrintError();
429         RETURN_AND_RELEASE_MUTEX(false)
430     }
431     while (i > 0) {
432         if (tracks.empty()) break;
433         auto& track = tracks.front();
434         if (track.artist.empty() || track.track.empty() || !track.timestamp) {
435             tracks.pop_front();
436             continue;
437         }
438         tracks.pop_front();
439         i--;
440     }
441     auto scrobbles = helper.scrobbles();
442     if (!scrobbles) RETURN_AND_RELEASE_MUTEX(false)
443     auto scrobble = scrobbles->FirstChildElement();
444     if (!scrobble) RETURN_AND_RELEASE_MUTEX(true)
445     do {
446         helper.PrintIgnoredMessage(scrobble);
447         scrobble = scrobble->NextSiblingElement();
448     } while (scrobble != nullptr);
449     RETURN_AND_RELEASE_MUTEX(true)
450 }
451 
Scrobble()452 bool LastFM::Scrobble() {
453     return Scrobble(ar.cached_tracks);
454 }
455 
PushCurrentTrackToCache()456 bool LastFM::PushCurrentTrackToCache() {
457     if (ar.is_pushed) return false;
458     ar.cached_tracks.push_back(LastFMTrack(ar.corrected_current_track));
459     ar.is_pushed = true;
460     return true;
461 }
462 
AddCurrentPlayedTime(int millisec)463 void LastFM::AddCurrentPlayedTime(int millisec) {
464     ar.current_played_time += millisec;
465 }
466 
CurrentPlayedTime()467 int32_t LastFM::CurrentPlayedTime() {
468     return ar.current_played_time;
469 }
470 
IsPushed()471 bool LastFM::IsPushed() {
472     return ar.is_pushed;
473 }
474 
IsScrobbeable()475 bool LastFM::IsScrobbeable() {
476     return static_cast<int>(ar.cached_tracks.size()) >= theApp.m_media_lib_setting_data.lastfm_auto_scrobble_min;
477 }
478 
CurrentTrackScrobbleable()479 bool LastFM::CurrentTrackScrobbleable() {
480     auto track_duration = ar.corrected_current_track.duration.toInt();
481     int32_t least_listened = min(max(track_duration * theApp.m_media_lib_setting_data.lastfm_least_perdur / 100, theApp.m_media_lib_setting_data.lastfm_least_dur * 1000), track_duration);
482     return ar.current_played_time > least_listened;
483 }
484 
CachedCount()485 size_t LastFM::CachedCount() {
486     return ar.cached_tracks.size();
487 }
488 
GetDefaultBase()489 wstring LastFM::GetDefaultBase() {
490     if (theApp.m_media_lib_setting_data.lastfm_enable_https) {
491         return L"https://ws.audioscrobbler.com/2.0/?";
492     } else {
493         return L"http://ws.audioscrobbler.com/2.0/?";
494     }
495 }
496 
GetUrl(map<wstring,wstring> & params)497 wstring LastFM::GetUrl(map<wstring, wstring>& params) {
498     return GetUrl(params, GetDefaultBase());
499 }
500