From ad816d21ce318c87d9ef5e11124ad5b56afedd1d Mon Sep 17 00:00:00 2001 From: waker Date: Wed, 26 Aug 2009 23:23:08 +0200 Subject: lastfm plugin WIP --- deadbeef.h | 30 ++++- playlist.c | 2 + playlist.h | 1 + plugins.c | 37 ++++++- plugins.h | 7 +- plugins/lastfm/lastfm.c | 284 ++++++++++++++++++++++++++++++++++++++---------- streamer.c | 6 + 7 files changed, 300 insertions(+), 67 deletions(-) diff --git a/deadbeef.h b/deadbeef.h index 416c890d..6106bfec 100644 --- a/deadbeef.h +++ b/deadbeef.h @@ -56,6 +56,7 @@ typedef struct { int startoffset; // offset to seek to skip tags and info-headers int endoffset; // offset from end of file where music data ends int shufflerating; // sort order for shuffle mode + float playtime; // total playtime const char *filetype; // e.g. MP3 or OGG } DB_playItem_t; @@ -67,19 +68,33 @@ enum { DB_PLUGIN_MISC = 4 }; +typedef struct { + int event; + double time; +} DB_event_t; + +typedef struct { + DB_event_t ev; + DB_playItem_t *song; +} DB_event_song_t; + // event callback type -typedef int (*db_callback_t)(int ev, uintptr_t data); -#define DB_CALLBACK(x) ((db_callback_t)(x)) +typedef int (*DB_callback_t)(DB_event_t *, uintptr_t data); // events enum { DB_EV_FRAMEUPDATE = 0, // ticks around 20 times per second, but ticker may stop sometimes DB_EV_SONGCHANGED = 1, // triggers when song was just changed + DB_EV_SONGSTARTED = 2, // triggers when song started playing (for scrobblers and such) + DB_EV_SONGFINISHED = 3, // triggers when song finished playing (for scrobblers and such) DB_EV_MAX }; // typecasting macros #define DB_PLUGIN(x) ((DB_plugin_t *)(x)) +#define DB_CALLBACK(x) ((DB_callback_t)(x)) +#define DB_EVENT(x) ((DB_event_t *)(x)) +#define DB_PLAYITEM(x) ((DB_playItem_t *)(x)) // forward decl for plugin struct struct DB_plugin_s; @@ -90,8 +105,8 @@ typedef struct { int vmajor; int vminor; // event subscribing - void (*ev_subscribe) (struct DB_plugin_s *plugin, int ev, db_callback_t callback, uintptr_t data); - void (*ev_unsubscribe) (struct DB_plugin_s *plugin, int ev, db_callback_t callback, uintptr_t data); + void (*ev_subscribe) (struct DB_plugin_s *plugin, int ev, DB_callback_t callback, uintptr_t data); + void (*ev_unsubscribe) (struct DB_plugin_s *plugin, int ev, DB_callback_t callback, uintptr_t data); // md5sum calc void (*md5) (uint8_t sig[16], const char *in, int len); void (*md5_to_str) (char *str, const uint8_t sig[16]); @@ -108,12 +123,19 @@ typedef struct { void (*mutex_free) (uintptr_t mtx); int (*mutex_lock) (uintptr_t mtx); int (*mutex_unlock) (uintptr_t mtx); + // playlist access + const char *(*pl_find_meta) (DB_playItem_t *song, const char *meta); + // web browser + int (*show_uri) (const char *uri); } DB_functions_t; // base plugin interface typedef struct DB_plugin_s { // type must be one of DB_PLUGIN_ types int32_t type; + // version + int16_t version_major; + int16_t version_minor; // may be deactivated on failures after load int inactive; // any of those can be left NULL diff --git a/playlist.c b/playlist.c index c033583a..4957f64d 100644 --- a/playlist.c +++ b/playlist.c @@ -31,6 +31,7 @@ #include "messagepump.h" #include "messages.h" #include "playback.h" +#include "plugins.h" #include "cvorbis.h" #include "cdumb.h" @@ -620,6 +621,7 @@ pl_set_current (playItem_t *it) { } codec_unlock (); it->played = 1; + plug_trigger_event (DB_EV_SONGSTARTED); messagepump_push (M_SONGCHANGED, 0, from, to); return ret; } diff --git a/playlist.h b/playlist.h index d8fe1d6e..7e2df4c2 100644 --- a/playlist.h +++ b/playlist.h @@ -38,6 +38,7 @@ typedef struct playItem_s { int startoffset; // offset to seek to skip tags and info-headers (mp3) int endoffset; // offset from end of file where music data ends (mp3) int shufflerating; // sort order for shuffle mode + float playtime; // total playtime const char *filetype; // e.g. MP3 or OGG struct playItem_s *next[PL_MAX_ITERATORS]; // next item in linked list struct playItem_s *prev[PL_MAX_ITERATORS]; // prev item in linked list diff --git a/plugins.c b/plugins.c index 15ed6424..e0d7f68b 100644 --- a/plugins.c +++ b/plugins.c @@ -4,6 +4,7 @@ #include #include #include +#include #ifdef HAVE_CONFIG_H # include #endif @@ -13,6 +14,7 @@ #include "messages.h" #include "threading.h" #include "progress.h" +#include "playlist.h" // deadbeef api DB_functions_t deadbeef_api = { @@ -34,6 +36,8 @@ DB_functions_t deadbeef_api = { .mutex_free = mutex_free, .mutex_lock = mutex_lock, .mutex_unlock = mutex_unlock, + .pl_find_meta = (const char *(*) (DB_playItem_t *song, const char *meta))pl_find_meta, + .show_uri = plug_show_uri, }; void @@ -49,7 +53,7 @@ plug_md5_to_str (char *str, const uint8_t sig[16]) { // event handlers typedef struct { DB_plugin_t *plugin; - db_callback_t callback; + DB_callback_t callback; uintptr_t data; } evhandler_t; #define MAX_HANDLERS 100 @@ -64,7 +68,7 @@ typedef struct plugin_s { plugin_t *plugins; void -plug_ev_subscribe (DB_plugin_t *plugin, int ev, db_callback_t callback, uintptr_t data) { +plug_ev_subscribe (DB_plugin_t *plugin, int ev, DB_callback_t callback, uintptr_t data) { assert (ev < DB_EV_MAX && ev >= 0); for (int i = 0; i < MAX_HANDLERS; i++) { if (!handlers[ev][i].plugin) { @@ -78,7 +82,7 @@ plug_ev_subscribe (DB_plugin_t *plugin, int ev, db_callback_t callback, uintptr_ } void -plug_ev_unsubscribe (DB_plugin_t *plugin, int ev, db_callback_t callback, uintptr_t data) { +plug_ev_unsubscribe (DB_plugin_t *plugin, int ev, DB_callback_t callback, uintptr_t data) { assert (ev < DB_EV_MAX && ev >= 0); for (int i = 0; i < MAX_HANDLERS; i++) { if (handlers[ev][i].plugin == plugin) { @@ -121,14 +125,39 @@ plug_quit (void) { messagepump_push (M_TERMINATE, 0, 0, 0); } +int +plug_show_uri (const char *uri) { + GError *error = NULL; + GDK_THREADS_ENTER(); + gboolean ret = gtk_show_uri (NULL, uri, GDK_CURRENT_TIME, &error); + GDK_THREADS_LEAVE(); + return ret; +} + /////// non-api functions (plugin support) void plug_trigger_event (int ev) { + DB_event_t *event; + switch (ev) { + case DB_EV_SONGSTARTED: + case DB_EV_SONGFINISHED: + { + DB_event_song_t *pev = malloc (sizeof (DB_event_song_t)); + pev->song = DB_PLAYITEM (&playlist_current); + event = DB_EVENT (pev); + } + break; + default: + event = malloc (sizeof (DB_event_t)); + } + event->event = ev; + event->time = (double)clock () / CLOCKS_PER_SEC; for (int i = 0; i < MAX_HANDLERS; i++) { if (handlers[ev][i].plugin && !handlers[ev][i].plugin->inactive) { - handlers[ev][i].callback (ev, handlers[ev][i].data); + handlers[ev][i].callback (event, handlers[ev][i].data); } } + free (event); } void diff --git a/plugins.h b/plugins.h index ef228b51..0525d9e4 100644 --- a/plugins.h +++ b/plugins.h @@ -3,10 +3,10 @@ #include "deadbeef.h" void -plug_ev_subscribe (DB_plugin_t *plugin, int ev, db_callback_t callback, uintptr_t data); +plug_ev_subscribe (DB_plugin_t *plugin, int ev, DB_callback_t callback, uintptr_t data); void -plug_ev_unsubscribe (DB_plugin_t *plugin, int ev, db_callback_t callback, uintptr_t data); +plug_ev_unsubscribe (DB_plugin_t *plugin, int ev, DB_callback_t callback, uintptr_t data); void plug_trigger_event (int ev); @@ -41,4 +41,7 @@ plug_playback_play (void); void plug_quit (void); +int +plug_show_uri (const char *uri); + #endif // __PLUGINS_H diff --git a/plugins/lastfm/lastfm.c b/plugins/lastfm/lastfm.c index 4b6bc21e..afee5471 100644 --- a/plugins/lastfm/lastfm.c +++ b/plugins/lastfm/lastfm.c @@ -25,127 +25,293 @@ static DB_misc_t plugin; static DB_functions_t *deadbeef; -static char lastfm_login[50]; -static char lastfm_pass[50]; -#define SCROBBLER_URL "http://ws.audioscrobbler.com/2.0" +#define SCROBBLER_URL_V1 "http://post.audioscrobbler.com" +#define SCROBBLER_URL_V2 "http://ws.audioscrobbler.com/2.0" #define LASTFM_API_KEY "6b33c8ae4d598a9aff8fe63e334e6e86" #define LASTFM_API_SECRET "a9f5e17e358377d96e96477d870b2b18" +char lfm_sess[33]; +char lfm_user[100]; +char lfm_pass[100]; + DB_plugin_t * lastfm_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); } -static char *lastfm_srv_res; -static char lastfm_srv_size; -static char lastfm_curl_err[CURL_ERROR_SIZE]; +static char *lfm_reply; +static char lfm_reply_sz; +static char lfm_err[CURL_ERROR_SIZE]; static size_t lastfm_curl_res (void *ptr, size_t size, size_t nmemb, void *stream) { int len = size * nmemb; - lastfm_srv_res = realloc (lastfm_srv_res, lastfm_srv_size + len + 1); - memcpy (lastfm_srv_res + lastfm_srv_size, ptr, len); - lastfm_srv_size += len; + lfm_reply = realloc (lfm_reply, lfm_reply_sz + len + 1); + memcpy (lfm_reply + lfm_reply_sz, ptr, len); + lfm_reply_sz += len; char s[size*nmemb+1]; memcpy (s, ptr, size*nmemb); s[size*nmemb] = 0; - printf ("%s\n", s); return len; } -int -lastfm_auth (void) { - // auth - char msg[4096]; - char sigstr[4096]; - uint8_t sig[16]; - snprintf (sigstr, sizeof (sigstr), "api_key%smethodauth.getToken%s", SCROBBLER_URL, LASTFM_API_KEY, LASTFM_API_SECRET); - deadbeef->md5 (sig, sigstr, strlen (sigstr)); - deadbeef->md5_to_str (sigstr, sig); - snprintf (msg, sizeof (msg), "%s/?method=auth.getToken&api_key=%s&api_sig=%s", SCROBBLER_URL, LASTFM_API_KEY, sigstr); - printf ("sending request: %s\n", msg); - // init curl +static int +curl_req_send (const char *req, const char *post) { + printf ("sending request: %s\n", req); CURL *curl; curl = curl_easy_init (); if (!curl) { fprintf (stderr, "lastfm: failed to init curl\n"); - return 0; + return -1; } curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); - curl_easy_setopt(curl, CURLOPT_URL, msg); + curl_easy_setopt(curl, CURLOPT_URL, req); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, lastfm_curl_res); - memset(lastfm_curl_err, 0, sizeof(lastfm_curl_err)); - curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, lastfm_curl_err); - curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + memset(lfm_err, 0, sizeof(lfm_err)); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, lfm_err); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + if (post) { + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(post)); + } int status = curl_easy_perform(curl); curl_easy_cleanup (curl); + if (!status) { + lfm_reply[lfm_reply_sz] = 0; + } + return status; +} + +static void +curl_req_cleanup (void) { + if (lfm_reply) { + free (lfm_reply); + lfm_reply = NULL; + lfm_reply_sz = 0; + } +} + +// {{{ lastfm v2 get session +#if 0 +int +auth_v2 (void) { + if (lfm_sess[0]) { + return 0; + } + char msg[4096]; + char sigstr[4096]; + uint8_t sig[16]; + snprintf (sigstr, sizeof (sigstr), "api_key%smethodauth.getToken%s", LASTFM_API_KEY, LASTFM_API_SECRET); + deadbeef->md5 (sig, sigstr, strlen (sigstr)); + deadbeef->md5_to_str (sigstr, sig); + snprintf (msg, sizeof (msg), "%s/?api_key=%s&method=auth.getToken&api_sig=%s", SCROBBLER_URL, LASTFM_API_KEY, sigstr); + // get token + char lfm_token[33] = ""; + int status = curl_req_send (msg, NULL); if (!status) { // parse output - if (strstr (lastfm_srv_res, "")) { - char *token = strstr (lastfm_srv_res, ""); + if (strstr (lfm_reply, "")) { + char *token = strstr (lfm_reply, ""); if (token) { token += 7; char *end = strstr (token, ""); if (end) { *end = 0; - printf ("token: %s\n", token); - } - else { - printf ("no \n"); + snprintf (msg, sizeof (msg), "http://www.last.fm/api/auth/?api_key=%s&token=%s", LASTFM_API_KEY, token); + printf ("Dear user. Please visit this URL and authenticate deadbeef. Thanks.\n"); + + printf ("%s\n", msg); + strncpy (lfm_token, token, 32); + lfm_token[32] = 0; } } - else { - printf ("no \n"); + } + } + curl_req_cleanup (); + if (!lfm_token[0]) { + // total fail, give up + return -1; + } + // get session + snprintf (sigstr, sizeof (sigstr), "api_key%smethodauth.getSessiontoken%s%s", LASTFM_API_KEY, lfm_token, LASTFM_API_SECRET); + deadbeef->md5 (sig, sigstr, strlen (sigstr)); + deadbeef->md5_to_str (sigstr, sig); + snprintf (msg, sizeof (msg), "method=auth.getSession&token=%s&api_key=%s&api_sig=%s", lfm_token, LASTFM_API_KEY, sigstr); + for (;;) { + status = curl_req_send (SCROBBLER_URL, msg); + if (!status) { + char *sess = strstr (lfm_reply, ""); + if (sess) { + sess += 5; + char *end = strstr (sess, ""); + if (end) { + *end = 0; + char config[1024]; + snprintf (config, sizeof (config), "%s/.config/deadbeef/lastfmv2", getenv ("HOME")); + fprintf (stderr, "got session key %s\n", sess); + FILE *fp = fopen (config, "w+b"); + if (!fp) { + fprintf (stderr, "lastfm: failed to write config file %s\n", config); + curl_req_cleanup (); + return -1; + } + if (fwrite (sess, 1, 32, fp) != 32) { + fclose (fp); + fprintf (stderr, "lastfm: failed to write config file %s\n", config); + curl_req_cleanup (); + return -1; + } + fclose (fp); + strcpy (lfm_sess, sess); + } } +// printf ("reply: %s\n", lfm_reply); } - else { - printf ("not ok\n"); + curl_req_cleanup (); + if (lfm_sess[0]) { + break; } + sleep (5); } + return 0; +} +#endif +// }}} + - if (lastfm_srv_res) { - free (lastfm_srv_res); - lastfm_srv_res = NULL; +int +auth (void) { + char req[4096]; + time_t timestamp = time(0); + uint8_t sig[16]; + char passmd5[33]; + char token[100]; + printf (">> %s:%s <<\n", lfm_user, lfm_pass); + deadbeef->md5 (sig, lfm_pass, strlen (lfm_pass)); + deadbeef->md5_to_str (passmd5, sig); + snprintf (token, sizeof (token), "%s%d", passmd5, timestamp); + deadbeef->md5 (sig, token, strlen (token)); + deadbeef->md5_to_str (token, sig); + + snprintf (req, sizeof (req), "%s/?hs=true&p=1.2.1&c=tst&v=1.0&u=%s&t=%d&a=%s", SCROBBLER_URL_V1, lfm_user, timestamp, token); + // handshake + int status = curl_req_send (req, NULL); + if (!status) { + printf ("%s\n", lfm_reply); } + curl_req_cleanup (); + return 0; } static int -lastfm_frameupdate (int ev, uintptr_t data) { -// printf ("lastfm tick\n"); +lfm_fetch_song_info (DB_playItem_t *song, const char **a, const char **t, const char **b, float *l, const char **n, const char **m) { + *a = deadbeef->pl_find_meta (song, "artist"); + if (!strcmp (*a, "?")) { + return -1; + } + *t = deadbeef->pl_find_meta (song, "title"); + if (!strcmp (*t, "?")) { + return -1; + } + *b = deadbeef->pl_find_meta (song, "album"); + if (!strcmp (*b, "?")) { + return -1; + } + *l = song->duration; + *n = deadbeef->pl_find_meta (song, "track"); + if (!strcmp (*n, "?")) { + *n = ""; + } + *m = deadbeef->pl_find_meta (song, "mbid"); + if (!strcmp (*m, "?")) { + *m = ""; + } return 0; } static int -lastfm_songchanged (int ev, uintptr_t data) { - printf ("song changed\n"); +lastfm_songstarted (DB_event_song_t *ev, uintptr_t data) { + printf ("song started, info:\n"); + const char *a; // artist + const char *t; // title + const char *b; // album + float l; // duration + const char *n; // tracknum + const char *m; // muzicbrainz id + if (lfm_fetch_song_info (ev->song, &a, &t, &b, &l, &n, &m) == 0) { + printf ("playtime: %f\nartist: %s\ntitle: %s\nalbum: %s\nduration: %f\ntracknum: %s\n---\n", ev->song->playtime, a, t, b, l, n); + } + else { + printf ("file %f doesn't have enough tags to submit to last.fm\n", ev->song->fname); + } + return 0; } +static int +lastfm_songfinished (DB_event_song_t *ev, uintptr_t data) { + printf ("song finished, info:\n"); + const char *a; // artist + const char *t; // title + const char *b; // album + float l; // duration + const char *n; // tracknum + const char *m; // muzicbrainz id + if (lfm_fetch_song_info (ev->song, &a, &t, &b, &l, &n, &m) == 0) { + printf ("playtime: %f\nartist: %s\ntitle: %s\nalbum: %s\nduration: %f\ntracknum: %s\n---\n", ev->song->playtime, a, t, b, l, n); + } + else { + printf ("file %f doesn't have enough tags to submit to last.fm\n", ev->song->fname); + } + + return 0; +} static int lastfm_start (void) { // subscribe to frameupdate event - deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_FRAMEUPDATE, lastfm_frameupdate, 0); - deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_SONGCHANGED, lastfm_songchanged, 0); +// deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_FRAMEUPDATE, lastfm_frameupdate, 0); +// deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_SONGCHANGED, lastfm_songchanged, 0); + deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_SONGSTARTED, DB_CALLBACK (lastfm_songstarted), 0); + deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_SONGFINISHED, DB_CALLBACK (lastfm_songfinished), 0); // load login/pass char config[1024]; + +// {{{ lastfm v2 auth +#if 0 + snprintf (config, 1024, "%s/.config/deadbeef/lastfmv2", getenv ("HOME")); + FILE *fp = fopen (config, "rt"); + if (fp) { + if (fread (lfm_sess, 1, 32, fp) != 32) { + lfm_sess[0] = 0; + } + else { + lfm_sess[32] = 0; + } + fclose (fp); + } + auth_v2 (); +#endif +// }}} + snprintf (config, 1024, "%s/.config/deadbeef/lastfm", getenv ("HOME")); FILE *fp = fopen (config, "rt"); if (!fp) { fprintf (stderr, "lastfm: failed open %s\n", config); return -1; } - if (!fgets (lastfm_login, 50, fp)) { + if (!fgets (lfm_user, 50, fp)) { fprintf (stderr, "lastfm: failed to read login from %s\n", config); fclose (fp); return -1; } - if (!fgets (lastfm_pass, 50, fp)) { + if (!fgets (lfm_pass, 50, fp)) { fprintf (stderr, "lastfm: failed to read pass from %s\n", config); fclose (fp); return -1; @@ -154,32 +320,36 @@ lastfm_start (void) { // remove trailing garbage int l; char *p; - l = strlen (lastfm_login); - p = lastfm_login+l-1; - while (p >= lastfm_login && *p < 0x20) { + l = strlen (lfm_user); + p = lfm_user+l-1; + while (p >= lfm_user && *p < 0x20) { p--; } + p++; *p = 0; - l = strlen (lastfm_pass); - p = lastfm_pass+l-1; - while (p >= lastfm_login && *p < 0x20) { + l = strlen (lfm_pass); + p = lfm_pass+l-1; + while (p >= lfm_pass && *p < 0x20) { p--; } + p++; *p = 0; - lastfm_auth (); + auth (); return 0; } static int lastfm_stop (void) { - deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_FRAMEUPDATE, lastfm_frameupdate, 0); - deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_SONGCHANGED, lastfm_songchanged, 0); + deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_SONGSTARTED, DB_CALLBACK (lastfm_songstarted), 0); + deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_SONGFINISHED, DB_CALLBACK (lastfm_songfinished), 0); return 0; } // define plugin interface static DB_misc_t plugin = { + .plugin.version_major = 0, + .plugin.version_minor = 1, .plugin.type = DB_PLUGIN_MISC, .plugin.name = "last.fm scrobbler", .plugin.descr = "sends played songs information to your last.fm account", diff --git a/streamer.c b/streamer.c index 06dd0e7f..56d86699 100644 --- a/streamer.c +++ b/streamer.c @@ -31,6 +31,7 @@ #include "messagepump.h" #include "messages.h" #include "conf.h" +#include "plugins.h" static SRC_STATE *src; static SRC_DATA srcdata; @@ -95,6 +96,7 @@ streamer_thread (uintptr_t ctx) { badsong = -1; continue; } + plug_trigger_event (DB_EV_SONGFINISHED); int ret = pl_set_current (pl_get_for_idx (sng)); if (ret < 0) { //printf ("bad file in playlist, skipping...\n"); @@ -339,6 +341,10 @@ streamer_read (char *bytes, int size) { } streambuffer_fill -= sz; playpos += (float)sz/p_get_rate ()/4.f; + playlist_current.playtime += (float)sz/p_get_rate ()/4.f; + if (playlist_current_ptr) { + playlist_current_ptr->playtime = playlist_current.playtime; + } bytes_until_next_song -= sz; if (bytes_until_next_song < 0) { bytes_until_next_song = 0; -- cgit v1.2.3