Fix whitespace inconsistencies.
[herrie-working.git] / herrie / src / scrobbler.c
blob62541b537f366256c166e4bdfc31e454e147624c
1 /*
2 * Copyright (c) 2006-2011 Ed Schouten <ed@80386.nl>
3 * All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 * SUCH DAMAGE.
26 /**
27 * @file scrobbler.c
28 * @brief AudioScrobbler submission.
31 #include "stdinc.h"
33 #include <curl/curl.h>
34 #include <curl/easy.h>
36 #include "audio_file.h"
37 #include "config.h"
38 #include "gui.h"
39 #include "md5.h"
40 #include "scrobbler.h"
41 #include "util.h"
42 #include "vfs.h"
44 /**
45 *@ brief The URL format used for the initial handshake.
47 #define SCROBBLER_URL "http://%s/?hs=true&p=1.2&c=her&v=0.1&u=%s&t=%u&a=%s"
49 /**
50 * @brief Flag indicating if the AudioScrobbler thread has already been
51 * launched, causing the submission queue to be filled.
53 static char scrobbler_enabled = 0;
55 /**
56 * @brief Lock used to provide safe access to the AudioScrobbler queue.
58 static GMutex *scrobbler_lock;
59 /**
60 * @brief Conditional variable used to notify the avaiability of new
61 * tracks ready for submission to AudioScrobbler.
63 static GCond *scrobbler_avail;
64 /**
65 * @brief Reference to AudioScrobbler submission thread.
67 static GThread *scrobbler_runner;
69 /**
70 * @brief An entry in the AudioScrobbler queue, ready to be submitted to
71 * AudioScrobbler.
73 struct scrobbler_entry {
74 /**
75 * @brief The artist of the song, escaped to conform with
76 * HTTP/1.1.
78 char *artist;
79 /**
80 * @brief The title of the song, escaped to conform with
81 * HTTP/1.1.
83 char *title;
84 /**
85 * @brief The album of the song, escaped to conform with
86 * HTTP/1.1.
88 char *album;
89 /**
90 * @brief The length in seconds of the song.
92 unsigned int length;
93 /**
94 * @brief The time the song was added to the queue.
96 time_t time;
98 /**
99 * @brief A reference to the next entry in the queue.
101 struct scrobbler_entry *next;
104 * @brief First item in the Scrobbler queue.
106 static struct scrobbler_entry *scrobbler_queue_first = NULL;
108 * @brief Last item in the Scrobbler queue.
110 static struct scrobbler_entry *scrobbler_queue_last = NULL;
113 * @brief Next item in the Scrobbler queue.
115 static inline struct scrobbler_entry *
116 scrobbler_queue_next(struct scrobbler_entry *se)
118 return (se->next);
122 * @brief Place a new item in our Scrobbler queue.
124 static inline void
125 scrobbler_queue_insert_tail(struct scrobbler_entry *se)
127 se->next = NULL;
128 if (scrobbler_queue_last != NULL)
129 scrobbler_queue_last->next = se;
130 else
131 scrobbler_queue_first = se;
132 scrobbler_queue_last = se;
136 * @brief Iterate through all entries in the Scrobbler queue.
138 #define SCROBBLER_QUEUE_FOREACH(se) \
139 for (se = scrobbler_queue_first; se != NULL; se = se->next)
142 * @brief Remove the first item from our Scrobbler queue.
144 static inline void
145 scrobbler_queue_remove_head(void)
147 g_assert(scrobbler_queue_first != NULL);
149 if (scrobbler_queue_first == scrobbler_queue_last)
150 scrobbler_queue_last = NULL;
151 scrobbler_queue_first = scrobbler_queue_first->next;
154 void
155 scrobbler_notify_read(struct audio_file *fd, int eof)
157 struct scrobbler_entry *nse;
158 unsigned int len;
160 /* Don't accept streams or submit songs twice */
161 if (!scrobbler_enabled || fd->stream || fd->_scrobbler_done)
162 return;
164 if (eof) {
165 /* Just take the position - catches formats without seeking */
166 len = fd->time_cur;
167 } else {
168 /* We may only submit if we're past four minutes or past 50% */
169 if ((fd->time_cur < 240) &&
170 (fd->time_cur < (fd->time_len / 2)))
171 return;
173 len = fd->time_len;
176 /* Track was too short */
177 if (len < 30)
178 return;
180 /* Mark it as processed */
181 fd->_scrobbler_done = 1;
183 /* We must have a title and an artist or an album */
184 if (fd->title == NULL || (fd->artist == NULL && fd->album == NULL))
185 return;
187 /* Place the track in our queue */
188 nse = g_slice_new(struct scrobbler_entry);
189 nse->artist = http_escape(fd->artist, NULL);
190 nse->title = http_escape(fd->title, NULL);
191 nse->album = http_escape(fd->album, NULL);
192 nse->length = len;
193 nse->time = time(NULL);
195 g_mutex_lock(scrobbler_lock);
196 scrobbler_queue_insert_tail(nse);
197 g_cond_signal(scrobbler_avail);
198 g_mutex_unlock(scrobbler_lock);
201 void
202 scrobbler_notify_seek(struct audio_file *fd)
204 fd->_scrobbler_done = 1;
208 * @brief Fetch as much tracks from the AudioScrobbler submission queue
209 * (at most 10), generate a HTTP/1.1 POST string for submission
210 * and return the amount of tracks described in the POST string.
212 static unsigned int
213 scrobbler_queue_fetch(const char key[32], char **poststr)
215 struct scrobbler_entry *ent;
216 unsigned int len;
217 GString *str;
219 g_mutex_lock(scrobbler_lock);
220 while ((ent = scrobbler_queue_first) == NULL)
221 g_cond_wait(scrobbler_avail, scrobbler_lock);
223 str = g_string_new("s=");
224 g_string_append_len(str, key, 32);
226 /* We can submit 50 tracks at a time */
227 for (len = 0; (len < 50) && (ent != NULL); len++) {
228 g_string_append_printf(str,
229 "&a[%u]=%s&t[%u]=%s&i[%u]=%u&o[%u]=P&r[%u]=&l[%u]=%u&b[%u]=%s&n[%u]=&m[%u]=",
230 len, ent->artist,
231 len, ent->title,
232 len, (unsigned int)ent->time,
233 len, /* No source */
234 len, /* No rating */
235 len, ent->length,
236 len, ent->album,
237 len, /* No track number */
238 len); /* No MusicBrainz ID */
240 /* Go on to the next item */
241 ent = scrobbler_queue_next(ent);
244 g_mutex_unlock(scrobbler_lock);
246 *poststr = g_string_free(str, FALSE);
248 /* Return the amount of entries in the poststring */
249 g_assert(len >= 1);
250 return (len);
254 * @brief Deallocate an AudioScrobbler queue entry.
256 static void
257 scrobbler_queue_item_free(struct scrobbler_entry *ent)
259 g_free(ent->artist);
260 g_free(ent->title);
261 g_free(ent->album);
262 g_slice_free(struct scrobbler_entry, ent);
266 * @brief Remove a specified amount of tracks from the AudioScrobbler
267 * submission queue.
269 static void
270 scrobbler_queue_remove(unsigned int amount)
272 int i;
273 struct scrobbler_entry *ent;
275 g_mutex_lock(scrobbler_lock);
276 for (i = amount; i > 0; i--) {
277 ent = scrobbler_queue_first;
278 scrobbler_queue_remove_head();
279 scrobbler_queue_item_free(ent);
281 g_mutex_unlock(scrobbler_lock);
285 * @brief Generate a response, based on the password and challenge.
287 static void
288 scrobbler_hash(time_t t, char out[32])
290 char tstr[16];
291 unsigned char bin_res[16];
292 struct md5_context ctx = MD5CONTEXT_INITIALIZER;
295 * Generate the new MD5 value
297 md5_update(&ctx, config_getopt("scrobbler.password"), 32);
298 sprintf(tstr, "%u", (unsigned int)t);
299 md5_update(&ctx, tstr, strlen(tstr));
300 md5_final(&ctx, bin_res);
303 * Convert the result back to hexadecimal string
305 hex_encode(bin_res, out, sizeof bin_res);
309 * @brief Concatenate cURL data to a string.
311 static size_t
312 scrobbler_curl_concat(void *ptr, size_t size, size_t nmemb, void *stream)
314 GString *response = stream;
315 size_t len;
317 len = size * nmemb;
318 g_string_append_len(response, ptr, len);
320 return (len);
324 * @brief Split a string into multiple nul-terminated lines.
326 static void
327 scrobbler_split_lines(char *str, char *lines[], unsigned int nlines)
329 unsigned int i;
331 for (i = 0; i < nlines; i++) {
332 /* Keep track of the line */
333 lines[i] = str;
336 * If we haven't finished processing the string, move it
337 * on to the next line.
339 if (str != NULL) {
340 str = strchr(str, '\n');
341 if (str != NULL)
342 *str++ = '\0';
348 * @brief Send a handshake to the AudioScrobbler server. Also catch
349 * whether the configured password is correct.
351 static int
352 scrobbler_send_handshake(char *key, char **url)
354 char *hsurl;
355 char hskey[33];
356 time_t hstime;
357 CURL *con;
358 GString *response;
359 char *lines[4];
360 int ret;
362 con = curl_easy_init();
363 if (con == NULL)
364 return (-1);
366 /* Generate the connection URL, including the password key */
367 hstime = time(NULL);
368 scrobbler_hash(hstime, hskey);
369 hskey[32] = '\0';
370 hsurl = g_strdup_printf(SCROBBLER_URL,
371 config_getopt("scrobbler.hostname"),
372 config_getopt("scrobbler.username"),
373 (unsigned int)hstime, hskey);
374 curl_easy_setopt(con, CURLOPT_URL, hsurl);
375 curl_easy_setopt(con, CURLOPT_USERAGENT, APP_NAME "/" APP_VERSION);
377 /* Callback function */
378 response = g_string_sized_new(128);
379 curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, scrobbler_curl_concat);
380 curl_easy_setopt(con, CURLOPT_WRITEDATA, response);
382 /* Here we go */
383 ret = curl_easy_perform(con);
384 curl_easy_cleanup(con);
385 g_free(hsurl);
387 /* Connection error */
388 if (ret != 0)
389 goto done;
391 ret = -1;
392 scrobbler_split_lines(response->str, lines, 4);
394 /* First line must be "OK" */
395 if (lines[0] == NULL || strcmp(lines[0], "OK") != 0) {
396 if (lines[0] != NULL && strcmp(lines[0], "BADAUTH") == 0)
397 ret = 0;
398 goto done;
400 /* Make sure the checksum is there */
401 if (lines[1] == NULL || strlen(lines[1]) != 32)
402 goto done;
403 memcpy(key, lines[1], 32);
405 /* Copy the submission URL */
406 if (lines[3] == NULL)
407 goto done;
408 g_free(*url);
409 *url = g_strdup(lines[3]);
411 ret = 0;
412 done:
413 g_string_free(response, TRUE);
414 return (ret);
418 * @brief Submit an amount of tracks to the AudioScrobbler server. Also
419 * make sure whether the tracks are submitted properly.
421 static int
422 scrobbler_send_tracks(char *key, const char *url, const char *poststr)
424 CURL *con;
425 GString *response;
426 char *lines[1];
427 int ret;
429 con = curl_easy_init();
430 if (con == NULL)
431 return (-1);
433 curl_easy_setopt(con, CURLOPT_URL, url);
434 curl_easy_setopt(con, CURLOPT_POSTFIELDS, poststr);
435 curl_easy_setopt(con, CURLOPT_USERAGENT, APP_NAME "/" APP_VERSION);
437 /* Callback function */
438 response = g_string_sized_new(128);
439 curl_easy_setopt(con, CURLOPT_WRITEFUNCTION, scrobbler_curl_concat);
440 curl_easy_setopt(con, CURLOPT_WRITEDATA, response);
442 ret = curl_easy_perform(con);
443 curl_easy_cleanup(con);
445 /* Connection error */
446 if (ret != 0)
447 goto done;
449 ret = -1;
450 scrobbler_split_lines(response->str, lines, 1);
452 /* Response string handling */
453 if (lines[0] != NULL) {
454 if (strcmp(lines[0], "OK") == 0)
455 ret = 0;
456 else if (strcmp(lines[0], "BADSESSION") == 0)
457 /* Invalidate the session */
458 key[0] = '\0';
460 done:
461 g_string_free(response, TRUE);
462 return (ret);
466 * @brief The actual thread that gets spawned to process AudioScrobbler
467 * submission.
469 static void *
470 scrobbler_runner_thread(void *unused)
472 unsigned int amount, interval;
473 char key[32] = "";
474 char *poststr, *msg, *url = NULL;
476 gui_input_sigmask();
478 for(;;) {
479 interval = 60;
481 if (key[0] == '\0') {
482 /* No key yet */
483 if (scrobbler_send_handshake(key, &url) != 0) {
484 /* Connection problems */
485 gui_msgbar_warn(_("Failed to authorize "
486 "at AudioScrobbler."));
487 } else if (key[0] == '\0') {
488 /* We got BADAUTH */
489 gui_msgbar_warn(_("Invalid AudioScrobbler "
490 "username/password."));
491 } else {
492 /* We've got a key */
493 interval = 1;
494 gui_msgbar_warn(_("Successfully "
495 "authorized at AudioScrobbler."));
497 } else {
499 * We are authorized. Send some tracks.
501 amount = scrobbler_queue_fetch(key, &poststr);
503 if (scrobbler_send_tracks(key, url, poststr) == 0) {
504 /* Done. Remove them from the queue. */
505 interval = 1;
506 scrobbler_queue_remove(amount);
508 /* Print a message on the GUI */
509 if (amount == 1) {
510 msg = g_strdup_printf(
511 _("Successfully sent 1 song "
512 "to AudioScrobbler."));
513 } else {
514 msg = g_strdup_printf(
515 _("Successfully sent %d songs "
516 "to AudioScrobbler."), amount);
518 gui_msgbar_warn(msg);
519 g_free(msg);
520 } else {
521 gui_msgbar_warn(_("Failed to submit songs "
522 "to AudioScrobbler."));
525 g_free(poststr);
528 /* Make sure we don't transmit too fast */
529 g_usleep(interval * 1000000);
532 g_assert_not_reached();
533 return (NULL);
536 void
537 scrobbler_init(void)
539 scrobbler_lock = g_mutex_new();
540 scrobbler_avail = g_cond_new();
544 * @brief Dump the songs that are still present in the queue to disk.
546 static void
547 scrobbler_queue_dump(void)
549 const char *filename;
550 FILE *fp;
551 struct scrobbler_entry *ent;
553 filename = config_getopt("scrobbler.dumpfile");
554 if (filename[0] == '\0')
555 return;
557 /* Nothing to be stored - remove queue */
558 if (scrobbler_queue_first == NULL) {
559 vfs_delete(filename);
560 return;
563 /* Write list to queue file */
564 fp = vfs_fopen(filename, "w");
565 if (fp == NULL)
566 return;
567 SCROBBLER_QUEUE_FOREACH(ent) {
568 fprintf(fp, "%s %s %s %u %d\n",
569 ent->artist, ent->title, ent->album,
570 ent->length, (int)ent->time);
572 fclose(fp);
576 * @brief Restore the AudioScrobbler queue from a file.
578 static void
579 scrobbler_queue_restore(void)
581 const char *filename;
582 FILE *fio;
583 char fbuf[1024], *s1, *s2;
584 struct scrobbler_entry **nse;
586 filename = config_getopt("scrobbler.dumpfile");
587 if (filename[0] == '\0')
588 return;
590 if ((fio = vfs_fopen(filename, "r")) == NULL)
591 return;
593 nse = &scrobbler_queue_first;
594 while (vfs_fgets(fbuf, sizeof fbuf, fio) == 0) {
595 /* String parsing sucks */
596 *nse = g_slice_new(struct scrobbler_entry);
598 /* Artist */
599 if ((s1 = strchr(fbuf, ' ')) == NULL)
600 goto bad;
601 (*nse)->artist = g_strndup(fbuf, s1 - fbuf);
602 /* Title */
603 if ((s2 = strchr(++s1, ' ')) == NULL)
604 goto bad;
605 (*nse)->title = g_strndup(s1, s2 - s1);
606 /* Album */
607 if ((s1 = strchr(++s2, ' ')) == NULL)
608 goto bad;
609 (*nse)->album = g_strndup(s2, s1 - s2);
610 /* Length and time */
611 if ((s2 = strchr(++s1, ' ')) == NULL)
612 goto bad;
613 (*nse)->length = strtoul(s1, NULL, 10);
614 (*nse)->time = strtol(++s2, NULL, 10);
616 /* Properly fix up the list */
617 scrobbler_queue_last = *nse;
618 nse = &(*nse)->next;
619 continue;
621 bad: scrobbler_queue_item_free(*nse);
623 fclose(fio);
625 /* Terminate our entry list */
626 *nse = NULL;
629 void
630 scrobbler_spawn(void)
632 /* Bail out if the username or password is not filled in */
633 if (config_getopt("scrobbler.username")[0] == '\0' ||
634 config_getopt("scrobbler.password")[0] == '\0')
635 return;
637 /* Restore unsubmitted tracks */
638 scrobbler_queue_restore();
640 scrobbler_runner = g_thread_create(scrobbler_runner_thread,
641 NULL, 0, NULL);
642 scrobbler_enabled = 1;
645 void
646 scrobbler_shutdown(void)
648 /* XXX: bring down the AudioScrobbler thread */
649 g_mutex_lock(scrobbler_lock);
650 scrobbler_queue_dump();
651 g_mutex_unlock(scrobbler_lock);