1 /* mpdscribble (MPD Client)
2 * Copyright (C) 2008-2010 The Music Player Daemon Project
3 * Copyright (C) 2005-2008 Kuno Woudt <kuno@frob.nl>
4 * Project homepage: http://musicpd.org
5
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21 #include "scrobbler.h"
22 #include "record.h"
23 #include "journal.h"
24 #include "http_client.h"
25 #include "config.h"
26 #include "log.h" /* for log_date() */
27 #include "compat.h"
28
29 #include <glib.h>
30
31 #if !GLIB_CHECK_VERSION(2,16,0)
32 #include <gcrypt.h>
33 #endif
34
35 #include <assert.h>
36 #include <stdbool.h>
37 #include <ctype.h>
38 #include <errno.h>
39 #include <stdio.h>
40 #include <stdlib.h>
41 #include <unistd.h>
42 #include <stdarg.h>
43 #include <string.h>
44
45 #define AS_CLIENT_ID "mdc"
46 #define AS_CLIENT_VERSION VERSION
47
48 /* don't submit more than this amount of songs in a batch. */
49 #define MAX_SUBMIT_COUNT 10
50
51 static const char OK[] = "OK";
52 static const char BADSESSION[] = "BADSESSION";
53 static const char FAILED[] = "FAILED";
54
55 enum scrobbler_state {
56 /**
57 * mpdscribble has started, and doesn't have a session yet.
58 * Handshake to be submitted.
59 */
60 SCROBBLER_STATE_NOTHING,
61
62 /**
63 * Handshake is in progress, waiting for the server's
64 * response.
65 */
66 SCROBBLER_STATE_HANDSHAKE,
67
68 /**
69 * We have a session, and we're ready to submit.
70 */
71 SCROBBLER_STATE_READY,
72
73 /**
74 * Submission in progress, waiting for the server's response.
75 */
76 SCROBBLER_STATE_SUBMITTING,
77 };
78
79 typedef enum {
80 AS_SUBMIT_OK,
81 AS_SUBMIT_FAILED,
82 AS_SUBMIT_HANDSHAKE,
83 } as_submitting;
84
85 struct scrobbler {
86 const struct scrobbler_config *config;
87
88 FILE *file;
89
90 enum scrobbler_state state;
91
92 unsigned interval;
93
94 guint handshake_source_id;
95 guint submit_source_id;
96
97 char *session;
98 char *nowplay_url;
99 char *submit_url;
100
101 struct record now_playing;
102
103 /**
104 * A queue of #record objects.
105 */
106 GQueue *queue;
107
108 /**
109 * How many songs are we trying to submit right now? This
110 * many will be shifted from #queue if the submit succeeds.
111 */
112 unsigned pending;
113 };
114
115 static GSList *scrobblers;
116
117 /**
118 * Creates a new scrobbler object based on the specified
119 * configuration.
120 */
121 static struct scrobbler *
scrobbler_new(const struct scrobbler_config * config)122 scrobbler_new(const struct scrobbler_config *config)
123 {
124 struct scrobbler *scrobbler = g_new(struct scrobbler, 1);
125
126 scrobbler->config = config;
127 scrobbler->file = NULL;
128 scrobbler->state = SCROBBLER_STATE_NOTHING;
129 scrobbler->interval = 1;
130 scrobbler->handshake_source_id = 0;
131 scrobbler->submit_source_id = 0;
132 scrobbler->session = NULL;
133 scrobbler->nowplay_url = NULL;
134 scrobbler->submit_url = NULL;
135
136 record_clear(&scrobbler->now_playing);
137
138 scrobbler->queue = g_queue_new();
139 scrobbler->pending = 0;
140
141 return scrobbler;
142 }
143
144 static void
record_free_callback(gpointer data,G_GNUC_UNUSED gpointer user_data)145 record_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data)
146 {
147 struct record *song = data;
148 record_free(song);
149 }
150
151 /**
152 * Frees a scrobbler object.
153 */
154 static void
scrobbler_free(struct scrobbler * scrobbler)155 scrobbler_free(struct scrobbler *scrobbler)
156 {
157 g_queue_foreach(scrobbler->queue, record_free_callback, NULL);
158 g_queue_free(scrobbler->queue);
159
160 record_deinit(&scrobbler->now_playing);
161
162 if (scrobbler->handshake_source_id != 0)
163 g_source_remove(scrobbler->handshake_source_id);
164 if (scrobbler->submit_source_id != 0)
165 g_source_remove(scrobbler->submit_source_id);
166
167 g_free(scrobbler->session);
168 g_free(scrobbler->nowplay_url);
169 g_free(scrobbler->submit_url);
170
171 if (scrobbler->file != NULL)
172 fclose(scrobbler->file);
173
174 g_free(scrobbler);
175 }
176
177 static void
add_var_internal(GString * s,char sep,const char * key,signed char idx,const char * val)178 add_var_internal(GString * s, char sep, const char *key,
179 signed char idx, const char *val)
180 {
181 g_string_append_c(s, sep);
182 g_string_append(s, key);
183
184 if (idx >= 0)
185 g_string_append_printf(s, "[%i]", idx);
186
187 g_string_append_c(s, '=');
188
189 if (val != NULL) {
190 char *escaped = http_client_uri_escape(val);
191 g_string_append(s, escaped);
192 g_free(escaped);
193 }
194 }
195
first_var(GString * s,const char * key,const char * val)196 static void first_var(GString * s, const char *key, const char *val)
197 {
198 add_var_internal(s, '?', key, -1, val);
199 }
200
add_var(GString * s,const char * key,const char * val)201 static void add_var(GString * s, const char *key, const char *val)
202 {
203 add_var_internal(s, '&', key, -1, val);
204 }
205
206 static void
add_var_i(GString * s,const char * key,signed char idx,const char * val)207 add_var_i(GString * s, const char *key, signed char idx, const char *val)
208 {
209 add_var_internal(s, '&', key, idx, val);
210 }
211
212 static void
213 scrobbler_schedule_handshake(struct scrobbler *scrobbler);
214
215 static void
216 scrobbler_submit(struct scrobbler *scrobbler);
217
218 static void
219 scrobbler_schedule_submit(struct scrobbler *scrobbler);
220
221 static void
scrobbler_increase_interval(struct scrobbler * scrobbler)222 scrobbler_increase_interval(struct scrobbler *scrobbler)
223 {
224 if (scrobbler->interval < 60)
225 scrobbler->interval = 60;
226 else
227 scrobbler->interval <<= 1;
228
229 if (scrobbler->interval > 60 * 60 * 2)
230 scrobbler->interval = 60 * 60 * 2;
231
232 g_warning("[%s] waiting %u seconds before trying again",
233 scrobbler->config->name, scrobbler->interval);
234 }
235
236 static as_submitting
scrobbler_parse_submit_response(const char * scrobbler_name,const char * line,size_t length)237 scrobbler_parse_submit_response(const char *scrobbler_name,
238 const char *line, size_t length)
239 {
240 if (length == sizeof(OK) - 1 && memcmp(line, OK, length) == 0) {
241 g_message("[%s] OK", scrobbler_name);
242
243 return AS_SUBMIT_OK;
244 } else if (length == sizeof(BADSESSION) - 1 &&
245 memcmp(line, BADSESSION, length) == 0) {
246 g_warning("[%s] invalid session", scrobbler_name);
247
248 return AS_SUBMIT_HANDSHAKE;
249 } else if (length == sizeof(FAILED) - 1 &&
250 memcmp(line, FAILED, length) == 0) {
251 if (length > strlen(FAILED))
252 g_warning("[%s] submission rejected: %.*s",
253 scrobbler_name,
254 (int)(length - strlen(FAILED)),
255 line + strlen(FAILED));
256 else
257 g_warning("[%s] submission rejected", scrobbler_name);
258 } else {
259 g_warning("[%s] unknown response: %.*s",
260 scrobbler_name, (int)length, line);
261 }
262
263 return AS_SUBMIT_FAILED;
264 }
265
266 static bool
scrobbler_parse_handshake_response(struct scrobbler * scrobbler,const char * line)267 scrobbler_parse_handshake_response(struct scrobbler *scrobbler, const char *line)
268 {
269 static const char *BANNED = "BANNED";
270 static const char *BADAUTH = "BADAUTH";
271 static const char *BADTIME = "BADTIME";
272
273 /* FIXME: some code duplication between this
274 and as_parse_submit_response. */
275 if (!strncmp(line, OK, strlen(OK))) {
276 g_message("[%s] handshake successful",
277 scrobbler->config->name);
278 return true;
279 } else if (!strncmp(line, BANNED, strlen(BANNED))) {
280 g_warning("[%s] handshake failed, we're banned (%s)",
281 scrobbler->config->name, line);
282 } else if (!strncmp(line, BADAUTH, strlen(BADAUTH))) {
283 g_warning("[%s] handshake failed, "
284 "username or password incorrect (%s)",
285 scrobbler->config->name, line);
286 } else if (!strncmp(line, BADTIME, strlen(BADTIME))) {
287 g_warning("[%s] handshake failed, clock not synchronized (%s)",
288 scrobbler->config->name, line);
289 } else if (!strncmp(line, FAILED, strlen(FAILED))) {
290 g_warning("[%s] handshake failed (%s)",
291 scrobbler->config->name, line);
292 } else {
293 g_warning("[%s] error parsing handshake response (%s)",
294 scrobbler->config->name, line);
295 }
296
297 return false;
298 }
299
300 static char *
next_line(const char ** input_r,const char * end)301 next_line(const char **input_r, const char *end)
302 {
303 const char *input = *input_r;
304 const char *newline = memchr(input, '\n', end - input);
305 char *line;
306
307 if (newline == NULL)
308 return g_strdup("");
309
310 line = g_strndup(input, newline - input);
311 *input_r = newline + 1;
312
313 return line;
314 }
315
316 static void
scrobbler_handshake_response(size_t length,const char * response,void * data)317 scrobbler_handshake_response(size_t length, const char *response, void *data)
318 {
319 struct scrobbler *scrobbler = data;
320 const char *end = response + length;
321 char *line;
322 bool ret;
323
324 assert(scrobbler != NULL);
325 assert(scrobbler->config->file == NULL);
326 assert(scrobbler->state == SCROBBLER_STATE_HANDSHAKE);
327
328 scrobbler->state = SCROBBLER_STATE_NOTHING;
329
330 line = next_line(&response, end);
331 ret = scrobbler_parse_handshake_response(scrobbler, line);
332 g_free(line);
333 if (!ret) {
334 scrobbler_increase_interval(scrobbler);
335 scrobbler_schedule_handshake(scrobbler);
336 return;
337 }
338
339 scrobbler->session = next_line(&response, end);
340 g_debug("[%s] session: %s",
341 scrobbler->config->name, scrobbler->session);
342
343 scrobbler->nowplay_url = next_line(&response, end);
344 g_debug("[%s] now playing url: %s",
345 scrobbler->config->name, scrobbler->nowplay_url);
346
347 scrobbler->submit_url = next_line(&response, end);
348 g_debug("[%s] submit url: %s",
349 scrobbler->config->name, scrobbler->submit_url);
350
351 if (*scrobbler->nowplay_url == 0 || *scrobbler->submit_url == 0) {
352 g_free(scrobbler->session);
353 scrobbler->session = NULL;
354
355 g_free(scrobbler->nowplay_url);
356 scrobbler->nowplay_url = NULL;
357
358 g_free(scrobbler->submit_url);
359 scrobbler->submit_url = NULL;
360
361 scrobbler_increase_interval(scrobbler);
362 scrobbler_schedule_handshake(scrobbler);
363 return;
364 }
365
366 scrobbler->state = SCROBBLER_STATE_READY;
367 scrobbler->interval = 1;
368
369 /* handshake was successful: see if we have songs to submit */
370 scrobbler_submit(scrobbler);
371 }
372
373 static void
scrobbler_handshake_error(GError * error,void * data)374 scrobbler_handshake_error(GError *error, void *data)
375 {
376 struct scrobbler *scrobbler = data;
377
378 assert(scrobbler != NULL);
379 assert(scrobbler->config->file == NULL);
380 assert(scrobbler->state == SCROBBLER_STATE_HANDSHAKE);
381
382 scrobbler->state = SCROBBLER_STATE_NOTHING;
383
384 g_warning("[%s] handshake error: %s",
385 scrobbler->config->name, error->message);
386 g_error_free(error);
387
388 scrobbler_increase_interval(scrobbler);
389 scrobbler_schedule_handshake(scrobbler);
390 }
391
392 static const struct http_client_handler scrobbler_handshake_handler = {
393 .response = scrobbler_handshake_response,
394 .error = scrobbler_handshake_error,
395 };
396
397 static void
scrobbler_queue_remove_oldest(GQueue * queue,unsigned count)398 scrobbler_queue_remove_oldest(GQueue *queue, unsigned count)
399 {
400 assert(count > 0);
401
402 while (count--) {
403 struct record *tmp = g_queue_pop_head(queue);
404 record_free(tmp);
405 }
406 }
407
408 static void
scrobbler_submit_response(size_t length,const char * response,void * data)409 scrobbler_submit_response(size_t length, const char *response, void *data)
410 {
411 struct scrobbler *scrobbler = data;
412 char *newline;
413
414 assert(scrobbler->config->file == NULL);
415 assert(scrobbler->state == SCROBBLER_STATE_SUBMITTING);
416 scrobbler->state = SCROBBLER_STATE_READY;
417
418 newline = memchr(response, '\n', length);
419 if (newline != NULL)
420 length = newline - response;
421
422 switch (scrobbler_parse_submit_response(scrobbler->config->name,
423 response, length)) {
424 case AS_SUBMIT_OK:
425 scrobbler->interval = 1;
426
427 /* submission was accepted, so clean up the cache. */
428 if (scrobbler->pending > 0) {
429 scrobbler_queue_remove_oldest(scrobbler->queue, scrobbler->pending);
430 scrobbler->pending = 0;
431 } else {
432 assert(record_is_defined(&scrobbler->now_playing));
433
434 record_deinit(&scrobbler->now_playing);
435 memset(&scrobbler->now_playing, 0,
436 sizeof(scrobbler->now_playing));
437 }
438
439
440 /* submit the next chunk (if there is some left) */
441 scrobbler_submit(scrobbler);
442 break;
443 case AS_SUBMIT_FAILED:
444 scrobbler_increase_interval(scrobbler);
445 scrobbler_schedule_submit(scrobbler);
446 break;
447 case AS_SUBMIT_HANDSHAKE:
448 scrobbler->state = SCROBBLER_STATE_NOTHING;
449 scrobbler_schedule_handshake(scrobbler);
450 break;
451 }
452 }
453
454 static void
scrobbler_submit_error(GError * error,void * data)455 scrobbler_submit_error(GError *error, void *data)
456 {
457 struct scrobbler *scrobbler = data;
458
459 assert(scrobbler->config->file == NULL);
460 assert(scrobbler->state == SCROBBLER_STATE_SUBMITTING);
461
462 scrobbler->state = SCROBBLER_STATE_READY;
463
464 g_warning("[%s] submit error: %s",
465 scrobbler->config->name, error->message);
466 g_error_free(error);
467
468 scrobbler_increase_interval(scrobbler);
469 scrobbler_schedule_submit(scrobbler);
470 }
471
472 static const struct http_client_handler scrobbler_submit_handler = {
473 .response = scrobbler_submit_response,
474 .error = scrobbler_submit_error,
475 };
476
as_timestamp(void)477 char *as_timestamp(void)
478 {
479 /* create timestamp for 1.2 protocol. */
480 GTimeVal time_val;
481
482 g_get_current_time(&time_val);
483 return g_strdup_printf("%ld", (glong)time_val.tv_sec);
484 }
485
486 /**
487 * Calculate the MD5 checksum of the specified string. The return
488 * value is a newly allocated string containing the hexadecimal
489 * checksum.
490 */
md5_hex(const char * p,int len)491 static char *md5_hex(const char *p, int len)
492 {
493 #if GLIB_CHECK_VERSION(2,16,0)
494 return g_compute_checksum_for_string(G_CHECKSUM_MD5, p, len);
495 #else
496 /* fall back to libgcrypt on GLib < 2.16 */
497 gcry_md_hd_t hd;
498 unsigned char *binary;
499 char *result;
500
501 if (len == -1)
502 len = strlen(p);
503
504 if (gcry_md_open(&hd, GCRY_MD_MD5, 0) != GPG_ERR_NO_ERROR)
505 g_error("gcry_md_open() failed\n");
506 gcry_md_write(hd, p, len);
507 binary = gcry_md_read(hd, GCRY_MD_MD5);
508 if (binary == NULL)
509 g_error("gcry_md_read() failed\n");
510 result = g_malloc(gcry_md_get_algo_dlen(GCRY_MD_MD5) * 2 + 1);
511 for (size_t i = 0; i < gcry_md_get_algo_dlen(GCRY_MD_MD5); ++i)
512 snprintf(result + i * 2, 3, "%02x", binary[i]);
513 gcry_md_close(hd);
514
515 return result;
516 #endif
517 }
518
as_md5(const char * password,const char * timestamp)519 static char *as_md5(const char *password, const char *timestamp)
520 {
521 char *password_md5, *cat, *result;
522
523 if (strlen(password) != 32)
524 /* assume it's not hashed yet */
525 password = password_md5 = md5_hex(password, -1);
526 else
527 password_md5 = NULL;
528
529 cat = g_strconcat(password, timestamp, NULL);
530 g_free(password_md5);
531
532 result = md5_hex(cat, -1);
533 g_free(cat);
534
535 return result;
536 }
537
538 static void
scrobbler_handshake(struct scrobbler * scrobbler)539 scrobbler_handshake(struct scrobbler *scrobbler)
540 {
541 assert(scrobbler->config->file == NULL);
542
543 GString *url;
544 char *timestr, *md5;
545
546 scrobbler->state = SCROBBLER_STATE_HANDSHAKE;
547
548 timestr = as_timestamp();
549 md5 = as_md5(scrobbler->config->password, timestr);
550
551 /* construct the handshake url. */
552 url = g_string_new(scrobbler->config->url);
553 first_var(url, "hs", "true");
554 add_var(url, "p", "1.2");
555 add_var(url, "c", AS_CLIENT_ID);
556 add_var(url, "v", AS_CLIENT_VERSION);
557 add_var(url, "u", scrobbler->config->username);
558 add_var(url, "t", timestr);
559 add_var(url, "a", md5);
560
561 g_free(timestr);
562 g_free(md5);
563
564 // notice ("handshake url:\n%s", url);
565
566 http_client_request(url->str, NULL,
567 &scrobbler_handshake_handler, scrobbler);
568
569 g_string_free(url, true);
570 }
571
572 static gboolean
scrobbler_handshake_timer(gpointer data)573 scrobbler_handshake_timer(gpointer data)
574 {
575 struct scrobbler *scrobbler = data;
576
577 assert(scrobbler->config->file == NULL);
578 assert(scrobbler->state == SCROBBLER_STATE_NOTHING);
579
580 scrobbler->handshake_source_id = 0;
581
582 scrobbler_handshake(data);
583 return false;
584 }
585
586 static void
scrobbler_schedule_handshake(struct scrobbler * scrobbler)587 scrobbler_schedule_handshake(struct scrobbler *scrobbler)
588 {
589 assert(scrobbler->config->file == NULL);
590 assert(scrobbler->state == SCROBBLER_STATE_NOTHING);
591 assert(scrobbler->handshake_source_id == 0);
592
593 scrobbler->handshake_source_id =
594 g_timeout_add_seconds(scrobbler->interval,
595 scrobbler_handshake_timer, scrobbler);
596 }
597
598 static void
scrobbler_send_now_playing(struct scrobbler * scrobbler,const char * artist,const char * track,const char * album,const char * number,const char * mbid,const int length)599 scrobbler_send_now_playing(struct scrobbler *scrobbler, const char *artist,
600 const char *track, const char *album,
601 const char *number,
602 const char *mbid, const int length)
603 {
604 GString *post_data;
605 char len[16];
606
607 assert(scrobbler->config->file == NULL);
608 assert(scrobbler->state == SCROBBLER_STATE_READY);
609 assert(scrobbler->submit_source_id == 0);
610
611 scrobbler->state = SCROBBLER_STATE_SUBMITTING;
612
613 snprintf(len, sizeof(len), "%i", length);
614
615 post_data = g_string_new(NULL);
616 add_var(post_data, "s", scrobbler->session);
617 add_var(post_data, "a", artist);
618 add_var(post_data, "t", track);
619 add_var(post_data, "b", album);
620 add_var(post_data, "l", len);
621 add_var(post_data, "n", number);
622 add_var(post_data, "m", mbid);
623
624 g_message("[%s] sending 'now playing' notification",
625 scrobbler->config->name);
626
627 http_client_request(scrobbler->nowplay_url,
628 post_data->str,
629 &scrobbler_submit_handler, scrobbler);
630
631 g_string_free(post_data, true);
632 }
633
634 static void
scrobbler_schedule_now_playing_callback(gpointer data,gpointer user_data)635 scrobbler_schedule_now_playing_callback(gpointer data, gpointer user_data)
636 {
637 struct scrobbler *scrobbler = data;
638 const struct record *song = user_data;
639
640 if (scrobbler->file != NULL)
641 /* there's no "now playing" support for files */
642 return;
643
644 record_deinit(&scrobbler->now_playing);
645 record_copy(&scrobbler->now_playing, song);
646
647 if (scrobbler->state == SCROBBLER_STATE_READY &&
648 scrobbler->submit_source_id == 0)
649 scrobbler_schedule_submit(scrobbler);
650 }
651
652 void
as_now_playing(const char * artist,const char * track,const char * album,const char * number,const char * mbid,const int length)653 as_now_playing(const char *artist, const char *track,
654 const char *album, const char *number,
655 const char *mbid, const int length)
656 {
657 struct record record;
658
659 record.artist = g_strdup(artist);
660 record.track = g_strdup(track);
661 record.album = g_strdup(album);
662 record.number = g_strdup(number);
663 record.mbid = g_strdup(mbid);
664 record.time = NULL;
665 record.length = length;
666
667 g_slist_foreach(scrobblers,
668 scrobbler_schedule_now_playing_callback, &record);
669
670 record_deinit(&record);
671 }
672
673 static void
scrobbler_submit(struct scrobbler * scrobbler)674 scrobbler_submit(struct scrobbler *scrobbler)
675 {
676 //MAX_SUBMIT_COUNT
677 unsigned count = 0;
678 GString *post_data;
679
680 assert(scrobbler->config->file == NULL);
681 assert(scrobbler->state == SCROBBLER_STATE_READY);
682 assert(scrobbler->submit_source_id == 0);
683
684 if (g_queue_is_empty(scrobbler->queue)) {
685 /* the submission queue is empty. See if a "now playing" song is
686 scheduled - these should be sent after song submissions */
687 if (record_is_defined(&scrobbler->now_playing))
688 scrobbler_send_now_playing(scrobbler,
689 scrobbler->now_playing.artist,
690 scrobbler->now_playing.track,
691 scrobbler->now_playing.album,
692 scrobbler->now_playing.number,
693 scrobbler->now_playing.mbid,
694 scrobbler->now_playing.length);
695
696 return;
697 }
698
699 scrobbler->state = SCROBBLER_STATE_SUBMITTING;
700
701 /* construct the handshake url. */
702 post_data = g_string_new(NULL);
703 add_var(post_data, "s", scrobbler->session);
704
705 for (GList *list = g_queue_peek_head_link(scrobbler->queue);
706 list != NULL && count < MAX_SUBMIT_COUNT;
707 list = g_list_next(list)) {
708 struct record *song = list->data;
709 char len[16];
710
711 snprintf(len, sizeof(len), "%i", song->length);
712
713 add_var_i(post_data, "a", count, song->artist);
714 add_var_i(post_data, "t", count, song->track);
715 add_var_i(post_data, "l", count, len);
716 add_var_i(post_data, "i", count, song->time);
717 add_var_i(post_data, "o", count, song->source);
718 add_var_i(post_data, "r", count, "");
719 add_var_i(post_data, "b", count, song->album);
720 add_var_i(post_data, "n", count, song->number);
721 add_var_i(post_data, "m", count, song->mbid);
722
723 if (song->love)
724 add_var_i(post_data, "r", count, "L");
725
726 count++;
727 }
728
729 g_message("[%s] submitting %i song%s",
730 scrobbler->config->name, count, count == 1 ? "" : "s");
731 g_debug("[%s] post data: %s", scrobbler->config->name, post_data->str);
732 g_debug("[%s] url: %s",
733 scrobbler->config->name, scrobbler->submit_url);
734
735 scrobbler->pending = count;
736 http_client_request(scrobbler->submit_url,
737 post_data->str,
738 &scrobbler_submit_handler, scrobbler);
739
740 g_string_free(post_data, true);
741 }
742
743 static void
scrobbler_push_callback(gpointer data,gpointer user_data)744 scrobbler_push_callback(gpointer data, gpointer user_data)
745 {
746 struct scrobbler *scrobbler = data;
747 const struct record *record = user_data;
748
749 if (scrobbler->file != NULL) {
750 fprintf(scrobbler->file, "%s %s - %s\n",
751 log_date(), record->artist, record->track);
752 fflush(scrobbler->file);
753 return;
754 }
755
756 g_queue_push_tail(scrobbler->queue, record_dup(record));
757
758 if (scrobbler->state == SCROBBLER_STATE_READY &&
759 scrobbler->submit_source_id == 0)
760 scrobbler_schedule_submit(scrobbler);
761 }
762
763 void
as_songchange(const char * file,const char * artist,const char * track,const char * album,const char * number,const char * mbid,const int length,bool love,const char * time2)764 as_songchange(const char *file, const char *artist, const char *track,
765 const char *album, const char *number,
766 const char *mbid, const int length,
767 bool love,
768 const char *time2)
769 {
770 struct record record;
771
772 /* from the 1.2 protocol draft:
773
774 You may still submit if there is no album title (variable b)
775 You may still submit if there is no musicbrainz id available (variable m)
776
777 everything else is mandatory.
778 */
779 if (!(artist && strlen(artist))) {
780 g_warning("empty artist, not submitting; "
781 "please check the tags on %s\n", file);
782 return;
783 }
784
785 if (!(track && strlen(track))) {
786 g_warning("empty title, not submitting; "
787 "please check the tags on %s", file);
788 return;
789 }
790
791 record.artist = g_strdup(artist);
792 record.track = g_strdup(track);
793 record.album = g_strdup(album);
794 record.number = g_strdup(number);
795 record.mbid = g_strdup(mbid);
796 record.length = length;
797 record.time = time2 ? g_strdup(time2) : as_timestamp();
798 record.love = love;
799 record.source = strstr(file, "://") == NULL ? "P" : "R";
800
801 g_message("%s, songchange: %s - %s (%i)\n",
802 record.time, record.artist,
803 record.track, record.length);
804
805 g_slist_foreach(scrobblers, scrobbler_push_callback, &record);
806
807 record_deinit(&record);
808 }
809
810 static void
scrobbler_new_callback(gpointer data,G_GNUC_UNUSED gpointer user_data)811 scrobbler_new_callback(gpointer data, G_GNUC_UNUSED gpointer user_data)
812 {
813 const struct scrobbler_config *config = data;
814 struct scrobbler *scrobbler = scrobbler_new(config);
815
816 if (config->journal != NULL) {
817 guint queue_length;
818
819 journal_read(config->journal, scrobbler->queue);
820
821 queue_length = g_queue_get_length(scrobbler->queue);
822 g_message("loaded %i song%s from %s",
823 queue_length, queue_length == 1 ? "" : "s",
824 config->journal);
825 }
826
827 scrobblers = g_slist_prepend(scrobblers, scrobbler);
828 if (config->file != NULL) {
829 scrobbler->file = fopen(config->file, "a");
830 if (scrobbler->file == NULL)
831 g_error("Failed to open file '%s' of scrobbler '%s': %s\n",
832 config->file, config->name, g_strerror(errno));
833 } else
834 scrobbler_schedule_handshake(scrobbler);
835 }
836
as_init(GSList * scrobbler_configs)837 void as_init(GSList *scrobbler_configs)
838 {
839 g_message("starting mpdscribble (" AS_CLIENT_ID " " AS_CLIENT_VERSION ")\n");
840
841 g_slist_foreach(scrobbler_configs, scrobbler_new_callback, NULL);
842 }
843
844 static gboolean
scrobbler_submit_timer(gpointer data)845 scrobbler_submit_timer(gpointer data)
846 {
847 struct scrobbler *scrobbler = data;
848
849 assert(scrobbler->state == SCROBBLER_STATE_READY);
850
851 scrobbler->submit_source_id = 0;
852
853 scrobbler_submit(scrobbler);
854 return false;
855 }
856
857 static void
scrobbler_schedule_submit(struct scrobbler * scrobbler)858 scrobbler_schedule_submit(struct scrobbler *scrobbler)
859 {
860 assert(scrobbler->submit_source_id == 0);
861 assert(!g_queue_is_empty(scrobbler->queue) ||
862 record_is_defined(&scrobbler->now_playing));
863
864 scrobbler->submit_source_id =
865 g_timeout_add_seconds(scrobbler->interval,
866 scrobbler_submit_timer, scrobbler);
867 }
868
869 static void
scrobbler_save_callback(gpointer data,G_GNUC_UNUSED gpointer user_data)870 scrobbler_save_callback(gpointer data, G_GNUC_UNUSED gpointer user_data)
871 {
872 struct scrobbler *scrobbler = data;
873
874 if (scrobbler->file != NULL || scrobbler->config->journal == NULL)
875 return;
876
877 if (journal_write(scrobbler->config->journal, scrobbler->queue)) {
878 guint queue_length = g_queue_get_length(scrobbler->queue);
879 g_message("[%s] saved %i song%s to %s",
880 scrobbler->config->name,
881 queue_length, queue_length == 1 ? "" : "s",
882 scrobbler->config->journal);
883 }
884 }
885
as_save_cache(void)886 void as_save_cache(void)
887 {
888 g_slist_foreach(scrobblers, scrobbler_save_callback, NULL);
889 }
890
891 static void
scrobbler_free_callback(gpointer data,G_GNUC_UNUSED gpointer user_data)892 scrobbler_free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data)
893 {
894 struct scrobbler *scrobbler = data;
895
896 scrobbler_free(scrobbler);
897 }
898
as_cleanup(void)899 void as_cleanup(void)
900 {
901 g_slist_foreach(scrobblers, scrobbler_free_callback, NULL);
902 g_slist_free(scrobblers);
903 }
904