Skip to content

Commit 287d1df

Browse files
committed
[SoundCloud] Use the HLS delivery method for all streams and extract only a single stream URL from HLS manifest for MP3 streams
SoundCloud broke the workaround used to get a single file from HLS manifests for Opus manifests, but it still works for MP3 ones. The code has been adapted to prevent an unneeded request (the one to the Opus HLS manifest) and the HLS delivery method is now used for SoundCloud MP3 and Opus streams, plus the progressive one (for tracks which have a progressive stream (MP3) and for the ones which doesn't have one, it is still used by trying to get a progressive stream, using the workaround). Streams extraction has been also moved to Java 8 Stream's API and the relevant test has been also updated.
1 parent b3c620f commit 287d1df

File tree

2 files changed

+109
-82
lines changed

2 files changed

+109
-82
lines changed

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

Lines changed: 100 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.schabi.newpipe.extractor.localization.DateWrapper;
2929
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
3030
import org.schabi.newpipe.extractor.stream.AudioStream;
31+
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
3132
import org.schabi.newpipe.extractor.stream.Description;
3233
import org.schabi.newpipe.extractor.stream.Stream;
3334
import org.schabi.newpipe.extractor.stream.StreamExtractor;
@@ -169,7 +170,6 @@ public List<AudioStream> getAudioStreams() throws ExtractionException {
169170
// Streams can be streamable and downloadable - or explicitly not.
170171
// For playing the track, it is only necessary to have a streamable track.
171172
// If this is not the case, this track might not be published yet.
172-
// If audio streams were calculated, return the calculated result
173173
if (!track.getBoolean("streamable") || !isAvailable) {
174174
return audioStreams;
175175
}
@@ -181,53 +181,37 @@ public List<AudioStream> getAudioStreams() throws ExtractionException {
181181
extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings),
182182
audioStreams);
183183
}
184+
184185
extractDownloadableFileIfAvailable(audioStreams);
185186
} catch (final NullPointerException e) {
186-
throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e);
187+
throw new ExtractionException("Could not get audio streams", e);
187188
}
188189

189190
return audioStreams;
190191
}
191192

192193
private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) {
193-
boolean presence = false;
194-
for (final Object transcoding : transcodings) {
195-
final JsonObject transcodingJsonObject = (JsonObject) transcoding;
196-
if (transcodingJsonObject.getString("preset").contains("mp3")
197-
&& transcodingJsonObject.getObject("format").getString("protocol")
198-
.equals("progressive")) {
199-
presence = true;
200-
break;
201-
}
202-
}
203-
return presence;
194+
return transcodings.stream()
195+
.filter(JsonObject.class::isInstance)
196+
.map(JsonObject.class::cast)
197+
.anyMatch(transcodingJsonObject -> transcodingJsonObject.getString("preset")
198+
.contains("mp3") && transcodingJsonObject.getObject("format")
199+
.getString("protocol").equals("progressive"));
204200
}
205201

206202
@Nonnull
207-
private String getTranscodingUrl(final String endpointUrl,
208-
final String protocol)
203+
private String getTranscodingUrl(final String endpointUrl)
209204
throws IOException, ExtractionException {
210-
final Downloader downloader = NewPipe.getDownloader();
211-
final String apiStreamUrl = endpointUrl + "?client_id="
212-
+ clientId();
213-
final String response = downloader.get(apiStreamUrl).responseBody();
205+
final String apiStreamUrl = endpointUrl + "?client_id=" + clientId();
206+
final String response = NewPipe.getDownloader().get(apiStreamUrl).responseBody();
214207
final JsonObject urlObject;
215208
try {
216209
urlObject = JsonParser.object().from(response);
217210
} catch (final JsonParserException e) {
218211
throw new ParsingException("Could not parse streamable URL", e);
219212
}
220213

221-
final String urlString = urlObject.getString("url");
222-
223-
if (protocol.equals("progressive")) {
224-
return urlString;
225-
} else if (protocol.equals("hls")) {
226-
return getSingleUrlFromHlsManifest(urlString);
227-
}
228-
229-
// else, unknown protocol
230-
return EMPTY_STRING;
214+
return urlObject.getString("url");
231215
}
232216

233217
@Nullable
@@ -252,50 +236,87 @@ private String getDownloadUrl(@Nonnull final String trackId)
252236
private void extractAudioStreams(@Nonnull final JsonArray transcodings,
253237
final boolean mp3ProgressiveInStreams,
254238
final List<AudioStream> audioStreams) {
255-
for (final Object transcoding : transcodings) {
256-
final JsonObject transcodingJsonObject = (JsonObject) transcoding;
257-
final String url = transcodingJsonObject.getString("url");
258-
if (isNullOrEmpty(url)) {
259-
continue;
260-
}
261-
262-
final String mediaUrl;
263-
final String preset = transcodingJsonObject.getString("preset", ID_UNKNOWN);
264-
final String protocol = transcodingJsonObject.getObject("format")
265-
.getString("protocol");
266-
MediaFormat mediaFormat = null;
267-
int averageBitrate = UNKNOWN_BITRATE;
268-
if (preset.contains("mp3")) {
269-
// Don't add the MP3 HLS stream if there is a progressive stream present
270-
// because the two have the same bitrate
271-
if (mp3ProgressiveInStreams && protocol.equals("hls")) {
272-
continue;
273-
}
274-
mediaFormat = MediaFormat.MP3;
275-
averageBitrate = 128;
276-
} else if (preset.contains("opus")) {
277-
mediaFormat = MediaFormat.OPUS;
278-
averageBitrate = 64;
279-
}
239+
transcodings.stream()
240+
.filter(JsonObject.class::isInstance)
241+
.map(JsonObject.class::cast)
242+
.forEachOrdered(transcoding -> {
243+
final String url = transcoding.getString("url");
244+
if (isNullOrEmpty(url)) {
245+
return;
246+
}
280247

281-
try {
282-
mediaUrl = getTranscodingUrl(url, protocol);
283-
if (!mediaUrl.isEmpty()) {
284-
final AudioStream audioStream = new AudioStream.Builder()
285-
.setId(preset)
286-
.setContent(mediaUrl, true)
287-
.setMediaFormat(mediaFormat)
288-
.setAverageBitrate(averageBitrate)
289-
.build();
290-
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
291-
audioStreams.add(audioStream);
248+
final String preset = transcoding.getString("preset", ID_UNKNOWN);
249+
final String protocol = transcoding.getObject("format").getString("protocol");
250+
final AudioStream.Builder builder = new AudioStream.Builder()
251+
.setId(preset);
252+
253+
try {
254+
// streamUrl can be either the MP3 progressive stream URL or the
255+
// manifest URL of the HLS MP3 stream (if there is no MP3 progressive
256+
// stream, see above)
257+
final String streamUrl = getTranscodingUrl(url);
258+
259+
if (preset.contains("mp3")) {
260+
// Don't add the MP3 HLS stream if there is a progressive stream
261+
// present because the two have the same bitrate
262+
final boolean isHls = protocol.equals("hls");
263+
if (mp3ProgressiveInStreams && isHls) {
264+
return;
265+
}
266+
267+
builder.setMediaFormat(MediaFormat.MP3);
268+
builder.setAverageBitrate(128);
269+
270+
if (isHls) {
271+
builder.setDeliveryMethod(DeliveryMethod.HLS);
272+
builder.setContent(streamUrl, true);
273+
274+
final AudioStream hlsStream = builder.build();
275+
if (!Stream.containSimilarStream(hlsStream, audioStreams)) {
276+
audioStreams.add(hlsStream);
277+
}
278+
279+
final String progressiveHlsUrl =
280+
getSingleUrlFromHlsManifest(streamUrl);
281+
builder.setDeliveryMethod(DeliveryMethod.PROGRESSIVE_HTTP);
282+
builder.setContent(progressiveHlsUrl, true);
283+
284+
final AudioStream progressiveHlsStream = builder.build();
285+
if (!Stream.containSimilarStream(
286+
progressiveHlsStream, audioStreams)) {
287+
audioStreams.add(progressiveHlsStream);
288+
}
289+
290+
// The MP3 HLS stream has been added in both versions (HLS and
291+
// progressive with the manifest parsing trick), so we need to
292+
// continue (otherwise the code would try to add again the stream,
293+
// which would be not added because the containsSimilarStream
294+
// method would return false and an audio stream object would be
295+
// created for nothing)
296+
return;
297+
} else {
298+
builder.setContent(streamUrl, true);
299+
}
300+
} else if (preset.contains("opus")) {
301+
// The HLS manifest trick doesn't work for opus streams
302+
builder.setContent(streamUrl, true);
303+
builder.setMediaFormat(MediaFormat.OPUS);
304+
builder.setAverageBitrate(64);
305+
builder.setDeliveryMethod(DeliveryMethod.HLS);
306+
} else {
307+
// Unknown format, skip to the next audio stream
308+
return;
309+
}
310+
311+
final AudioStream audioStream = builder.build();
312+
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
313+
audioStreams.add(audioStream);
314+
}
315+
} catch (final ExtractionException | IOException ignored) {
316+
// Something went wrong when trying to get and add this audio stream,
317+
// skip to the next one
292318
}
293-
}
294-
} catch (final Exception ignored) {
295-
// Something went wrong when parsing this transcoding URL, so don't add it to the
296-
// audioStreams
297-
}
298-
}
319+
});
299320
}
300321

301322
/**
@@ -332,25 +353,28 @@ public void extractDownloadableFileIfAvailable(final List<AudioStream> audioStre
332353
}
333354

334355
/**
335-
* Parses a SoundCloud HLS manifest to get a single URL of HLS streams.
356+
* Parses a SoundCloud HLS MP3 manifest to get a single URL of HLS streams.
336357
*
337358
* <p>
338359
* This method downloads the provided manifest URL, finds all web occurrences in the manifest,
339360
* gets the last segment URL, changes its segment range to {@code 0/track-length}, and return
340361
* this as a string.
341362
* </p>
342363
*
364+
* <p>
365+
* This was working before for Opus streams, but has been broken by SoundCloud.
366+
* </p>
367+
*
343368
* @param hlsManifestUrl the URL of the manifest to be parsed
344369
* @return a single URL that contains a range equal to the length of the track
345370
*/
346371
@Nonnull
347372
private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManifestUrl)
348373
throws ParsingException {
349-
final Downloader dl = NewPipe.getDownloader();
350374
final String hlsManifestResponse;
351375

352376
try {
353-
hlsManifestResponse = dl.get(hlsManifestUrl).responseBody();
377+
hlsManifestResponse = NewPipe.getDownloader().get(hlsManifestUrl).responseBody();
354378
} catch (final IOException | ReCaptchaException e) {
355379
throw new ParsingException("Could not get SoundCloud HLS manifest");
356380
}
@@ -359,12 +383,13 @@ private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManif
359383
for (int l = lines.length - 1; l >= 0; l--) {
360384
final String line = lines[l];
361385
// Get the last URL from manifest, because it contains the range of the stream
362-
if (line.trim().length() != 0 && !line.startsWith("#") && line.startsWith("https")) {
386+
if (line.trim().length() != 0 && !line.startsWith("#") && line.startsWith(HTTPS)) {
363387
final String[] hlsLastRangeUrlArray = line.split("/");
364388
return HTTPS + hlsLastRangeUrlArray[2] + "/media/0/" + hlsLastRangeUrlArray[5]
365389
+ "/" + hlsLastRangeUrlArray[6];
366390
}
367391
}
392+
368393
throw new ParsingException("Could not get any URL from HLS manifest");
369394
}
370395

extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,25 +188,27 @@ public void testAudioStreams() throws Exception {
188188
super.testAudioStreams();
189189
final List<AudioStream> audioStreams = extractor.getAudioStreams();
190190
assertEquals(2, audioStreams.size());
191-
for (final AudioStream audioStream : audioStreams) {
191+
audioStreams.forEach(audioStream -> {
192192
final DeliveryMethod deliveryMethod = audioStream.getDeliveryMethod();
193-
assertSame(DeliveryMethod.PROGRESSIVE_HTTP, deliveryMethod,
194-
"Wrong delivery method for stream " + audioStream.getId() + ": "
195-
+ deliveryMethod);
196193
final String mediaUrl = audioStream.getContent();
197194
if (audioStream.getFormat() == MediaFormat.OPUS) {
198195
// Assert that it's an OPUS 64 kbps media URL with a single range which comes
199196
// from an HLS SoundCloud CDN
200197
ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl);
201198
ExtractorAsserts.assertContains(".64.opus", mediaUrl);
202-
}
203-
if (audioStream.getFormat() == MediaFormat.MP3) {
199+
assertSame(DeliveryMethod.HLS, deliveryMethod,
200+
"Wrong delivery method for stream " + audioStream.getId() + ": "
201+
+ deliveryMethod);
202+
} else if (audioStream.getFormat() == MediaFormat.MP3) {
204203
// Assert that it's a MP3 128 kbps media URL which comes from a progressive
205204
// SoundCloud CDN
206205
ExtractorAsserts.assertContains("-media.sndcdn.com/bKOA7Pwbut93.128.mp3",
207206
mediaUrl);
207+
assertSame(DeliveryMethod.PROGRESSIVE_HTTP, deliveryMethod,
208+
"Wrong delivery method for stream " + audioStream.getId() + ": "
209+
+ deliveryMethod);
208210
}
209-
}
211+
});
210212
}
211213
}
212214
}

0 commit comments

Comments
 (0)