SoundCloudPlaylistPlugin.cxx 6.96 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2021 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/AllocatedString.hxx"
30
#include "util/ASCII.hxx"
31
#include "util/StringCompare.hxx"
32 33
#include "util/Domain.hxx"
#include "Log.hxx"
34

35 36
#include <string>

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

40 41
using std::string_view_literals::operator""sv;

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

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

48
static bool
49
soundcloud_init(const ConfigBlock &block)
50
{
51
	// APIKEY for MPD application, registered under DarkFox' account.
52
	soundcloud_config.apikey = block.GetBlockValue("apikey", "a25e51780f7f86af0afa91f241d091f8");
53
	if (soundcloud_config.apikey.empty()) {
54 55 56
		LogDebug(soundcloud_domain,
			 "disabling the soundcloud playlist plugin "
			 "because API key is not set");
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)
66
 * @return Constructed URL. Must be freed with free().
67
 */
68 69
static AllocatedString
soundcloud_resolve(StringView uri) noexcept
70
{
71 72 73 74
	if (uri.StartsWithIgnoreCase("https://")) {
		return AllocatedString{uri};
	} else if (uri.StartsWith("soundcloud.com")) {
		return AllocatedString{"https://"sv, uri};
75 76 77
	}


78 79 80 81 82 83 84 85
	/* assume it's just a path on soundcloud.com */
	AllocatedString u{"https://soundcloud.com/"sv, uri};

	return AllocatedString{
		"https://api.soundcloud.com/resolve.json?url="sv,
		u, "&client_id="sv,
		soundcloud_config.apikey,
	};
86 87
}

88 89
static AllocatedString
TranslateSoundCloudUri(StringView uri) noexcept
90
{
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
	if (uri.SkipPrefix("track/"sv)) {
		return AllocatedString{
			"https://api.soundcloud.com/tracks/"sv,
			uri, ".json?client_id="sv,
			soundcloud_config.apikey,
		};
	} else if (uri.SkipPrefix("playlist/"sv)) {
		return AllocatedString{
			"https://api.soundcloud.com/playlists/"sv,
			uri, ".json?client_id="sv,
			soundcloud_config.apikey,
		};
	} else if (uri.SkipPrefix("user/"sv)) {
		return AllocatedString{
			"https://api.soundcloud.com/users/"sv,
			uri, "/tracks.json?client_id="sv,
			soundcloud_config.apikey,
		};
	} else if (uri.SkipPrefix("search/"sv)) {
		return AllocatedString{
			"https://api.soundcloud.com/tracks.json?q="sv,
			uri, "&client_id="sv,
			soundcloud_config.apikey,
		};
	} else if (uri.SkipPrefix("url/"sv)) {
116 117
		/* Translate to soundcloud resolver call. libcurl will automatically
		   follow the redirect to the right resource. */
118
		return soundcloud_resolve(uri);
119 120 121 122
	} else
		return nullptr;
}

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

125
static const char *const key_str[] = {
126 127 128
	"duration",
	"title",
	"stream_url",
129
	nullptr,
130 131
};

132
struct SoundCloudJsonData {
133 134 135 136 137 138 139 140
	enum class Key {
		DURATION,
		TITLE,
		STREAM_URL,
		OTHER,
	};

	Key key;
141
	std::string stream_url;
142
	long duration;
143
	std::string title;
144
	int got_url = 0; /* nesting level of last stream_url */
145

146
	std::forward_list<DetachedSong> songs;
147 148 149 150 151 152

	bool Integer(long long value) noexcept;
	bool String(StringView value) noexcept;
	bool StartMap() noexcept;
	bool MapKey(StringView value) noexcept;
	bool EndMap() noexcept;
153 154
};

155 156
inline bool
SoundCloudJsonData::Integer(long long intval) noexcept
157
{
158
	switch (key) {
159
	case SoundCloudJsonData::Key::DURATION:
160
		duration = intval;
161 162 163 164 165
		break;
	default:
		break;
	}

166
	return true;
167 168
}

169 170
inline bool
SoundCloudJsonData::String(StringView value) noexcept
171
{
172
	switch (key) {
173
	case SoundCloudJsonData::Key::TITLE:
174
		title.assign(value.data, value.size);
175
		break;
176 177

	case SoundCloudJsonData::Key::STREAM_URL:
178 179
		stream_url.assign(value.data, value.size);
		got_url = 1;
180
		break;
181

182 183 184 185
	default:
		break;
	}

186
	return true;
187 188
}

189 190
inline bool
SoundCloudJsonData::MapKey(StringView value) noexcept
191
{
192
	const auto *i = key_str;
193
	while (*i != nullptr && !StringStartsWith(*i, value))
194
		++i;
195

196 197
	key = SoundCloudJsonData::Key(i - key_str);
	return true;
198 199
}

200 201
inline bool
SoundCloudJsonData::StartMap() noexcept
202
{
203 204
	if (got_url > 0)
		got_url++;
205

206
	return true;
207 208
}

209 210
inline bool
SoundCloudJsonData::EndMap() noexcept
211
{
212 213
	if (got_url > 1) {
		got_url--;
214
		return true;
215 216
	}

217
	if (got_url == 0)
218
		return true;
219 220

	/* got_url == 1, track finished, make it into a song */
221
	got_url = 0;
222

223
	const std::string u = stream_url + "?client_id=" +
224
		soundcloud_config.apikey;
Max Kellermann's avatar
Max Kellermann committed
225

226
	TagBuilder tag;
227 228 229
	tag.SetDuration(SignedSongTime::FromMS(duration));
	if (!title.empty())
		tag.AddItem(TAG_NAME, title.c_str());
230

231
	songs.emplace_front(u.c_str(), tag.Commit());
232

233
	return true;
234 235
}

236
using Wrapper = Yajl::CallbacksWrapper<SoundCloudJsonData>;
237
static constexpr yajl_callbacks parse_callbacks = {
238 239
	nullptr,
	nullptr,
240
	Wrapper::Integer,
241 242
	nullptr,
	nullptr,
243 244 245 246
	Wrapper::String,
	Wrapper::StartMap,
	Wrapper::MapKey,
	Wrapper::EndMap,
247 248
	nullptr,
	nullptr,
249 250 251 252 253
};

/**
 * Read JSON data and parse it using the given YAJL parser.
 * @param url URL of the JSON data.
254
 * @param handle YAJL parser handle.
255
 */
256
static void
257
soundcloud_parse_json(const char *url, Yajl::Handle &handle,
258
		      Mutex &mutex)
259
{
260
	auto input_stream = InputStream::OpenReady(url, mutex);
261
	Yajl::ParseInputStream(handle, *input_stream);
262 263 264 265 266 267 268 269 270
}

/**
 * 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>
 */
271
static std::unique_ptr<SongEnumerator>
272
soundcloud_open_uri(const char *uri, Mutex &mutex)
273
{
274
	assert(StringEqualsCaseASCII(uri, "soundcloud://", 13));
275
	uri += 13;
276

277
	auto u = TranslateSoundCloudUri(uri);
278
	if (u == nullptr) {
279
		LogWarning(soundcloud_domain, "unknown soundcloud URI");
280
		return nullptr;
281 282
	}

283
	SoundCloudJsonData data;
284
	Yajl::Handle handle(&parse_callbacks, nullptr, &data);
285
	soundcloud_parse_json(u.c_str(), handle, mutex);
286

287
	data.songs.reverse();
288
	return std::make_unique<MemorySongEnumerator>(std::move(data.songs));
289 290 291 292
}

static const char *const soundcloud_schemes[] = {
	"soundcloud",
293
	nullptr
294 295
};

296 297 298 299
const PlaylistPlugin soundcloud_playlist_plugin =
	PlaylistPlugin("soundcloud", soundcloud_open_uri)
	.WithInit(soundcloud_init)
	.WithSchemes(soundcloud_schemes);