SoundCloudPlaylistPlugin.cxx 7.03 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2020 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 * 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.
 */

20
#include "SoundCloudPlaylistPlugin.hxx"
21 22
#include "../PlaylistPlugin.hxx"
#include "../MemorySongEnumerator.hxx"
23
#include "lib/yajl/Handle.hxx"
24
#include "lib/yajl/Callbacks.hxx"
25
#include "lib/yajl/ParseInputStream.hxx"
26
#include "config/Block.hxx"
Max Kellermann's avatar
Max Kellermann committed
27
#include "input/InputStream.hxx"
28
#include "tag/Builder.hxx"
29
#include "util/ASCII.hxx"
30
#include "util/StringCompare.hxx"
31
#include "util/Alloc.hxx"
32
#include "util/Domain.hxx"
33
#include "util/ScopeExit.hxx"
34
#include "Log.hxx"
35

36 37
#include <string>

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

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

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

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

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

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

	return ru;
}

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

91
static const char *const key_str[] = {
92 93 94
	"duration",
	"title",
	"stream_url",
95
	nullptr,
96 97
};

98
struct SoundCloudJsonData {
99 100 101 102 103 104 105 106
	enum class Key {
		DURATION,
		TITLE,
		STREAM_URL,
		OTHER,
	};

	Key key;
107
	std::string stream_url;
108
	long duration;
109
	std::string title;
110
	int got_url = 0; /* nesting level of last stream_url */
111

112
	std::forward_list<DetachedSong> songs;
113 114 115 116 117 118

	bool Integer(long long value) noexcept;
	bool String(StringView value) noexcept;
	bool StartMap() noexcept;
	bool MapKey(StringView value) noexcept;
	bool EndMap() noexcept;
119 120
};

121 122
inline bool
SoundCloudJsonData::Integer(long long intval) noexcept
123
{
124
	switch (key) {
125
	case SoundCloudJsonData::Key::DURATION:
126
		duration = intval;
127 128 129 130 131
		break;
	default:
		break;
	}

132
	return true;
133 134
}

135 136
inline bool
SoundCloudJsonData::String(StringView value) noexcept
137
{
138
	switch (key) {
139
	case SoundCloudJsonData::Key::TITLE:
140
		title.assign(value.data, value.size);
141
		break;
142 143

	case SoundCloudJsonData::Key::STREAM_URL:
144 145
		stream_url.assign(value.data, value.size);
		got_url = 1;
146
		break;
147

148 149 150 151
	default:
		break;
	}

152
	return true;
153 154
}

155 156
inline bool
SoundCloudJsonData::MapKey(StringView value) noexcept
157
{
158
	const auto *i = key_str;
159
	while (*i != nullptr && !StringStartsWith(*i, value))
160
		++i;
161

162 163
	key = SoundCloudJsonData::Key(i - key_str);
	return true;
164 165
}

166 167
inline bool
SoundCloudJsonData::StartMap() noexcept
168
{
169 170
	if (got_url > 0)
		got_url++;
171

172
	return true;
173 174
}

175 176
inline bool
SoundCloudJsonData::EndMap() noexcept
177
{
178 179
	if (got_url > 1) {
		got_url--;
180
		return true;
181 182
	}

183
	if (got_url == 0)
184
		return true;
185 186

	/* got_url == 1, track finished, make it into a song */
187
	got_url = 0;
188

189
	const std::string u = stream_url + "?client_id=" +
190
		soundcloud_config.apikey;
Max Kellermann's avatar
Max Kellermann committed
191

192
	TagBuilder tag;
193 194 195
	tag.SetDuration(SignedSongTime::FromMS(duration));
	if (!title.empty())
		tag.AddItem(TAG_NAME, title.c_str());
196

197
	songs.emplace_front(u.c_str(), tag.Commit());
198

199
	return true;
200 201
}

202
using Wrapper = Yajl::CallbacksWrapper<SoundCloudJsonData>;
203
static constexpr yajl_callbacks parse_callbacks = {
204 205
	nullptr,
	nullptr,
206
	Wrapper::Integer,
207 208
	nullptr,
	nullptr,
209 210 211 212
	Wrapper::String,
	Wrapper::StartMap,
	Wrapper::MapKey,
	Wrapper::EndMap,
213 214
	nullptr,
	nullptr,
215 216 217 218 219
};

/**
 * Read JSON data and parse it using the given YAJL parser.
 * @param url URL of the JSON data.
220
 * @param handle YAJL parser handle.
221
 */
222
static void
223
soundcloud_parse_json(const char *url, Yajl::Handle &handle,
224
		      Mutex &mutex)
225
{
226
	auto input_stream = InputStream::OpenReady(url, mutex);
227
	Yajl::ParseInputStream(handle, *input_stream);
228 229 230 231 232 233 234 235 236
}

/**
 * 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>
 */
237
static std::unique_ptr<SongEnumerator>
238
soundcloud_open_uri(const char *uri, Mutex &mutex)
239
{
240
	assert(StringEqualsCaseASCII(uri, "soundcloud://", 13));
241
	uri += 13;
242

243
	char *u = nullptr;
244
	if (strncmp(uri, "track/", 6) == 0) {
245
		const char *rest = uri + 6;
246 247 248
		u = xstrcatdup("https://api.soundcloud.com/tracks/",
			       rest, ".json?client_id=",
			       soundcloud_config.apikey.c_str());
249
	} else if (strncmp(uri, "playlist/", 9) == 0) {
250
		const char *rest = uri + 9;
251 252 253
		u = xstrcatdup("https://api.soundcloud.com/playlists/",
			       rest, ".json?client_id=",
			       soundcloud_config.apikey.c_str());
254
	} else if (strncmp(uri, "user/", 5) == 0) {
255
		const char *rest = uri + 5;
256 257 258
		u = xstrcatdup("https://api.soundcloud.com/users/",
			       rest, "/tracks.json?client_id=",
			       soundcloud_config.apikey.c_str());
259
	} else if (strncmp(uri, "search/", 7) == 0) {
260
		const char *rest = uri + 7;
261 262 263
		u = xstrcatdup("https://api.soundcloud.com/tracks.json?q=",
			       rest, "&client_id=",
			       soundcloud_config.apikey.c_str());
264
	} else if (strncmp(uri, "url/", 4) == 0) {
265
		const char *rest = uri + 4;
266 267 268 269 270
		/* Translate to soundcloud resolver call. libcurl will automatically
		   follow the redirect to the right resource. */
		u = soundcloud_resolve(rest);
	}

271 272
	AtScopeExit(u) { free(u); };

273
	if (u == nullptr) {
274
		LogWarning(soundcloud_domain, "unknown soundcloud URI");
275
		return nullptr;
276 277
	}

278
	SoundCloudJsonData data;
279
	Yajl::Handle handle(&parse_callbacks, nullptr, &data);
280
	soundcloud_parse_json(u, handle, mutex);
281

282
	data.songs.reverse();
283
	return std::make_unique<MemorySongEnumerator>(std::move(data.songs));
284 285 286 287
}

static const char *const soundcloud_schemes[] = {
	"soundcloud",
288
	nullptr
289 290
};

291 292 293 294
const PlaylistPlugin soundcloud_playlist_plugin =
	PlaylistPlugin("soundcloud", soundcloud_open_uri)
	.WithInit(soundcloud_init)
	.WithSchemes(soundcloud_schemes);