00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00031 #include "stdinc.h"
00032
00033 #include <curl/curl.h>
00034 #include <curl/easy.h>
00035
00036 #include "audio_file.h"
00037 #include "config.h"
00038 #include "gui.h"
00039 #include "md5.h"
00040 #include "scrobbler.h"
00041 #include "util.h"
00042 #include "vfs.h"
00043
00047 #define SCROBBLER_URL "http://%s/?hs=true&p=1.2&c=her&v=0.1&u=%s&t=%u&a=%s"
00048
00053 static char scrobbler_enabled = 0;
00054
00058 static GMutex *scrobbler_lock;
00063 static GCond *scrobbler_avail;
00067 static GThread *scrobbler_runner;
00068
00073 struct scrobbler_entry {
00078 char *artist;
00083 char *title;
00088 char *album;
00092 unsigned int length;
00096 time_t time;
00097
00101 struct scrobbler_entry *next;
00102 };
00106 static struct scrobbler_entry *scrobbler_queue_first = NULL;
00110 static struct scrobbler_entry *scrobbler_queue_last = NULL;
00111
00115 static inline struct scrobbler_entry *
00116 scrobbler_queue_next(struct scrobbler_entry *se)
00117 {
00118 return (se->next);
00119 }
00120
00124 static inline void
00125 scrobbler_queue_insert_tail(struct scrobbler_entry *se)
00126 {
00127 se->next = NULL;
00128 if (scrobbler_queue_last != NULL)
00129 scrobbler_queue_last->next = se;
00130 else
00131 scrobbler_queue_first = se;
00132 scrobbler_queue_last = se;
00133 }
00134
00138 #define SCROBBLER_QUEUE_FOREACH(se) \
00139 for (se = scrobbler_queue_first; se != NULL; se = se->next)
00140
00144 static inline void
00145 scrobbler_queue_remove_head(void)
00146 {
00147 g_assert(scrobbler_queue_first != NULL);
00148
00149 if (scrobbler_queue_first == scrobbler_queue_last)
00150 scrobbler_queue_last = NULL;
00151 scrobbler_queue_first = scrobbler_queue_first->next;
00152 }
00153
00154 void
00155 scrobbler_notify_read(struct audio_file *fd, int eof)
00156 {
00157 struct scrobbler_entry *nse;
00158 unsigned int len;
00159
00160
00161 if (!scrobbler_enabled || fd->stream || fd->_scrobbler_done)
00162 return;
00163
00164 if (eof) {
00165
00166 len = fd->time_cur;
00167 } else {
00168
00169 if ((fd->time_cur < 240) &&
00170 (fd->time_cur < (fd->time_len / 2)))
00171 return;
00172
00173 len = fd->time_len;
00174 }
00175
00176
00177 if (len < 30)
00178 return;
00179
00180
00181 fd->_scrobbler_done = 1;
00182
00183
00184 if (fd->title == NULL || (fd->artist == NULL && fd->album == NULL))
00185 return;
00186
00187
00188 nse = g_slice_new(struct scrobbler_entry);
00189 nse->artist = http_escape(fd->artist, NULL);
00190 nse->title = http_escape(fd->title, NULL);
00191 nse->album = http_escape(fd->album, NULL);
00192 nse->length = len;
00193 nse->time = time(NULL);
00194
00195 g_mutex_lock(scrobbler_lock);
00196 scrobbler_queue_insert_tail(nse);
00197 g_cond_signal(scrobbler_avail);
00198 g_mutex_unlock(scrobbler_lock);
00199 }
00200
00201 void
00202 scrobbler_notify_seek(struct audio_file *fd)
00203 {
00204 fd->_scrobbler_done = 1;
00205 }
00206
00212 static unsigned int
00213 scrobbler_queue_fetch(const char key[32], char **poststr)
00214 {
00215 struct scrobbler_entry *ent;
00216 unsigned int len;
00217 GString *str;
00218
00219 g_mutex_lock(scrobbler_lock);
00220 while ((ent = scrobbler_queue_first) == NULL)
00221 g_cond_wait(scrobbler_avail, scrobbler_lock);
00222
00223 str = g_string_new("s=");
00224 g_string_append_len(str, key, 32);
00225
00226
00227 for (len = 0; (len < 50) && (ent != NULL); len++) {
00228 g_string_append_printf(str,
00229 "&a[%u]=%s&t[%u]=%s&i[%u]=%u&o[%u]=P&r[%u]=&l[%u]=%u&b[%u]=%s&n[%u]=&m[%u]=",
00230 len, ent->artist,
00231 len, ent->title,
00232 len, (unsigned int)ent->time,
00233 len,
00234 len,
00235 len, ent->length,
00236 len, ent->album,
00237 len,
00238 len);
00239
00240
00241 ent = scrobbler_queue_next(ent);
00242 }
00243
00244 g_mutex_unlock(scrobbler_lock);
00245
00246 *poststr = g_string_free(str, FALSE);
00247
00248
00249 g_assert(len >= 1);
00250 return (len);
00251 }
00252
00256 static void
00257 scrobbler_queue_item_free(struct scrobbler_entry *ent)
00258 {
00259 g_free(ent->artist);
00260 g_free(ent->title);
00261 g_free(ent->album);
00262 g_slice_free(struct scrobbler_entry, ent);
00263 }
00264
00269 static void
00270 scrobbler_queue_remove(unsigned int amount)
00271 {
00272 int i;
00273 struct scrobbler_entry *ent;
00274
00275 g_mutex_lock(scrobbler_lock);
00276 for (i = amount; i > 0; i--) {
00277 ent = scrobbler_queue_first;
00278 scrobbler_queue_remove_head();
00279 scrobbler_queue_item_free(ent);
00280 }
00281 g_mutex_unlock(scrobbler_lock);
00282 }
00283
00287 static void
00288 scrobbler_hash(time_t t, char out[32])
00289 {
00290 char tstr[16];
00291 unsigned char bin_res[16];
00292 struct md5_context ctx = MD5CONTEXT_INITIALIZER;
00293
00294
00295
00296
00297 md5_update(&ctx, config_getopt("scrobbler.password"), 32);
00298 sprintf(tstr, "%u", (unsigned int)t);
00299 md5_update(&ctx, tstr, strlen(tstr));
00300 md5_final(&ctx, bin_res);
00301
00302
00303
00304
00305 hex_encode(bin_res, out, sizeof bin_res);
00306 }
00307
00311 static size_t
00312 scrobbler_curl_concat(void *ptr, size_t size, size_t nmemb, void *stream)
00313 {
00314 GString *response = stream;
00315 size_t len;
00316
00317 len = size * nmemb;
00318 g_string_append_len(response, ptr, len);
00319
00320 return (len);
00321 }
00322
00326 static void
00327 scrobbler_split_lines(char *str, char *lines[], unsigned int nlines)
00328 {
00329 unsigned int i;
00330
00331 for (i = 0; i < nlines; i++) {
00332
00333 lines[i] = str;
00334
00335
00336
00337
00338
00339 if (str != NULL) {
00340 str = strchr(str, '\n');
00341 if (str != NULL)
00342 *str++ = '\0';
00343 }
00344 }
00345 }
00346
00351 static int
00352 scrobbler_send_handshake(char *key, char **url)
00353 {
00354 char *hsurl;
00355 char hskey[33];
00356 time_t hstime;
00357 CURL *con;
00358 GString *response;
00359 char *lines[4];
00360 int ret;
00361
00362 con = curl_easy_init();
00363 if (con == NULL)
00364 return (-1);
00365
00366
00367 hstime = time(NULL);
00368 scrobbler_hash(hstime, hskey);
00369 hskey[32] = '\0';
00370 hsurl = g_strdup_printf(SCROBBLER_URL,
00371 config_getopt("scrobbler.hostname"),
00372 config_getopt("scrobbler.username"),
00373 (unsigned int)hstime, hskey);
00374 curl_easy_setopt(con, CURLOPT_URL, hsurl);
00375 curl_easy_setopt(con, CURLOPT_USERAGENT, APP_NAME "/" APP_VERSION);
00376
00377
00378 response = g_string_sized_new(128);
00379 curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, scrobbler_curl_concat);
00380 curl_easy_setopt(con, CURLOPT_WRITEDATA, response);
00381
00382
00383 ret = curl_easy_perform(con);
00384 curl_easy_cleanup(con);
00385 g_free(hsurl);
00386
00387
00388 if (ret != 0)
00389 goto done;
00390
00391 ret = -1;
00392 scrobbler_split_lines(response->str, lines, 4);
00393
00394
00395 if (lines[0] == NULL || strcmp(lines[0], "OK") != 0) {
00396 if (lines[0] != NULL && strcmp(lines[0], "BADAUTH") == 0)
00397 ret = 0;
00398 goto done;
00399 }
00400
00401 if (lines[1] == NULL || strlen(lines[1]) != 32)
00402 goto done;
00403 memcpy(key, lines[1], 32);
00404
00405
00406 if (lines[3] == NULL)
00407 goto done;
00408 g_free(*url);
00409 *url = g_strdup(lines[3]);
00410
00411 ret = 0;
00412 done:
00413 g_string_free(response, TRUE);
00414 return (ret);
00415 }
00416
00421 static int
00422 scrobbler_send_tracks(char *key, const char *url, const char *poststr)
00423 {
00424 CURL *con;
00425 GString *response;
00426 char *lines[1];
00427 int ret;
00428
00429 con = curl_easy_init();
00430 if (con == NULL)
00431 return (-1);
00432
00433 curl_easy_setopt(con, CURLOPT_URL, url);
00434 curl_easy_setopt(con, CURLOPT_POSTFIELDS, poststr);
00435 curl_easy_setopt(con, CURLOPT_USERAGENT, APP_NAME "/" APP_VERSION);
00436
00437
00438 response = g_string_sized_new(128);
00439 curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, scrobbler_curl_concat);
00440 curl_easy_setopt(con, CURLOPT_WRITEDATA, response);
00441
00442 ret = curl_easy_perform(con);
00443 curl_easy_cleanup(con);
00444
00445
00446 if (ret != 0)
00447 goto done;
00448
00449 ret = -1;
00450 scrobbler_split_lines(response->str, lines, 1);
00451
00452
00453 if (lines[0] != NULL) {
00454 if (strcmp(lines[0], "OK") == 0)
00455 ret = 0;
00456 else if (strcmp(lines[0], "BADSESSION") == 0)
00457
00458 key[0] = '\0';
00459 }
00460 done:
00461 g_string_free(response, TRUE);
00462 return (ret);
00463 }
00464
00469 static void *
00470 scrobbler_runner_thread(void *unused)
00471 {
00472 unsigned int amount, interval;
00473 char key[32] = "";
00474 char *poststr, *msg, *url = NULL;
00475
00476 gui_input_sigmask();
00477
00478 for(;;) {
00479 interval = 60;
00480
00481 if (key[0] == '\0') {
00482
00483 if (scrobbler_send_handshake(key, &url) != 0) {
00484
00485 gui_msgbar_warn(_("Failed to authorize "
00486 "at AudioScrobbler."));
00487 } else if (key[0] == '\0') {
00488
00489 gui_msgbar_warn(_("Invalid AudioScrobbler "
00490 "username/password."));
00491 } else {
00492
00493 interval = 1;
00494 gui_msgbar_warn(_("Successfully "
00495 "authorized at AudioScrobbler."));
00496 }
00497 } else {
00498
00499
00500
00501 amount = scrobbler_queue_fetch(key, &poststr);
00502
00503 if (scrobbler_send_tracks(key, url, poststr) == 0) {
00504
00505 interval = 1;
00506 scrobbler_queue_remove(amount);
00507
00508
00509 if (amount == 1) {
00510 msg = g_strdup_printf(
00511 _("Successfully sent 1 song "
00512 "to AudioScrobbler."));
00513 } else {
00514 msg = g_strdup_printf(
00515 _("Successfully sent %d songs "
00516 "to AudioScrobbler."), amount);
00517 }
00518 gui_msgbar_warn(msg);
00519 g_free(msg);
00520 } else {
00521 gui_msgbar_warn(_("Failed to submit songs "
00522 "to AudioScrobbler."));
00523 }
00524
00525 g_free(poststr);
00526 }
00527
00528
00529 g_usleep(interval * 1000000);
00530 }
00531
00532 g_assert_not_reached();
00533 return (NULL);
00534 }
00535
00536 void
00537 scrobbler_init(void)
00538 {
00539 scrobbler_lock = g_mutex_new();
00540 scrobbler_avail = g_cond_new();
00541 }
00542
00546 static void
00547 scrobbler_queue_dump(void)
00548 {
00549 const char *filename;
00550 FILE *fp;
00551 struct scrobbler_entry *ent;
00552
00553 filename = config_getopt("scrobbler.dumpfile");
00554 if (filename[0] == '\0')
00555 return;
00556
00557
00558 if (scrobbler_queue_first == NULL) {
00559 vfs_delete(filename);
00560 return;
00561 }
00562
00563
00564 fp = vfs_fopen(filename, "w");
00565 if (fp == NULL)
00566 return;
00567 SCROBBLER_QUEUE_FOREACH(ent) {
00568 fprintf(fp, "%s %s %s %u %d\n",
00569 ent->artist, ent->title, ent->album,
00570 ent->length, (int)ent->time);
00571 }
00572 fclose(fp);
00573 }
00574
00578 static void
00579 scrobbler_queue_restore(void)
00580 {
00581 const char *filename;
00582 FILE *fio;
00583 char fbuf[1024], *s1, *s2;
00584 struct scrobbler_entry **nse;
00585
00586 filename = config_getopt("scrobbler.dumpfile");
00587 if (filename[0] == '\0')
00588 return;
00589
00590 if ((fio = vfs_fopen(filename, "r")) == NULL)
00591 return;
00592
00593 nse = &scrobbler_queue_first;
00594 while (vfs_fgets(fbuf, sizeof fbuf, fio) == 0) {
00595
00596 *nse = g_slice_new(struct scrobbler_entry);
00597
00598
00599 if ((s1 = strchr(fbuf, ' ')) == NULL)
00600 goto bad;
00601 (*nse)->artist = g_strndup(fbuf, s1 - fbuf);
00602
00603 if ((s2 = strchr(++s1, ' ')) == NULL)
00604 goto bad;
00605 (*nse)->title = g_strndup(s1, s2 - s1);
00606
00607 if ((s1 = strchr(++s2, ' ')) == NULL)
00608 goto bad;
00609 (*nse)->album = g_strndup(s2, s1 - s2);
00610
00611 if ((s2 = strchr(++s1, ' ')) == NULL)
00612 goto bad;
00613 (*nse)->length = strtoul(s1, NULL, 10);
00614 (*nse)->time = strtol(++s2, NULL, 10);
00615
00616
00617 scrobbler_queue_last = *nse;
00618 nse = &(*nse)->next;
00619 continue;
00620
00621 bad: scrobbler_queue_item_free(*nse);
00622 }
00623 fclose(fio);
00624
00625
00626 *nse = NULL;
00627 }
00628
00629 void
00630 scrobbler_spawn(void)
00631 {
00632
00633 if (config_getopt("scrobbler.username")[0] == '\0' ||
00634 config_getopt("scrobbler.password")[0] == '\0')
00635 return;
00636
00637
00638 scrobbler_queue_restore();
00639
00640 scrobbler_runner = g_thread_create(scrobbler_runner_thread,
00641 NULL, 0, NULL);
00642 scrobbler_enabled = 1;
00643 }
00644
00645 void
00646 scrobbler_shutdown(void)
00647 {
00648
00649 g_mutex_lock(scrobbler_lock);
00650 scrobbler_queue_dump();
00651 g_mutex_unlock(scrobbler_lock);
00652 }