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:
parent
e7ce362d22
commit
7cef52478d
|
@ -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
1
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
|
||||
|
|
13
configure.ac
13
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])
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue