aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCourtney Cavin <ccavin@gmail.com>2009-07-28 16:41:50 +0200
committerMax Kellermann <max@duempel.org>2009-07-28 16:41:50 +0200
commit6d71094ce5b6d02538b32693b32355d373b46883 (patch)
treefb7ac20fb019b48f823aa5e1ae8cbf8743e5032c
parent9322f04529659a01ccf313f2883f79d1525de99c (diff)
downloadmpd-6d71094ce5b6d02538b32693b32355d373b46883.tar.gz
mpd-6d71094ce5b6d02538b32693b32355d373b46883.tar.xz
mpd-6d71094ce5b6d02538b32693b32355d373b46883.zip
input/lastfm: use metadata
Added a patch to flush out the last.fm input plugin slightly. It basically turns it into a wrapper for the appropriate plugin. Most notably metadata is now extracted.
-rw-r--r--NEWS2
-rw-r--r--src/input/lastfm_input_plugin.c340
2 files changed, 299 insertions, 43 deletions
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 <stdlib.h>
#include <string.h>
#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;
+
+#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;
+}
- return lastfm_user != NULL && lastfm_password != NULL;
+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[] = {
+ {"&amp;", '&'},
+ {"&quot;", '"'},
+ {"&apos;", '\''},
+ {"&gt;", '>'},
+ {"&lt;", '<'}
+ };
+ 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("</", tag, ">", 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, "<location>");
- if (p == 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.
+ */
- p += 10;
- q = strchr(p, '<');
+ g_free(stream_url);
+ track = lastfm_xmltag(response, "track");
+ g_free(response);
- if (q == NULL) {
- g_free(response);
- g_free(stream_url);
+ /* If there are no tracks in the tracklist, it's possible that the
+ * station doesn't have enough content.
+ */
+
+ if (track == NULL)
return false;
- }
- g_free(stream_url);
- stream_url = g_strndup(p, q - p);
- g_free(response);
+ 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,
};