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; +} +