1 /*
2 * Copyright 2003-2021 The Music Player Daemon Project
3 * http://www.musicpd.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "SoundCloudPlaylistPlugin.hxx"
21 #include "../PlaylistPlugin.hxx"
22 #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"
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 #include "util/Domain.hxx"
33 #include "Log.hxx"
34
35 #include <string>
36
37 #include <string.h>
38 #include <stdlib.h>
39
40 using std::string_view_literals::operator""sv;
41
42 static struct {
43 std::string apikey;
44 } soundcloud_config;
45
46 static constexpr Domain soundcloud_domain("soundcloud");
47
48 static bool
soundcloud_init(const ConfigBlock & block)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 LogDebug(soundcloud_domain,
55 "disabling the soundcloud playlist plugin "
56 "because API key is not set");
57 return false;
58 }
59
60 return true;
61 }
62
63 /**
64 * Construct a full soundcloud resolver URL from the given fragment.
65 * @param uri uri of a soundcloud page (or just the path)
66 * @return Constructed URL. Must be freed with free().
67 */
68 static AllocatedString
soundcloud_resolve(StringView uri)69 soundcloud_resolve(StringView uri) noexcept
70 {
71 if (uri.StartsWithIgnoreCase("https://")) {
72 return AllocatedString{uri};
73 } else if (uri.StartsWith("soundcloud.com")) {
74 return AllocatedString{"https://"sv, uri};
75 }
76
77
78 /* assume it's just a path on soundcloud.com */
79 AllocatedString u{"https://soundcloud.com/"sv, uri};
80
81 return AllocatedString{
82 "https://api.soundcloud.com/resolve.json?url="sv,
83 u, "&client_id="sv,
84 soundcloud_config.apikey,
85 };
86 }
87
88 static AllocatedString
TranslateSoundCloudUri(StringView uri)89 TranslateSoundCloudUri(StringView uri) noexcept
90 {
91 if (uri.SkipPrefix("track/"sv)) {
92 return AllocatedString{
93 "https://api.soundcloud.com/tracks/"sv,
94 uri, ".json?client_id="sv,
95 soundcloud_config.apikey,
96 };
97 } else if (uri.SkipPrefix("playlist/"sv)) {
98 return AllocatedString{
99 "https://api.soundcloud.com/playlists/"sv,
100 uri, ".json?client_id="sv,
101 soundcloud_config.apikey,
102 };
103 } else if (uri.SkipPrefix("user/"sv)) {
104 return AllocatedString{
105 "https://api.soundcloud.com/users/"sv,
106 uri, "/tracks.json?client_id="sv,
107 soundcloud_config.apikey,
108 };
109 } else if (uri.SkipPrefix("search/"sv)) {
110 return AllocatedString{
111 "https://api.soundcloud.com/tracks.json?q="sv,
112 uri, "&client_id="sv,
113 soundcloud_config.apikey,
114 };
115 } else if (uri.SkipPrefix("url/"sv)) {
116 /* Translate to soundcloud resolver call. libcurl will automatically
117 follow the redirect to the right resource. */
118 return soundcloud_resolve(uri);
119 } else
120 return nullptr;
121 }
122
123 /* YAJL parser for track data from both /tracks/ and /playlists/ JSON */
124
125 static const char *const key_str[] = {
126 "duration",
127 "title",
128 "stream_url",
129 nullptr,
130 };
131
132 struct SoundCloudJsonData {
133 enum class Key {
134 DURATION,
135 TITLE,
136 STREAM_URL,
137 OTHER,
138 };
139
140 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 bool Integer(long long value) noexcept;
149 bool String(StringView value) noexcept;
150 bool StartMap() noexcept;
151 bool MapKey(StringView value) noexcept;
152 bool EndMap() noexcept;
153 };
154
155 inline bool
Integer(long long intval)156 SoundCloudJsonData::Integer(long long intval) noexcept
157 {
158 switch (key) {
159 case SoundCloudJsonData::Key::DURATION:
160 duration = intval;
161 break;
162 default:
163 break;
164 }
165
166 return true;
167 }
168
169 inline bool
String(StringView value)170 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 stream_url.assign(value.data, value.size);
179 got_url = 1;
180 break;
181
182 default:
183 break;
184 }
185
186 return true;
187 }
188
189 inline bool
MapKey(StringView value)190 SoundCloudJsonData::MapKey(StringView value) noexcept
191 {
192 const auto *i = key_str;
193 while (*i != nullptr && !StringStartsWith(*i, value))
194 ++i;
195
196 key = SoundCloudJsonData::Key(i - key_str);
197 return true;
198 }
199
200 inline bool
StartMap()201 SoundCloudJsonData::StartMap() noexcept
202 {
203 if (got_url > 0)
204 got_url++;
205
206 return true;
207 }
208
209 inline bool
EndMap()210 SoundCloudJsonData::EndMap() noexcept
211 {
212 if (got_url > 1) {
213 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;
225
226 TagBuilder tag;
227 tag.SetDuration(SignedSongTime::FromMS(duration));
228 if (!title.empty())
229 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 nullptr,
239 nullptr,
240 Wrapper::Integer,
241 nullptr,
242 nullptr,
243 Wrapper::String,
244 Wrapper::StartMap,
245 Wrapper::MapKey,
246 Wrapper::EndMap,
247 nullptr,
248 nullptr,
249 };
250
251 /**
252 * Read JSON data and parse it using the given YAJL parser.
253 * @param url URL of the JSON data.
254 * @param handle YAJL parser handle.
255 */
256 static void
soundcloud_parse_json(const char * url,Yajl::Handle & handle,Mutex & mutex)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 * Parse a soundcloud:// URL and create a playlist.
266 * @param uri A soundcloud URL. Accepted forms:
267 * soundcloud://track/<track-id>
268 * soundcloud://playlist/<playlist-id>
269 * soundcloud://url/<url or path of soundcloud page>
270 */
271 static std::unique_ptr<SongEnumerator>
soundcloud_open_uri(const char * uri,Mutex & mutex)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 static const char *const soundcloud_schemes[] = {
292 "soundcloud",
293 nullptr
294 };
295
296 const PlaylistPlugin soundcloud_playlist_plugin =
297 PlaylistPlugin("soundcloud", soundcloud_open_uri)
298 .WithInit(soundcloud_init)
299 .WithSchemes(soundcloud_schemes);
300