Skip to content

Commit

Permalink
Add GeographicRestrictionException and SoundCloudGoPlusException to b…
Browse files Browse the repository at this point in the history
…e able to display a different error message

This commit tries also to support HLS only tracks by requesting a segment request equal as the duration of the track, by parsing the HLS manifest (/0/track-length/)
  • Loading branch information
AudricV committed Jan 17, 2021
1 parent beb7050 commit ea780f2
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;

public class GeographicRestrictionException extends ParsingException {
public GeographicRestrictionException(String message) {
super(message);
}

public GeographicRestrictionException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;

public class SoundCloudGoPlusException extends ParsingException {
public SoundCloudGoPlusException() {
super("This track is a SoundCloud Go+ track");
}

public SoundCloudGoPlusException(Throwable cause) {
super("This track is a SoundCloud Go+ track", cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
Expand All @@ -33,11 +35,14 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;

public class SoundcloudStreamExtractor extends StreamExtractor {
Expand All @@ -53,6 +58,12 @@ public void onFetchPage(@Nonnull Downloader downloader) throws IOException, Extr

String policy = track.getString("policy", EMPTY_STRING);
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
if (policy.equals("SNIP")) {
throw new SoundCloudGoPlusException();
}
if (policy.equals("BLOCK")) {
throw new GeographicRestrictionException("This track is not available in user's country");
}
throw new ContentNotAvailableException("Content not available: policy " + policy);
}
}
Expand Down Expand Up @@ -179,7 +190,7 @@ public String getHlsUrl() {

@Override
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
List<AudioStream> audioStreams = new ArrayList<>();
final List<AudioStream> audioStreams = new ArrayList<>();
final Downloader dl = NewPipe.getDownloader();

// Streams can be streamable and downloadable - or explicitly not.
Expand All @@ -190,54 +201,102 @@ public List<AudioStream> getAudioStreams() throws IOException, ExtractionExcepti
try {
final JsonArray transcodings = track.getObject("media").getArray("transcodings");

// get information about what stream formats are available
for (Object transcoding : transcodings) {

// Get information about what stream formats are available
for (final Object transcoding : transcodings) {
final JsonObject t = (JsonObject) transcoding;
String url = t.getString("url");
String mediaUrl = null;
final MediaFormat mediaFormat;
final int bitrate;

if (!isNullOrEmpty(url)) {
if (t.getString("preset").contains("mp3")) {
mediaFormat = MediaFormat.MP3;
bitrate = 128;
} else if (t.getString("preset").contains("opus")) {
mediaFormat = MediaFormat.OPUS;
bitrate = 64;
} else {
continue;
}

// We can only play the mp3 format, but not handle m3u playlists / streams.
// what about Opus?
if (t.getString("preset").contains("mp3")
&& t.getObject("format").getString("protocol").equals("progressive")) {
// TODO: move this to a separate method to generate valid urls when needed (e.g. resuming a paused stream)

if (t.getObject("format").getString("protocol").equals("progressive")) {
// This url points to the endpoint which generates a unique and short living url to the stream.
// TODO: move this to a separate method to generate valid urls when needed (e.g. resuming a paused stream)
url += "?client_id=" + SoundcloudParsingHelper.clientId();
final String res = dl.get(url).responseBody();

try {
JsonObject mp3UrlObject = JsonParser.object().from(res);
// Links in this file are also only valid for a short period.
audioStreams.add(new AudioStream(mp3UrlObject.getString("url"),
MediaFormat.MP3, 128));
} catch (JsonParserException e) {
mediaUrl = mp3UrlObject.getString("url");
} catch (final JsonParserException e) {
throw new ParsingException("Could not parse streamable url", e);
}
} else if (t.getObject("format").getString("protocol").equals("hls")) {
// This url points to the endpoint which generates a unique and short living url to the stream.
url += "?client_id=" + SoundcloudParsingHelper.clientId();
final String res = dl.get(url).responseBody();

try {
final JsonObject mp3HlsUrlObject = JsonParser.object().from(res);
// Links in this file are also only valid for a short period.
try {
mediaUrl = getUrlFromHlsManifest(mp3HlsUrlObject.getString("url"));
} catch (final ParsingException e) {
continue;
}
} catch (final JsonParserException e) {
throw new ParsingException("Could not parse streamable url", e);
}
} else {
continue;
}

audioStreams.add(new AudioStream(mediaUrl, mediaFormat, bitrate));
}
}

} catch (NullPointerException e) {
} catch (final NullPointerException e) {
throw new ExtractionException("Could not get SoundCloud's track audio url", e);
}

if (audioStreams.isEmpty()) {
throw new ContentNotSupportedException("HLS audio streams are not yet supported");
}

return audioStreams;
}

private static String urlEncode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
} catch (final UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}

private String getUrlFromHlsManifest(final String hlsManifestUrl) throws ParsingException {
final Downloader dl = NewPipe.getDownloader();
final String hlsManifestResponse;

try {
hlsManifestResponse = dl.get(hlsManifestUrl).responseBody();
} catch (final IOException | ReCaptchaException e) {
throw new ParsingException("Could not get SoundCloud HLS manifest");
}

final List<String> hlsRangesList = new ArrayList<String>();
final Matcher regex = Pattern.compile("^(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")
.matcher(hlsManifestResponse);

while (regex.find()) {
hlsRangesList.add(hlsManifestResponse.substring(regex.start(0), regex.end(0)));
}

final String hlsLastRangeUrl = hlsRangesList.get(hlsRangesList.size() - 1);
final String[] hlsLastRangeUrlArray = hlsLastRangeUrl.split("/");

return HTTPS + hlsLastRangeUrlArray[0] + "/media/0/" + hlsLastRangeUrlArray[3] + "/" + hlsLastRangeUrlArray[4];
}

@Override
public List<VideoStream> getVideoStreams() {
return Collections.emptyList();
Expand Down

0 comments on commit ea780f2

Please sign in to comment.