SoundCloudPlaylistPlugin.cxx 7.7 KB
Newer Older
1
/*
2
 * Copyright 2003-2016 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 * 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"
21
#include "SoundCloudPlaylistPlugin.hxx"
22 23
#include "../PlaylistPlugin.hxx"
#include "../MemorySongEnumerator.hxx"
24
#include "config/Block.hxx"
Max Kellermann's avatar
Max Kellermann committed
25
#include "input/InputStream.hxx"
26
#include "tag/TagBuilder.hxx"
27
#include "util/StringCompare.hxx"
28
#include "util/Alloc.hxx"
29
#include "util/Domain.hxx"
30
#include "util/ScopeExit.hxx"
31
#include "Log.hxx"
32 33 34

#include <yajl/yajl_parse.h>

35 36
#include <string>

37
#include <string.h>
38
#include <stdlib.h>
39 40

static struct {
41
	std::string apikey;
42 43
} soundcloud_config;

44 45
static constexpr Domain soundcloud_domain("soundcloud");

46
static bool
47
soundcloud_init(const ConfigBlock &block)
48
{
49
	// APIKEY for MPD application, registered under DarkFox' account.
50
	soundcloud_config.apikey = block.GetBlockValue("apikey", "a25e51780f7f86af0afa91f241d091f8");
51
	if (soundcloud_config.apikey.empty()) {
52 53 54
		LogDebug(soundcloud_domain,
			 "disabling the soundcloud playlist plugin "
			 "because API key is not set");
55 56 57 58 59 60 61 62 63
		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)
64
 * @return Constructed URL. Must be freed with free().
65 66
 */
static char *
67 68
soundcloud_resolve(const char* uri)
{
69 70
	char *u, *ru;

71
	if (StringStartsWith(uri, "https://")) {
72
		u = xstrdup(uri);
73
	} else if (StringStartsWith(uri, "soundcloud.com")) {
74
		u = xstrcatdup("https://", uri);
75 76
	} else {
		/* assume it's just a path on soundcloud.com */
77
		u = xstrcatdup("https://soundcloud.com/", uri);
78 79
	}

80 81 82 83
	ru = xstrcatdup("https://api.soundcloud.com/resolve.json?url=",
			u, "&client_id=",
			soundcloud_config.apikey.c_str());
	free(u);
84 85 86 87 88 89 90 91 92 93 94 95 96

	return ru;
}

/* YAJL parser for track data from both /tracks/ and /playlists/ JSON */

enum key {
	Duration,
	Title,
	Stream_URL,
	Other,
};

97
static const char *const key_str[] = {
98 99 100
	"duration",
	"title",
	"stream_url",
101
	nullptr,
102 103
};

104
struct SoundCloudJsonData {
105
	int key;
106
	std::string stream_url;
107
	long duration;
108
	std::string title;
109
	int got_url = 0; /* nesting level of last stream_url */
110

111
	std::forward_list<DetachedSong> songs;
112 113
};

114
static int
115
handle_integer(void *ctx, long long intval)
116
{
117
	auto *data = (SoundCloudJsonData *) ctx;
118 119 120 121 122 123 124 125 126 127 128 129

	switch (data->key) {
	case Duration:
		data->duration = intval;
		break;
	default:
		break;
	}

	return 1;
}

130
static int
131
handle_string(void *ctx, const unsigned char *stringval, size_t stringlen)
132
{
133
	auto *data = (SoundCloudJsonData *) ctx;
134 135 136 137
	const char *s = (const char *) stringval;

	switch (data->key) {
	case Title:
138
		data->title.assign(s, stringlen);
139 140
		break;
	case Stream_URL:
141
		data->stream_url.assign(s, stringlen);
142 143 144 145 146 147 148 149 150
		data->got_url = 1;
		break;
	default:
		break;
	}

	return 1;
}

151
static int
152
handle_mapkey(void *ctx, const unsigned char *stringval, size_t stringlen)
153
{
154
	auto *data = (SoundCloudJsonData *) ctx;
155 156 157 158 159

	int i;
	data->key = Other;

	for (i = 0; i < Other; ++i) {
160
		if (memcmp((const char *)stringval, key_str[i], stringlen) == 0) {
161 162 163 164 165 166 167 168
			data->key = i;
			break;
		}
	}

	return 1;
}

169 170
static int
handle_start_map(void *ctx)
171
{
172
	auto *data = (SoundCloudJsonData *) ctx;
173 174 175 176 177 178 179

	if (data->got_url > 0)
		data->got_url++;

	return 1;
}

180 181
static int
handle_end_map(void *ctx)
182
{
183
	auto *data = (SoundCloudJsonData *) ctx;
184 185 186 187 188 189 190 191 192 193 194 195

	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;

196 197
	const std::string u = data->stream_url + "?client_id=" +
		soundcloud_config.apikey;
Max Kellermann's avatar
Max Kellermann committed
198

199
	TagBuilder tag;
200
	tag.SetDuration(SignedSongTime::FromMS(data->duration));
201 202
	if (!data->title.empty())
		tag.AddItem(TAG_NAME, data->title.c_str());
203

204
	data->songs.emplace_front(u.c_str(), tag.Commit());
205 206 207 208

	return 1;
}

209
static constexpr yajl_callbacks parse_callbacks = {
210 211
	nullptr,
	nullptr,
212
	handle_integer,
213 214
	nullptr,
	nullptr,
215 216 217 218
	handle_string,
	handle_start_map,
	handle_mapkey,
	handle_end_map,
219 220
	nullptr,
	nullptr,
221 222 223 224 225 226 227 228 229
};

/**
 * 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
230 231
soundcloud_parse_json(const char *url, yajl_handle hand,
		      Mutex &mutex, Cond &cond)
232 233
try {
	auto input_stream = InputStream::OpenReady(url, mutex, cond);
234

235
	const ScopeLock protect(mutex);
236 237

	yajl_status stat;
238
	bool done = false;
239 240

	while (!done) {
241 242
		char buffer[4096];
		unsigned char *ubuffer = (unsigned char *)buffer;
243
		const size_t nbytes =
244 245 246
			input_stream->Read(buffer, sizeof(buffer));
		if (nbytes == 0)
			done = true;
247

248 249 250
		if (done) {
			stat = yajl_complete_parse(hand);
		} else
251 252
			stat = yajl_parse(hand, ubuffer, nbytes);

253
		if (stat != yajl_status_ok) {
254
			unsigned char *str = yajl_get_error(hand, 1, ubuffer, nbytes);
255
			LogError(soundcloud_domain, (const char *)str);
256 257 258 259 260 261
			yajl_free_error(hand, str);
			break;
		}
	}

	return 0;
262 263 264
} catch (const std::exception &e) {
	LogError(e);
	return -1;
265 266 267 268 269 270 271 272 273
}

/**
 * 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>
 */
274
static SongEnumerator *
275
soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond)
276
{
277
	assert(memcmp(uri, "soundcloud://", 13) == 0);
278
	uri += 13;
279

280
	char *u = nullptr;
281 282
	if (memcmp(uri, "track/", 6) == 0) {
		const char *rest = uri + 6;
283 284 285
		u = xstrcatdup("https://api.soundcloud.com/tracks/",
			       rest, ".json?client_id=",
			       soundcloud_config.apikey.c_str());
286 287
	} else if (memcmp(uri, "playlist/", 9) == 0) {
		const char *rest = uri + 9;
288 289 290
		u = xstrcatdup("https://api.soundcloud.com/playlists/",
			       rest, ".json?client_id=",
			       soundcloud_config.apikey.c_str());
291 292
	} else if (memcmp(uri, "user/", 5) == 0) {
		const char *rest = uri + 5;
293 294 295
		u = xstrcatdup("https://api.soundcloud.com/users/",
			       rest, "/tracks.json?client_id=",
			       soundcloud_config.apikey.c_str());
296 297
	} else if (memcmp(uri, "search/", 7) == 0) {
		const char *rest = uri + 7;
298 299 300
		u = xstrcatdup("https://api.soundcloud.com/tracks.json?q=",
			       rest, "&client_id=",
			       soundcloud_config.apikey.c_str());
301 302
	} else if (memcmp(uri, "url/", 4) == 0) {
		const char *rest = uri + 4;
303 304 305 306 307
		/* Translate to soundcloud resolver call. libcurl will automatically
		   follow the redirect to the right resource. */
		u = soundcloud_resolve(rest);
	}

308 309
	AtScopeExit(u) { free(u); };

310
	if (u == nullptr) {
311
		LogWarning(soundcloud_domain, "unknown soundcloud URI");
312
		return nullptr;
313 314
	}

315
	SoundCloudJsonData data;
316
	yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, &data);
317
	AtScopeExit(hand, &data) { yajl_free(hand); };
318 319 320 321

	int ret = soundcloud_parse_json(u, hand, mutex, cond);

	if (ret == -1)
322
		return nullptr;
323

324
	data.songs.reverse();
325
	return new MemorySongEnumerator(std::move(data.songs));
326 327 328 329
}

static const char *const soundcloud_schemes[] = {
	"soundcloud",
330
	nullptr
331 332 333
};

const struct playlist_plugin soundcloud_playlist_plugin = {
334
	"soundcloud",
335

336
	soundcloud_init,
337
	nullptr,
338 339
	soundcloud_open_uri,
	nullptr,
340

341 342 343
	soundcloud_schemes,
	nullptr,
	nullptr,
344 345 346
};