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/ .
This commit is contained in:
Robert Vollmert 2012-02-27 13:19:45 +01:00 committed by Max Kellermann
parent e7ce362d22
commit 7cef52478d
6 changed files with 471 additions and 0 deletions

View File

@ -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

1
NEWS
View File

@ -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

View File

@ -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])

View File

@ -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,
};

View File

@ -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

View File

@ -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,