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