2828import org .schabi .newpipe .extractor .localization .DateWrapper ;
2929import org .schabi .newpipe .extractor .services .soundcloud .SoundcloudParsingHelper ;
3030import org .schabi .newpipe .extractor .stream .AudioStream ;
31+ import org .schabi .newpipe .extractor .stream .DeliveryMethod ;
3132import org .schabi .newpipe .extractor .stream .Description ;
3233import org .schabi .newpipe .extractor .stream .Stream ;
3334import 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
0 commit comments