aboutsummaryrefslogtreecommitdiffstats
path: root/src/playlist/soundcloud_playlist_plugin.c
diff options
context:
space:
mode:
authorRobert Vollmert <rvollmert@gmx.net>2012-02-27 13:19:45 +0100
committerMax Kellermann <max@duempel.org>2012-03-01 20:11:09 +0100
commit7cef52478dc2d5c5b9d2a75e34e4e57ca5cdb122 (patch)
tree1aa8b8a61aba3963f8cf376f4ab9310b0f98d3f2 /src/playlist/soundcloud_playlist_plugin.c
parente7ce362d22dd2292a357b167b74dccefc2b7612b (diff)
downloadmpd-7cef52478dc2d5c5b9d2a75e34e4e57ca5cdb122.tar.gz
mpd-7cef52478dc2d5c5b9d2a75e34e4e57ca5cdb122.tar.xz
mpd-7cef52478dc2d5c5b9d2a75e34e4e57ca5cdb122.zip
A soundcloud playlist plugin.
Requires YAJL to build, and this doesn't include the necessary automake changes. Can be built using ./configure CFLAGS="-I/usr/include/yajl" LIBS="-lyajl" --enable-soundcloud Add the following to your config: playlist_plugin { name "soundcloud" enabled "true" apikey "c4c979fd6f241b5b30431d722af212e8" } Then you can stream from soundcloud using calls like: mpc load soundcloud://track/<track-id> mpc load soundcloud://playlist/<playlist-id> mpc load soundcloud://url/http://soundcloud.com/some/track/or/playlist For the last case, you can leave off the http:// or http://soundcloud.com/ .
Diffstat (limited to 'src/playlist/soundcloud_playlist_plugin.c')
-rw-r--r--src/playlist/soundcloud_playlist_plugin.c422
1 files changed, 422 insertions, 0 deletions
diff --git a/src/playlist/soundcloud_playlist_plugin.c b/src/playlist/soundcloud_playlist_plugin.c
new file mode 100644
index 000000000..b987de962
--- /dev/null
+++ b/src/playlist/soundcloud_playlist_plugin.c
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2003-2011 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "playlist/soundcloud_playlist_plugin.h"
+#include "conf.h"
+#include "input_stream.h"
+#include "playlist_plugin.h"
+#include "song.h"
+#include "tag.h"
+
+#include <glib.h>
+#include <yajl/yajl_parse.h>
+
+#include <string.h>
+
+struct soundcloud_playlist {
+ struct playlist_provider base;
+
+ GSList *songs;
+};
+
+static struct {
+ char *apikey;
+} soundcloud_config;
+
+static bool
+soundcloud_init(const struct config_param *param)
+{
+ const char *apikey = config_get_block_string(param, "apikey", NULL);
+
+ if (apikey == NULL) {
+ g_debug("disabling the soundcloud playlist plugin "
+ "because API key is not set");
+ return false;
+ }
+
+ soundcloud_config.apikey = g_strdup(apikey);
+
+ return true;
+}
+
+static void
+soundcloud_finish(void)
+{
+ g_free(soundcloud_config.apikey);
+}
+
+/**
+ * Construct a full soundcloud resolver URL from the given fragment.
+ * @param uri uri of a soundcloud page (or just the path)
+ * @return Constructed URL. Must be freed with g_free.
+ */
+static char *
+soundcloud_resolve(const char* uri) {
+ char *u, *ru;
+
+ if (g_str_has_prefix(uri, "http://")) {
+ u = g_strdup(uri);
+ } else if (g_str_has_prefix(uri, "soundcloud.com")) {
+ u = g_strconcat("http://", uri, NULL);
+ } else {
+ /* assume it's just a path on soundcloud.com */
+ u = g_strconcat("http://soundcloud.com/", uri, NULL);
+ }
+
+ ru = g_strconcat("http://api.soundcloud.com/resolve.json?url=",
+ u, "&client_id=", soundcloud_config.apikey, NULL);
+ g_free(u);
+
+ return ru;
+}
+
+/* YAJL parser for track data from both /tracks/ and /playlists/ JSON */
+
+enum key {
+ Duration,
+ Title,
+ Stream_URL,
+ Other,
+};
+
+const char* key_str[] = {
+ "duration",
+ "title",
+ "stream_url",
+ NULL,
+};
+
+struct parse_data {
+ int key;
+ char* stream_url;
+ long duration;
+ char* title;
+ int got_url; /* nesting level of last stream_url */
+ GSList* songs;
+};
+
+static int handle_integer(void *ctx, long intval)
+{
+ struct parse_data *data = (struct parse_data *) ctx;
+
+ switch (data->key) {
+ case Duration:
+ data->duration = intval;
+ break;
+ default:
+ break;
+ }
+
+ return 1;
+}
+
+static int handle_string(void *ctx, const unsigned char* stringval, unsigned int stringlen)
+{
+ struct parse_data *data = (struct parse_data *) ctx;
+ const char *s = (const char *) stringval;
+
+ switch (data->key) {
+ case Title:
+ if (data->title != NULL)
+ g_free(data->title);
+ data->title = g_strndup(s, stringlen);
+ break;
+ case Stream_URL:
+ if (data->stream_url != NULL)
+ g_free(data->stream_url);
+ data->stream_url = g_strndup(s, stringlen);
+ data->got_url = 1;
+ break;
+ default:
+ break;
+ }
+
+ return 1;
+}
+
+static int handle_mapkey(void *ctx, const unsigned char* stringval, unsigned int stringlen)
+{
+ struct parse_data *data = (struct parse_data *) ctx;
+
+ int i;
+ data->key = Other;
+
+ for (i = 0; i < Other; ++i) {
+ if (strncmp((const char *)stringval, key_str[i], stringlen) == 0) {
+ data->key = i;
+ break;
+ }
+ }
+
+ return 1;
+}
+
+static int handle_start_map(void *ctx)
+{
+ struct parse_data *data = (struct parse_data *) ctx;
+
+ if (data->got_url > 0)
+ data->got_url++;
+
+ return 1;
+}
+
+static int handle_end_map(void *ctx)
+{
+ struct parse_data *data = (struct parse_data *) ctx;
+
+ if (data->got_url > 1) {
+ data->got_url--;
+ return 1;
+ }
+
+ if (data->got_url == 0)
+ return 1;
+
+ /* got_url == 1, track finished, make it into a song */
+ data->got_url = 0;
+
+ struct song *s;
+ struct tag *t;
+ char *u;
+
+ u = g_strconcat(data->stream_url, "?client_id=", soundcloud_config.apikey, NULL);
+ s = song_remote_new(u);
+ g_free(u);
+ t = tag_new();
+ t->time = data->duration / 1000;
+ if (data->title != NULL)
+ tag_add_item(t, TAG_NAME, data->title);
+ s->tag = t;
+
+ data->songs = g_slist_prepend(data->songs, s);
+
+ return 1;
+}
+
+static yajl_callbacks parse_callbacks = {
+ NULL,
+ NULL,
+ handle_integer,
+ NULL,
+ NULL,
+ handle_string,
+ handle_start_map,
+ handle_mapkey,
+ handle_end_map,
+ NULL,
+ NULL,
+};
+
+/**
+ * Read JSON data and parse it using the given YAJL parser.
+ * @param url URL of the JSON data.
+ * @param hand YAJL parser handle.
+ * @return -1 on error, 0 on success.
+ */
+static int
+soundcloud_parse_json(const char *url, yajl_handle hand, GMutex* mutex, GCond* cond)
+{
+ struct input_stream *input_stream;
+ GError *error = NULL;
+ char buffer[4096];
+ unsigned char *ubuffer = (unsigned char *)buffer;
+ size_t nbytes;
+
+ input_stream = input_stream_open(url, mutex, cond, &error);
+ if (input_stream == NULL) {
+ if (error != NULL) {
+ g_warning("%s", error->message);
+ g_error_free(error);
+ }
+ return -1;
+ }
+
+ g_mutex_lock(mutex);
+ input_stream_wait_ready(input_stream);
+
+ yajl_status stat;
+ int done = 0;
+
+ while (!done) {
+ nbytes = input_stream_read(input_stream, buffer, sizeof(buffer), &error);
+ if (nbytes == 0) {
+ if (error != NULL) {
+ g_warning("%s", error->message);
+ g_error_free(error);
+ }
+ if (input_stream_eof(input_stream)) {
+ done = true;
+ } else {
+ g_mutex_unlock(mutex);
+ input_stream_close(input_stream);
+ return -1;
+ }
+ }
+
+ if (done)
+ stat = yajl_parse_complete(hand);
+ else
+ stat = yajl_parse(hand, ubuffer, nbytes);
+
+ if (stat != yajl_status_ok &&
+ stat != yajl_status_insufficient_data)
+ {
+ unsigned char *str = yajl_get_error(hand, 1, ubuffer, nbytes);
+ g_warning("%s", str);
+ yajl_free_error(hand, str);
+ break;
+ }
+ }
+
+ g_mutex_unlock(mutex);
+ input_stream_close(input_stream);
+
+ return 0;
+}
+
+/**
+ * Parse a soundcloud:// URL and create a playlist.
+ * @param uri A soundcloud URL. Accepted forms:
+ * soundcloud://track/<track-id>
+ * soundcloud://playlist/<playlist-id>
+ * soundcloud://url/<url or path of soundcloud page>
+ */
+
+static struct playlist_provider *
+soundcloud_open_uri(const char *uri, GMutex *mutex, GCond *cond)
+{
+ struct soundcloud_playlist *playlist = NULL;
+
+ char *s, *p;
+ char *scheme, *arg, *rest;
+ s = g_strdup(uri);
+ scheme = s;
+ for (p = s; *p; p++) {
+ if (*p == ':' && *(p+1) == '/' && *(p+2) == '/') {
+ *p = 0;
+ p += 3;
+ break;
+ }
+ }
+ arg = p;
+ for (; *p; p++) {
+ if (*p == '/') {
+ *p = 0;
+ p++;
+ break;
+ }
+ }
+ rest = p;
+
+ if (strcmp(scheme, "soundcloud") != 0) {
+ g_warning("incompatible scheme for soundcloud plugin: %s", scheme);
+ g_free(s);
+ return NULL;
+ }
+
+ char *u = NULL;
+ if (strcmp(arg, "track") == 0) {
+ u = g_strconcat("http://api.soundcloud.com/tracks/",
+ rest, ".json?client_id=", soundcloud_config.apikey, NULL);
+ } else if (strcmp(arg, "playlist") == 0) {
+ u = g_strconcat("http://api.soundcloud.com/playlists/",
+ rest, ".json?client_id=", soundcloud_config.apikey, NULL);
+ } else if (strcmp(arg, "url") == 0) {
+ /* Translate to soundcloud resolver call. libcurl will automatically
+ follow the redirect to the right resource. */
+ u = soundcloud_resolve(rest);
+ }
+ g_free(s);
+
+ if (u == NULL) {
+ g_warning("unknown soundcloud URI");
+ return NULL;
+ }
+
+ yajl_handle hand;
+ struct parse_data data;
+
+ data.got_url = 0;
+ data.songs = NULL;
+ data.title = NULL;
+ data.stream_url = NULL;
+ hand = yajl_alloc(&parse_callbacks, NULL, NULL, (void *) &data);
+
+ int ret = soundcloud_parse_json(u, hand, mutex, cond);
+
+ g_free(u);
+ yajl_free(hand);
+ if (data.title != NULL)
+ g_free(data.title);
+ if (data.stream_url != NULL)
+ g_free(data.stream_url);
+
+ if (ret == -1)
+ return NULL;
+
+ playlist = g_new(struct soundcloud_playlist, 1);
+ playlist_provider_init(&playlist->base, &soundcloud_playlist_plugin);
+ playlist->songs = g_slist_reverse(data.songs);
+
+ return &playlist->base;
+}
+
+static void
+soundcloud_close(struct playlist_provider *_playlist)
+{
+ struct soundcloud_playlist *playlist = (struct soundcloud_playlist *)_playlist;
+
+ g_free(playlist);
+}
+
+
+static struct song *
+soundcloud_read(struct playlist_provider *_playlist)
+{
+ struct soundcloud_playlist *playlist = (struct soundcloud_playlist *)_playlist;
+
+ if (playlist->songs == NULL)
+ return NULL;
+
+ struct song* s;
+ s = (struct song *)playlist->songs->data;
+ playlist->songs = g_slist_remove(playlist->songs, s);
+ return s;
+}
+
+static const char *const soundcloud_schemes[] = {
+ "soundcloud",
+ NULL
+};
+
+const struct playlist_plugin soundcloud_playlist_plugin = {
+ .name = "soundcloud",
+
+ .init = soundcloud_init,
+ .finish = soundcloud_finish,
+ .open_uri = soundcloud_open_uri,
+ .close = soundcloud_close,
+ .read = soundcloud_read,
+
+ .schemes = soundcloud_schemes,
+};
+
+