SoundCloudPlaylistPlugin.cxx 8.52 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2014 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/ConfigData.hxx"
Max Kellermann's avatar
Max Kellermann committed
25
#include "input/InputStream.hxx"
26
#include "tag/TagBuilder.hxx"
27
#include "util/StringUtil.hxx"
28
#include "util/Error.hxx"
29 30
#include "util/Domain.hxx"
#include "Log.hxx"
31 32 33 34

#include <glib.h>
#include <yajl/yajl_parse.h>

35 36
#include <string>

37 38 39
#include <string.h>

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

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

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

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

79
	ru = g_strconcat("https://api.soundcloud.com/resolve.json?url=",
80 81
			 u, "&client_id=",
			 soundcloud_config.apikey.c_str(), nullptr);
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
	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",
100
	nullptr,
101 102 103 104 105 106 107 108
};

struct parse_data {
	int key;
	char* stream_url;
	long duration;
	char* title;
	int got_url; /* nesting level of last stream_url */
109

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

113 114 115
static int
handle_integer(void *ctx,
	       long
116
#ifndef HAVE_YAJL1
117
	       long
118
#endif
119
	       intval)
120 121 122 123 124 125 126 127 128 129 130 131 132 133
{
	struct parse_data *data = (struct parse_data *) ctx;

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

	return 1;
}

134 135
static int
handle_string(void *ctx, const unsigned char* stringval,
136
#ifdef HAVE_YAJL1
137
	      unsigned int
138
#else
139
	      size_t
140
#endif
141
	      stringlen)
142 143 144 145 146 147
{
	struct parse_data *data = (struct parse_data *) ctx;
	const char *s = (const char *) stringval;

	switch (data->key) {
	case Title:
148
		g_free(data->title);
149 150 151
		data->title = g_strndup(s, stringlen);
		break;
	case Stream_URL:
152
		g_free(data->stream_url);
153 154 155 156 157 158 159 160 161 162
		data->stream_url = g_strndup(s, stringlen);
		data->got_url = 1;
		break;
	default:
		break;
	}

	return 1;
}

163 164
static int
handle_mapkey(void *ctx, const unsigned char* stringval,
165
#ifdef HAVE_YAJL1
166
	      unsigned int
167
#else
168
	      size_t
169
#endif
170
	      stringlen)
171 172 173 174 175 176 177
{
	struct parse_data *data = (struct parse_data *) ctx;

	int i;
	data->key = Other;

	for (i = 0; i < Other; ++i) {
178
		if (memcmp((const char *)stringval, key_str[i], stringlen) == 0) {
179 180 181 182 183 184 185 186
			data->key = i;
			break;
		}
	}

	return 1;
}

187 188
static int
handle_start_map(void *ctx)
189 190 191 192 193 194 195 196 197
{
	struct parse_data *data = (struct parse_data *) ctx;

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

	return 1;
}

198 199
static int
handle_end_map(void *ctx)
200 201 202 203 204 205 206 207 208 209 210 211 212 213
{
	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;

214 215
	char *u = g_strconcat(data->stream_url, "?client_id=",
			      soundcloud_config.apikey.c_str(), nullptr);
Max Kellermann's avatar
Max Kellermann committed
216

217 218
	TagBuilder tag;
	tag.SetTime(data->duration / 1000);
219
	if (data->title != nullptr)
220
		tag.AddItem(TAG_NAME, data->title);
221

222 223
	data->songs.emplace_front(u, tag.Commit());
	g_free(u);
224 225 226 227 228

	return 1;
}

static yajl_callbacks parse_callbacks = {
229 230
	nullptr,
	nullptr,
231
	handle_integer,
232 233
	nullptr,
	nullptr,
234 235 236 237
	handle_string,
	handle_start_map,
	handle_mapkey,
	handle_end_map,
238 239
	nullptr,
	nullptr,
240 241 242 243 244 245 246 247 248
};

/**
 * 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
249 250
soundcloud_parse_json(const char *url, yajl_handle hand,
		      Mutex &mutex, Cond &cond)
251
{
252
	Error error;
253 254
	InputStream *input_stream = InputStream::OpenReady(url, mutex, cond,
							   error);
255
	if (input_stream == nullptr) {
256
		if (error.IsDefined())
257
			LogError(error);
258 259 260
		return -1;
	}

261
	mutex.lock();
262 263 264 265 266

	yajl_status stat;
	int done = 0;

	while (!done) {
267 268
		char buffer[4096];
		unsigned char *ubuffer = (unsigned char *)buffer;
269
		const size_t nbytes =
270
			input_stream->Read(buffer, sizeof(buffer), error);
271
		if (nbytes == 0) {
272
			if (error.IsDefined())
273
				LogError(error);
274

275
			if (input_stream->IsEOF()) {
276 277
				done = true;
			} else {
278
				mutex.unlock();
279
				delete input_stream;
280 281 282 283
				return -1;
			}
		}

284 285
		if (done) {
#ifdef HAVE_YAJL1
286
			stat = yajl_parse_complete(hand);
287 288 289 290
#else
			stat = yajl_complete_parse(hand);
#endif
		} else
291 292
			stat = yajl_parse(hand, ubuffer, nbytes);

293 294 295 296 297
		if (stat != yajl_status_ok
#ifdef HAVE_YAJL1
		    && stat != yajl_status_insufficient_data
#endif
		    )
298 299
		{
			unsigned char *str = yajl_get_error(hand, 1, ubuffer, nbytes);
300
			LogError(soundcloud_domain, (const char *)str);
301 302 303 304 305
			yajl_free_error(hand, str);
			break;
		}
	}

306
	mutex.unlock();
307
	delete input_stream;
308 309 310 311 312 313 314 315 316 317 318

	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>
 */
319
static SongEnumerator *
320
soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond)
321
{
322
	assert(memcmp(uri, "soundcloud://", 13) == 0);
323
	uri += 13;
324

325
	char *u = nullptr;
326 327
	if (memcmp(uri, "track/", 6) == 0) {
		const char *rest = uri + 6;
328
		u = g_strconcat("https://api.soundcloud.com/tracks/",
329 330
				rest, ".json?client_id=",
				soundcloud_config.apikey.c_str(), nullptr);
331 332
	} else if (memcmp(uri, "playlist/", 9) == 0) {
		const char *rest = uri + 9;
333
		u = g_strconcat("https://api.soundcloud.com/playlists/",
334 335
				rest, ".json?client_id=",
				soundcloud_config.apikey.c_str(), nullptr);
336 337
	} else if (memcmp(uri, "user/", 5) == 0) {
		const char *rest = uri + 5;
338 339 340
		u = g_strconcat("https://api.soundcloud.com/users/",
				rest, "/tracks.json?client_id=",
				soundcloud_config.apikey.c_str(), nullptr);
341 342
	} else if (memcmp(uri, "search/", 7) == 0) {
		const char *rest = uri + 7;
343 344 345
		u = g_strconcat("https://api.soundcloud.com/tracks.json?q=",
				rest, "&client_id=",
				soundcloud_config.apikey.c_str(), nullptr);
346 347
	} else if (memcmp(uri, "url/", 4) == 0) {
		const char *rest = uri + 4;
348 349 350 351 352
		/* Translate to soundcloud resolver call. libcurl will automatically
		   follow the redirect to the right resource. */
		u = soundcloud_resolve(rest);
	}

353
	if (u == nullptr) {
354
		LogWarning(soundcloud_domain, "unknown soundcloud URI");
355
		return nullptr;
356 357 358 359
	}

	struct parse_data data;
	data.got_url = 0;
360 361
	data.title = nullptr;
	data.stream_url = nullptr;
362
#ifdef HAVE_YAJL1
363 364
	yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, nullptr,
				      &data);
365
#else
366
	yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, &data);
367
#endif
368 369 370 371 372

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

	g_free(u);
	yajl_free(hand);
373 374
	g_free(data.title);
	g_free(data.stream_url);
375 376

	if (ret == -1)
377
		return nullptr;
378

379
	data.songs.reverse();
380
	return new MemorySongEnumerator(std::move(data.songs));
381 382 383 384
}

static const char *const soundcloud_schemes[] = {
	"soundcloud",
385
	nullptr
386 387 388
};

const struct playlist_plugin soundcloud_playlist_plugin = {
389
	"soundcloud",
390

391
	soundcloud_init,
392
	nullptr,
393 394
	soundcloud_open_uri,
	nullptr,
395

396 397 398
	soundcloud_schemes,
	nullptr,
	nullptr,
399 400 401
};