2012-02-27 13:19:45 +01:00
|
|
|
/*
|
2016-02-26 17:54:05 +01:00
|
|
|
* Copyright 2003-2016 The Music Player Daemon Project
|
2012-02-27 13:19:45 +01:00
|
|
|
* 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"
|
2013-01-26 01:04:02 +01:00
|
|
|
#include "SoundCloudPlaylistPlugin.hxx"
|
2014-01-23 23:30:12 +01:00
|
|
|
#include "../PlaylistPlugin.hxx"
|
|
|
|
#include "../MemorySongEnumerator.hxx"
|
2015-01-21 22:13:44 +01:00
|
|
|
#include "config/Block.hxx"
|
2014-01-24 16:18:21 +01:00
|
|
|
#include "input/InputStream.hxx"
|
2013-12-03 12:30:00 +01:00
|
|
|
#include "tag/TagBuilder.hxx"
|
2015-11-06 09:09:02 +01:00
|
|
|
#include "util/StringCompare.hxx"
|
2014-10-24 20:21:55 +02:00
|
|
|
#include "util/Alloc.hxx"
|
2013-08-10 18:02:44 +02:00
|
|
|
#include "util/Error.hxx"
|
2013-09-27 22:31:24 +02:00
|
|
|
#include "util/Domain.hxx"
|
|
|
|
#include "Log.hxx"
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
#include <yajl/yajl_parse.h>
|
|
|
|
|
2013-10-15 23:17:53 +02:00
|
|
|
#include <string>
|
|
|
|
|
2012-02-27 13:19:45 +01:00
|
|
|
#include <string.h>
|
2014-10-25 23:49:29 +02:00
|
|
|
#include <stdlib.h>
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
static struct {
|
2013-10-15 23:17:53 +02:00
|
|
|
std::string apikey;
|
2012-02-27 13:19:45 +01:00
|
|
|
} soundcloud_config;
|
|
|
|
|
2013-09-27 22:31:24 +02:00
|
|
|
static constexpr Domain soundcloud_domain("soundcloud");
|
|
|
|
|
2012-02-27 13:19:45 +01:00
|
|
|
static bool
|
2015-01-21 22:13:44 +01:00
|
|
|
soundcloud_init(const ConfigBlock &block)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2014-01-04 10:13:17 +01:00
|
|
|
// APIKEY for MPD application, registered under DarkFox' account.
|
2015-01-21 22:13:44 +01:00
|
|
|
soundcloud_config.apikey = block.GetBlockValue("apikey", "a25e51780f7f86af0afa91f241d091f8");
|
2013-10-15 23:17:53 +02:00
|
|
|
if (soundcloud_config.apikey.empty()) {
|
2013-09-27 22:31:24 +02:00
|
|
|
LogDebug(soundcloud_domain,
|
|
|
|
"disabling the soundcloud playlist plugin "
|
|
|
|
"because API key is not set");
|
2012-02-27 13:19:45 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct a full soundcloud resolver URL from the given fragment.
|
|
|
|
* @param uri uri of a soundcloud page (or just the path)
|
2014-10-24 20:21:55 +02:00
|
|
|
* @return Constructed URL. Must be freed with free().
|
2012-02-27 13:19:45 +01:00
|
|
|
*/
|
|
|
|
static char *
|
2013-12-14 22:09:27 +01:00
|
|
|
soundcloud_resolve(const char* uri)
|
|
|
|
{
|
2012-02-27 13:19:45 +01:00
|
|
|
char *u, *ru;
|
|
|
|
|
2014-01-02 12:09:40 +01:00
|
|
|
if (StringStartsWith(uri, "https://")) {
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrdup(uri);
|
2013-11-28 18:48:35 +01:00
|
|
|
} else if (StringStartsWith(uri, "soundcloud.com")) {
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrcatdup("https://", uri);
|
2012-02-27 13:19:45 +01:00
|
|
|
} else {
|
|
|
|
/* assume it's just a path on soundcloud.com */
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrcatdup("https://soundcloud.com/", uri);
|
2012-02-27 13:19:45 +01:00
|
|
|
}
|
|
|
|
|
2014-10-24 20:21:55 +02:00
|
|
|
ru = xstrcatdup("https://api.soundcloud.com/resolve.json?url=",
|
|
|
|
u, "&client_id=",
|
|
|
|
soundcloud_config.apikey.c_str());
|
|
|
|
free(u);
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
return ru;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* YAJL parser for track data from both /tracks/ and /playlists/ JSON */
|
|
|
|
|
|
|
|
enum key {
|
|
|
|
Duration,
|
|
|
|
Title,
|
|
|
|
Stream_URL,
|
|
|
|
Other,
|
|
|
|
};
|
|
|
|
|
2015-11-06 09:52:35 +01:00
|
|
|
static const char *const key_str[] = {
|
2012-02-27 13:19:45 +01:00
|
|
|
"duration",
|
|
|
|
"title",
|
|
|
|
"stream_url",
|
2013-10-28 23:58:17 +01:00
|
|
|
nullptr,
|
2012-02-27 13:19:45 +01:00
|
|
|
};
|
|
|
|
|
2016-09-09 18:00:26 +02:00
|
|
|
struct SoundCloudJsonData {
|
2012-02-27 13:19:45 +01:00
|
|
|
int key;
|
|
|
|
char* stream_url;
|
|
|
|
long duration;
|
|
|
|
char* title;
|
|
|
|
int got_url; /* nesting level of last stream_url */
|
2013-01-29 18:51:40 +01:00
|
|
|
|
2014-01-07 21:39:47 +01:00
|
|
|
std::forward_list<DetachedSong> songs;
|
2012-02-27 13:19:45 +01:00
|
|
|
};
|
|
|
|
|
2013-12-14 22:09:27 +01:00
|
|
|
static int
|
2013-12-14 21:42:51 +01:00
|
|
|
handle_integer(void *ctx, long long intval)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2016-09-09 18:00:26 +02:00
|
|
|
auto *data = (SoundCloudJsonData *) ctx;
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
switch (data->key) {
|
|
|
|
case Duration:
|
|
|
|
data->duration = intval;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2013-12-14 22:09:27 +01:00
|
|
|
static int
|
2013-12-14 21:42:51 +01:00
|
|
|
handle_string(void *ctx, const unsigned char *stringval, size_t stringlen)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2016-09-09 18:00:26 +02:00
|
|
|
auto *data = (SoundCloudJsonData *) ctx;
|
2012-02-27 13:19:45 +01:00
|
|
|
const char *s = (const char *) stringval;
|
|
|
|
|
|
|
|
switch (data->key) {
|
|
|
|
case Title:
|
2014-10-24 20:21:55 +02:00
|
|
|
free(data->title);
|
|
|
|
data->title = xstrndup(s, stringlen);
|
2012-02-27 13:19:45 +01:00
|
|
|
break;
|
|
|
|
case Stream_URL:
|
2014-10-24 20:21:55 +02:00
|
|
|
free(data->stream_url);
|
|
|
|
data->stream_url = xstrndup(s, stringlen);
|
2012-02-27 13:19:45 +01:00
|
|
|
data->got_url = 1;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2013-12-14 22:09:27 +01:00
|
|
|
static int
|
2013-12-14 21:42:51 +01:00
|
|
|
handle_mapkey(void *ctx, const unsigned char *stringval, size_t stringlen)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2016-09-09 18:00:26 +02:00
|
|
|
auto *data = (SoundCloudJsonData *) ctx;
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
int i;
|
|
|
|
data->key = Other;
|
|
|
|
|
|
|
|
for (i = 0; i < Other; ++i) {
|
2013-10-14 22:00:21 +02:00
|
|
|
if (memcmp((const char *)stringval, key_str[i], stringlen) == 0) {
|
2012-02-27 13:19:45 +01:00
|
|
|
data->key = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2013-12-14 22:09:27 +01:00
|
|
|
static int
|
|
|
|
handle_start_map(void *ctx)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2016-09-09 18:00:26 +02:00
|
|
|
auto *data = (SoundCloudJsonData *) ctx;
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
if (data->got_url > 0)
|
|
|
|
data->got_url++;
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2013-12-14 22:09:27 +01:00
|
|
|
static int
|
|
|
|
handle_end_map(void *ctx)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2016-09-09 18:00:26 +02:00
|
|
|
auto *data = (SoundCloudJsonData *) ctx;
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2014-10-24 20:21:55 +02:00
|
|
|
char *u = xstrcatdup(data->stream_url, "?client_id=",
|
|
|
|
soundcloud_config.apikey.c_str());
|
2013-07-30 20:11:57 +02:00
|
|
|
|
2013-12-03 12:30:00 +01:00
|
|
|
TagBuilder tag;
|
2014-08-29 12:14:27 +02:00
|
|
|
tag.SetDuration(SignedSongTime::FromMS(data->duration));
|
2013-10-28 23:58:17 +01:00
|
|
|
if (data->title != nullptr)
|
2013-12-03 12:30:00 +01:00
|
|
|
tag.AddItem(TAG_NAME, data->title);
|
2012-02-27 13:19:45 +01:00
|
|
|
|
2014-01-07 21:39:47 +01:00
|
|
|
data->songs.emplace_front(u, tag.Commit());
|
2014-10-24 20:21:55 +02:00
|
|
|
free(u);
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-09-09 17:59:42 +02:00
|
|
|
static constexpr yajl_callbacks parse_callbacks = {
|
2013-10-28 23:58:17 +01:00
|
|
|
nullptr,
|
|
|
|
nullptr,
|
2012-02-27 13:19:45 +01:00
|
|
|
handle_integer,
|
2013-10-28 23:58:17 +01:00
|
|
|
nullptr,
|
|
|
|
nullptr,
|
2012-02-27 13:19:45 +01:00
|
|
|
handle_string,
|
|
|
|
handle_start_map,
|
|
|
|
handle_mapkey,
|
|
|
|
handle_end_map,
|
2013-10-28 23:58:17 +01:00
|
|
|
nullptr,
|
|
|
|
nullptr,
|
2012-02-27 13:19:45 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2013-01-27 17:20:50 +01:00
|
|
|
soundcloud_parse_json(const char *url, yajl_handle hand,
|
|
|
|
Mutex &mutex, Cond &cond)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2013-08-10 18:02:44 +02:00
|
|
|
Error error;
|
2016-02-21 08:03:32 +01:00
|
|
|
auto input_stream = InputStream::OpenReady(url, mutex, cond,
|
|
|
|
error);
|
2013-10-28 23:58:17 +01:00
|
|
|
if (input_stream == nullptr) {
|
2013-08-10 18:02:44 +02:00
|
|
|
if (error.IsDefined())
|
2013-09-27 22:31:24 +02:00
|
|
|
LogError(error);
|
2012-02-27 13:19:45 +01:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2016-09-09 17:57:49 +02:00
|
|
|
const ScopeLock protect(mutex);
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
yajl_status stat;
|
|
|
|
int done = 0;
|
|
|
|
|
|
|
|
while (!done) {
|
2013-12-14 21:25:07 +01:00
|
|
|
char buffer[4096];
|
|
|
|
unsigned char *ubuffer = (unsigned char *)buffer;
|
2013-08-10 18:02:44 +02:00
|
|
|
const size_t nbytes =
|
2013-09-05 00:06:31 +02:00
|
|
|
input_stream->Read(buffer, sizeof(buffer), error);
|
2012-02-27 13:19:45 +01:00
|
|
|
if (nbytes == 0) {
|
2013-08-10 18:02:44 +02:00
|
|
|
if (error.IsDefined())
|
2013-09-27 22:31:24 +02:00
|
|
|
LogError(error);
|
2013-08-10 18:02:44 +02:00
|
|
|
|
2013-09-05 00:06:31 +02:00
|
|
|
if (input_stream->IsEOF()) {
|
2012-02-27 13:19:45 +01:00
|
|
|
done = true;
|
|
|
|
} else {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-19 21:16:48 +01:00
|
|
|
if (done) {
|
|
|
|
stat = yajl_complete_parse(hand);
|
|
|
|
} else
|
2012-02-27 13:19:45 +01:00
|
|
|
stat = yajl_parse(hand, ubuffer, nbytes);
|
|
|
|
|
2013-12-14 21:42:51 +01:00
|
|
|
if (stat != yajl_status_ok) {
|
2012-02-27 13:19:45 +01:00
|
|
|
unsigned char *str = yajl_get_error(hand, 1, ubuffer, nbytes);
|
2013-09-27 22:31:24 +02:00
|
|
|
LogError(soundcloud_domain, (const char *)str);
|
2012-02-27 13:19:45 +01:00
|
|
|
yajl_free_error(hand, str);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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>
|
|
|
|
*/
|
2013-09-05 09:37:54 +02:00
|
|
|
static SongEnumerator *
|
2013-01-27 17:20:50 +01:00
|
|
|
soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond)
|
2012-02-27 13:19:45 +01:00
|
|
|
{
|
2014-01-07 09:40:31 +01:00
|
|
|
assert(memcmp(uri, "soundcloud://", 13) == 0);
|
2014-01-07 10:21:42 +01:00
|
|
|
uri += 13;
|
2012-02-27 13:19:45 +01:00
|
|
|
|
2013-10-28 23:58:17 +01:00
|
|
|
char *u = nullptr;
|
2014-01-07 10:21:42 +01:00
|
|
|
if (memcmp(uri, "track/", 6) == 0) {
|
|
|
|
const char *rest = uri + 6;
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrcatdup("https://api.soundcloud.com/tracks/",
|
|
|
|
rest, ".json?client_id=",
|
|
|
|
soundcloud_config.apikey.c_str());
|
2014-01-07 10:21:42 +01:00
|
|
|
} else if (memcmp(uri, "playlist/", 9) == 0) {
|
|
|
|
const char *rest = uri + 9;
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrcatdup("https://api.soundcloud.com/playlists/",
|
|
|
|
rest, ".json?client_id=",
|
|
|
|
soundcloud_config.apikey.c_str());
|
2014-01-07 10:21:42 +01:00
|
|
|
} else if (memcmp(uri, "user/", 5) == 0) {
|
|
|
|
const char *rest = uri + 5;
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrcatdup("https://api.soundcloud.com/users/",
|
|
|
|
rest, "/tracks.json?client_id=",
|
|
|
|
soundcloud_config.apikey.c_str());
|
2014-01-07 10:21:42 +01:00
|
|
|
} else if (memcmp(uri, "search/", 7) == 0) {
|
|
|
|
const char *rest = uri + 7;
|
2014-10-24 20:21:55 +02:00
|
|
|
u = xstrcatdup("https://api.soundcloud.com/tracks.json?q=",
|
|
|
|
rest, "&client_id=",
|
|
|
|
soundcloud_config.apikey.c_str());
|
2014-01-07 10:21:42 +01:00
|
|
|
} else if (memcmp(uri, "url/", 4) == 0) {
|
|
|
|
const char *rest = uri + 4;
|
2012-02-27 13:19:45 +01:00
|
|
|
/* Translate to soundcloud resolver call. libcurl will automatically
|
|
|
|
follow the redirect to the right resource. */
|
|
|
|
u = soundcloud_resolve(rest);
|
|
|
|
}
|
|
|
|
|
2013-10-28 23:58:17 +01:00
|
|
|
if (u == nullptr) {
|
2013-09-27 22:31:24 +02:00
|
|
|
LogWarning(soundcloud_domain, "unknown soundcloud URI");
|
2013-10-28 23:58:17 +01:00
|
|
|
return nullptr;
|
2012-02-27 13:19:45 +01:00
|
|
|
}
|
|
|
|
|
2016-09-09 18:00:26 +02:00
|
|
|
SoundCloudJsonData data;
|
2012-02-27 13:19:45 +01:00
|
|
|
data.got_url = 0;
|
2013-10-28 23:58:17 +01:00
|
|
|
data.title = nullptr;
|
|
|
|
data.stream_url = nullptr;
|
2013-12-14 21:25:07 +01:00
|
|
|
yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, &data);
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
int ret = soundcloud_parse_json(u, hand, mutex, cond);
|
|
|
|
|
2014-10-24 20:21:55 +02:00
|
|
|
free(u);
|
2012-02-27 13:19:45 +01:00
|
|
|
yajl_free(hand);
|
2014-10-24 20:21:55 +02:00
|
|
|
free(data.title);
|
|
|
|
free(data.stream_url);
|
2012-02-27 13:19:45 +01:00
|
|
|
|
|
|
|
if (ret == -1)
|
2013-10-28 23:58:17 +01:00
|
|
|
return nullptr;
|
2012-02-27 13:19:45 +01:00
|
|
|
|
2013-01-29 18:51:40 +01:00
|
|
|
data.songs.reverse();
|
2013-09-05 09:37:54 +02:00
|
|
|
return new MemorySongEnumerator(std::move(data.songs));
|
2012-02-27 13:19:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static const char *const soundcloud_schemes[] = {
|
|
|
|
"soundcloud",
|
2013-10-28 23:58:17 +01:00
|
|
|
nullptr
|
2012-02-27 13:19:45 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const struct playlist_plugin soundcloud_playlist_plugin = {
|
2013-01-26 01:04:02 +01:00
|
|
|
"soundcloud",
|
2012-02-27 13:19:45 +01:00
|
|
|
|
2013-01-26 01:04:02 +01:00
|
|
|
soundcloud_init,
|
2013-10-15 23:17:53 +02:00
|
|
|
nullptr,
|
2013-01-26 01:04:02 +01:00
|
|
|
soundcloud_open_uri,
|
|
|
|
nullptr,
|
2012-02-27 13:19:45 +01:00
|
|
|
|
2013-01-26 01:04:02 +01:00
|
|
|
soundcloud_schemes,
|
|
|
|
nullptr,
|
|
|
|
nullptr,
|
2012-02-27 13:19:45 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|