diff --git a/NEWS b/NEWS index 6fee901db..4c38be831 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ ver 0.16 (20??/??/??) - send song modification time to client - added "update" idle event - removed the deprecated "volume" command +* input: + - lastfm: use metadata * tags: - added tags "ArtistSort", "AlbumArtistSort" - id3: revised "performer" tag support diff --git a/src/input/lastfm_input_plugin.c b/src/input/lastfm_input_plugin.c index 0039f7069..41882b00e 100644 --- a/src/input/lastfm_input_plugin.c +++ b/src/input/lastfm_input_plugin.c @@ -18,26 +18,72 @@ */ #include "input/lastfm_input_plugin.h" -#include "input/curl_input_plugin.h" #include "input_plugin.h" +#include "tag.h" #include "conf.h" +#include #include #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "input_lastfm" -static const char *lastfm_user, *lastfm_password; +static struct lastfm_data { + char *user; + char *md5; +} lastfm_data; + +struct lastfm_input { + /* our very own plugin wrapper */ + struct input_plugin wrap_plugin; + + /* pointer to input stream's plugin */ + const struct input_plugin *plugin; + + /* pointer to input stream's data */ + void *data; + + /* current track's tag */ + struct tag *tag; +}; static bool lastfm_input_init(const struct config_param *param) { - lastfm_user = config_get_block_string(param, "user", NULL); - lastfm_password = config_get_block_string(param, "password", NULL); + const char *passwd = config_get_block_string(param, "password", NULL); + const char *user = config_get_block_string(param, "user", NULL); + if (passwd == NULL || user == NULL) + return false; - return lastfm_user != NULL && lastfm_password != NULL; +#if GLIB_CHECK_VERSION(2,16,0) + lastfm_data.user = g_uri_escape_string(user, NULL, false); +#else + lastfm_data.user = g_strdup(user); +#endif + +#if GLIB_CHECK_VERSION(2,16,0) + if (strlen(passwd) != 32) + lastfm_data.md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, + passwd, strlen(passwd)); + else +#endif + lastfm_data.md5 = g_strdup(passwd); + + return true; } +static void +lastfm_input_finish(void) +{ + g_free(lastfm_data.user); + g_free(lastfm_data.md5); +} + +/** + * Simple data fetcher. + * @param url path or url of data to fetch. + * @return data fetched, or NULL on error. Must be freed with g_free. + */ static char * lastfm_get(const char *url) { @@ -78,6 +124,12 @@ lastfm_get(const char *url) return g_strndup(buffer, length); } +/** + * Ini-style value fetcher. + * @param response data through which to search. + * @param name name of value to search for. + * @return value for param name in param reponse or NULL on error. Free with g_free. + */ static char * lastfm_find(const char *response, const char *name) { @@ -98,10 +150,192 @@ lastfm_find(const char *response, const char *name) } } +/** + * Replace XML's five predefined entities with the equivalant characters. + * glib doesn't seem to have code to do this, even in the xml parser. + * We don't manage numerical character references such as &#nnnn; or &#xhhhh;. + * @param value XML text to decode. + * @return decoded string, which must be freed with g_free. + * @todo make sure this is ok for utf-8. + */ +static char * +lastfm_xmldecode(const char *value) +{ + struct entity { + const char *text; + char repl; + } entities[] = { + {"&", '&'}, + {""", '"'}, + {"'", '\''}, + {">", '>'}, + {"<", '<'} + }; + char *txt = g_strdup(value); + unsigned int i; + + for (i = 0; i < sizeof(entities)/sizeof(entities[0]); ++i) { + int slen = strlen(entities[i].text); + char *p = strstr(txt, entities[i].text); + if (p == NULL) + continue; + + *p = entities[i].repl; + g_strlcpy(p + 1, p + slen, strlen(p) - slen); + } + return txt; +} + +/** + * Extract the text between xml start and end tags specified by param tag. + * Caveat: This function does not handle nested tags properly. + * @param response XML to extract text from. + * @param tag name of tag of which text should be extracted from. + * @return text between tags specified by param tag, NULL on error; Must be freed with g_free. + */ +static char * +lastfm_xmltag(const char *response, const char *tag) +{ + char *tn = g_strconcat("<", tag, ">", NULL); + char *p, *q; + + if (!(p = strstr(response, tn))) { + g_free(tn); + return NULL; + } + + p += strlen(tn); + g_free(tn); + + tn = g_strconcat("", NULL); + + if (!(q = strstr(p, tn))) { + g_free(tn); + return NULL; + } + + g_free(tn); + + return g_strndup(p, q - p); +} + +/** + * Parses xspf track and generates mpd tag. + * @return tag which must be freed with tag_free. + */ +static struct tag * +lastfm_read_tag(const char *response) +{ + struct tagalias { + enum tag_type type; + const char *xmltag; + } aliases[] = { + {TAG_ITEM_ARTIST, "creator"}, + {TAG_ITEM_TITLE, "title"}, + {TAG_ITEM_ALBUM, "album"} + }; + struct tag *tag = tag_new(); + unsigned int i; + char *track_time = lastfm_xmltag(response, "duration"); + + if (track_time != NULL) { + int mtime = strtol(track_time, 0, 0); + g_free(track_time); + + /* make sure to round up */ + tag->time = ((mtime + 999) / 1000); + } + else + tag->time = 0; + + for (i = 0; i < sizeof(aliases)/sizeof(aliases[0]); ++i) { + char *p, *value = lastfm_xmltag(response, aliases[i].xmltag); + if (value == NULL) + continue; + + p = lastfm_xmldecode(value); + g_free(value); + value = p; + + tag_add_item(tag, aliases[i].type, value); + g_free(value); + } + return tag; +} + +static size_t +lastfm_input_read_wrap(struct input_stream *is, void *ptr, size_t size) +{ + size_t ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->read)(is, ptr, size); + is->data = d; + return ret; +} + +static bool +lastfm_input_eof_wrap(struct input_stream *is) +{ + bool ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->eof)(is); + is->data = d; + return ret; +} + +static bool +lastfm_input_seek_wrap(struct input_stream *is, off_t offset, int whence) +{ + bool ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->seek)(is, offset, whence); + is->data = d; + return ret; +} + +static int +lastfm_input_buffer_wrap(struct input_stream *is) +{ + int ret; + struct lastfm_input *d = is->data; + is->data = d->data; + ret = (* d->plugin->buffer)(is); + is->data = d; + return ret; +} + +static struct tag * +lastfm_input_tag(struct input_stream *is) +{ + struct lastfm_input *d = is->data; + struct tag *tag = d->tag; + d->tag = NULL; + return tag; +} + +static void +lastfm_input_close(struct input_stream *is) +{ + struct lastfm_input *d = is->data; + + if (is->plugin->close) { + is->data = d->data; + is->plugin = d->plugin; + (* is->plugin->close)(is); + } + + if (d->tag) + tag_free(d->tag); + g_free(d); +} + static bool lastfm_input_open(struct input_stream *is, const char *url) { - char *md5, *p, *q, *response, *session, *stream_url; + char *p, *q, *response, *track, *session, *stream_url; bool success; if (strncmp(url, "lastfm://", 9) != 0) @@ -109,27 +343,11 @@ lastfm_input_open(struct input_stream *is, const char *url) /* handshake */ -#if GLIB_CHECK_VERSION(2,16,0) - q = g_uri_escape_string(lastfm_user, NULL, false); -#else - q = g_strdup(lastfm_username); -#endif - -#if GLIB_CHECK_VERSION(2,16,0) - if (strlen(lastfm_password) != 32) - md5 = g_compute_checksum_for_string(G_CHECKSUM_MD5, - lastfm_password, - strlen(lastfm_password)); - else -#endif - md5 = g_strdup(lastfm_password); - p = g_strconcat("http://ws.audioscrobbler.com/radio/handshake.php?" "version=1.1.1&platform=linux&" - "username=", q, "&" - "passwordmd5=", md5, "&debug=0&partner=", NULL); - g_free(q); - g_free(md5); + "username=", lastfm_data.user, "&" + "passwordmd5=", lastfm_data.md5, "&" + "debug=0&partner=", NULL); response = lastfm_get(p); g_free(p); @@ -148,9 +366,9 @@ lastfm_input_open(struct input_stream *is, const char *url) } #if GLIB_CHECK_VERSION(2,16,0) - q = g_uri_escape_string(session, NULL, false); - g_free(session); - session = q; + q = g_uri_escape_string(session, NULL, false); + g_free(session); + session = q; #endif /* "adjust" last.fm radio */ @@ -195,35 +413,71 @@ lastfm_input_open(struct input_stream *is, const char *url) return false; } - p = strstr(response, ""); - if (p == NULL) { - g_free(response); - g_free(stream_url); - return false; - } - - p += 10; - q = strchr(p, '<'); - - if (q == NULL) { - g_free(response); - g_free(stream_url); - return false; - } + /* From here on, we only care about the first track, extract that + * + * Note: if you want to get information about the next track (needed + * for continuous playback) extract the other track info here too. + */ g_free(stream_url); - stream_url = g_strndup(p, q - p); + track = lastfm_xmltag(response, "track"); g_free(response); + /* If there are no tracks in the tracklist, it's possible that the + * station doesn't have enough content. + */ + + if (track == NULL) + return false; + + stream_url = lastfm_xmltag(track, "location"); + if (stream_url == NULL) { + g_free(track); + return false; + } + /* now really open the last.fm radio stream */ success = input_stream_open(is, stream_url); + if (success) { + /* instantiate our transparent wrapper plugin + * this is needed so that the backend knows what functions are + * properly available. + */ + + struct lastfm_input *d = g_new0(struct lastfm_input, 1); + d->wrap_plugin.name = "lastfm"; + d->wrap_plugin.open = lastfm_input_open; + d->wrap_plugin.close = lastfm_input_close; + d->wrap_plugin.read = lastfm_input_read_wrap; + d->wrap_plugin.eof = lastfm_input_eof_wrap; + d->wrap_plugin.tag = lastfm_input_tag; + if (is->seekable) + d->wrap_plugin.seek = lastfm_input_seek_wrap; + if (is->plugin->buffer) + d->wrap_plugin.buffer = lastfm_input_buffer_wrap; + + d->tag = lastfm_read_tag(track); + d->plugin = is->plugin; + d->data = is->data; + + /* give the backend our wrapper plugin */ + + is->plugin = &d->wrap_plugin; + is->data = d; + } + g_free(stream_url); + g_free(track); + return success; } const struct input_plugin lastfm_input_plugin = { .name = "lastfm", .init = lastfm_input_init, + .finish = lastfm_input_finish, .open = lastfm_input_open, + .close = lastfm_input_close, + .tag = lastfm_input_tag, };