Skip to content

Commit ede34f5

Browse files
committed
Add GeographicRestrictionException and SoundCloudGoPlusException to be able to display a different 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/) Warning: The corresponding code throws a StreamExtractException on every track, even if the track has a progressive stream.
1 parent 48a9993 commit ede34f5

File tree

3 files changed

+101
-19
lines changed

3 files changed

+101
-19
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.schabi.newpipe.extractor.exceptions;
2+
3+
public class GeographicRestrictionException extends ParsingException {
4+
public GeographicRestrictionException(String message) {
5+
super(message);
6+
}
7+
8+
public GeographicRestrictionException(String message, Throwable cause) {
9+
super(message, cause);
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.schabi.newpipe.extractor.exceptions;
2+
3+
public class SoundCloudGoPlusException extends ParsingException {
4+
public SoundCloudGoPlusException() {
5+
super("This track is a SoundCloud Go+ track");
6+
}
7+
8+
public SoundCloudGoPlusException(Throwable cause) {
9+
super("This track is a SoundCloud Go+ track", cause);
10+
}
11+
}

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

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
import org.schabi.newpipe.extractor.StreamingService;
1212
import org.schabi.newpipe.extractor.downloader.Downloader;
1313
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
14-
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
1514
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
15+
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
1616
import org.schabi.newpipe.extractor.exceptions.ParsingException;
17+
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
18+
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusException;
1719
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
1820
import org.schabi.newpipe.extractor.localization.DateWrapper;
1921
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
@@ -33,11 +35,14 @@
3335
import java.util.Collections;
3436
import java.util.List;
3537
import java.util.Locale;
38+
import java.util.regex.Matcher;
39+
import java.util.regex.Pattern;
3640

3741
import javax.annotation.Nonnull;
3842
import javax.annotation.Nullable;
3943

4044
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
45+
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
4146
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
4247

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

5459
String policy = track.getString("policy", EMPTY_STRING);
5560
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
61+
if (policy.equals("SNIP")) {
62+
throw new SoundCloudGoPlusException();
63+
}
64+
if (policy.equals("BLOCK")) {
65+
throw new GeographicRestrictionException("This track is not available in user's country");
66+
}
5667
throw new ContentNotAvailableException("Content not available: policy " + policy);
5768
}
5869
}
@@ -179,7 +190,7 @@ public String getHlsUrl() {
179190

180191
@Override
181192
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
182-
List<AudioStream> audioStreams = new ArrayList<>();
193+
final List<AudioStream> audioStreams = new ArrayList<>();
183194
final Downloader dl = NewPipe.getDownloader();
184195

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

193-
// get information about what stream formats are available
194-
for (Object transcoding : transcodings) {
195-
204+
// Get information about what stream formats are available
205+
for (final Object transcoding : transcodings) {
196206
final JsonObject t = (JsonObject) transcoding;
197207
String url = t.getString("url");
208+
String mediaUrl = null;
209+
final MediaFormat mediaFormat;
210+
final int bitrate;
198211

199212
if (!isNullOrEmpty(url)) {
213+
if (t.getString("preset").contains("mp3")) {
214+
mediaFormat = MediaFormat.MP3;
215+
bitrate = 128;
216+
} else if (t.getString("preset").contains("opus")) {
217+
mediaFormat = MediaFormat.OPUS;
218+
bitrate = 64;
219+
} else {
220+
continue;
221+
}
200222

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

210230
try {
211231
JsonObject mp3UrlObject = JsonParser.object().from(res);
212232
// Links in this file are also only valid for a short period.
213-
audioStreams.add(new AudioStream(mp3UrlObject.getString("url"),
214-
MediaFormat.MP3, 128));
215-
} catch (JsonParserException e) {
233+
mediaUrl = mp3UrlObject.getString("url");
234+
} catch (final JsonParserException e) {
235+
throw new ParsingException("Could not parse streamable url", e);
236+
}
237+
} else if (t.getObject("format").getString("protocol").equals("hls")) {
238+
// This url points to the endpoint which generates a unique and short living url to the stream.
239+
url += "?client_id=" + SoundcloudParsingHelper.clientId();
240+
final String res = dl.get(url).responseBody();
241+
242+
try {
243+
final JsonObject mp3HlsUrlObject = JsonParser.object().from(res);
244+
// Links in this file are also only valid for a short period.
245+
try {
246+
mediaUrl = getUrlFromHlsManifest(mp3HlsUrlObject.getString("url"));
247+
} catch (final ParsingException e) {
248+
continue;
249+
}
250+
} catch (final JsonParserException e) {
216251
throw new ParsingException("Could not parse streamable url", e);
217252
}
253+
} else {
254+
continue;
218255
}
256+
257+
audioStreams.add(new AudioStream(mediaUrl, mediaFormat, bitrate));
219258
}
220259
}
221260

222-
} catch (NullPointerException e) {
261+
} catch (final NullPointerException e) {
223262
throw new ExtractionException("Could not get SoundCloud's track audio url", e);
224263
}
225264

226-
if (audioStreams.isEmpty()) {
227-
throw new ContentNotSupportedException("HLS audio streams are not yet supported");
228-
}
229-
230265
return audioStreams;
231266
}
232267

233268
private static String urlEncode(String value) {
234269
try {
235270
return URLEncoder.encode(value, "UTF-8");
236-
} catch (UnsupportedEncodingException e) {
271+
} catch (final UnsupportedEncodingException e) {
237272
throw new IllegalStateException(e);
238273
}
239274
}
240275

276+
private String getUrlFromHlsManifest(final String hlsManifestUrl) throws ParsingException {
277+
final Downloader dl = NewPipe.getDownloader();
278+
final String hlsManifestResponse;
279+
280+
try {
281+
hlsManifestResponse = dl.get(hlsManifestUrl).responseBody();
282+
} catch (final IOException | ReCaptchaException e) {
283+
throw new ParsingException("Could not get SoundCloud HLS manifest");
284+
}
285+
286+
final List<String> hlsRangesList = new ArrayList<String>();
287+
final Matcher regex = Pattern.compile("/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)?/gi")
288+
.matcher(hlsManifestResponse);
289+
290+
while (regex.find()) {
291+
hlsRangesList.add(regex.group());
292+
}
293+
294+
final String hlsLastRangeUrl = hlsRangesList.get(hlsRangesList.size() - 1);
295+
final String[] hlsLastRangeUrlArray = hlsLastRangeUrl.split("/");
296+
final String hlsStreamingUrl = HTTPS + hlsLastRangeUrlArray[0] + "/media/0/" + hlsLastRangeUrlArray[3] + "/" + hlsLastRangeUrlArray[4];
297+
298+
return hlsStreamingUrl;
299+
}
300+
241301
@Override
242302
public List<VideoStream> getVideoStreams() {
243303
return Collections.emptyList();

0 commit comments

Comments
 (0)