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