Skip to content

Commit 26f1b4e

Browse files
committed
Support SoundCloud HLS by using a workaround
This commit tries to support SoundCloud HLS streams by parsing M3U manifests, get the last segment URL (in order to get track length) and request a segment URL equals to track's duration so it's a single URL.
1 parent def745b commit 26f1b4e

1 file changed

Lines changed: 54 additions & 18 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import org.schabi.newpipe.extractor.StreamingService;
1111
import org.schabi.newpipe.extractor.downloader.Downloader;
1212
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
13-
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
1413
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
1514
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
1615
import org.schabi.newpipe.extractor.exceptions.ParsingException;
@@ -29,7 +28,10 @@
2928
import java.util.Collections;
3029
import java.util.List;
3130
import java.util.Locale;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
3233

34+
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
3335
import static org.schabi.newpipe.extractor.utils.Utils.*;
3436

3537
public class SoundcloudStreamExtractor extends StreamExtractor {
@@ -182,7 +184,7 @@ public String getHlsUrl() {
182184

183185
@Override
184186
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
185-
List<AudioStream> audioStreams = new ArrayList<>();
187+
final List<AudioStream> audioStreams = new ArrayList<>();
186188
final Downloader dl = NewPipe.getDownloader();
187189

188190
// Streams can be streamable and downloadable - or explicitly not.
@@ -193,43 +195,77 @@ public List<AudioStream> getAudioStreams() throws IOException, ExtractionExcepti
193195
try {
194196
final JsonArray transcodings = track.getObject("media").getArray("transcodings");
195197

196-
// get information about what stream formats are available
197-
for (Object transcoding : transcodings) {
198-
198+
// Get information about what stream formats are available
199+
for (final Object transcoding : transcodings) {
199200
final JsonObject t = (JsonObject) transcoding;
200201
String url = t.getString("url");
202+
final String mediaUrl;
203+
final MediaFormat mediaFormat;
204+
final int bitrate;
201205

202206
if (!isNullOrEmpty(url)) {
207+
if (t.getString("preset").contains("mp3")) {
208+
mediaFormat = MediaFormat.MP3;
209+
bitrate = 128;
210+
} else if (t.getString("preset").contains("opus")) {
211+
mediaFormat = MediaFormat.OPUS;
212+
bitrate = 64;
213+
} else {
214+
continue;
215+
}
216+
217+
// TODO: move this to a separate method to generate valid urls when needed (e.g. resuming a paused stream)
203218

204-
// We can only play the mp3 format, but not handle m3u playlists / streams.
205-
// what about Opus?
206-
if (t.getString("preset").contains("mp3")
207-
&& t.getObject("format").getString("protocol").equals("progressive")) {
219+
if (t.getObject("format").getString("protocol").equals("progressive")) {
208220
// This url points to the endpoint which generates a unique and short living url to the stream.
209-
// TODO: move this to a separate method to generate valid urls when needed (e.g. resuming a paused stream)
210221
url += "?client_id=" + SoundcloudParsingHelper.clientId();
211222
final String res = dl.get(url).responseBody();
212223

213224
try {
214225
JsonObject mp3UrlObject = JsonParser.object().from(res);
215226
// Links in this file are also only valid for a short period.
216-
audioStreams.add(new AudioStream(mp3UrlObject.getString("url"),
217-
MediaFormat.MP3, 128));
218-
} catch (JsonParserException e) {
227+
mediaUrl = mp3UrlObject.getString("url");
228+
} catch (final JsonParserException e) {
229+
throw new ParsingException("Could not parse streamable url", e);
230+
}
231+
} else if (t.getObject("format").getString("protocol").equals("hls")) {
232+
// This url points to the endpoint which generates a unique and short living url to the stream.
233+
url += "?client_id=" + SoundcloudParsingHelper.clientId();
234+
final String res = dl.get(url).responseBody();
235+
236+
try {
237+
final JsonObject mp3HlsUrlObject = JsonParser.object().from(res);
238+
// Links in this file are also only valid for a short period.
239+
240+
// Parsing the HLS manifest to get a single file by requesting a range equal to 0-track_length
241+
final String hlsManifestResponse = dl.get(mp3HlsUrlObject.getString("url")).responseBody();
242+
final List<String> hlsRangesList = new ArrayList<>();
243+
final Matcher regex = Pattern.compile("((https?):((//)|(\\\\))+[\\w\\d:#@%/;$()~_?+-=\\\\.&]*)")
244+
.matcher(hlsManifestResponse);
245+
246+
while (regex.find()) {
247+
hlsRangesList.add(hlsManifestResponse.substring(regex.start(0), regex.end(0)));
248+
}
249+
250+
final String hlsLastRangeUrl = hlsRangesList.get(hlsRangesList.size() - 1);
251+
final String[] hlsLastRangeUrlArray = hlsLastRangeUrl.split("/");
252+
253+
mediaUrl = HTTPS + hlsLastRangeUrlArray[2] + "/media/0/" + hlsLastRangeUrlArray[5] + "/" + hlsLastRangeUrlArray[6];
254+
} catch (final JsonParserException e) {
219255
throw new ParsingException("Could not parse streamable url", e);
220256
}
257+
} else {
258+
continue;
221259
}
260+
261+
audioStreams.add(new AudioStream(mediaUrl, mediaFormat, bitrate));
222262
}
223263
}
224264

225-
} catch (NullPointerException e) {
265+
} catch (final NullPointerException e) {
226266
throw new ExtractionException("Could not get SoundCloud's track audio url", e);
227267
}
228268

229-
if (audioStreams.isEmpty()) {
230-
throw new ContentNotSupportedException("HLS audio streams are not yet supported");
231-
}
232-
233269
return audioStreams;
234270
}
235271

0 commit comments

Comments
 (0)