1111import org .schabi .newpipe .extractor .StreamingService ;
1212import org .schabi .newpipe .extractor .downloader .Downloader ;
1313import org .schabi .newpipe .extractor .exceptions .ContentNotAvailableException ;
14- import org .schabi .newpipe .extractor .exceptions .ContentNotSupportedException ;
1514import org .schabi .newpipe .extractor .exceptions .ExtractionException ;
15+ import org .schabi .newpipe .extractor .exceptions .GeographicRestrictionException ;
1616import org .schabi .newpipe .extractor .exceptions .ParsingException ;
17+ import org .schabi .newpipe .extractor .exceptions .ReCaptchaException ;
18+ import org .schabi .newpipe .extractor .exceptions .SoundCloudGoPlusException ;
1719import org .schabi .newpipe .extractor .linkhandler .LinkHandler ;
1820import org .schabi .newpipe .extractor .localization .DateWrapper ;
1921import org .schabi .newpipe .extractor .services .soundcloud .SoundcloudParsingHelper ;
3335import java .util .Collections ;
3436import java .util .List ;
3537import java .util .Locale ;
38+ import java .util .regex .Matcher ;
39+ import java .util .regex .Pattern ;
3640
3741import javax .annotation .Nonnull ;
3842import javax .annotation .Nullable ;
3943
4044import static org .schabi .newpipe .extractor .utils .JsonUtils .EMPTY_STRING ;
45+ import static org .schabi .newpipe .extractor .utils .Utils .HTTPS ;
4146import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
4247
4348public 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