diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4d40434
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Libraries
+*.lib
+*.a
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f66e940
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+#
+# cmusfm - makefile
+# Copyright (c) 2010 Arkadiusz Bokowy
+#
+
+CC = gcc
+
+CFLAGS = -pipe -Wall -Os
+LDFLAGS = -lcurl
+
+OBJS = main.o libscrobbler2.o server.o
+PROG = cmusfm
+
+$(PROG): $(OBJS)
+ $(CC) $(LDFLAGS) $(OBJS) -o $(PROG)
+
+%.o: src/%.c
+ $(CC) $(CFLAGS) -c $<
+
+all: $(PROG)
+
+clean:
+ rm -f *.o
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..182e06c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+cmusfm
+======
+
+Last.fm standalone scrobbler for the cmus music player.
diff --git a/src/cmusfm.h b/src/cmusfm.h
new file mode 100644
index 0000000..de6eff1
--- /dev/null
+++ b/src/cmusfm.h
@@ -0,0 +1,60 @@
+/*
+ cmusfm - cmusfm.h
+ Copyright (c) 2010-2011 Arkadiusz Bokowy
+*/
+
+#define APP_NAME "cmusfm"
+#define APP_VER "0.1.0"
+
+#define __CMUSFM_H
+
+#define CONFIG_FNAME "cmusfm.conf"
+#define SOCKET_FNAME "cmusfm.socket"
+#define CACHE_FNAME "cmusfm.cache"
+
+unsigned char SC_api_key[16], SC_secret[16];
+
+struct cmusfm_config {
+ char user_name[64];
+ char session_key[16*2 + 1];
+ char parse_file_name, submit_radio;
+};
+
+struct cmtrack_info {
+ char status;
+ char *file, *url, *artist, *album, *title;
+ int tracknb, duration;
+};
+#define CMSTATUS_PLAYING 1
+#define CMSTATUS_PAUSED 2
+#define CMSTATUS_STOPPED 3
+
+// socket transmission stuff
+char sock_buff[1024];
+struct sock_data_tag {
+ char status;
+ int tracknb, duration;
+ int artoff, titoff;
+// char album[];
+// char artist[];
+// char title[];
+}__attribute__ ((packed));
+#define CMSTATUS_SHOUTCASTMASK 0xf0
+
+// in "server.c"
+#define LOGIN_RETRY_DELAY 60*30 //time in seconds
+int run_server();
+void process_server_data(int fd, scrobbler_session_t *sbs, int submit_radio);
+void update_cache(const scrobbler_trackinfo_t *sb_tinf);
+void submit_cache(scrobbler_session_t *sbs);
+void fill_trackinfo(scrobbler_trackinfo_t *sbt, const struct sock_data_tag *dt);
+
+// in "main.c"
+int send_data_to_server(struct cmtrack_info *tinf);
+int cmusfm_initialization();
+void cmusfm_socket_sanity_check();
+int parse_argv(struct cmtrack_info *tinf, int argc, char *argv[]);
+int read_cmusfm_config(struct cmusfm_config *cm_conf);
+int write_cmusfm_config(struct cmusfm_config *cm_conf);
+char *get_cmus_home_dir();
+int make_data_hash(const unsigned char *data, int len);
diff --git a/src/libscrobbler.c b/src/libscrobbler.c
new file mode 100644
index 0000000..216dac3
--- /dev/null
+++ b/src/libscrobbler.c
@@ -0,0 +1,394 @@
+/*
+ cmusfm - libscrobbler.c
+ Copyright (c) 2010 Arkadiusz Bokowy
+
+ 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 3 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.
+
+ If you want to read full version of the GNU General Public License
+ see .
+
+ ** Note: **
+ For contact information and the latest version of this program see
+ my webpage .
+
+*/
+
+#include
+#include
+#include
+#include
+#include "libscrobbler.h"
+
+char sb_srv_response[512];
+char libsb_error_str[CURL_ERROR_SIZE]; //256-bytes
+char libsb_error_tag[] = "scrobbler: %s";
+
+typedef struct scrobbler_session* sbs_tp;
+struct scrobbler_session {
+ char user_name[64];
+ unsigned char password_md5[16];
+
+ unsigned char session_id[16*2+1];
+ char now_playing_url[128];
+ char submission_url[128];
+
+ char *error_str;
+
+ int global_init;
+};
+
+static size_t sb_write_callback(void *ptr, size_t size, size_t nmemb,
+ void *stream)
+{
+ bzero(sb_srv_response, sizeof(sb_srv_response));
+ strncpy(sb_srv_response, ptr, sizeof(sb_srv_response) - 1);
+ return size*nmemb;
+}
+
+// Convert buffer containing md5 hash to hex string
+char *md5_to_hex(const unsigned char *md5)
+{
+ static char md5hex[MD5_DIGEST_LENGTH*2 + 1];
+ char *ptr = md5hex;
+ char hexchars[] = "0123456789abcdef";
+ int i;
+
+ bzero(md5hex, sizeof(md5hex));
+ for(i = 0; i < MD5_DIGEST_LENGTH; i++){
+ *(ptr++) = hexchars[(md5[i] >> 4) & 0x0f];
+ *(ptr++) = hexchars[md5[i] & 0x0f];}
+ *ptr = 0;
+ return md5hex;
+}
+
+// NOTE: returned pointer must be freed!
+// if submitnb == -1 make string for nowplay_notify
+char *make_post_track_string(CURL *curl, scrobbler_trackinf_t *sbn,
+ int submitnb)
+{
+ char len_str[16], tnb_str[16], src_str[16], rat_str[4];
+ char *art, *tra, *alb, *mbi, *len, *tnb, *src, *rat;
+ char *track_string;
+ char zero_len_string[] = "";
+
+ len = len_str;
+ tnb = tnb_str;
+ src = src_str;
+ rat = rat_str;
+
+ // this should be enough for escaped POST data, but if not what then...?
+ track_string = (char*)malloc(2048);
+
+ if(track_string != NULL) {
+ // replace NULL char* with pointer to ""
+ if(sbn->album == NULL) sbn->album = zero_len_string;
+ if(sbn->mb_trackid == NULL) sbn->mb_trackid = zero_len_string;
+
+ // create POST data string (escape values)
+ art = curl_easy_escape(curl, sbn->artist, 0);
+ tra = curl_easy_escape(curl, sbn->track, 0);
+ alb = curl_easy_escape(curl, sbn->album, 0);
+ mbi = curl_easy_escape(curl, sbn->mb_trackid, 0);
+
+ if(sbn->length == 0) len = zero_len_string;
+ else sprintf(len_str, "%d", sbn->length);
+ if(sbn->tracknb == 0) tnb = zero_len_string;
+ else sprintf(tnb_str, "%d", sbn->tracknb);
+
+ if(submitnb == -1)
+ sprintf(track_string, "a=%s&t=%s&b=%s&l=%s&n=%s&m=%s",
+ art, tra, alb, len, tnb, mbi);
+ else {//make string for submission
+
+ if(sbn->source == 0) src = zero_len_string;
+ else sprintf(src_str, "%c", sbn->source);
+ if(sbn->rating == 0) rat = zero_len_string;
+ else sprintf(rat_str, "%c", sbn->rating);
+
+ if(sbn->source == SCROBBSUBMIT_LASTFM)
+ sprintf(src_str, "%c%x", sbn->source, sbn->lastfmid);
+
+ sprintf(track_string, "a[%d]=%s&t[%d]=%s&i[%d]=%ld&o[%d]=%s"
+ "&r[%d]=%s&l[%d]=%s&b[%d]=%s&n[%d]=%s&m[%d]=%s",
+ submitnb, art, submitnb, tra, submitnb, sbn->started,
+ submitnb, src, submitnb, rat, submitnb, len,
+ submitnb, alb, submitnb, tnb, submitnb, mbi);
+ }
+
+ curl_free(art);
+ curl_free(tra);
+ curl_free(alb);
+ curl_free(mbi);
+ }
+
+ return track_string;
+}
+
+// ----- Library interface ------
+
+// Dump trackinfos to stdout
+void scrobbler_dump_trackinf(scrobbler_trackinf_t (*sbn)[], int size)
+{
+ int x;
+
+ for(x = 0; x < size; x++)
+ printf("trackinf_t nb: %d\n"
+" artist: %s\n track: %s\n album: %s\n mb_trackid: %s\n"
+" started: %d\n rating: %c\n source: %c\n lastfmid: %x\n",
+x + 1, (*sbn)[x].artist, (*sbn)[x].track, (*sbn)[x].album, (*sbn)[x].mb_trackid,
+(int)(*sbn)[x].started, (*sbn)[x].rating, (*sbn)[x].source, (*sbn)[x].lastfmid);
+}
+
+// Scrobbler submission
+int scrobbler_submit(scrobbler_t *sbs, scrobbler_trackinf_t (*sbn)[], int size)
+{
+ CURL *curl;
+ char *post_data, *track_data;
+ int x, argerr_count, status;
+
+ if(size > SCROBBSUBMIT_MAX) return SCROBBERR_BADARG;
+
+ // this should be enough for escaped POST data, but if not...
+ post_data = (char*)malloc(2048*size);
+ if(post_data == NULL) return -1;
+
+ curl = curl_easy_init();
+ if(curl == NULL){
+ free(post_data);
+ return SCROBBERR_CURLINIT;}
+
+ sprintf(post_data, "s=%s", ((sbs_tp)sbs)->session_id);
+
+ // add all trackinfo we've got to post_data buffer
+ for(x = 0, argerr_count = 0; x < size; x++) {
+ // check required fields
+ if((*sbn)[x].artist == NULL || (*sbn)[x].track == NULL){
+ argerr_count++; continue;}
+ if((*sbn)[x].source == SCROBBSUBMIT_USER && (*sbn)[x].length == 0){
+ argerr_count++; continue;}
+
+ track_data = make_post_track_string(curl, &(*sbn)[x], x - argerr_count);
+ strcat(post_data, "&");
+ strcat(post_data, track_data);
+ free(track_data);
+ }
+
+ // if there was more then 30% of bad tracks info raise error
+ if((argerr_count*100)/size > 30){
+ free(post_data);
+ curl_easy_cleanup(curl);
+ return SCROBBERR_BADARG;}
+
+ // initialize curl for HTTP POST
+#ifdef CURLOPT_PROTOCOLS
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
+#endif
+ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_easy_setopt(curl, CURLOPT_POST, 1);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, libsb_error_str);
+ curl_easy_setopt(curl, CURLOPT_URL, ((sbs_tp)sbs)->submission_url);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, sb_write_callback);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+
+ status = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+ free(post_data);
+
+ if(status == 0) {
+ if(memcmp(sb_srv_response, "OK", 2) == 0) return 0;
+ if(memcmp(sb_srv_response, "BADSESSION", 10) == 0){
+ sprintf(libsb_error_str, libsb_error_tag, "bad session ID");
+ return SCROBBERR_SESSION;}
+
+ sprintf(libsb_error_str, libsb_error_tag, "hard failure");
+ return SCROBBERR_HARD;
+ }
+
+ // network transfer failure
+ return SCROBBERR_CURLPERF;
+}
+
+// Now-playing notification
+int scrobbler_nowplay_notify(scrobbler_t *sbs, scrobbler_trackinf_t *sbn)
+{
+ CURL *curl;
+ char *post_data, *track_data;
+ int status;
+
+ // this fields are required
+ if(sbn->artist == NULL || sbn->track == NULL) return SCROBBERR_BADARG;
+
+ // this should be enough for escaped POST data, but if not...
+ post_data = (char*)malloc(2048);
+ if(post_data == NULL) return -1;
+
+ curl = curl_easy_init();
+ if(curl == NULL){
+ free(post_data);
+ return SCROBBERR_CURLINIT;}
+
+ track_data = make_post_track_string(curl, sbn, -1);
+
+ sprintf(post_data, "s=%s&%s", ((sbs_tp)sbs)->session_id, track_data);
+ free(track_data);
+
+ // initialize curl for HTTP POST
+#ifdef CURLOPT_PROTOCOLS
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
+#endif
+ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_easy_setopt(curl, CURLOPT_POST, 1);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, libsb_error_str);
+ curl_easy_setopt(curl, CURLOPT_URL, ((sbs_tp)sbs)->now_playing_url);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, sb_write_callback);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+
+ status = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+ free(post_data);
+
+ if(status == 0) {
+ if(memcmp(sb_srv_response, "OK", 2) == 0) return 0;
+ if(memcmp(sb_srv_response, "BADSESSION", 10) == 0){
+ sprintf(libsb_error_str, libsb_error_tag, "bad session ID");
+ return SCROBBERR_SESSION;}
+
+ sprintf(libsb_error_str, libsb_error_tag, "hard failure");
+ return SCROBBERR_HARD;
+ }
+
+ // network transfer failure
+ return SCROBBERR_CURLPERF;
+}
+
+// Initialize libscrobbler variables and memory
+scrobbler_t *scrobbler_initialize(const char *user, const char *pass_raw,
+ const unsigned char pass_md5[16])
+{
+ struct scrobbler_session *sbs;
+
+ // we need some form of password, right?
+ if(pass_raw == NULL && pass_md5 == NULL) return NULL;
+
+ // alloc memory for scrobbler struct
+ sbs = (sbs_tp)malloc(sizeof(struct scrobbler_session));
+ if(sbs == NULL) return NULL;
+
+ bzero(sbs, sizeof(struct scrobbler_session));
+
+ //TODO: some other way to handle error's strings
+ sbs->error_str = libsb_error_str;
+ bzero(libsb_error_str, 5);
+
+ sbs->global_init = curl_global_init(CURL_GLOBAL_NOTHING);
+ if(sbs->global_init != 0){
+ free(sbs);
+ return NULL;}
+
+ // save authentications informations (keep password in MD5 format)
+ strcpy(sbs->user_name, user);
+ if(pass_raw != NULL)
+ MD5((unsigned char*)pass_raw, strlen(pass_raw), sbs->password_md5);
+ else memcpy(sbs->password_md5, pass_md5, MD5_DIGEST_LENGTH);
+
+ return (scrobbler_t*)sbs;
+}
+
+// Make handshake with Scrobbler server
+// On error function returns non-zero value
+int scrobbler_login(scrobbler_t *sbs)
+{
+ CURL *curl;
+ time_t ts;
+ int status;
+ char auth_raw[MD5_DIGEST_LENGTH*2*2 + 1];
+ unsigned char auth[MD5_DIGEST_LENGTH];
+ char get_url[1024];
+ char *stat_str, *ptr;
+
+ curl = curl_easy_init();
+ if(curl == NULL) return SCROBBERR_CURLINIT;
+
+ // create handshake GET URL
+ ts = time(NULL);
+ sprintf(auth_raw, "%s%ld", md5_to_hex(((sbs_tp)sbs)->password_md5), ts);
+ MD5((unsigned char*)auth_raw, strlen(auth_raw), auth);
+ sprintf(get_url, SCROBBLER_POSTHOST "/?hs=true&p=" SCROBBLER_PROTOVER
+ "&c=" SCROBBLER_CLIENTID "&v=" SCROBBLER_CLIENTNB "&u=%s&t=%ld&a=%s",
+ ((sbs_tp)sbs)->user_name, ts, md5_to_hex(auth));
+
+ // initialize curl for HTTP handshake
+#ifdef CURLOPT_PROTOCOLS
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
+#endif
+ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, libsb_error_str);
+ curl_easy_setopt(curl, CURLOPT_URL, get_url);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, sb_write_callback);
+
+ status = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+
+ if(status == 0) {
+ // network transfer performed, so now check scrobbler status
+ stat_str = strtok_r(sb_srv_response, "\n", &ptr);
+
+ if(memcmp(stat_str, "OK", 2) == 0) {
+ memcpy(((sbs_tp)sbs)->session_id, strtok_r(NULL, "\n", &ptr),
+ sizeof(((sbs_tp)sbs)->session_id) - 1);
+ strncpy(((sbs_tp)sbs)->now_playing_url, strtok_r(NULL, "\n", &ptr),
+ sizeof(((sbs_tp)sbs)->now_playing_url) - 1);
+ strncpy(((sbs_tp)sbs)->submission_url, strtok_r(NULL, "\n", &ptr),
+ sizeof(((sbs_tp)sbs)->submission_url) - 1);
+ return 0;
+ }
+ if(memcmp(stat_str, "BANNED", 6) == 0){
+ sprintf(libsb_error_str, libsb_error_tag,
+ "this client version has been banned");
+ return SCROBBERR_BANNED;}
+ if(memcmp(stat_str, "BADAUTH", 7) == 0){
+ sprintf(libsb_error_str, libsb_error_tag, "authentication error");
+ return SCROBBERR_AUTH;}
+ if(memcmp(stat_str, "BADTIME", 7) == 0){
+ sprintf(libsb_error_str, libsb_error_tag, "bad system time");
+ return SCROBBERR_TIME;}
+ if(memcmp(stat_str, "FAILED", 6) == 0){
+ sprintf(libsb_error_str, libsb_error_tag, "temporary server failure");
+ return SCROBBERR_TEMPERR;}
+
+ sprintf(libsb_error_str, libsb_error_tag, "hard failure");
+ return SCROBBERR_HARD;
+ }
+
+ // network transfer failure (curl error)
+ return SCROBBERR_CURLPERF;
+}
+
+char *scrobbler_get_strerr(scrobbler_t *sbs)
+{
+ return ((sbs_tp)sbs)->error_str;
+}
+
+void scrobbler_get_passmd5(scrobbler_t *sbs, unsigned char pass_md5[16])
+{
+ memcpy(pass_md5, ((sbs_tp)sbs)->password_md5, MD5_DIGEST_LENGTH);
+}
+
+void scrobbler_cleanup(scrobbler_t *sbs)
+{
+ if(((sbs_tp)sbs)->global_init == 0) curl_global_cleanup();
+ free(sbs);
+}
+
diff --git a/src/libscrobbler.h b/src/libscrobbler.h
new file mode 100644
index 0000000..1ff4ae3
--- /dev/null
+++ b/src/libscrobbler.h
@@ -0,0 +1,81 @@
+/*
+ cmusfm - libscrobbler.h
+ Copyright (c) 2010 Arkadiusz Bokowy
+
+ For more information see:
+ http://www.audioscrobbler.net/development/protocol/
+*/
+
+#define __LIBSCROBBLER_H
+
+#include
+
+#define SCROBBLER_POSTHOST "post.audioscrobbler.com"
+#define SCROBBLER_PROTOVER "1.2"
+#define SCROBBLER_CLIENTID "cmf"
+#define SCROBBLER_CLIENTNB "1.0"
+
+typedef void scrobbler_t;
+typedef struct scrobbler_trackinf_tag {
+ char *artist,
+ *track,
+ *album, //can be NULL
+ *mb_trackid; //MusicBrainz Track ID - can be NULL
+ int length, //track length in [s] - if not known set 0
+ tracknb; //if not known set 0
+
+ // submission fields:
+ time_t started; //the time the track started playing
+ char rating, //track rating (not required) - see definition below
+ source; //track source - see definition below
+ int lastfmid; //required only if source = Last.fm
+
+}scrobbler_trackinf_t;
+
+// ----- Submission preferences -----
+// track ratings:
+#define SCROBBSUBMIT_LOVE 'L'
+#define SCROBBSUBMIT_BAN 'B'
+#define SCROBBSUBMIT_SKIP 'S'
+// track sources:
+#define SCROBBSUBMIT_USER 'P'
+#define SCROBBSUBMIT_RADIO 'R' //e.g. Shoutcast, BBC Radio 1
+#define SCROBBSUBMIT_PERSON 'E' //e.g. Pandora, Launchcast
+#define SCROBBSUBMIT_LASTFM 'L'
+#define SCROBBSUBMIT_UNKNOWN 'U'
+
+// maximum number of tract tu submit at once
+#define SCROBBSUBMIT_MAX 50
+
+// ----- Error definitions -----
+// [s] means that you can obtain human readable error message
+// by calling scrobbler_get_strerr()
+// common errors:
+#define SCROBBERR_CURLINIT 1 //curl initialization error
+#define SCROBBERR_CURLPERF 2 //curl perform error - network issue [s]
+#define SCROBBERR_HARD 3 //hard server failure [s]
+// login errors:
+#define SCROBBERR_BANNED 4 //this client version has been banned [s]
+#define SCROBBERR_AUTH 5 //authentication error [s]
+#define SCROBBERR_TIME 6 //bad system time -> sync your time [s]
+#define SCROBBERR_TEMPERR 7 //temporary failure [s]
+// nowplay_notify and submit errors:
+#define SCROBBERR_BADARG 8 //bad argument(s) in function call
+#define SCROBBERR_SESSION 9 //bad session -> relogin [s]
+
+
+// On error this function returns NULL, otherwise it returns pointer
+// which must be freed with scrobbler_cleanup().
+scrobbler_t *scrobbler_initialize(const char *user, const char *pass_raw,
+ const unsigned char pass_md5[16]);
+void scrobbler_cleanup(scrobbler_t *sbs);
+
+// On successful functions return 0, otherwise see errors defined above.
+int scrobbler_login(scrobbler_t *sbs);
+int scrobbler_nowplay_notify(scrobbler_t *sbs, scrobbler_trackinf_t *sbn);
+int scrobbler_submit(scrobbler_t *sbs, scrobbler_trackinf_t (*sbn)[], int size);
+
+char *scrobbler_get_strerr(scrobbler_t *sbs);
+void scrobbler_get_passmd5(scrobbler_t *sbs, unsigned char pass_md5[16]);
+void scrobbler_dump_trackinf(scrobbler_trackinf_t (*sbn)[], int size);
+
diff --git a/src/libscrobbler2.c b/src/libscrobbler2.c
new file mode 100644
index 0000000..23a015d
--- /dev/null
+++ b/src/libscrobbler2.c
@@ -0,0 +1,405 @@
+/*
+ cmusfm - libscrobbler2.c
+ Copyright (c) 2011 Arkadiusz Bokowy
+
+ 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 3 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.
+
+ If you want to read full version of the GNU General Public License
+ see .
+
+ ** Note: **
+ For contact information and the latest version of this program see
+ my webpage .
+
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "libscrobbler2.h"
+
+char *mem2hex(const unsigned char *mem, int len, char *str);
+unsigned char *hex2mem(const char *str, int len, unsigned char *mem);
+
+char sb_srv_response[512]; //buffer for GET/POST server response
+
+// used for quick url and signature creation process
+struct sb_getpost_data {
+ char *name, data_format;
+ void *data;
+};
+
+// curl write callback function
+static size_t sb_write_callback(void *ptr, size_t size, size_t nmemb,
+ void *stream)
+{
+ char *data_ptr;
+
+ // strip tag if exists
+ if(memcmp(ptr, "") + 2;
+
+ memset(sb_srv_response, 0, sizeof(sb_srv_response));
+ strncpy(sb_srv_response, data_ptr, sizeof(sb_srv_response) - 1);
+ return size*nmemb;
+}
+
+// Check scrobble API response status (and curl itself)
+int sb_check_response(scrobbler_session_t *sbs, int curl_status)
+{
+ if(curl_status != 0){ //network transfer failure (curl error)
+ sbs->error_code = curl_status;
+ return SCROBBERR_CURLPERF;}
+ if(strstr(sb_srv_response, "status=\"ok\"") == NULL){ //scrobbler failure
+ sbs->error_code = atoi(strstr(sb_srv_response, " no need for escaping
+ offset += sprintf(str_buffer + offset, format,
+ sb_data[x].name, sb_data[x].data);
+ }
+
+ str_buffer[offset - 1] = 0; //strip '&' at the end of string
+ return str_buffer;
+}
+
+// Scrobble a track.
+int scrobbler_scrobble(scrobbler_session_t *sbs, scrobbler_trackinfo_t *sbt)
+{
+ CURL *curl;
+ int status;
+ uint8_t sign[MD5_DIGEST_LENGTH];
+ char api_key_hex[sizeof(sbs->api_key)*2 + 1];
+ char session_key_hex[sizeof(sbs->session_key)*2 + 1];
+ char sign_hex[sizeof(sign)*2 + 1];
+ char post_data[2048];
+
+ // data in alphabetical order sorted by name field (except api_sig)
+ struct sb_getpost_data sb_data[] = {
+ {"album", 's', sbt->album},
+ {"albumArtist", 's', sbt->album_artist},
+ {"api_key", 's', api_key_hex},
+ {"artist", 's', sbt->artist},
+ //{"context", 's', NULL},
+ {"duration", 'd', (void*)sbt->duration},
+ {"mbid", 's', sbt->mbid},
+ {"method", 's', "track.scrobble"},
+ {"sk", 's', session_key_hex},
+ {"timestamp", 'd', (void*)sbt->timestamp},
+ {"track", 's', sbt->track},
+ {"trackNumber", 'd', (void*)sbt->track_number},
+ //{"streamId", 's', NULL},
+ {"api_sig", 's', sign_hex}};
+
+ if(sbt->artist == NULL || sbt->track == NULL || sbt->timestamp == 0)
+ return SCROBBERR_TRACKINF;
+
+ // initialize curl
+ if((curl = curl_easy_init()) == NULL) return SCROBBERR_CURLINIT;
+#ifdef CURLOPT_PROTOCOLS
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
+#endif
+ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_easy_setopt(curl, CURLOPT_POST, 1);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, sb_write_callback);
+ curl_easy_setopt(curl, CURLOPT_URL, SCROBBLER_URL);
+
+ mem2hex(sbs->api_key, sizeof(sbs->api_key), api_key_hex);
+ mem2hex(sbs->session_key, sizeof(sbs->session_key), session_key_hex);
+
+ // make signature for track.updateNowPlaying API call
+ sb_generate_method_signature(sb_data, 11, sbs->secret, sign);
+ mem2hex(sign, sizeof(sign), sign_hex);
+
+ // make track.updateNowPlaying POST request
+ sb_make_curl_getpost_string(curl, post_data, sb_data, 12);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+ status = curl_easy_perform(curl);
+
+ curl_easy_cleanup(curl);
+ return sb_check_response(sbs, status);
+}
+
+// Notify Last.fm that a user has started listening to a track.
+// This is engine function (without required argument check)
+int sb_update_now_playing(scrobbler_session_t *sbs,
+ scrobbler_trackinfo_t *sbt)
+{
+ CURL *curl;
+ int status;
+ uint8_t sign[MD5_DIGEST_LENGTH];
+ char api_key_hex[sizeof(sbs->api_key)*2 + 1];
+ char session_key_hex[sizeof(sbs->session_key)*2 + 1];
+ char sign_hex[sizeof(sign)*2 + 1];
+ char post_data[2048];
+
+ // data in alphabetical order sorted by name field (except api_sig)
+ struct sb_getpost_data sb_data[] = {
+ {"album", 's', sbt->album},
+ {"albumArtist", 's', sbt->album_artist},
+ {"api_key", 's', api_key_hex},
+ {"artist", 's', sbt->artist},
+ //{"context", 's', NULL},
+ {"duration", 'd', (void*)sbt->duration},
+ {"mbid", 's', sbt->mbid},
+ {"method", 's', "track.updateNowPlaying"},
+ {"sk", 's', session_key_hex},
+ {"track", 's', sbt->track},
+ {"trackNumber", 'd', (void*)sbt->track_number},
+ {"api_sig", 's', sign_hex}};
+
+ // initialize curl
+ if((curl = curl_easy_init()) == NULL) return SCROBBERR_CURLINIT;
+#ifdef CURLOPT_PROTOCOLS
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
+#endif
+ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_easy_setopt(curl, CURLOPT_POST, 1);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, sb_write_callback);
+ curl_easy_setopt(curl, CURLOPT_URL, SCROBBLER_URL);
+
+ mem2hex(sbs->api_key, sizeof(sbs->api_key), api_key_hex);
+ mem2hex(sbs->session_key, sizeof(sbs->session_key), session_key_hex);
+
+ // make signature for track.updateNowPlaying API call
+ sb_generate_method_signature(sb_data, 10, sbs->secret, sign);
+ mem2hex(sign, sizeof(sign), sign_hex);
+
+ // make track.updateNowPlaying POST request
+ sb_make_curl_getpost_string(curl, post_data, sb_data, 11);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+ status = curl_easy_perform(curl);
+
+ curl_easy_cleanup(curl);
+ return sb_check_response(sbs, status);
+}
+int scrobbler_update_now_playing(scrobbler_session_t *sbs,
+ scrobbler_trackinfo_t *sbt)
+{
+ if(sbt->artist == NULL || sbt->track == NULL)
+ return SCROBBERR_TRACKINF;
+ return sb_update_now_playing(sbs, sbt);
+}
+
+// Hard codded method for validating session key. Use updateNotify method
+// call with wrong number of parameters as test call.
+int scrobbler_test_session_key(scrobbler_session_t *sbs)
+{
+ scrobbler_trackinfo_t sbt;
+ int status;
+
+ memset(&sbt, 0, sizeof(sbt));
+ status = sb_update_now_playing(sbs, &sbt);
+
+ // 'invalid parameters' is not the error in this case :)
+ if(status == SCROBBERR_SBERROR && sbs->error_code == 6) return 0;
+ else return status;
+}
+
+// Return session key in string hex dump. The memory block to which points
+// *str has to be big enough to contain sizeof(session_key)*2 + 1.
+char *scrobbler_get_session_key_str(scrobbler_session_t *sbs, char *str) {
+ return mem2hex(sbs->session_key, sizeof(sbs->session_key), str);
+}
+// Set session key by parsing hex dump of this key.
+void scrobbler_set_session_key_str(scrobbler_session_t *sbs, const char *str) {
+ hex2mem(str, sizeof(sbs->session_key), sbs->session_key);
+}
+
+// Perform scrobbler service authentication process
+int scrobbler_authentication(scrobbler_session_t *sbs,
+ scrobbler_authuser_callback_t callback)
+{
+ CURL *curl;
+ int status;
+ uint8_t sign[MD5_DIGEST_LENGTH];
+ char api_key_hex[sizeof(sbs->api_key)*2 + 1];
+ char sign_hex[sizeof(sign)*2 + 1], token_hex[33];
+ char get_url[1024], *ptr;
+
+ // data in alphabetical order sorted by name field (except api_sig)
+ struct sb_getpost_data sb_data_token[] = {
+ {"api_key", 's', api_key_hex},
+ {"method", 's', "auth.getToken"},
+ {"api_sig", 's', sign_hex}};
+ struct sb_getpost_data sb_data_session[] = {
+ {"api_key", 's', api_key_hex},
+ {"method", 's', "auth.getSession"},
+ {"token", 's', token_hex},
+ {"api_sig", 's', sign_hex}};
+
+ mem2hex(sbs->api_key, sizeof(sbs->api_key), api_key_hex);
+
+ // initialize curl
+ if((curl = curl_easy_init()) == NULL) return SCROBBERR_CURLINIT;
+#ifdef CURLOPT_PROTOCOLS
+ curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
+#endif
+ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, sb_write_callback);
+
+ // make signature for auth.getToken API call
+ sb_generate_method_signature(sb_data_token, 2, sbs->secret, sign);
+ mem2hex(sign, sizeof(sign), sign_hex);
+
+ // make auth.getToken GET request
+ strcpy(get_url, SCROBBLER_URL "?");
+ sb_make_curl_getpost_string(curl, get_url + strlen(get_url), sb_data_token, 3);
+ curl_easy_setopt(curl, CURLOPT_URL, get_url);
+ status = curl_easy_perform(curl);
+
+ if((status = sb_check_response(sbs, status)) != 0){
+ curl_easy_cleanup(curl);
+ return status;}
+
+ memcpy(token_hex, strstr(sb_srv_response, "") + 7, 32);
+ token_hex[32] = 0;
+
+ // perform user authorization (callback function)
+ sprintf(get_url, SCROBBLER_USERAUTH_URL "?api_key=%s&token=%s",
+ api_key_hex, token_hex);
+ if(callback(get_url) != 0){
+ curl_easy_cleanup(curl);
+ return SCROBBERR_CALLBACK;}
+
+ // make signature for auth.getSession API call
+ sb_generate_method_signature(sb_data_session, 3, sbs->secret, sign);
+ mem2hex(sign, sizeof(sign), sign_hex);
+
+ // make auth.getSession GET request
+ strcpy(get_url, SCROBBLER_URL "?");
+ sb_make_curl_getpost_string(curl, get_url + strlen(get_url), sb_data_session, 4);
+ curl_easy_setopt(curl, CURLOPT_URL, get_url);
+ status = curl_easy_perform(curl);
+
+ // we can free it now, it's not needed any more
+ curl_easy_cleanup(curl);
+
+ if((status = sb_check_response(sbs, status)) != 0) return status;
+
+ strncpy(sbs->user_name, strstr(sb_srv_response, "") + 6,
+ sizeof(sbs->user_name));
+ sbs->user_name[sizeof(sbs->user_name) - 1] = 0;
+ if((ptr = strchr(sbs->user_name, '<')) != NULL) *ptr = 0;
+ memcpy(get_url, strstr(sb_srv_response, "") + 5, 32);
+ hex2mem(get_url, sizeof(sbs->session_key), sbs->session_key);
+
+ return 0;
+}
+
+// Initialize scrobbler session. On success this function returns pointer
+// to allocated scrobbler_session structure, which must be freed by call
+// to scrobbler_free. On error NULL is returned.
+scrobbler_session_t *scrobbler_initialize(uint8_t api_key[16],
+ uint8_t secret[16])
+{
+ scrobbler_session_t *sbs;
+
+ // allocate space for scrobbler session structure
+ if((sbs = calloc(1, sizeof(scrobbler_session_t))) == NULL)
+ return NULL;
+
+ if(curl_global_init(CURL_GLOBAL_NOTHING) != 0){
+ free(sbs); return NULL;}
+
+ memcpy(sbs->api_key, api_key, sizeof(sbs->api_key));
+ memcpy(sbs->secret, secret, sizeof(sbs->secret));
+
+ return sbs;
+}
+
+void scrobbler_free(scrobbler_session_t *sbs)
+{
+ curl_global_cleanup();
+ free(sbs);
+}
+
+// Dump memory into hexadecimal string format. Note that *str has to be
+// big enough to contain 2*len+1.
+char *mem2hex(const unsigned char *mem, int len, char *str)
+{
+ char hexchars[] = "0123456789abcdef", *ptr;
+ int x;
+
+ for(x = 0, ptr = str; x < len; x++){
+ *(ptr++) = hexchars[(mem[x] >> 4) & 0x0f];
+ *(ptr++) = hexchars[mem[x] & 0x0f];}
+ *ptr = 0;
+
+ return str;
+}
+
+// Opposite to mem2hex. Len is the number of bytes in hex representation,
+// so strlen(str) should be 2*len.
+unsigned char *hex2mem(const char *str, int len, unsigned char *mem)
+{
+ int x;
+
+ for(x = 0; x < len; x++) {
+ if(isdigit(str[x*2])) mem[x] = (str[x*2] - '0') << 4;
+ else mem[x] = (toupper(str[x*2]) - 'A' + 10) << 4;
+
+ if(isdigit(str[x*2 + 1])) mem[x] += str[x*2 + 1] - '0';
+ else mem[x] += toupper(str[x*2 + 1]) - 'A' + 10;
+ }
+
+ return mem;
+}
diff --git a/src/libscrobbler2.h b/src/libscrobbler2.h
new file mode 100644
index 0000000..2fb7433
--- /dev/null
+++ b/src/libscrobbler2.h
@@ -0,0 +1,54 @@
+/*
+ cmusfm - libscrobbler2.h
+ Copyright (c) 2011 Arkadiusz Bokowy
+
+ For more information see:
+ http://www.last.fm/api/scrobbling
+*/
+
+#define __LIBSCROBBLER20_H
+
+#include
+#include
+
+#define SCROBBLER_URL "http://ws.audioscrobbler.com/2.0/"
+#define SCROBBLER_USERAUTH_URL "http://www.last.fm/api/auth/"
+
+typedef struct scrobbler_session_tag {
+ uint8_t api_key[16]; //128-bit API key
+ uint8_t secret[16]; //128-bit secter
+
+ uint8_t session_key[16]; //128-bit session key (authentication)
+ char user_name[64];
+
+ int error_code;
+} scrobbler_session_t;
+
+typedef struct scrobbler_trackinfo_tag {
+ char *artist, *album, *album_artist, *track, *mbid;
+ unsigned int track_number, duration;
+ time_t timestamp;
+} scrobbler_trackinfo_t;
+
+// ----- scrobbler_* error types definitions -----
+#define SCROBBERR_CURLINIT 1 //curl initialization error
+#define SCROBBERR_CURLPERF 2 //curl perform error - network issue
+#define SCROBBERR_SBERROR 3 //scrobbler API error
+#define SCROBBERR_CALLBACK 4 //callback error (authentication)
+#define SCROBBERR_TRACKINF 5 //missing required field(s) in trackinfo structure
+
+scrobbler_session_t *scrobbler_initialize(uint8_t api_key[16],
+ uint8_t secret[16]);
+void scrobbler_free(scrobbler_session_t *sbs);
+
+typedef int (*scrobbler_authuser_callback_t)(const char *auth_url);
+int scrobbler_authentication(scrobbler_session_t *sbs,
+ scrobbler_authuser_callback_t callback);
+int scrobbler_test_session_key(scrobbler_session_t *sbs);
+
+char *scrobbler_get_session_key_str(scrobbler_session_t *sbs, char *str);
+void scrobbler_set_session_key_str(scrobbler_session_t *sbs, const char *str);
+
+int scrobbler_update_now_playing(scrobbler_session_t *sbs,
+ scrobbler_trackinfo_t *sbt);
+int scrobbler_scrobble(scrobbler_session_t *sbs, scrobbler_trackinfo_t *sbt);
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..d06d658
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,314 @@
+/*
+ cmusfm - main.c
+ Copyright (c) 2010-2011 Arkadiusz Bokowy
+
+ 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 3 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.
+
+ If you want to read full version of the GNU General Public License
+ see .
+
+ ** Note: **
+ For contact information and the latest version of this program see
+ my webpage .
+
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include /* sockaddr_un */
+#include "libscrobbler2.h"
+#include "cmusfm.h"
+
+unsigned char SC_api_key[16] = {0x67, 0x08, 0x2e, 0x45, 0xda, 0xb1,
+ 0xf6, 0x43, 0x3d, 0xa7, 0x2a, 0x00, 0xe3, 0xbc, 0x03, 0x7a};
+unsigned char SC_secret[16] = {0x02, 0xfc, 0xbc, 0x90, 0x34, 0x1a,
+ 0x01, 0xf2, 0x1c, 0x3b, 0xfc, 0x05, 0xb6, 0x36, 0xe3, 0xae};
+
+int main(int argc, char *argv[])
+{
+ struct cmtrack_info ti;
+
+ if(argc == 1){ //print help
+ printf("usage: " APP_NAME " [init]\n\n"
+" init\t\tset/change scrobbler settings\n\n"
+"NOTE: Before usage with cmus you MUST invoke this program with 'init'\n"
+" argument. After that you can set status_display_program in cmus\n"
+" (for more informations see 'man cmus'). Enjoy!\n");
+ return 0;}
+
+ if(argc == 2 && strcmp(argv[1], "init") == 0)
+ return cmusfm_initialization();
+
+ memset(&ti, 0, sizeof(ti));
+ cmusfm_socket_sanity_check();
+
+ switch(parse_argv(&ti, argc, argv)){
+ case 1: return run_server();
+ case 2: return send_data_to_server(&ti);}
+
+ printf("Run it again without any arguments ;)\n");
+ return 0;
+}
+
+// Send track info to server instance.
+// If playing from stream guess format: '$ARTIST - $TITLE'
+int send_data_to_server(struct cmtrack_info *tinf)
+{
+ struct cmusfm_config cm_conf;
+ struct sock_data_tag *sock_data = (struct sock_data_tag*)sock_buff;
+ struct sockaddr_un sock_a;
+ char *album, *artist, *title;
+ char *ptr;
+ int sock;
+
+ // read configurations
+ if(read_cmusfm_config(&cm_conf) != 0) return -1;
+
+ // check if server is running
+ memset(&sock_a, 0, sizeof(sock_a));
+ sprintf(sock_a.sun_path, "%s/" SOCKET_FNAME , get_cmus_home_dir());
+ if(access(sock_a.sun_path, R_OK) != 0) return -1;
+
+ // connect to communication socket (no error check - if socket file is
+ // created we assumed that everything should be OK)
+ sock_a.sun_family = AF_UNIX;
+ sock = socket(PF_UNIX, SOCK_STREAM, 0);
+ connect(sock, (struct sockaddr*)(&sock_a), sizeof(sock_a));
+
+ memset(sock_buff, 0, sizeof(sock_buff));
+
+ // load data into sock container
+ sock_data->status = tinf->status;
+ sock_data->tracknb = tinf->tracknb;
+ // if no duration time assume 3 min
+ sock_data->duration = tinf->duration == 0 ? 180 : tinf->duration;
+
+ // add Shoutcast (stream) flag
+ if(tinf->url != NULL) sock_data->status |= CMSTATUS_SHOUTCASTMASK;
+
+ album = (char *)(sock_data + 1);
+ if(tinf->album != NULL) strcpy(album, tinf->album);
+ artist = &album[strlen(album) + 1];
+
+ if(tinf->url != NULL && tinf->artist == NULL && tinf->title != NULL) {
+ // URL: try to fetch artist and track tile form 'title' field
+ if((ptr = strstr(tinf->title, " - ")) == NULL)
+ goto normal_case;
+
+ *ptr = 0; //terminate artist tag
+ strcpy(artist, tinf->title);
+ title = &artist[strlen(artist) + 1];
+ strcpy(title, &ptr[3]);
+ }
+ else if(tinf->file != NULL && tinf->artist == NULL
+ && tinf->title == NULL && cm_conf.parse_file_name) {
+ // FILE: try to fetch artist and track title from 'file' field
+
+ // strip PATH segment from 'file' field
+ if((ptr = strrchr(tinf->file, '/')) != NULL) ptr++;
+ else ptr = tinf->file;
+
+ strcpy(artist, ptr);
+ if((ptr = strstr(artist, " - ")) == NULL)
+ goto normal_case;
+
+ *ptr = 0; //terminate artist tag
+ title = &artist[strlen(artist) + 1];
+ strcpy(title, &ptr[3]);
+
+ // strip file extension (everything after last dot)
+ if((ptr = strrchr(title, '.')) != NULL) *ptr = 0;
+ }
+ else { //no title and artist guessing
+normal_case:
+ if(tinf->artist != NULL) strcpy(artist, tinf->artist);
+ title = &artist[strlen(artist) + 1];
+ if(tinf->title != NULL) strcpy(title, tinf->title);
+ }
+
+ // calculate data offsets
+ sock_data->artoff = artist - album;
+ sock_data->titoff = title - album;
+
+ write(sock, sock_buff, sizeof(struct sock_data_tag)
+ + sock_data->titoff + strlen(title) + 1);
+ return close(sock);
+}
+
+// Check if behind our socket is server, if not remove socket node.
+void cmusfm_socket_sanity_check()
+{
+ struct sockaddr_un sock_a;
+ int sock;
+
+ memset(&sock_a, 0, sizeof(sock_a));
+ sock_a.sun_family = AF_UNIX;
+ sprintf(sock_a.sun_path, "%s/" SOCKET_FNAME , get_cmus_home_dir());
+
+ // if there is connection failure try to remove socket node
+ sock = socket(PF_UNIX, SOCK_STREAM, 0);
+ if(connect(sock, (struct sockaddr*)(&sock_a), sizeof(sock_a)) == -1)
+ unlink(sock_a.sun_path);
+
+ close(sock);
+}
+
+char *get_cmus_home_dir()
+{
+ static char fname[128];
+ sprintf(fname, "%s/.cmus", getenv("HOME"));
+ return fname;
+}
+
+// Parse arguments which we've get from cmus
+int parse_argv(struct cmtrack_info *tinf, int argc, char *argv[])
+{
+ int x;
+
+ for(x = 1; x + 1 < argc; x += 2) {
+ if(strcmp(argv[x], "status") == 0) {
+ if(strcmp(argv[x + 1], "playing") == 0)
+ tinf->status = CMSTATUS_PLAYING;
+ else if(strcmp(argv[x + 1], "paused") == 0)
+ tinf->status = CMSTATUS_PAUSED;
+ else if(strcmp(argv[x + 1], "stopped") == 0)
+ tinf->status = CMSTATUS_STOPPED;
+ }
+ else if(strcmp(argv[x], "file") == 0)
+ tinf->file = argv[x + 1];
+ else if(strcmp(argv[x], "url") == 0)
+ tinf->url = argv[x + 1];
+ else if(strcmp(argv[x], "artist") == 0)
+ tinf->artist = argv[x + 1];
+ else if(strcmp(argv[x], "album") == 0)
+ tinf->album = argv[x + 1];
+ else if(strcmp(argv[x], "tracknumber") == 0)
+ tinf->tracknb = atoi(argv[x + 1]);
+ else if(strcmp(argv[x], "title") == 0)
+ tinf->title = argv[x + 1];
+ else if(strcmp(argv[x], "duration") == 0)
+ tinf->duration = atoi(argv[x + 1]);
+// else if(strcmp(argv[x], "date") == 0)
+// tinf->date = atoi(argv[x + 1]);
+ }
+
+ if(tinf->status == 0) return -1;
+
+ // check required fields - verify cmus program invocation
+ if(tinf->file != NULL || tinf->url != NULL) return 2;
+
+ // initial run -> start server
+ return 1;
+}
+
+// Load information from configuration file
+int read_cmusfm_config(struct cmusfm_config *cm_conf)
+{
+ int fd;
+ char buff[128], *ptr;
+
+ // read data from configuration file
+ sprintf(buff, "%s/" CONFIG_FNAME, get_cmus_home_dir());
+ if((fd = open(buff, O_RDONLY)) == -1) return -1;
+
+ read(fd, buff, sizeof(buff));
+ strcpy(cm_conf->user_name, strtok_r(buff, "\n", &ptr));
+ strcpy(cm_conf->session_key, strtok_r(NULL, "\n", &ptr));
+ cm_conf->parse_file_name = atoi(strtok_r(NULL, "\n", &ptr));
+ cm_conf->submit_radio = atoi(strtok_r(NULL, "\n", &ptr));
+ return close(fd);
+}
+
+// Save information to configuration file
+int write_cmusfm_config(struct cmusfm_config *cm_conf)
+{
+ int fd;
+ char buff[128];
+
+ // create config file (truncate previous one)
+ sprintf(buff, "%s/" CONFIG_FNAME, get_cmus_home_dir());
+ if((fd = open(buff, O_CREAT|O_WRONLY|O_TRUNC, S_IWUSR|S_IRUSR)) == -1)
+ return -1;
+
+ sprintf(buff, "%s\n%s\n%d\n%d\n", cm_conf->user_name, cm_conf->session_key,
+ cm_conf->parse_file_name, cm_conf->submit_radio);
+ write(fd, buff, strlen(buff));
+ return close(fd);
+}
+
+// Initialize cmusfm (get session key, tune some options)
+int user_authorization(const char *url) {
+ printf("Open this URL in your favorite web browser and then "
+ "press ENTER:\n %s\n", url);
+ getchar(); return 0;}
+int cmusfm_initialization()
+{
+ struct cmusfm_config cm_conf;
+ scrobbler_session_t *sbs;
+ int fetch_session_key;
+ char yesno_buf[8];
+
+ fetch_session_key = 1;
+ memset(&cm_conf, 0, sizeof(cm_conf));
+ sbs = scrobbler_initialize(SC_api_key, SC_secret);
+
+ // try to read previous configuration
+ if(read_cmusfm_config(&cm_conf) == 0) {
+ printf("Testing previous session key (Last.fm user name: %s) ...",
+ cm_conf.user_name); fflush(stdout);
+ scrobbler_set_session_key_str(sbs, cm_conf.session_key);
+ if(scrobbler_test_session_key(sbs) == 0) printf("OK.\n");
+ else printf("Failed.\n");
+
+ printf("Fetch new session key [yes/NO]: ");
+ fgets(yesno_buf, sizeof(yesno_buf), stdin);
+ if(memcmp(yesno_buf, "yes", 3) == 0) fetch_session_key = 1;
+ else fetch_session_key = 0;
+ }
+
+ if(fetch_session_key) { //fetch new session key
+ if(scrobbler_authentication(sbs, user_authorization) == 0){
+ scrobbler_get_session_key_str(sbs, cm_conf.session_key);
+ strcpy(cm_conf.user_name, sbs->user_name);}
+ else{ //if user_name and/or session_key is NULL -> segfault :)
+ strcpy(cm_conf.user_name, "(none)");
+ strcpy(cm_conf.session_key, "xxx");
+ printf("Error (scobbler authentication failed)\n\n");}
+ }
+ scrobbler_free(sbs);
+
+ // ask user few questions
+ printf("Submit tracks played from radio [yes/NO]: ");
+ fgets(yesno_buf, sizeof(yesno_buf), stdin);
+ if(memcmp(yesno_buf, "yes", 3) == 0) cm_conf.submit_radio = 1;
+ printf("Try to parse file name if tags are not supplied [yes/NO]: ");
+ fgets(yesno_buf, sizeof(yesno_buf), stdin);
+ if(memcmp(yesno_buf, "yes", 3) == 0) cm_conf.parse_file_name = 1;
+
+ if(write_cmusfm_config(&cm_conf) != 0)
+ printf("Error (cannot create configuration file)\n");
+ return 0;
+}
+
+// Simple and fast "hashing" function
+int make_data_hash(const unsigned char *data, int len)
+{
+ int x, hash;
+
+ for(x = hash = 0; x < len; x++)
+ hash += data[x]*(x + 1);
+ return hash;
+}
diff --git a/src/server.c b/src/server.c
new file mode 100644
index 0000000..e52d8dd
--- /dev/null
+++ b/src/server.c
@@ -0,0 +1,314 @@
+/*
+ cmusfm - server.c
+ Copyright (c) 2010-2011 Arkadiusz Bokowy
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include /* sockaddr_un */
+#include
+#include "libscrobbler2.h"
+#include "cmusfm.h"
+
+#ifdef DEBUG
+void dump_scrobbler_(const char *info, const scrobbler_trackinfo_t *sb_tinf)
+{
+ printf("--= %s =--\n", info);
+ printf(" timestamp: %d duration: %ds MbID: %s\n",
+ sb_tinf->timestamp, sb_tinf->duration, sb_tinf->mbid);
+ printf(" %s - %s (%s) - %02d. %s\n", sb_tinf->artist, sb_tinf->album,
+ sb_tinf->album_artist, sb_tinf->track_number, sb_tinf->track);
+ fflush(stdout);
+}
+#endif
+
+// Write data to cache file which should be submitted later.
+// Cache file record structure:
+// record_size(int)|scrobbler_trackinf_t|artist(NULL-term string)
+// track(NULL-term string)|album(NULL-term string)
+void update_cache(const scrobbler_trackinfo_t *sb_tinf)
+{
+ char cache_fname[128];
+ int fd, data_size;
+ int artlen, tralen, alblen;
+
+ sprintf(cache_fname, "%s/" CACHE_FNAME, get_cmus_home_dir());
+ fd = open(cache_fname, O_CREAT|O_APPEND|O_WRONLY, 00644);
+ if(fd == -1) return;
+
+ artlen = strlen(sb_tinf->artist);
+ tralen = strlen(sb_tinf->track);
+ alblen = strlen(sb_tinf->album);
+
+ // calculate record size
+ data_size = sizeof(int) + sizeof(scrobbler_trackinfo_t)
+ + artlen + 1 + tralen + 1 + alblen + 1;
+
+ write(fd, &data_size, sizeof(int));
+ write(fd, sb_tinf, sizeof(scrobbler_trackinfo_t));
+ write(fd, sb_tinf->artist, artlen + 1);
+ write(fd, sb_tinf->track, tralen + 1);
+ write(fd, sb_tinf->album, alblen + 1);
+
+ close(fd);
+}
+
+// Submit saved in cache track to Last.fm
+void submit_cache(scrobbler_session_t *sbs)
+{
+ char cache_fname[128];
+ char rd_buff[4096];
+ int fd, rd_len;
+ scrobbler_trackinfo_t sb_tinf;
+ void *record;
+ int rec_size;
+
+ sprintf(cache_fname, "%s/" CACHE_FNAME, get_cmus_home_dir());
+
+ // no cache file -> nothing to submit :)
+ if((fd = open(cache_fname, O_RDONLY)) == -1) return;
+
+ // read file until EOF
+ while((rd_len = read(fd, rd_buff, sizeof(rd_buff))) != 0) {
+ if(rd_len == -1) break;
+
+ for(record = rd_buff;;) {
+ rec_size = *((int*)record);
+
+ memcpy(&sb_tinf, record + sizeof(int), sizeof(sb_tinf));
+ sb_tinf.artist = record + sizeof(int) + sizeof(sb_tinf);
+ sb_tinf.track = &sb_tinf.artist[strlen(sb_tinf.artist) + 1];
+ sb_tinf.album = &sb_tinf.track[strlen(sb_tinf.track) + 1];
+
+ // submit tracks to Last.fm
+#ifndef DEBUG
+ scrobbler_scrobble(sbs, &sb_tinf);
+#else
+ dump_scrobbler_("submit_cache", &sb_tinf);
+#endif
+
+ // point to next record
+ record += rec_size;
+
+ // break if there is no more data in buffer
+ if(record - (void*)rd_buff >= rd_len) break;
+ }
+
+ if(record - (void*)rd_buff != rd_len)
+ // seek to the beginning of current record, because
+ // it is truncated, so we have to read it one more time
+ lseek(fd, (int)(record - (void*)rd_buff) - rec_size
+ - rd_len, SEEK_CUR);
+ }
+
+ close(fd);
+
+ // remove cache file
+ unlink(cache_fname);
+}
+
+// Process real server task - Last.fm submission
+void process_server_data(int fd, scrobbler_session_t *sbs, int submit_radio)
+{
+ static char saved_data[sizeof(sock_buff)], saved_is_radio = 0;
+ struct sock_data_tag *sock_data = (struct sock_data_tag*)sock_buff;
+ int rd_len;
+ // scrobbler stuff
+ static time_t scrobbler_fail_time = 1; //0 -> login OK
+ static time_t started = 0, playtime = 0, fulltime = 10, reinitime = 0;
+ static int prev_hash = 0, paused = 0;
+ scrobbler_trackinfo_t sb_tinf;
+ int new_hash;
+ char raw_status;
+
+ memset(&sb_tinf, 0, sizeof(sb_tinf));
+
+ rd_len = read(fd, sock_buff, sizeof(sock_buff));
+
+ // at least that amount of data we should receive
+ if(rd_len < sizeof(struct sock_data_tag)) return;
+
+#ifdef DEBUG
+ printf("sockrdlen: %d status: 0x%02x duration: %ds\n %s - %s - %02d. %s\n",
+ rd_len, sock_data->status, sock_data->duration,
+ &((char *)(sock_data + 1))[sock_data->artoff], (char *)(sock_data + 1),
+ sock_data->tracknb, &((char *)(sock_data + 1))[sock_data->titoff]);
+ fflush(stdout);
+#endif
+
+ // make data hash without status field
+ new_hash = make_data_hash((unsigned char*)&sock_buff[1], rd_len - 1);
+
+ raw_status = sock_data->status & ~CMSTATUS_SHOUTCASTMASK;
+
+ // test connection to server (on failure try again in some time)
+ if(scrobbler_fail_time != 0 &&
+ time(NULL) - scrobbler_fail_time > LOGIN_RETRY_DELAY) {
+ if(scrobbler_test_session_key(sbs) == 0) { //everything should be OK now
+ scrobbler_fail_time = 0;
+
+ // if there is something in cache submit it
+ submit_cache(sbs);
+ }
+ else scrobbler_fail_time = time(NULL);
+ }
+
+ if(new_hash != prev_hash) {//maybe it's time to submit :)
+ prev_hash = new_hash;
+submission_place:
+ playtime += time(NULL) - reinitime;
+ if(started != 0 && (playtime*100/fulltime > 50 || playtime > 240)) {
+ // playing duration is OK so submit track
+ fill_trackinfo(&sb_tinf, (struct sock_data_tag*)saved_data);
+ sb_tinf.timestamp = started;
+
+ // skip radio submission if we don't want it
+ if(saved_is_radio && !submit_radio) goto submission_skip;
+
+ if(scrobbler_fail_time == 0) {
+#ifndef DEBUG
+ if(scrobbler_scrobble(sbs, &sb_tinf) != 0){
+ scrobbler_fail_time = 1;
+ // scrobbler fail -> write data to cache
+ update_cache(&sb_tinf);}
+#else
+ dump_scrobbler_("submission", &sb_tinf);
+#endif
+ }
+ else //write data to cache
+ update_cache(&sb_tinf);
+ }
+
+submission_skip:
+ if(raw_status == CMSTATUS_STOPPED)
+ started = 0;
+ else {
+ // reinitialize variables, save track info in save_data
+ started = reinitime = time(NULL);
+ playtime = paused = 0;
+
+ if((sock_data->status & CMSTATUS_SHOUTCASTMASK) != 0)
+ // you have to listen radio min 90s (50% of 180)
+ fulltime = 180; //overrun DEVBYZERO in URL mode :)
+ else
+ fulltime = sock_data->duration;
+
+ // save information for later submission purpose
+ memcpy(saved_data, sock_data, rd_len);
+ saved_is_radio = sock_data->status & CMSTATUS_SHOUTCASTMASK;
+
+ if(raw_status == CMSTATUS_PLAYING && scrobbler_fail_time == 0) {
+ fill_trackinfo(&sb_tinf, sock_data);
+#ifndef DEBUG
+ if(scrobbler_update_now_playing(sbs, &sb_tinf) != 0)
+ scrobbler_fail_time = 1;
+#else
+ dump_scrobbler_("update_now_playing", &sb_tinf);
+#endif
+ }
+ }
+ }
+ else {// new_hash == prev_hash
+ if(raw_status == CMSTATUS_STOPPED)
+ goto submission_place;
+
+ if(raw_status == CMSTATUS_PAUSED){
+ playtime += time(NULL) - reinitime;
+ paused = 1;}
+
+ //NOTE: There is no possibility to distinguish between replayed track
+ // and unpaused. We assumed that if track was paused before this
+ // indicate that track is continued to playing in other case
+ // track is played again so we should submit previous playing.
+ if(raw_status == CMSTATUS_PLAYING) {
+ if(paused){
+ reinitime = time(NULL);
+ paused = 0;}
+ else
+ goto submission_place;
+ }
+ }
+}
+
+// server shutdown stuff
+static int server_on = 1;
+static void stop_server(int sig){server_on = 0;}
+
+// Run server instance (fork to background) and manage connections to it
+int run_server()
+{
+ struct sigaction sigact;
+ struct sockaddr_un sock_a;
+ int sock, peer_fd;
+ fd_set rd_fds;
+ // scrobbler stuff
+ struct cmusfm_config *cm_conf;
+ scrobbler_session_t *sbs;
+ int submit_radio;
+
+ // check previous instance of process
+ memset(&sock_a, 0, sizeof(sock_a));
+ sprintf(sock_a.sun_path, "%s/" SOCKET_FNAME , get_cmus_home_dir());
+ if(access(sock_a.sun_path, R_OK) == 0) return 0;
+
+ // get session key from configuration file
+ cm_conf = malloc(sizeof(*cm_conf));
+ if(read_cmusfm_config(cm_conf) != 0){
+ free(cm_conf); return -1;}
+
+ // initialize scrobbling library
+ sbs = scrobbler_initialize(SC_api_key, SC_secret);
+ scrobbler_set_session_key_str(sbs, cm_conf->session_key);
+ submit_radio = cm_conf->submit_radio;
+ free(cm_conf);
+
+ // fork server into background
+#ifndef DEBUG
+ if(fork() != 0) return 0; //run only child process
+#endif
+
+ // catch signals which are used to quit server
+ memset(&sigact, 0, sizeof(sigact));
+ sigact.sa_handler = stop_server;
+ sigaction(SIGHUP, &sigact, NULL);
+ sigaction(SIGTERM, &sigact, NULL);
+ sigaction(SIGINT, &sigact, NULL);
+
+ // create server communication socket (no error check)
+ sock_a.sun_family = AF_UNIX;
+ sock = socket(PF_UNIX, SOCK_STREAM, 0);
+ bind(sock, (struct sockaddr*)(&sock_a), sizeof(sock_a));
+ listen(sock, 2);
+
+ // server loop mode :)
+ for(peer_fd = -1; server_on;) {
+ FD_ZERO(&rd_fds);
+ FD_SET(sock, &rd_fds);
+ if(peer_fd != -1) FD_SET(peer_fd, &rd_fds);
+ if(select(sock > peer_fd ? sock + 1 : peer_fd + 1, &rd_fds,
+ NULL, NULL, NULL) == -1) break;
+
+ // we've got some data in our server sockets, hmm...
+ if(FD_ISSET(sock, &rd_fds)) peer_fd = accept(sock, NULL, NULL);
+ if(FD_ISSET(peer_fd, &rd_fds)){
+ process_server_data(peer_fd, sbs, submit_radio);
+ close(peer_fd); peer_fd = -1;}
+ }
+
+ close(sock);
+ scrobbler_free(sbs);
+ return unlink(sock_a.sun_path);
+}
+
+void fill_trackinfo(scrobbler_trackinfo_t *sbt, const struct sock_data_tag *dt)
+{
+ sbt->artist = &((char *)(dt + 1))[dt->artoff];
+ sbt->track = &((char *)(dt + 1))[dt->titoff];
+ sbt->album = (char *)(dt + 1);
+ sbt->track_number = dt->tracknb;
+ sbt->duration = dt->duration;
+}
+