diff --git a/Makefile.am b/Makefile.am index 07782fdcb..ec78f6b96 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 2bd1e55f1..9343f3f77 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 95bc0183b..0bd8f9aba 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 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 +#include + +#include + +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/ + * soundcloud://playlist/ + * soundcloud://url/ + */ + +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 000000000..e09e2dd46 --- /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 defc85249..64f03d46d 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" @@ -53,6 +54,9 @@ static const struct playlist_plugin *const playlist_plugins[] = { #endif #ifdef ENABLE_LASTFM &lastfm_playlist_plugin, +#endif +#ifdef ENABLE_SOUNDCLOUD + &soundcloud_playlist_plugin, #endif &cue_playlist_plugin, &embcue_playlist_plugin,