aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am6
-rw-r--r--NEWS1
-rw-r--r--configure.ac13
-rw-r--r--src/playlist/soundcloud_playlist_plugin.c422
-rw-r--r--src/playlist/soundcloud_playlist_plugin.h25
-rw-r--r--src/playlist_list.c4
6 files changed, 471 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am
index 07782fdc..ec78f6b9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -902,6 +902,12 @@ if ENABLE_DESPOTIFY
libplaylist_plugins_a_SOURCES += src/playlist/despotify_playlist_plugin.c
endif
+if ENABLE_SOUNDCLOUD
+libplaylist_plugins_a_SOURCES += \
+ src/playlist/soundcloud_playlist_plugin.h \
+ src/playlist/soundcloud_playlist_plugin.c
+PLAYLIST_LIBS += -lyajl
+endif
#
# Filter plugins
diff --git a/NEWS b/NEWS
index 2bd1e55f..9343f3f7 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,7 @@ ver 0.17 (2011/??/??)
* playlist:
- allow references to songs outside the music directory
- new CUE parser, without libcue
+ - soundcloud: new plugin for accessing soundcloud.com
* state_file: add option "restore_paused"
* cue: show CUE track numbers
* allow port specification in "bind_to_address" settings
diff --git a/configure.ac b/configure.ac
index 95bc0183..0bd8f9ab 100644
--- a/configure.ac
+++ b/configure.ac
@@ -272,6 +272,11 @@ AC_ARG_ENABLE(despotify,
[enable support for despotify (default: disable)]),,
[enable_despotify=no])
+AC_ARG_ENABLE(soundcloud,
+ AS_HELP_STRING([--enable-soundcloud],
+ [enable support for soundcloud (default: disable)]),,
+ [enable_soundcloud=no])
+
AC_ARG_ENABLE(lame-encoder,
AS_HELP_STRING([--enable-lame-encoder],
[enable the LAME mp3 encoder]),,
@@ -702,6 +707,12 @@ if test x$enable_despotify = xyes; then
fi
AM_CONDITIONAL(ENABLE_DESPOTIFY, test x$enable_despotify = xyes)
+dnl --------------------------------- Soundcloud ------------------------------
+if test x$enable_soundcloud = xyes; then
+ AC_DEFINE(ENABLE_SOUNDCLOUD, 1, [Define when soundcloud is enabled])
+fi
+AM_CONDITIONAL(ENABLE_SOUNDCLOUD, test x$enable_soundcloud = xyes)
+
dnl ---------------------------------- cdio ---------------------------------
MPD_AUTO_PKG(cdio_paranoia, CDIO_PARANOIA, [libcdio_paranoia],
[libcdio_paranoia audio CD library], [libcdio_paranoia not found])
@@ -1604,6 +1615,8 @@ results(cdio_paranoia, [CDIO_PARANOIA])
results(curl,[CURL])
results(despotify,[Despotify])
results(lastfm,[Last.FM])
+results(soundcloud,[Soundcloud])
+printf '\n\t'
results(mms,[MMS])
results(soup, [SOUP])
diff --git a/src/playlist/soundcloud_playlist_plugin.c b/src/playlist/soundcloud_playlist_plugin.c
new file mode 100644
index 00000000..b987de96
--- /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,
+};
+
+
diff --git a/src/playlist/soundcloud_playlist_plugin.h b/src/playlist/soundcloud_playlist_plugin.h
new file mode 100644
index 00000000..e09e2dd4
--- /dev/null
+++ b/src/playlist/soundcloud_playlist_plugin.h
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+#ifndef MPD_PLAYLIST_SOUNDCLOUD_PLAYLIST_PLUGIN_H
+#define MPD_PLAYLIST_SOUNDCLOUD_PLAYLIST_PLUGIN_H
+
+extern const struct playlist_plugin soundcloud_playlist_plugin;
+
+#endif
diff --git a/src/playlist_list.c b/src/playlist_list.c
index defc8524..64f03d46 100644
--- a/src/playlist_list.c
+++ b/src/playlist_list.c
@@ -25,6 +25,7 @@
#include "playlist/xspf_playlist_plugin.h"
#include "playlist/lastfm_playlist_plugin.h"
#include "playlist/despotify_playlist_plugin.h"
+#include "playlist/soundcloud_playlist_plugin.h"
#include "playlist/pls_playlist_plugin.h"
#include "playlist/asx_playlist_plugin.h"
#include "playlist/rss_playlist_plugin.h"
@@ -54,6 +55,9 @@ static const struct playlist_plugin *const playlist_plugins[] = {
#ifdef ENABLE_LASTFM
&lastfm_playlist_plugin,
#endif
+#ifdef ENABLE_SOUNDCLOUD
+ &soundcloud_playlist_plugin,
+#endif
&cue_playlist_plugin,
&embcue_playlist_plugin,
NULL