1010import org .schabi .newpipe .extractor .StreamingService ;
1111import org .schabi .newpipe .extractor .downloader .Downloader ;
1212import org .schabi .newpipe .extractor .exceptions .ContentNotAvailableException ;
13- import org .schabi .newpipe .extractor .exceptions .ContentNotSupportedException ;
1413import org .schabi .newpipe .extractor .exceptions .ExtractionException ;
1514import org .schabi .newpipe .extractor .exceptions .GeographicRestrictionException ;
1615import org .schabi .newpipe .extractor .exceptions .ParsingException ;
2928import java .util .Collections ;
3029import java .util .List ;
3130import 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 ;
3335import static org .schabi .newpipe .extractor .utils .Utils .*;
3436
3537public 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