2 * Copyright (c) 2006-2011 Ed Schouten <ed@80386.nl>
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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
28 * @brief AudioScrobbler submission.
33 #include <curl/curl.h>
34 #include <curl/easy.h>
36 #include "audio_file.h"
40 #include "scrobbler.h"
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"
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;
56 * @brief Lock used to provide safe access to the AudioScrobbler queue.
58 static GMutex
*scrobbler_lock
;
60 * @brief Conditional variable used to notify the avaiability of new
61 * tracks ready for submission to AudioScrobbler.
63 static GCond
*scrobbler_avail
;
65 * @brief Reference to AudioScrobbler submission thread.
67 static GThread
*scrobbler_runner
;
70 * @brief An entry in the AudioScrobbler queue, ready to be submitted to
73 struct scrobbler_entry
{
75 * @brief The artist of the song, escaped to conform with
80 * @brief The title of the song, escaped to conform with
85 * @brief The album of the song, escaped to conform with
90 * @brief The length in seconds of the song.
94 * @brief The time the song was added to the queue.
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
)
122 * @brief Place a new item in our Scrobbler queue.
125 scrobbler_queue_insert_tail(struct scrobbler_entry
*se
)
128 if (scrobbler_queue_last
!= NULL
)
129 scrobbler_queue_last
->next
= se
;
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.
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
;
155 scrobbler_notify_read(struct audio_file
*fd
, int eof
)
157 struct scrobbler_entry
*nse
;
160 /* Don't accept streams or submit songs twice */
161 if (!scrobbler_enabled
|| fd
->stream
|| fd
->_scrobbler_done
)
165 /* Just take the position - catches formats without seeking */
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)))
176 /* Track was too short */
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
))
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
);
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
);
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.
213 scrobbler_queue_fetch(const char key
[32], char **poststr
)
215 struct scrobbler_entry
*ent
;
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]=",
232 len
, (unsigned int)ent
->time
,
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 */
254 * @brief Deallocate an AudioScrobbler queue entry.
257 scrobbler_queue_item_free(struct scrobbler_entry
*ent
)
262 g_slice_free(struct scrobbler_entry
, ent
);
266 * @brief Remove a specified amount of tracks from the AudioScrobbler
270 scrobbler_queue_remove(unsigned int amount
)
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.
288 scrobbler_hash(time_t t
, char out
[32])
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.
312 scrobbler_curl_concat(void *ptr
, size_t size
, size_t nmemb
, void *stream
)
314 GString
*response
= stream
;
318 g_string_append_len(response
, ptr
, len
);
324 * @brief Split a string into multiple nul-terminated lines.
327 scrobbler_split_lines(char *str
, char *lines
[], unsigned int nlines
)
331 for (i
= 0; i
< nlines
; i
++) {
332 /* Keep track of the line */
336 * If we haven't finished processing the string, move it
337 * on to the next line.
340 str
= strchr(str
, '\n');
348 * @brief Send a handshake to the AudioScrobbler server. Also catch
349 * whether the configured password is correct.
352 scrobbler_send_handshake(char *key
, char **url
)
362 con
= curl_easy_init();
366 /* Generate the connection URL, including the password key */
368 scrobbler_hash(hstime
, hskey
);
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
);
383 ret
= curl_easy_perform(con
);
384 curl_easy_cleanup(con
);
387 /* Connection error */
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)
400 /* Make sure the checksum is there */
401 if (lines
[1] == NULL
|| strlen(lines
[1]) != 32)
403 memcpy(key
, lines
[1], 32);
405 /* Copy the submission URL */
406 if (lines
[3] == NULL
)
409 *url
= g_strdup(lines
[3]);
413 g_string_free(response
, TRUE
);
418 * @brief Submit an amount of tracks to the AudioScrobbler server. Also
419 * make sure whether the tracks are submitted properly.
422 scrobbler_send_tracks(char *key
, const char *url
, const char *poststr
)
429 con
= curl_easy_init();
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 */
450 scrobbler_split_lines(response
->str
, lines
, 1);
452 /* Response string handling */
453 if (lines
[0] != NULL
) {
454 if (strcmp(lines
[0], "OK") == 0)
456 else if (strcmp(lines
[0], "BADSESSION") == 0)
457 /* Invalidate the session */
461 g_string_free(response
, TRUE
);
466 * @brief The actual thread that gets spawned to process AudioScrobbler
470 scrobbler_runner_thread(void *unused
)
472 unsigned int amount
, interval
;
474 char *poststr
, *msg
, *url
= NULL
;
481 if (key
[0] == '\0') {
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') {
489 gui_msgbar_warn(_("Invalid AudioScrobbler "
490 "username/password."));
492 /* We've got a key */
494 gui_msgbar_warn(_("Successfully "
495 "authorized at AudioScrobbler."));
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. */
506 scrobbler_queue_remove(amount
);
508 /* Print a message on the GUI */
510 msg
= g_strdup_printf(
511 _("Successfully sent 1 song "
512 "to AudioScrobbler."));
514 msg
= g_strdup_printf(
515 _("Successfully sent %d songs "
516 "to AudioScrobbler."), amount
);
518 gui_msgbar_warn(msg
);
521 gui_msgbar_warn(_("Failed to submit songs "
522 "to AudioScrobbler."));
528 /* Make sure we don't transmit too fast */
529 g_usleep(interval
* 1000000);
532 g_assert_not_reached();
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.
547 scrobbler_queue_dump(void)
549 const char *filename
;
551 struct scrobbler_entry
*ent
;
553 filename
= config_getopt("scrobbler.dumpfile");
554 if (filename
[0] == '\0')
557 /* Nothing to be stored - remove queue */
558 if (scrobbler_queue_first
== NULL
) {
559 vfs_delete(filename
);
563 /* Write list to queue file */
564 fp
= vfs_fopen(filename
, "w");
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
);
576 * @brief Restore the AudioScrobbler queue from a file.
579 scrobbler_queue_restore(void)
581 const char *filename
;
583 char fbuf
[1024], *s1
, *s2
;
584 struct scrobbler_entry
**nse
;
586 filename
= config_getopt("scrobbler.dumpfile");
587 if (filename
[0] == '\0')
590 if ((fio
= vfs_fopen(filename
, "r")) == NULL
)
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
);
599 if ((s1
= strchr(fbuf
, ' ')) == NULL
)
601 (*nse
)->artist
= g_strndup(fbuf
, s1
- fbuf
);
603 if ((s2
= strchr(++s1
, ' ')) == NULL
)
605 (*nse
)->title
= g_strndup(s1
, s2
- s1
);
607 if ((s1
= strchr(++s2
, ' ')) == NULL
)
609 (*nse
)->album
= g_strndup(s2
, s1
- s2
);
610 /* Length and time */
611 if ((s2
= strchr(++s1
, ' ')) == NULL
)
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
;
621 bad
: scrobbler_queue_item_free(*nse
);
625 /* Terminate our entry list */
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')
637 /* Restore unsubmitted tracks */
638 scrobbler_queue_restore();
640 scrobbler_runner
= g_thread_create(scrobbler_runner_thread
,
642 scrobbler_enabled
= 1;
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
);