Petr Holasek
2011-08-27 15:01:13 UTC
---
as.c | 931 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
as.h | 210 +++++++++++++++
2 files changed, 1141 insertions(+), 0 deletions(-)
create mode 100644 as.c
create mode 100644 as.h
diff --git a/as.c b/as.c
new file mode 100644
index 0000000..d39ed87
--- /dev/null
+++ b/as.c
@@ -0,0 +1,931 @@
+/*
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * as.[ch] by Frank Terbeck <***@bewatermyfriend.org>
+ * and Clay Barnes <***@hci-matters.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+/*
+ * see as.h and http://www.audioscrobbler.net
+ */
+
+#include <pthread.h>
+#include <time.h>
+
+#include "as.h"
+#include "cmus.h"
+#include "debug.h"
+#include "keyval.h"
+#include "list.h"
+#include "locking.h"
+#include "md5.h"
+#include "network.h"
+#include "player.h"
+#include "strlib.h"
+#include "utils.h"
+
+struct as_authinfo as_authinfo;
+struct as_hsinfo as_hsinfo;
+const char *as_numbers[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
+time_t as_lasttimestamp = 0;
+int as_enable = 0;
+int running = 0;
+
+static pthread_mutex_t as_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_t as_thread;
+static LIST_HEAD(as_queue_head);
+
+static unsigned int as_count_entries(void)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *entry;
+ int count=0;
+ list_for_each_entry(entry, &as_queue_head, node) {
+ ++count;
+ }
+ return(count);
+} /*}}}*/
+
+static struct as_queue *as_get_last_entry(void)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *entry;
+ list_for_each_entry(entry, &as_queue_head, node) { }
+ return(entry);
+} /*}}}*/
+
+static void as_urlencode(string_t *str)
+{ /*{{{*/
+ const char *transtab[] =
+ { /*{{{*/
+ " ", "%20", /* 00 01 */
+ "!", "%21", /* 02 03 */
+ "\"", "%22", /* 04 05 */
+ "#", "%23", /* 06 07 */
+ "%", "%25", /* 08 09 */
+ "&", "%26", /* 10 11 */
+ "'", "%27", /* 12 13 */
+ "(", "%28", /* 14 15 */
+ ")", "%29", /* 16 17 */
+ "*", "%2A", /* 18 19 */
+ "-", "%2D", /* 20 21 */
+ "/", "%2F", /* 22 23 */
+ ":", "%3A", /* 24 25 */
+ ";", "%3B", /* 26 27 */
+ "<", "%3C", /* 28 29 */
+ "=", "%3D", /* 30 31 */
+ ">", "%3E", /* 32 33 */
+ "?", "%3F", /* 34 35 */
+ "@", "%40", /* 36 37 */
+ "[", "%5B", /* 38 39 */
+ "\\", "%5C", /* 40 41 */
+ "]", "%5D", /* 42 43 */
+ "^", "%5E", /* 44 45 */
+ "`", "%60", /* 46 47 */
+ "{", "%7B", /* 48 49 */
+ "|", "%7C", /* 50 51 */
+ "}", "%7D", /* 52 53 */
+ "~", "%7E", /* 54 55 */
+ }; /*}}}*/
+ int i, e, idx=-1;
+ for (i=0; i<=str->len-1; ++i) {
+ if (mustquote(str->s[i])) {
+ for (e=0; e<=55; e+=2) {
+ if (transtab[e][0] == str->s[i]) {
+ idx=e+1;
+ break;
+ }
+ }
+ if (idx >= 0)
+ str_nreplace(str, i, 1, transtab[idx], 3);
+ }
+ }
+} /*}}}*/
+
+static void as_update_timestamp(void)
+{ /*{{{*/
+ as_lasttimestamp=time(NULL);
+} /*}}}*/
+
+static void as_handshake(void)
+{ /*{{{*/
+ int sockfd;
+ unsigned long int idx, odx;
+ char buf[AS_BUFSIZE+1];
+ ssize_t n;
+ string_t msg, tmp;
+
+ /* no authentication values set, don't do anything */
+ if (as_authinfo.user[0] == '\0' || as_authinfo.pass[0] == '\0')
+ return;
+
+ /* prepare handshake request */
+ str_init(&msg);
+ str_init(&tmp);
+ str_ncat(&msg, "GET ", 4); str_scat(&msg, AS_HS_URI);
+ str_ncat(&msg, "&p=", 3); str_scat(&msg, AS_PROTO);
+ str_ncat(&msg, "&c=", 3); str_ncat(&msg, AS_CLIENT_ID, 3);
+ str_ncat(&msg, "&v=", 3); str_scat(&msg, VERSION); /* cmus' Version, that is */
+ as_lock();
+ str_scat(&tmp, as_authinfo.user);
+ as_unlock();
+ as_urlencode(&tmp);
+ str_ncat(&msg, "&u=", 3); str_cat(&msg, &tmp);
+ str_ncat(&msg, " HTTP/1.1\r\nHost: ", 17); str_scat(&msg, AS_HOSTNAME);
+ str_ncat(&msg, "\r\nConnection: CLOSE\r\n\r\n", 23);
+
+ /* connect() */
+ if ( (sockfd = tcp_connect(AS_HOSTNAME, AS_HOSTPORT)) < 0 ) {
+ d_print("tcp_connect() failed!\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+
+ /* submit our handshake request */
+ xwriten(sockfd, msg.s, msg.len);
+
+ msg.s[0] = '\0';
+ msg.len = 0;
+
+ /* read server's response */
+ for (;;) {
+ if ( (n = xreadn(sockfd, buf, AS_BUFSIZE) ) <= 0)
+ break;
+ buf[n] = '\0';
+ str_ncat(&msg, buf, n);
+ }
+ xclose(sockfd);
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("Serverreply:\n\"%s\"\n", msg.s);
+#endif
+
+ /* parse the response we read */
+ as_lock();
+ if (str_ngetidx(&msg, 0, "\r\n\r\n", 4, &idx)) {
+ str_nreplace(&msg, 0, idx+4, "", 0);
+ }
+ else {
+ d_print("broken reply, couldn't find end of headers (\\r\\n\\r\\n)\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+
+ /*
+ * UPTODATE/UPDATE/FAILED/BADUSER
+ */
+ idx=odx=0;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ if (str_ngetidx(&tmp, 0, "UPTODATE", 8, NULL)) {
+ d_print("received UPTODATE, good.\n");
+ }
+ else if (str_ngetidx(&tmp, 0, "UPDATE", 6, NULL)) {
+ d_print("received UPDATE, get a new client, baby.\n");
+ }
+ else if (str_ngetidx(&tmp, 0, "FAILED", 6, NULL)) {
+ d_print("received FAILED, crap :-/\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ else if (str_ngetidx(&tmp, 0, "BADUSER", 7, NULL)) {
+ d_print("received BADUSER, err, check as_user setting.\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ else {
+ d_print("Unknown response: \"%s\"\n", tmp.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+
+ /*
+ * -challenge-
+ */
+ odx=idx+1;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ str_cpy(&(as_hsinfo.challenge), &tmp);
+
+ /*
+ * -url-to-submit-script
+ */
+ odx=idx+1;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ odx=idx+1;
+ if (str_ngetidx(&tmp, 0, "://", 3, &idx)) {
+ if (idx+3 <= tmp.len)
+ str_nreplace(&tmp, 0, idx+3, "", 0);
+ }
+ else
+ d_print("hrm, no service mnemonic? (%s)\n)", tmp.s);
+
+ if (str_ngetidx(&tmp, 0, "/", 1, &idx) && (idx <= tmp.len)) {
+ str_ncpy(&as_hsinfo.path, tmp.s+idx, tmp.len-idx);
+ str_nreplace(&tmp, idx, tmp.len-idx, "", 0);
+ }
+ else {
+ d_print("um, no path? (%s)\n", tmp.s);
+ as_hsinfo.failed++;
+ }
+
+ if (str_ngetidx(&tmp, 0, ":", 1, &idx)) {
+ strncpy(as_hsinfo.port, tmp.s+idx+1, (tmp.len-idx < AS_MAXTMPLEN ? tmp.len-idx : AS_MAXTMPLEN));
+ as_hsinfo.port[(tmp.len-idx < AS_MAXTMPLEN ? tmp.len-idx : AS_MAXTMPLEN)] = '\0';
+ }
+ else {
+ d_print("well, no port definition? That, would be okay, default: 80. (%s)", tmp.s);
+ as_hsinfo.port[0] = '8'; as_hsinfo.port[1] = '0'; as_hsinfo.port[2] = '\0';
+ }
+ str_ncpy(&(as_hsinfo.host), tmp.s, idx);
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("debug as_hsinfo:\n\thost: \"%s\"\n\tpath: \"%s\"\n\tport: \"%s\"\n",
+ as_hsinfo.host.s, as_hsinfo.path.s, as_hsinfo.port);
+#endif
+
+ /*
+ * -INTERVAL-
+ */
+ odx=idx+1;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ if (str_ngetidx(&tmp, 0, "INTERVAL ", 9, NULL)) {
+ as_hsinfo.interval=atoi(tmp.s+9);
+ d_print("Our Interval is: %d\n", (int)as_hsinfo.interval);
+ }
+
+ /* handshake suceeded, all values set. */
+ as_hsinfo.need_handshake=0;
+
+cleanup:
+ as_unlock();
+ str_free(&tmp);
+ str_free(&msg);
+} /*}}}*/
+
+static int as_submit_allowed(void)
+{ /*{{{*/
+ /*
+ * honor INTERVAL response from the server
+ */
+ const time_t now = time(NULL);
+ time_t at;
+ as_lock();
+ at = as_hsinfo.interval + as_lasttimestamp;
+ as_unlock();
+ if (now >= at)
+ return(1);
+ return(0);
+} /*}}}*/
+
+static int as_submit_forced(void)
+{ /*{{{*/
+ /*
+ * queue maximum is 10, so we force submission at 8 entries
+ */
+ unsigned int n;
+ as_lock();
+ n = as_count_entries();
+ as_unlock();
+ if (n >= AS_FORCE_SUBMIT)
+ return(1);
+ return(0);
+} /*}}}*/
+
+static void as_del_queue(struct list_head *item)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *tmp;
+
+ tmp=list_entry(item, struct as_queue, node);
+ d_print(" string: %s\n", tmp->s.s);
+ d_print(" NEEDRN: %d\n", tmp->needrenumber);
+ d_print(" SUBMIT: %d\n", tmp->sc);
+ d_print(" num : %d\n", tmp->num);
+ str_free(&(tmp->s));
+ list_del(item);
+ free(tmp);
+} /*}}}*/
+
+static void as_destroy_queue(int scmask)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *pos, *tmp;
+
+ list_for_each_entry_safe(pos, tmp, &as_queue_head, node) {
+ if (pos->sc & scmask)
+ as_del_queue(&(pos->node));
+ }
+} /*}}}*/
+
+static void as_renumberentry(string_t *entry, unsigned short int num)
+{ /*{{{*/
+ unsigned long idx, odx = 0;
+ while (str_ngetidx(entry, odx, "]=", 2, &idx)) {
+ if (idx > 0) {
+ d_print("replacing %c with %c\n", entry->s[idx-1], as_numbers[num][0]);
+ entry->s[idx-1] = as_numbers[num][0];
+ }
+ odx=idx+2;
+ }
+} /*}}}*/
+
+static unsigned short int as_uint2string(unsigned int integer, char *str)
+{ /*{{{*/
+ unsigned short int dn, i, digit;
+ unsigned int tmp;
+
+ if (integer <= 9) {
+ dn=1;
+ str[0] = as_numbers[integer][0];
+ str[1] = '\0';
+ }
+ else {
+ dn=0;
+ tmp=integer;
+ while (tmp) {
+ ++dn;
+ tmp/=10;
+ }
+ }
+ for (i=dn; i > 0 ; --i) {
+ digit=integer%10;
+ integer/=10;
+ str[i-1] = as_numbers[digit][0];
+ }
+ str[dn] = '\0';
+ return(dn);
+} /*}}}*/
+
+static void as_digest2hex(unsigned char *hex, const unsigned char *digest)
+{ /*{{{*/
+ char hc[] = "0123456789abcdef";
+ unsigned int i, e=0;
+
+ memset(hex, 0, 33*sizeof(unsigned char));
+
+ for(i = 0; i < 16; i++) {
+ hex[e++] = hc[(digest[i] >> 4) & 0x0f];
+ hex[e++] = hc[ digest[i] & 0x0f];
+ }
+ hex[32] = '\0';
+} /*}}}*/
+
+static void as_submit(void)
+{ /*{{{*/
+ string_t msg_header, msg_content, stmp, msg;
+ struct as_queue *entry;
+ unsigned char digest[16], tmp[65], md5response[33];
+ char i2s[AS_MAXTMPLEN];
+ char buf[AS_BUFSIZE];
+ unsigned long int idx, odx, submissions=0;
+ ssize_t n;
+ int sockfd;
+ int testval;
+ int ign=0;
+
+ /*
+ * we need authentication setup, data to submit in our queue
+ * and the handshake must have taken place already. abort otherwise.
+ */
+ as_lock();
+ testval = (as_authinfo.user[0] == '\0' || as_authinfo.pass[0] == '\0'
+ || as_hsinfo.need_handshake == 1 || list_empty(&as_queue_head));
+ as_unlock();
+ if (testval)
+ return;
+
+ /*
+ * creating the md5response described in the protocol v1.1 definition
+ * - create md5 checksum of $as_pass and covert it to hex
+ * - append the md5challenge received in handshake to the just created string
+ * - create md5 checksum of the concatenated string and convert it to hex.
+ * That's it.
+ */
+ as_lock();
+ md5_csum(as_authinfo.pass, strlen(as_authinfo.pass), digest);
+ as_digest2hex(tmp, digest);
+ strncpy(tmp+32, as_hsinfo.challenge.s, 32);
+ tmp[64] = '\0';
+ md5_csum(tmp, 64, digest);
+ as_digest2hex(md5response, digest);
+
+ str_init(&msg_header);
+ str_init(&msg_content);
+ str_init(&stmp);
+ str_init(&msg);
+ /*
+ * assemble our payload
+ */
+ str_ncat(&msg_content, "u=", 2); str_scat(&msg_content, as_authinfo.user);
+ str_ncat(&msg_content, "&s=", 3); str_scat(&msg_content, md5response);
+ list_for_each_entry(entry, &as_queue_head, node) {
+ if (ign && entry->sc == AS_SC_DO_SUBMIT) {
+ entry->num-=ign;
+ entry->needrenumber=1;
+ }
+ if (entry->needrenumber) {
+ as_renumberentry(&(entry->s), entry->num);
+ entry->needrenumber=0;
+ }
+ if (entry->sc == AS_SC_DO_SUBMIT) {
+ str_cat(&msg_content, &(entry->s));
+ entry->sc = AS_SC_SUBMITTED;
+ ++submissions;
+ d_print("scrobbling: \"%s\"\n", entry->s.s);
+ }
+ else {
+ ++ign;
+ d_print("ignoring: \"%s\"\n", entry->s.s);
+ }
+ }
+ if (!submissions) {
+ /* no submissions, eh? */
+ as_unlock();
+ goto cleanup;
+ }
+ as_uint2string(msg_content.len, i2s); /* note the length of our payload for later use */
+ str_ncat(&msg_content, "\n", 1);
+
+ /* create http/1.1 header for our submission */
+ str_ncat(&msg_header, "POST ", 5); str_cat(&msg_header, &(as_hsinfo.path)); str_ncat(&msg_header, " HTTP/1.1\r\n", 11);
+ str_ncat(&msg_header, "Host: ", 6); str_cat(&msg_header, &(as_hsinfo.host)); str_ncat(&msg_header, "\r\n", 2);
+ str_ncat(&msg_header, AS_UASTRING, AS_UASTRINGLEN);
+ str_ncat(&msg_header, "Pragma: no-cache", 16);
+ str_ncat(&msg_header, "\r\nContent-Type: application/x-www-form-urlencoded", 49);
+ str_ncat(&msg_header, "\r\nContent-Length: ", 18); str_scat(&msg_header, i2s);
+ str_ncat(&msg_header, "\r\nConnection: CLOSE\r\n\r\n", 23);
+
+ /* turn payload's special chars to utf8 */
+ /*str_lat1_to_utf8(&msg, &msg_content);
+ str_cpy(&msg_content, &msg);*/
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("POSTING: \"%s%s\"\n", msg_header.s, msg_content.s);
+#endif
+
+ as_unlock();
+
+ /*
+ * connect to the server we got in the handshake
+ */
+ if ( (sockfd = tcp_connect(as_hsinfo.host.s, as_hsinfo.port)) < 0 ) {
+ d_print("tcp_connect() failed!\n");
+ as_lock();
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+
+ /*
+ * send our submission to the server
+ */
+ str_cpy(&msg, &msg_header);
+ str_cat(&msg, &msg_content);
+ xwriten(sockfd, msg.s, msg.len);
+ str_free(&msg);
+
+ msg_content.s[0] = '\0';
+ msg_content.len = 0;
+
+ /* read response to our submission */
+ for (;;) {
+ if ( (n = xreadn(sockfd, buf, AS_BUFSIZE) ) <= 0)
+ break;
+ buf[n] = '\0';
+ str_ncat(&msg_content, buf, n);
+ }
+ xclose(sockfd);
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("Serverreply:\n\"%s\"\n", msg_content.s);
+#endif
+
+ /* parse the response */
+ as_lock();
+ if (str_ngetidx(&msg_content, 0, "\r\n\r\n", 4, &idx)) {
+ str_nreplace(&msg_content, 0, idx+4, "", 0);
+ }
+ else {
+ d_print("broken reply, couldn't find end of headers (\\r\\n\\r\\n)\n");
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ odx=0;
+ if (!str_ngetidx(&msg_content, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg_content.s);
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ str_ncpy(&stmp, msg_content.s+odx, idx-odx);
+
+ /* OK/FAILED/BADAUTH */
+ if (str_ngetidx(&stmp, 0, "OK", 2, NULL)) {
+ d_print("received an OK, good.\n");
+ }
+ else if (str_ngetidx(&stmp, 0, "FAILED", 6, NULL)) {
+ stmp.s[idx] = '\0';
+ d_print("received FAILED (%s)\n", stmp.s+odx);
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ else if (str_ngetidx(&stmp, 0, "BADAUTH", 7, NULL)) {
+ d_print("received BADUSER, err, check as_user setting.\n");
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ else {
+ d_print("Unknown response: \"%s\"\n", stmp.s);
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+
+ /* INTERVAL is optional here. */
+ odx=idx+1;
+ if (str_ngetidx(&msg_content, odx, "\n", 1, &idx)) {
+ str_ncpy(&stmp, msg_content.s+odx, idx-odx);
+ if (str_ngetidx(&stmp, 0, "INTERVAL ", 9, NULL)) {
+ as_hsinfo.interval=atoi(stmp.s+9);
+ d_print("Our Interval is: %d\n", (int)as_hsinfo.interval);
+ }
+ }
+ as_unlock();
+
+ /*
+ * submission suceeded
+ * destroy the part of the queue, that holds these
+ */
+ as_destroy_queue(AS_SC_SUBMITTED);
+cleanup:
+ if (as_hsinfo.failed) {
+ /* resetting AS_SC_SUBMITTED, because something went wrong */
+ list_for_each_entry(entry, &as_queue_head, node) {
+ if (entry->sc == AS_SC_SUBMITTED)
+ entry->sc = AS_SC_DO_SUBMIT;
+ }
+ }
+ str_free(&msg);
+ str_free(&stmp);
+ str_free(&msg_header);
+ str_free(&msg_content);
+} /*}}}*/
+
+static void as_gettimestring(char *str, int maxlen)
+{ /*{{{*/
+ const time_t now = time(NULL);
+ const struct tm *timeinfo = gmtime(&now);
+ strftime(str, maxlen, "%Y%%2d%m%%2d%d%%20%H%%3a%M%%3a%S", timeinfo);
+} /*}}}*/
+
+static void as_add_queue(string_t *trackartist,
+ string_t *trackalbum,
+ string_t *trackname,
+ int tracklen, int submissioncode)
+{ /*{{{*/
+ /*
+ * Description:
+ * This adds a track to the submission-queue.
+ * Only 10 Tracks are allowed to be submitted at once.
+ * So, if we run past this limit, delete this first item of the list
+ * and append this track to the end of the list.
+ */
+ struct as_queue *ptr, *tmp;
+ char tmpstr[AS_MAXTMPLEN+1];
+ int tmplen;
+ int queue_size;
+
+ d_print("adding track to as_queue:\n");
+ d_print(" Artist: %s\n", trackartist->s);
+ d_print(" Album : %s\n", trackalbum->s);
+ d_print(" Name : %s\n", trackname->s);
+ d_print(" Length: %d\n", tracklen);
+
+ if (!(ptr=malloc(sizeof(struct as_queue)))) {
+ d_print("uark! out of memory? I give up.");
+ return;
+ }
+
+ as_lock();
+
+ if (list_empty(&as_queue_head)) {
+ ptr->num=0;
+ list_add_tail(&(ptr->node), &as_queue_head);
+ }
+ else {
+ /* let's find our place in the queue */
+ queue_size=as_count_entries();
+ tmp = as_get_last_entry();
+ if (queue_size >= AS_QUEUE_MAX) {
+ as_del_queue(as_queue_head.next);
+ ptr->num=AS_QUEUE_MAX;
+ list_add_tail(&(ptr->node), &as_queue_head);
+ list_for_each_entry(tmp, &as_queue_head, node) {
+ tmp->num--;
+ tmp->needrenumber=1;
+ }
+ }
+ else {
+ list_add_tail(&(ptr->node), &as_queue_head);
+ ptr->num=queue_size;
+ }
+ }
+
+ str_init(&(ptr->s));
+ ptr->sc=submissioncode;
+ ptr->needrenumber=0;
+ ptr->len=tracklen;
+ /* set up the request string */
+ as_urlencode(trackartist);
+ as_urlencode(trackalbum);
+ as_urlencode(trackname);
+ as_addtag(ptr->s, trackartist, "&a[");
+ as_addtag(ptr->s, trackname, "&t[");
+ as_addtag(ptr->s, trackalbum, "&b[");
+ /*
+ * MusicBrainz ID, I'm just sending an empty one, because the
+ * protocol definition says "don't skip it"
+ */
+ str_readya(&(ptr->s), 6);
+ str_ncat( &(ptr->s), "&m[", 3);
+ str_ncat( &(ptr->s), as_numbers[ptr->num], 1);
+ str_ncat( &(ptr->s), "]=", 2);
+ tmplen = as_uint2string(ptr->len, tmpstr);
+ as_addntag(ptr->s, tmpstr, tmplen, "&l[");
+ as_gettimestring(tmpstr, AS_MAXTMPLEN);
+ as_addntag(ptr->s, tmpstr, AS_MAXTMPLEN-1, "&i[");
+ as_unlock();
+} /*}}}*/
+
+void as_hook(enum as_actions action)
+{ /*{{{*/
+ /*
+ * When do we need to call as_hook()?
+ * + stop
+ * + seek
+ * + next
+ * + prev
+ * + automatic song change
+ *
+ * What do we need to know?
+ * + track length
+ * + track position (we query player_info for that)
+ * + the action that called us
+ * + track name
+ * + artist
+ * + album
+ */
+ static int lastwasinvalid = 0; /* these 2 static ints are there */
+ static int alreadysubmitted = 0; /* to handle AS_SEEK properly. */
+ int trackpos, tracklen;
+ int testval;
+ const char *tmp;
+ string_t trackartist, trackalbum, trackname;
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("-- ping -----------------HOOOOK!1!!-------------------------------------\n");
+#endif
+
+ as_lock();
+ testval=running;
+ as_unlock();
+ if (!testval)
+ return;
+
+ if (lastwasinvalid) {
+ /*
+ * last was invalid; if something else than SEEK
+ * was done, this would submit the invalid track, so we
+ * reset lastwasinvalid here and return.
+ *
+ * if SEEK was invoked again, we just return, because
+ * the track was marked as invalid before.
+ */
+ if (action != AS_SEEK)
+ lastwasinvalid = 0;
+ return;
+ }
+ if (alreadysubmitted) {
+ if (action != AS_SEEK)
+ alreadysubmitted = 0;
+ return;
+ }
+
+ player_info_lock();
+ trackpos=player_info.pos;
+
+ if (player_info.ti == NULL) {
+ player_info_unlock();
+ return;
+ }
+
+ tracklen=player_info.ti->duration;
+
+ if (tracklen < AS_TRACK_MINLEN) { /* track is not long enough */
+ player_info_unlock();
+ return;
+ }
+ if (trackpos < AS_TRACK_MINSECS && (tracklen/2 > trackpos)) { /* track did not play long enough */
+ /*
+ * if we were called in NEXT, PREV, STOP or *_ENTER
+ * we already did skip to the next track (or stopped),
+ * which means, we don't need to save this one for later review.
+ * So, we return.
+ */
+ player_info_unlock();
+ if (action != AS_SEEK)
+ return;
+
+ lastwasinvalid = 1;
+ return;
+ }
+
+ str_init(&trackartist);
+ str_init(&trackalbum);
+ str_init(&trackname);
+ tmp=keyvals_get_val(player_info.ti->comments, "artist");
+ str_scpy(&trackartist, tmp);
+ tmp=keyvals_get_val(player_info.ti->comments, "album");
+ str_scpy(&trackalbum, tmp);
+ tmp=keyvals_get_val(player_info.ti->comments, "title");
+ str_scpy(&trackname, tmp);
+ player_info_unlock();
+
+ if (trackartist.len == 0 || trackalbum.len == 0 || trackname.len == 0) {
+ /* One of the tags we must supply is empty */
+ return;
+ }
+
+ as_add_queue(&trackartist, &trackalbum, &trackname, tracklen, AS_SC_DO_SUBMIT);
+
+ if (action == AS_SEEK)
+ alreadysubmitted = 1;
+
+ str_free(&trackartist);
+ str_free(&trackalbum);
+ str_free(&trackname);
+} /*}}}*/
+
+static void as_sleep(void)
+{ /*{{{*/
+ struct timespec r;
+ r.tv_sec=AS_SLEEPSEC;
+ r.tv_nsec=AS_SLEEPNANO;
+ nanosleep(&r, NULL);
+} /*}}}*/
+
+void *as_loop(void *arg)
+{ /*{{{*/
+ /*
+ * main audio scrobbler loop
+ * Here, all communication is done.
+ * Needed information is provided by *as_queue,
+ * which is filled by as_hook()
+ *
+ * TODO: AS_SC_DONT_SUBMIT cases must be handled (eg. the user used seek on a track before it fitted our needs).
+ */
+ static int last_as_state=0;
+ int testval;
+ struct as_queue *e;
+
+ for (;;) {
+#ifdef AS_HEAVY_DEBUG
+ d_print("-- ping ----------------------------------------------------------------\n");
+#endif
+ as_update_timestamp();
+ as_lock();
+ testval = list_empty(&as_queue_head);
+ as_unlock();
+ if (testval) {
+ /* nothing to submit */
+ if (!running) {
+ /*
+ * running isn't set,
+ * seems like the user switched us off
+ */
+ return(NULL);
+ }
+ as_sleep();
+ }
+ else {
+ as_lock();
+ list_for_each_entry(e, &as_queue_head, node) {
+ d_print("--DEBUG--\n");
+ d_print(" Number: %d\n", e->num);
+ d_print(" SCode : %d\n", e->sc);
+ d_print(" Length: %d\n", e->len);
+ d_print(" NEEDRN: %d\n", e->needrenumber);
+ d_print(" string: %s\n\n", e->s.s);
+ }
+ as_unlock();
+ as_sleep();
+ as_lock();
+ testval = as_hsinfo.need_handshake;
+ as_unlock();
+ if (testval) {
+ as_handshake();
+ }
+ as_lock();
+ testval = as_hsinfo.need_handshake;
+ as_unlock();
+ if (!testval) {
+ /*
+ * we are enabled, got data to submit and don't need to handshake.
+ * check, if we may (or need to force to) submit our data,
+ * and do so, if needed.
+ *
+ * XXX: as_submit_allowed() and as_submit_forced() do locking on their own.
+ */
+ if (as_submit_allowed() || as_submit_forced()) {
+ as_submit();
+ }
+ }
+ }
+
+ as_lock();
+ if (!as_enable && !last_as_state) {
+ /*
+ * user didn't enable us in the first place.
+ * so, set running to 0 and rerun loop, so we can exit
+ */
+ running=0;
+ }
+ else if (!as_enable && last_as_state) {
+ /* as_enable was on before, so terminate session */
+ running=0;
+ last_as_state=0;
+ }
+ else if (!last_as_state && as_enable) {
+ /* (re)start as session */
+ running=1;
+ as_hsinfo.need_handshake=1;
+ last_as_state=1;
+ }
+ as_unlock();
+ }
+} /*}}}*/
+
+void as_init(void)
+{ /*{{{*/
+ int rc;
+ as_lock();
+ running=1;
+ str_init(&(as_hsinfo.challenge));
+ str_init(&(as_hsinfo.host));
+ str_init(&(as_hsinfo.path));
+ as_hsinfo.need_handshake = 1;
+ as_hsinfo.interval = 0;
+ as_hsinfo.failed = 0;
+ as_lasttimestamp = 0;
+ as_unlock();
+ rc = pthread_create(&as_thread, NULL, as_loop, NULL);
+ BUG_ON(rc);
+} /*}}}*/
+
+void as_exit(void)
+{ /*{{{*/
+ as_lock();
+ running = 0;
+ as_destroy_queue(AS_SC_ALL);
+ str_free(&(as_hsinfo.challenge));
+ str_free(&(as_hsinfo.host));
+ str_free(&(as_hsinfo.path));
+ as_unlock();
+ pthread_join(as_thread, NULL);
+} /*}}}*/
diff --git a/as.h b/as.h
new file mode 100644
index 0000000..e1dc638
--- /dev/null
+++ b/as.h
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * as.[ch] by Frank Terbeck <***@bewatermyfriend.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#ifndef x__AS_H
+#define x__AS_H
+
+/*
+ * audioscrobbler code for cmus {{{
+ * (read: get cmus properly working with http://last.fm/
+ *
+ * When are songs submitted?
+ * + Each song should be posted to the server when it is 50%
+ * or 240 seconds complete, whichever comes first.
+ *
+ * + If a user seeks (i.e. manually changes position) within a song
+ * before the song is due to be submitted, do not submit that song.
+ *
+ * + Songs with a duration of less than 30 seconds should not be
+ * submitted (so, we don't do that).
+ *
+ * + Do not try to guess info from the filename - if there are no
+ * tags, don't submit the file (okeydoke, herman).
+ *
+ * + If a MusicBrainz ID is present in the file (as defined here),
+ * then you should send it (whatever that is...[1]).
+ *
+ * + If a user is playing a stream instead of a regular file, do
+ * not submit that stream/song.
+ *
+ *********************************************************************
+ *
+ * [1] About musicbrainz.org:
+ * I don't know what that is, nor do I care. Feel free to
+ * implement support for it, if you need it desperately.
+ *}}}
+ */
+
+#include "list.h"
+#include "options.h"
+#include "strlib.h"
+#include "track_info.h"
+
+/* #defines */
+
+/* We are currently testing things, so "tst" is okay. */
+#define AS_UASTRING "User-Agent: AudioScrobbler/1.1 cmus-scrobbler code v0.01\r\n"
+#define AS_UASTRINGLEN 58
+#define AS_CLIENT_ID "tst"
+#define AS_PROTO "1.1"
+
+#define AS_HOSTNAME "post.audioscrobbler.com"
+#define AS_HOSTPORT "80"
+#define AS_HS_URI "/?hs=true"
+
+#define AS_BUFSIZE 4096
+
+/* AS_QUEUE_MAX must of an element of [1..10] */
+#define AS_QUEUE_MAX 10
+
+/*
+ * AS_FORCE_SUBMIT:
+ * if our queue holds this many entries, force submission,
+ * no matter what the last INTERVAL response said.
+ */
+#define AS_FORCE_SUBMIT 8
+
+/* AS_DO_SUBMIT marks a track for submission */
+#define AS_SC_DO_SUBMIT 1
+
+/* AS_DONT_SUBMIT is used if seek was used at the beginning of a track */
+#define AS_SC_DONT_SUBMIT 2
+
+/* AS_SC_SUBMITTED marks tracks, that where included in a submission msg */
+#define AS_SC_SUBMITTED 4
+
+/* AS_SC_ALL is the same as ORing together all other AS_SC_* macros */
+#define AS_SC_ALL 15
+
+/* The minimum length of a track to be considered for submission */
+#define AS_TRACK_MINLEN 30
+
+/* A track must have run 50% of its time or AS_TRACK_MINSECS seconds */
+#define AS_TRACK_MINSECS 240
+
+/* AS_SLEEP* sets up the resolution of as_loop() */
+#define AS_SLEEPSEC 5
+#define AS_SLEEPNANO 0
+
+/*
+ * for temporary static strings, that need to be long enough
+ * to hold an url_encoded time string (25+1); this is used for
+ * strings that are converted integers.
+ */
+#define AS_MAXTMPLEN 30
+
+/* data types */
+enum as_actions {
+ AS_AUTONEXT=1,
+ AS_SEEK,
+ AS_NEXT,
+ AS_PREV,
+ AS_STOP,
+ AS_TREE_ENTER,
+ AS_SORTED_ENTER,
+ AS_PL_ENTER
+};
+
+struct as_queue {
+ struct list_head node;
+ string_t s;
+ unsigned int len;
+ unsigned short int sc; /* AS_SC_* */
+ unsigned short int needrenumber; /* we must renumber 'string_t s' */
+ unsigned short int num; /* number of the current entry */
+};
+
+struct as_hsinfo {
+ unsigned int need_handshake;
+ unsigned int failed;
+ time_t interval;
+ string_t challenge;
+ string_t host;
+ string_t path;
+ char port[AS_MAXTMPLEN+1]; /* yes, port as a string. tcp_connect expects its port as char[] */
+};
+
+struct as_authinfo {
+ char user[OPTION_MAX_SIZE];
+ char pass[OPTION_MAX_SIZE];
+};
+
+/* function prototypes */
+void as_hook(enum as_actions action);
+void *as_loop(void *arg);
+void as_exit(void);
+void as_init(void);
+
+/* macros */
+#define as_addtag(dest, src, tag) \
+ str_readya(&(dest), 6); \
+ str_ncat( &(dest), tag, 3); \
+ str_ncat( &(dest), as_numbers[ptr->num], 1); \
+ str_ncat( &(dest), "]=", 2); \
+ str_readya(&(dest), src->len); \
+ str_cat( &(dest), src)
+
+#define as_addntag(dest, src, len, tag) \
+ str_readya(&(dest), 6); \
+ str_ncat( &(dest), tag, 3); \
+ str_ncat( &(dest), as_numbers[ptr->num], 1); \
+ str_ncat( &(dest), "]=", 2); \
+ str_readya(&(dest), len); \
+ str_ncat( &(dest), src, len)
+
+#define mustquote(c) \
+ c == ' ' || \
+ c == '!' || \
+ c == '\"' || \
+ c == '#' || \
+ c == '%' || \
+ c == '&' || \
+ c == '\'' || \
+ c == '(' || \
+ c == ')' || \
+ c == '*' || \
+ c == '-' || \
+ c == '/' || \
+ c == ':' || \
+ c == ';' || \
+ c == '<' || \
+ c == '=' || \
+ c == '>' || \
+ c == '?' || \
+ c == '@' || \
+ c == '[' || \
+ c == '\\' || \
+ c == ']' || \
+ c == '^' || \
+ c == '`' || \
+ c == '{' || \
+ c == '|' || \
+ c == '}' || \
+ c == '~'
+
+#define as_lock() cmus_mutex_lock(&as_mutex)
+#define as_unlock() cmus_mutex_unlock(&as_mutex)
+
+/* exported variables */
+extern int as_enable;
+extern struct as_authinfo as_authinfo;
+
+#endif /* x__AS_H */
as.c | 931 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
as.h | 210 +++++++++++++++
2 files changed, 1141 insertions(+), 0 deletions(-)
create mode 100644 as.c
create mode 100644 as.h
diff --git a/as.c b/as.c
new file mode 100644
index 0000000..d39ed87
--- /dev/null
+++ b/as.c
@@ -0,0 +1,931 @@
+/*
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * as.[ch] by Frank Terbeck <***@bewatermyfriend.org>
+ * and Clay Barnes <***@hci-matters.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+/*
+ * see as.h and http://www.audioscrobbler.net
+ */
+
+#include <pthread.h>
+#include <time.h>
+
+#include "as.h"
+#include "cmus.h"
+#include "debug.h"
+#include "keyval.h"
+#include "list.h"
+#include "locking.h"
+#include "md5.h"
+#include "network.h"
+#include "player.h"
+#include "strlib.h"
+#include "utils.h"
+
+struct as_authinfo as_authinfo;
+struct as_hsinfo as_hsinfo;
+const char *as_numbers[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
+time_t as_lasttimestamp = 0;
+int as_enable = 0;
+int running = 0;
+
+static pthread_mutex_t as_mutex = CMUS_MUTEX_INITIALIZER;
+static pthread_t as_thread;
+static LIST_HEAD(as_queue_head);
+
+static unsigned int as_count_entries(void)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *entry;
+ int count=0;
+ list_for_each_entry(entry, &as_queue_head, node) {
+ ++count;
+ }
+ return(count);
+} /*}}}*/
+
+static struct as_queue *as_get_last_entry(void)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *entry;
+ list_for_each_entry(entry, &as_queue_head, node) { }
+ return(entry);
+} /*}}}*/
+
+static void as_urlencode(string_t *str)
+{ /*{{{*/
+ const char *transtab[] =
+ { /*{{{*/
+ " ", "%20", /* 00 01 */
+ "!", "%21", /* 02 03 */
+ "\"", "%22", /* 04 05 */
+ "#", "%23", /* 06 07 */
+ "%", "%25", /* 08 09 */
+ "&", "%26", /* 10 11 */
+ "'", "%27", /* 12 13 */
+ "(", "%28", /* 14 15 */
+ ")", "%29", /* 16 17 */
+ "*", "%2A", /* 18 19 */
+ "-", "%2D", /* 20 21 */
+ "/", "%2F", /* 22 23 */
+ ":", "%3A", /* 24 25 */
+ ";", "%3B", /* 26 27 */
+ "<", "%3C", /* 28 29 */
+ "=", "%3D", /* 30 31 */
+ ">", "%3E", /* 32 33 */
+ "?", "%3F", /* 34 35 */
+ "@", "%40", /* 36 37 */
+ "[", "%5B", /* 38 39 */
+ "\\", "%5C", /* 40 41 */
+ "]", "%5D", /* 42 43 */
+ "^", "%5E", /* 44 45 */
+ "`", "%60", /* 46 47 */
+ "{", "%7B", /* 48 49 */
+ "|", "%7C", /* 50 51 */
+ "}", "%7D", /* 52 53 */
+ "~", "%7E", /* 54 55 */
+ }; /*}}}*/
+ int i, e, idx=-1;
+ for (i=0; i<=str->len-1; ++i) {
+ if (mustquote(str->s[i])) {
+ for (e=0; e<=55; e+=2) {
+ if (transtab[e][0] == str->s[i]) {
+ idx=e+1;
+ break;
+ }
+ }
+ if (idx >= 0)
+ str_nreplace(str, i, 1, transtab[idx], 3);
+ }
+ }
+} /*}}}*/
+
+static void as_update_timestamp(void)
+{ /*{{{*/
+ as_lasttimestamp=time(NULL);
+} /*}}}*/
+
+static void as_handshake(void)
+{ /*{{{*/
+ int sockfd;
+ unsigned long int idx, odx;
+ char buf[AS_BUFSIZE+1];
+ ssize_t n;
+ string_t msg, tmp;
+
+ /* no authentication values set, don't do anything */
+ if (as_authinfo.user[0] == '\0' || as_authinfo.pass[0] == '\0')
+ return;
+
+ /* prepare handshake request */
+ str_init(&msg);
+ str_init(&tmp);
+ str_ncat(&msg, "GET ", 4); str_scat(&msg, AS_HS_URI);
+ str_ncat(&msg, "&p=", 3); str_scat(&msg, AS_PROTO);
+ str_ncat(&msg, "&c=", 3); str_ncat(&msg, AS_CLIENT_ID, 3);
+ str_ncat(&msg, "&v=", 3); str_scat(&msg, VERSION); /* cmus' Version, that is */
+ as_lock();
+ str_scat(&tmp, as_authinfo.user);
+ as_unlock();
+ as_urlencode(&tmp);
+ str_ncat(&msg, "&u=", 3); str_cat(&msg, &tmp);
+ str_ncat(&msg, " HTTP/1.1\r\nHost: ", 17); str_scat(&msg, AS_HOSTNAME);
+ str_ncat(&msg, "\r\nConnection: CLOSE\r\n\r\n", 23);
+
+ /* connect() */
+ if ( (sockfd = tcp_connect(AS_HOSTNAME, AS_HOSTPORT)) < 0 ) {
+ d_print("tcp_connect() failed!\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+
+ /* submit our handshake request */
+ xwriten(sockfd, msg.s, msg.len);
+
+ msg.s[0] = '\0';
+ msg.len = 0;
+
+ /* read server's response */
+ for (;;) {
+ if ( (n = xreadn(sockfd, buf, AS_BUFSIZE) ) <= 0)
+ break;
+ buf[n] = '\0';
+ str_ncat(&msg, buf, n);
+ }
+ xclose(sockfd);
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("Serverreply:\n\"%s\"\n", msg.s);
+#endif
+
+ /* parse the response we read */
+ as_lock();
+ if (str_ngetidx(&msg, 0, "\r\n\r\n", 4, &idx)) {
+ str_nreplace(&msg, 0, idx+4, "", 0);
+ }
+ else {
+ d_print("broken reply, couldn't find end of headers (\\r\\n\\r\\n)\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+
+ /*
+ * UPTODATE/UPDATE/FAILED/BADUSER
+ */
+ idx=odx=0;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ if (str_ngetidx(&tmp, 0, "UPTODATE", 8, NULL)) {
+ d_print("received UPTODATE, good.\n");
+ }
+ else if (str_ngetidx(&tmp, 0, "UPDATE", 6, NULL)) {
+ d_print("received UPDATE, get a new client, baby.\n");
+ }
+ else if (str_ngetidx(&tmp, 0, "FAILED", 6, NULL)) {
+ d_print("received FAILED, crap :-/\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ else if (str_ngetidx(&tmp, 0, "BADUSER", 7, NULL)) {
+ d_print("received BADUSER, err, check as_user setting.\n");
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ else {
+ d_print("Unknown response: \"%s\"\n", tmp.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+
+ /*
+ * -challenge-
+ */
+ odx=idx+1;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ str_cpy(&(as_hsinfo.challenge), &tmp);
+
+ /*
+ * -url-to-submit-script
+ */
+ odx=idx+1;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ as_hsinfo.failed++;
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ odx=idx+1;
+ if (str_ngetidx(&tmp, 0, "://", 3, &idx)) {
+ if (idx+3 <= tmp.len)
+ str_nreplace(&tmp, 0, idx+3, "", 0);
+ }
+ else
+ d_print("hrm, no service mnemonic? (%s)\n)", tmp.s);
+
+ if (str_ngetidx(&tmp, 0, "/", 1, &idx) && (idx <= tmp.len)) {
+ str_ncpy(&as_hsinfo.path, tmp.s+idx, tmp.len-idx);
+ str_nreplace(&tmp, idx, tmp.len-idx, "", 0);
+ }
+ else {
+ d_print("um, no path? (%s)\n", tmp.s);
+ as_hsinfo.failed++;
+ }
+
+ if (str_ngetidx(&tmp, 0, ":", 1, &idx)) {
+ strncpy(as_hsinfo.port, tmp.s+idx+1, (tmp.len-idx < AS_MAXTMPLEN ? tmp.len-idx : AS_MAXTMPLEN));
+ as_hsinfo.port[(tmp.len-idx < AS_MAXTMPLEN ? tmp.len-idx : AS_MAXTMPLEN)] = '\0';
+ }
+ else {
+ d_print("well, no port definition? That, would be okay, default: 80. (%s)", tmp.s);
+ as_hsinfo.port[0] = '8'; as_hsinfo.port[1] = '0'; as_hsinfo.port[2] = '\0';
+ }
+ str_ncpy(&(as_hsinfo.host), tmp.s, idx);
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("debug as_hsinfo:\n\thost: \"%s\"\n\tpath: \"%s\"\n\tport: \"%s\"\n",
+ as_hsinfo.host.s, as_hsinfo.path.s, as_hsinfo.port);
+#endif
+
+ /*
+ * -INTERVAL-
+ */
+ odx=idx+1;
+ if (!str_ngetidx(&msg, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg.s);
+ goto cleanup;
+ }
+ str_ncpy(&tmp, msg.s+odx, idx-odx);
+ if (str_ngetidx(&tmp, 0, "INTERVAL ", 9, NULL)) {
+ as_hsinfo.interval=atoi(tmp.s+9);
+ d_print("Our Interval is: %d\n", (int)as_hsinfo.interval);
+ }
+
+ /* handshake suceeded, all values set. */
+ as_hsinfo.need_handshake=0;
+
+cleanup:
+ as_unlock();
+ str_free(&tmp);
+ str_free(&msg);
+} /*}}}*/
+
+static int as_submit_allowed(void)
+{ /*{{{*/
+ /*
+ * honor INTERVAL response from the server
+ */
+ const time_t now = time(NULL);
+ time_t at;
+ as_lock();
+ at = as_hsinfo.interval + as_lasttimestamp;
+ as_unlock();
+ if (now >= at)
+ return(1);
+ return(0);
+} /*}}}*/
+
+static int as_submit_forced(void)
+{ /*{{{*/
+ /*
+ * queue maximum is 10, so we force submission at 8 entries
+ */
+ unsigned int n;
+ as_lock();
+ n = as_count_entries();
+ as_unlock();
+ if (n >= AS_FORCE_SUBMIT)
+ return(1);
+ return(0);
+} /*}}}*/
+
+static void as_del_queue(struct list_head *item)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *tmp;
+
+ tmp=list_entry(item, struct as_queue, node);
+ d_print(" string: %s\n", tmp->s.s);
+ d_print(" NEEDRN: %d\n", tmp->needrenumber);
+ d_print(" SUBMIT: %d\n", tmp->sc);
+ d_print(" num : %d\n", tmp->num);
+ str_free(&(tmp->s));
+ list_del(item);
+ free(tmp);
+} /*}}}*/
+
+static void as_destroy_queue(int scmask)
+{ /*{{{*/
+ /* make sure as_lock() was called. */
+ struct as_queue *pos, *tmp;
+
+ list_for_each_entry_safe(pos, tmp, &as_queue_head, node) {
+ if (pos->sc & scmask)
+ as_del_queue(&(pos->node));
+ }
+} /*}}}*/
+
+static void as_renumberentry(string_t *entry, unsigned short int num)
+{ /*{{{*/
+ unsigned long idx, odx = 0;
+ while (str_ngetidx(entry, odx, "]=", 2, &idx)) {
+ if (idx > 0) {
+ d_print("replacing %c with %c\n", entry->s[idx-1], as_numbers[num][0]);
+ entry->s[idx-1] = as_numbers[num][0];
+ }
+ odx=idx+2;
+ }
+} /*}}}*/
+
+static unsigned short int as_uint2string(unsigned int integer, char *str)
+{ /*{{{*/
+ unsigned short int dn, i, digit;
+ unsigned int tmp;
+
+ if (integer <= 9) {
+ dn=1;
+ str[0] = as_numbers[integer][0];
+ str[1] = '\0';
+ }
+ else {
+ dn=0;
+ tmp=integer;
+ while (tmp) {
+ ++dn;
+ tmp/=10;
+ }
+ }
+ for (i=dn; i > 0 ; --i) {
+ digit=integer%10;
+ integer/=10;
+ str[i-1] = as_numbers[digit][0];
+ }
+ str[dn] = '\0';
+ return(dn);
+} /*}}}*/
+
+static void as_digest2hex(unsigned char *hex, const unsigned char *digest)
+{ /*{{{*/
+ char hc[] = "0123456789abcdef";
+ unsigned int i, e=0;
+
+ memset(hex, 0, 33*sizeof(unsigned char));
+
+ for(i = 0; i < 16; i++) {
+ hex[e++] = hc[(digest[i] >> 4) & 0x0f];
+ hex[e++] = hc[ digest[i] & 0x0f];
+ }
+ hex[32] = '\0';
+} /*}}}*/
+
+static void as_submit(void)
+{ /*{{{*/
+ string_t msg_header, msg_content, stmp, msg;
+ struct as_queue *entry;
+ unsigned char digest[16], tmp[65], md5response[33];
+ char i2s[AS_MAXTMPLEN];
+ char buf[AS_BUFSIZE];
+ unsigned long int idx, odx, submissions=0;
+ ssize_t n;
+ int sockfd;
+ int testval;
+ int ign=0;
+
+ /*
+ * we need authentication setup, data to submit in our queue
+ * and the handshake must have taken place already. abort otherwise.
+ */
+ as_lock();
+ testval = (as_authinfo.user[0] == '\0' || as_authinfo.pass[0] == '\0'
+ || as_hsinfo.need_handshake == 1 || list_empty(&as_queue_head));
+ as_unlock();
+ if (testval)
+ return;
+
+ /*
+ * creating the md5response described in the protocol v1.1 definition
+ * - create md5 checksum of $as_pass and covert it to hex
+ * - append the md5challenge received in handshake to the just created string
+ * - create md5 checksum of the concatenated string and convert it to hex.
+ * That's it.
+ */
+ as_lock();
+ md5_csum(as_authinfo.pass, strlen(as_authinfo.pass), digest);
+ as_digest2hex(tmp, digest);
+ strncpy(tmp+32, as_hsinfo.challenge.s, 32);
+ tmp[64] = '\0';
+ md5_csum(tmp, 64, digest);
+ as_digest2hex(md5response, digest);
+
+ str_init(&msg_header);
+ str_init(&msg_content);
+ str_init(&stmp);
+ str_init(&msg);
+ /*
+ * assemble our payload
+ */
+ str_ncat(&msg_content, "u=", 2); str_scat(&msg_content, as_authinfo.user);
+ str_ncat(&msg_content, "&s=", 3); str_scat(&msg_content, md5response);
+ list_for_each_entry(entry, &as_queue_head, node) {
+ if (ign && entry->sc == AS_SC_DO_SUBMIT) {
+ entry->num-=ign;
+ entry->needrenumber=1;
+ }
+ if (entry->needrenumber) {
+ as_renumberentry(&(entry->s), entry->num);
+ entry->needrenumber=0;
+ }
+ if (entry->sc == AS_SC_DO_SUBMIT) {
+ str_cat(&msg_content, &(entry->s));
+ entry->sc = AS_SC_SUBMITTED;
+ ++submissions;
+ d_print("scrobbling: \"%s\"\n", entry->s.s);
+ }
+ else {
+ ++ign;
+ d_print("ignoring: \"%s\"\n", entry->s.s);
+ }
+ }
+ if (!submissions) {
+ /* no submissions, eh? */
+ as_unlock();
+ goto cleanup;
+ }
+ as_uint2string(msg_content.len, i2s); /* note the length of our payload for later use */
+ str_ncat(&msg_content, "\n", 1);
+
+ /* create http/1.1 header for our submission */
+ str_ncat(&msg_header, "POST ", 5); str_cat(&msg_header, &(as_hsinfo.path)); str_ncat(&msg_header, " HTTP/1.1\r\n", 11);
+ str_ncat(&msg_header, "Host: ", 6); str_cat(&msg_header, &(as_hsinfo.host)); str_ncat(&msg_header, "\r\n", 2);
+ str_ncat(&msg_header, AS_UASTRING, AS_UASTRINGLEN);
+ str_ncat(&msg_header, "Pragma: no-cache", 16);
+ str_ncat(&msg_header, "\r\nContent-Type: application/x-www-form-urlencoded", 49);
+ str_ncat(&msg_header, "\r\nContent-Length: ", 18); str_scat(&msg_header, i2s);
+ str_ncat(&msg_header, "\r\nConnection: CLOSE\r\n\r\n", 23);
+
+ /* turn payload's special chars to utf8 */
+ /*str_lat1_to_utf8(&msg, &msg_content);
+ str_cpy(&msg_content, &msg);*/
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("POSTING: \"%s%s\"\n", msg_header.s, msg_content.s);
+#endif
+
+ as_unlock();
+
+ /*
+ * connect to the server we got in the handshake
+ */
+ if ( (sockfd = tcp_connect(as_hsinfo.host.s, as_hsinfo.port)) < 0 ) {
+ d_print("tcp_connect() failed!\n");
+ as_lock();
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+
+ /*
+ * send our submission to the server
+ */
+ str_cpy(&msg, &msg_header);
+ str_cat(&msg, &msg_content);
+ xwriten(sockfd, msg.s, msg.len);
+ str_free(&msg);
+
+ msg_content.s[0] = '\0';
+ msg_content.len = 0;
+
+ /* read response to our submission */
+ for (;;) {
+ if ( (n = xreadn(sockfd, buf, AS_BUFSIZE) ) <= 0)
+ break;
+ buf[n] = '\0';
+ str_ncat(&msg_content, buf, n);
+ }
+ xclose(sockfd);
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("Serverreply:\n\"%s\"\n", msg_content.s);
+#endif
+
+ /* parse the response */
+ as_lock();
+ if (str_ngetidx(&msg_content, 0, "\r\n\r\n", 4, &idx)) {
+ str_nreplace(&msg_content, 0, idx+4, "", 0);
+ }
+ else {
+ d_print("broken reply, couldn't find end of headers (\\r\\n\\r\\n)\n");
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ odx=0;
+ if (!str_ngetidx(&msg_content, odx, "\n", 1, &idx)) {
+ d_print("Broken input: \"%s\"\n", msg_content.s);
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ str_ncpy(&stmp, msg_content.s+odx, idx-odx);
+
+ /* OK/FAILED/BADAUTH */
+ if (str_ngetidx(&stmp, 0, "OK", 2, NULL)) {
+ d_print("received an OK, good.\n");
+ }
+ else if (str_ngetidx(&stmp, 0, "FAILED", 6, NULL)) {
+ stmp.s[idx] = '\0';
+ d_print("received FAILED (%s)\n", stmp.s+odx);
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ else if (str_ngetidx(&stmp, 0, "BADAUTH", 7, NULL)) {
+ d_print("received BADUSER, err, check as_user setting.\n");
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+ else {
+ d_print("Unknown response: \"%s\"\n", stmp.s);
+ as_hsinfo.failed++;
+ as_unlock();
+ goto cleanup;
+ }
+
+ /* INTERVAL is optional here. */
+ odx=idx+1;
+ if (str_ngetidx(&msg_content, odx, "\n", 1, &idx)) {
+ str_ncpy(&stmp, msg_content.s+odx, idx-odx);
+ if (str_ngetidx(&stmp, 0, "INTERVAL ", 9, NULL)) {
+ as_hsinfo.interval=atoi(stmp.s+9);
+ d_print("Our Interval is: %d\n", (int)as_hsinfo.interval);
+ }
+ }
+ as_unlock();
+
+ /*
+ * submission suceeded
+ * destroy the part of the queue, that holds these
+ */
+ as_destroy_queue(AS_SC_SUBMITTED);
+cleanup:
+ if (as_hsinfo.failed) {
+ /* resetting AS_SC_SUBMITTED, because something went wrong */
+ list_for_each_entry(entry, &as_queue_head, node) {
+ if (entry->sc == AS_SC_SUBMITTED)
+ entry->sc = AS_SC_DO_SUBMIT;
+ }
+ }
+ str_free(&msg);
+ str_free(&stmp);
+ str_free(&msg_header);
+ str_free(&msg_content);
+} /*}}}*/
+
+static void as_gettimestring(char *str, int maxlen)
+{ /*{{{*/
+ const time_t now = time(NULL);
+ const struct tm *timeinfo = gmtime(&now);
+ strftime(str, maxlen, "%Y%%2d%m%%2d%d%%20%H%%3a%M%%3a%S", timeinfo);
+} /*}}}*/
+
+static void as_add_queue(string_t *trackartist,
+ string_t *trackalbum,
+ string_t *trackname,
+ int tracklen, int submissioncode)
+{ /*{{{*/
+ /*
+ * Description:
+ * This adds a track to the submission-queue.
+ * Only 10 Tracks are allowed to be submitted at once.
+ * So, if we run past this limit, delete this first item of the list
+ * and append this track to the end of the list.
+ */
+ struct as_queue *ptr, *tmp;
+ char tmpstr[AS_MAXTMPLEN+1];
+ int tmplen;
+ int queue_size;
+
+ d_print("adding track to as_queue:\n");
+ d_print(" Artist: %s\n", trackartist->s);
+ d_print(" Album : %s\n", trackalbum->s);
+ d_print(" Name : %s\n", trackname->s);
+ d_print(" Length: %d\n", tracklen);
+
+ if (!(ptr=malloc(sizeof(struct as_queue)))) {
+ d_print("uark! out of memory? I give up.");
+ return;
+ }
+
+ as_lock();
+
+ if (list_empty(&as_queue_head)) {
+ ptr->num=0;
+ list_add_tail(&(ptr->node), &as_queue_head);
+ }
+ else {
+ /* let's find our place in the queue */
+ queue_size=as_count_entries();
+ tmp = as_get_last_entry();
+ if (queue_size >= AS_QUEUE_MAX) {
+ as_del_queue(as_queue_head.next);
+ ptr->num=AS_QUEUE_MAX;
+ list_add_tail(&(ptr->node), &as_queue_head);
+ list_for_each_entry(tmp, &as_queue_head, node) {
+ tmp->num--;
+ tmp->needrenumber=1;
+ }
+ }
+ else {
+ list_add_tail(&(ptr->node), &as_queue_head);
+ ptr->num=queue_size;
+ }
+ }
+
+ str_init(&(ptr->s));
+ ptr->sc=submissioncode;
+ ptr->needrenumber=0;
+ ptr->len=tracklen;
+ /* set up the request string */
+ as_urlencode(trackartist);
+ as_urlencode(trackalbum);
+ as_urlencode(trackname);
+ as_addtag(ptr->s, trackartist, "&a[");
+ as_addtag(ptr->s, trackname, "&t[");
+ as_addtag(ptr->s, trackalbum, "&b[");
+ /*
+ * MusicBrainz ID, I'm just sending an empty one, because the
+ * protocol definition says "don't skip it"
+ */
+ str_readya(&(ptr->s), 6);
+ str_ncat( &(ptr->s), "&m[", 3);
+ str_ncat( &(ptr->s), as_numbers[ptr->num], 1);
+ str_ncat( &(ptr->s), "]=", 2);
+ tmplen = as_uint2string(ptr->len, tmpstr);
+ as_addntag(ptr->s, tmpstr, tmplen, "&l[");
+ as_gettimestring(tmpstr, AS_MAXTMPLEN);
+ as_addntag(ptr->s, tmpstr, AS_MAXTMPLEN-1, "&i[");
+ as_unlock();
+} /*}}}*/
+
+void as_hook(enum as_actions action)
+{ /*{{{*/
+ /*
+ * When do we need to call as_hook()?
+ * + stop
+ * + seek
+ * + next
+ * + prev
+ * + automatic song change
+ *
+ * What do we need to know?
+ * + track length
+ * + track position (we query player_info for that)
+ * + the action that called us
+ * + track name
+ * + artist
+ * + album
+ */
+ static int lastwasinvalid = 0; /* these 2 static ints are there */
+ static int alreadysubmitted = 0; /* to handle AS_SEEK properly. */
+ int trackpos, tracklen;
+ int testval;
+ const char *tmp;
+ string_t trackartist, trackalbum, trackname;
+
+#ifdef AS_HEAVY_DEBUG
+ d_print("-- ping -----------------HOOOOK!1!!-------------------------------------\n");
+#endif
+
+ as_lock();
+ testval=running;
+ as_unlock();
+ if (!testval)
+ return;
+
+ if (lastwasinvalid) {
+ /*
+ * last was invalid; if something else than SEEK
+ * was done, this would submit the invalid track, so we
+ * reset lastwasinvalid here and return.
+ *
+ * if SEEK was invoked again, we just return, because
+ * the track was marked as invalid before.
+ */
+ if (action != AS_SEEK)
+ lastwasinvalid = 0;
+ return;
+ }
+ if (alreadysubmitted) {
+ if (action != AS_SEEK)
+ alreadysubmitted = 0;
+ return;
+ }
+
+ player_info_lock();
+ trackpos=player_info.pos;
+
+ if (player_info.ti == NULL) {
+ player_info_unlock();
+ return;
+ }
+
+ tracklen=player_info.ti->duration;
+
+ if (tracklen < AS_TRACK_MINLEN) { /* track is not long enough */
+ player_info_unlock();
+ return;
+ }
+ if (trackpos < AS_TRACK_MINSECS && (tracklen/2 > trackpos)) { /* track did not play long enough */
+ /*
+ * if we were called in NEXT, PREV, STOP or *_ENTER
+ * we already did skip to the next track (or stopped),
+ * which means, we don't need to save this one for later review.
+ * So, we return.
+ */
+ player_info_unlock();
+ if (action != AS_SEEK)
+ return;
+
+ lastwasinvalid = 1;
+ return;
+ }
+
+ str_init(&trackartist);
+ str_init(&trackalbum);
+ str_init(&trackname);
+ tmp=keyvals_get_val(player_info.ti->comments, "artist");
+ str_scpy(&trackartist, tmp);
+ tmp=keyvals_get_val(player_info.ti->comments, "album");
+ str_scpy(&trackalbum, tmp);
+ tmp=keyvals_get_val(player_info.ti->comments, "title");
+ str_scpy(&trackname, tmp);
+ player_info_unlock();
+
+ if (trackartist.len == 0 || trackalbum.len == 0 || trackname.len == 0) {
+ /* One of the tags we must supply is empty */
+ return;
+ }
+
+ as_add_queue(&trackartist, &trackalbum, &trackname, tracklen, AS_SC_DO_SUBMIT);
+
+ if (action == AS_SEEK)
+ alreadysubmitted = 1;
+
+ str_free(&trackartist);
+ str_free(&trackalbum);
+ str_free(&trackname);
+} /*}}}*/
+
+static void as_sleep(void)
+{ /*{{{*/
+ struct timespec r;
+ r.tv_sec=AS_SLEEPSEC;
+ r.tv_nsec=AS_SLEEPNANO;
+ nanosleep(&r, NULL);
+} /*}}}*/
+
+void *as_loop(void *arg)
+{ /*{{{*/
+ /*
+ * main audio scrobbler loop
+ * Here, all communication is done.
+ * Needed information is provided by *as_queue,
+ * which is filled by as_hook()
+ *
+ * TODO: AS_SC_DONT_SUBMIT cases must be handled (eg. the user used seek on a track before it fitted our needs).
+ */
+ static int last_as_state=0;
+ int testval;
+ struct as_queue *e;
+
+ for (;;) {
+#ifdef AS_HEAVY_DEBUG
+ d_print("-- ping ----------------------------------------------------------------\n");
+#endif
+ as_update_timestamp();
+ as_lock();
+ testval = list_empty(&as_queue_head);
+ as_unlock();
+ if (testval) {
+ /* nothing to submit */
+ if (!running) {
+ /*
+ * running isn't set,
+ * seems like the user switched us off
+ */
+ return(NULL);
+ }
+ as_sleep();
+ }
+ else {
+ as_lock();
+ list_for_each_entry(e, &as_queue_head, node) {
+ d_print("--DEBUG--\n");
+ d_print(" Number: %d\n", e->num);
+ d_print(" SCode : %d\n", e->sc);
+ d_print(" Length: %d\n", e->len);
+ d_print(" NEEDRN: %d\n", e->needrenumber);
+ d_print(" string: %s\n\n", e->s.s);
+ }
+ as_unlock();
+ as_sleep();
+ as_lock();
+ testval = as_hsinfo.need_handshake;
+ as_unlock();
+ if (testval) {
+ as_handshake();
+ }
+ as_lock();
+ testval = as_hsinfo.need_handshake;
+ as_unlock();
+ if (!testval) {
+ /*
+ * we are enabled, got data to submit and don't need to handshake.
+ * check, if we may (or need to force to) submit our data,
+ * and do so, if needed.
+ *
+ * XXX: as_submit_allowed() and as_submit_forced() do locking on their own.
+ */
+ if (as_submit_allowed() || as_submit_forced()) {
+ as_submit();
+ }
+ }
+ }
+
+ as_lock();
+ if (!as_enable && !last_as_state) {
+ /*
+ * user didn't enable us in the first place.
+ * so, set running to 0 and rerun loop, so we can exit
+ */
+ running=0;
+ }
+ else if (!as_enable && last_as_state) {
+ /* as_enable was on before, so terminate session */
+ running=0;
+ last_as_state=0;
+ }
+ else if (!last_as_state && as_enable) {
+ /* (re)start as session */
+ running=1;
+ as_hsinfo.need_handshake=1;
+ last_as_state=1;
+ }
+ as_unlock();
+ }
+} /*}}}*/
+
+void as_init(void)
+{ /*{{{*/
+ int rc;
+ as_lock();
+ running=1;
+ str_init(&(as_hsinfo.challenge));
+ str_init(&(as_hsinfo.host));
+ str_init(&(as_hsinfo.path));
+ as_hsinfo.need_handshake = 1;
+ as_hsinfo.interval = 0;
+ as_hsinfo.failed = 0;
+ as_lasttimestamp = 0;
+ as_unlock();
+ rc = pthread_create(&as_thread, NULL, as_loop, NULL);
+ BUG_ON(rc);
+} /*}}}*/
+
+void as_exit(void)
+{ /*{{{*/
+ as_lock();
+ running = 0;
+ as_destroy_queue(AS_SC_ALL);
+ str_free(&(as_hsinfo.challenge));
+ str_free(&(as_hsinfo.host));
+ str_free(&(as_hsinfo.path));
+ as_unlock();
+ pthread_join(as_thread, NULL);
+} /*}}}*/
diff --git a/as.h b/as.h
new file mode 100644
index 0000000..e1dc638
--- /dev/null
+++ b/as.h
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2004-2006 Timo Hirvonen
+ *
+ * as.[ch] by Frank Terbeck <***@bewatermyfriend.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#ifndef x__AS_H
+#define x__AS_H
+
+/*
+ * audioscrobbler code for cmus {{{
+ * (read: get cmus properly working with http://last.fm/
+ *
+ * When are songs submitted?
+ * + Each song should be posted to the server when it is 50%
+ * or 240 seconds complete, whichever comes first.
+ *
+ * + If a user seeks (i.e. manually changes position) within a song
+ * before the song is due to be submitted, do not submit that song.
+ *
+ * + Songs with a duration of less than 30 seconds should not be
+ * submitted (so, we don't do that).
+ *
+ * + Do not try to guess info from the filename - if there are no
+ * tags, don't submit the file (okeydoke, herman).
+ *
+ * + If a MusicBrainz ID is present in the file (as defined here),
+ * then you should send it (whatever that is...[1]).
+ *
+ * + If a user is playing a stream instead of a regular file, do
+ * not submit that stream/song.
+ *
+ *********************************************************************
+ *
+ * [1] About musicbrainz.org:
+ * I don't know what that is, nor do I care. Feel free to
+ * implement support for it, if you need it desperately.
+ *}}}
+ */
+
+#include "list.h"
+#include "options.h"
+#include "strlib.h"
+#include "track_info.h"
+
+/* #defines */
+
+/* We are currently testing things, so "tst" is okay. */
+#define AS_UASTRING "User-Agent: AudioScrobbler/1.1 cmus-scrobbler code v0.01\r\n"
+#define AS_UASTRINGLEN 58
+#define AS_CLIENT_ID "tst"
+#define AS_PROTO "1.1"
+
+#define AS_HOSTNAME "post.audioscrobbler.com"
+#define AS_HOSTPORT "80"
+#define AS_HS_URI "/?hs=true"
+
+#define AS_BUFSIZE 4096
+
+/* AS_QUEUE_MAX must of an element of [1..10] */
+#define AS_QUEUE_MAX 10
+
+/*
+ * AS_FORCE_SUBMIT:
+ * if our queue holds this many entries, force submission,
+ * no matter what the last INTERVAL response said.
+ */
+#define AS_FORCE_SUBMIT 8
+
+/* AS_DO_SUBMIT marks a track for submission */
+#define AS_SC_DO_SUBMIT 1
+
+/* AS_DONT_SUBMIT is used if seek was used at the beginning of a track */
+#define AS_SC_DONT_SUBMIT 2
+
+/* AS_SC_SUBMITTED marks tracks, that where included in a submission msg */
+#define AS_SC_SUBMITTED 4
+
+/* AS_SC_ALL is the same as ORing together all other AS_SC_* macros */
+#define AS_SC_ALL 15
+
+/* The minimum length of a track to be considered for submission */
+#define AS_TRACK_MINLEN 30
+
+/* A track must have run 50% of its time or AS_TRACK_MINSECS seconds */
+#define AS_TRACK_MINSECS 240
+
+/* AS_SLEEP* sets up the resolution of as_loop() */
+#define AS_SLEEPSEC 5
+#define AS_SLEEPNANO 0
+
+/*
+ * for temporary static strings, that need to be long enough
+ * to hold an url_encoded time string (25+1); this is used for
+ * strings that are converted integers.
+ */
+#define AS_MAXTMPLEN 30
+
+/* data types */
+enum as_actions {
+ AS_AUTONEXT=1,
+ AS_SEEK,
+ AS_NEXT,
+ AS_PREV,
+ AS_STOP,
+ AS_TREE_ENTER,
+ AS_SORTED_ENTER,
+ AS_PL_ENTER
+};
+
+struct as_queue {
+ struct list_head node;
+ string_t s;
+ unsigned int len;
+ unsigned short int sc; /* AS_SC_* */
+ unsigned short int needrenumber; /* we must renumber 'string_t s' */
+ unsigned short int num; /* number of the current entry */
+};
+
+struct as_hsinfo {
+ unsigned int need_handshake;
+ unsigned int failed;
+ time_t interval;
+ string_t challenge;
+ string_t host;
+ string_t path;
+ char port[AS_MAXTMPLEN+1]; /* yes, port as a string. tcp_connect expects its port as char[] */
+};
+
+struct as_authinfo {
+ char user[OPTION_MAX_SIZE];
+ char pass[OPTION_MAX_SIZE];
+};
+
+/* function prototypes */
+void as_hook(enum as_actions action);
+void *as_loop(void *arg);
+void as_exit(void);
+void as_init(void);
+
+/* macros */
+#define as_addtag(dest, src, tag) \
+ str_readya(&(dest), 6); \
+ str_ncat( &(dest), tag, 3); \
+ str_ncat( &(dest), as_numbers[ptr->num], 1); \
+ str_ncat( &(dest), "]=", 2); \
+ str_readya(&(dest), src->len); \
+ str_cat( &(dest), src)
+
+#define as_addntag(dest, src, len, tag) \
+ str_readya(&(dest), 6); \
+ str_ncat( &(dest), tag, 3); \
+ str_ncat( &(dest), as_numbers[ptr->num], 1); \
+ str_ncat( &(dest), "]=", 2); \
+ str_readya(&(dest), len); \
+ str_ncat( &(dest), src, len)
+
+#define mustquote(c) \
+ c == ' ' || \
+ c == '!' || \
+ c == '\"' || \
+ c == '#' || \
+ c == '%' || \
+ c == '&' || \
+ c == '\'' || \
+ c == '(' || \
+ c == ')' || \
+ c == '*' || \
+ c == '-' || \
+ c == '/' || \
+ c == ':' || \
+ c == ';' || \
+ c == '<' || \
+ c == '=' || \
+ c == '>' || \
+ c == '?' || \
+ c == '@' || \
+ c == '[' || \
+ c == '\\' || \
+ c == ']' || \
+ c == '^' || \
+ c == '`' || \
+ c == '{' || \
+ c == '|' || \
+ c == '}' || \
+ c == '~'
+
+#define as_lock() cmus_mutex_lock(&as_mutex)
+#define as_unlock() cmus_mutex_unlock(&as_mutex)
+
+/* exported variables */
+extern int as_enable;
+extern struct as_authinfo as_authinfo;
+
+#endif /* x__AS_H */
--
1.7.6
1.7.6