1313import org .schabi .newpipe .extractor .exceptions .ExtractionException ;
1414import org .schabi .newpipe .extractor .exceptions .GeographicRestrictionException ;
1515import org .schabi .newpipe .extractor .exceptions .ParsingException ;
16+ import org .schabi .newpipe .extractor .exceptions .ReCaptchaException ;
1617import org .schabi .newpipe .extractor .exceptions .SoundCloudGoPlusContentException ;
1718import org .schabi .newpipe .extractor .linkhandler .LinkHandler ;
1819import org .schabi .newpipe .extractor .localization .DateWrapper ;
@@ -45,7 +46,7 @@ public SoundcloudStreamExtractor(StreamingService service, LinkHandler linkHandl
4546 public void onFetchPage (@ Nonnull Downloader downloader ) throws IOException , ExtractionException {
4647 track = SoundcloudParsingHelper .resolveFor (downloader , getUrl ());
4748
48- String policy = track .getString ("policy" , EMPTY_STRING );
49+ final String policy = track .getString ("policy" , EMPTY_STRING );
4950 if (!policy .equals ("ALLOW" ) && !policy .equals ("MONETIZE" )) {
5051 if (policy .equals ("SNIP" )) {
5152 throw new SoundCloudGoPlusContentException ();
@@ -194,49 +195,49 @@ public List<AudioStream> getAudioStreams() throws IOException, ExtractionExcepti
194195
195196 try {
196197 final JsonArray transcodings = track .getObject ("media" ).getArray ("transcodings" );
197-
198198 // Iterate a first time to see if there is a progressive MP3 stream available.
199199 // If yes, the MP3 HLS stream will be not added to audioStreams.
200-
201200 boolean mp3ProgressiveStreamInTranscodings = false ;
202201
203202 for (final Object transcoding : transcodings ) {
204203 final JsonObject t = (JsonObject ) transcoding ;
205204 if (t .getString ("preset" ).contains ("mp3" ) &&
206205 t .getObject ("format" ).getString ("protocol" ).equals ("progressive" )) {
207206 mp3ProgressiveStreamInTranscodings = true ;
207+ break ;
208208 }
209209 }
210210
211211 // Get information about what stream formats are available
212212 for (final Object transcoding : transcodings ) {
213213 final JsonObject t = (JsonObject ) transcoding ;
214- String url = t .getString ("url" );
215214 final String mediaUrl ;
215+ final String preset = t .getString ("preset" );
216+ final String protocol = t .getObject ("format" ).getString ("protocol" );
217+ String url = t .getString ("url" );
216218 final MediaFormat mediaFormat ;
217219 final int bitrate ;
218220
219221 if (!isNullOrEmpty (url )) {
220- if (t . getString ( " preset" ) .contains ("mp3" )) {
222+ if (preset .contains ("mp3" )) {
221223 // Don't add the MP3 HLS stream if there is a progressive stream present
222224 // because the two have the same bitrate
223- if (t .getObject ("format" ).getString ("protocol" ).equals ("hls" ) &&
224- mp3ProgressiveStreamInTranscodings ) {
225+ if (mp3ProgressiveStreamInTranscodings && protocol .equals ("hls" )) {
225226 continue ;
226227 }
227-
228228 mediaFormat = MediaFormat .MP3 ;
229229 bitrate = 128 ;
230- } else if (t . getString ( " preset" ) .contains ("opus" )) {
230+ } else if (preset .contains ("opus" )) {
231231 mediaFormat = MediaFormat .OPUS ;
232232 bitrate = 64 ;
233233 } else {
234+ // Unknown format
234235 continue ;
235236 }
236237
237238 // TODO: move this to a separate method to generate valid urls when needed (e.g. resuming a paused stream)
238239
239- if (t . getObject ( "format" ). getString ( " protocol" ) .equals ("progressive" )) {
240+ if (protocol .equals ("progressive" )) {
240241 // This url points to the endpoint which generates a unique and short living url to the stream.
241242 url += "?client_id=" + SoundcloudParsingHelper .clientId ();
242243 final String res = dl .get (url ).responseBody ();
@@ -248,38 +249,25 @@ public List<AudioStream> getAudioStreams() throws IOException, ExtractionExcepti
248249 } catch (final JsonParserException e ) {
249250 throw new ParsingException ("Could not parse streamable url" , e );
250251 }
251- } else if (t .getObject ("format" ).getString ("protocol" ).equals ("hls" )) {
252-
252+ } else if (protocol .equals ("hls" )) {
253253 // This url points to the endpoint which generates a unique and short living url to the stream.
254254 url += "?client_id=" + SoundcloudParsingHelper .clientId ();
255255 final String res = dl .get (url ).responseBody ();
256256
257257 try {
258258 final JsonObject mp3HlsUrlObject = JsonParser .object ().from (res );
259259 // Links in this file are also only valid for a short period.
260- // Parsing the HLS manifest to get a single file by requesting a range equal to 0-track_length
261- final String hlsManifestResponse ;
262260 try {
263- hlsManifestResponse = dl .get (mp3HlsUrlObject .getString ("url" )).responseBody ();
264- } catch (final IOException e ) {
261+ mediaUrl = getSingleUrlFromHlsManifest (mp3HlsUrlObject .getString ("url" ));
262+ } catch (final ParsingException e ) {
263+ // Something went during HLS manifest parsing, don't add this stream to audioStreams
265264 continue ;
266265 }
267- final List <String > hlsRangesList = new ArrayList <>();
268- final Matcher regex = Pattern .compile ("((https?):((//)|(\\ \\ ))+[\\ w\\ d:#@%/;$()~_?+-=\\ \\ .&]*)" )
269- .matcher (hlsManifestResponse );
270-
271- while (regex .find ()) {
272- hlsRangesList .add (hlsManifestResponse .substring (regex .start (0 ), regex .end (0 )));
273- }
274-
275- final String hlsLastRangeUrl = hlsRangesList .get (hlsRangesList .size () - 1 );
276- final String [] hlsLastRangeUrlArray = hlsLastRangeUrl .split ("/" );
277-
278- mediaUrl = HTTPS + hlsLastRangeUrlArray [2 ] + "/media/0/" + hlsLastRangeUrlArray [5 ] + "/" + hlsLastRangeUrlArray [6 ];
279266 } catch (final JsonParserException e ) {
280267 throw new ParsingException ("Could not parse streamable url" , e );
281268 }
282269 } else {
270+ // Unknown protocol
283271 continue ;
284272 }
285273
@@ -294,10 +282,43 @@ public List<AudioStream> getAudioStreams() throws IOException, ExtractionExcepti
294282 return audioStreams ;
295283 }
296284
297- private static String urlEncode (String value ) {
285+ private final static Pattern PATTERN_WEB_URLS_IN_HLS_MANIFESTS = Pattern .compile ("((http?|https?):((//)|(\\ \\ ))+[\\ w\\ d:#@%/;$()~_?+-=\\ \\ .&]*)" );
286+
287+ /** Parses a SoundCloud HLS manifest to get a single URL of HLS streams.
288+ * <p>
289+ * This method downloads the provided manifest URL, find all web occurrences using a regex, get
290+ * the last segment URL, changes its segment range to {@code 0/track-length} and return this string.
291+ * @param hlsManifestUrl the URL of the manifest to be parsed
292+ * @return a single URL that contains a range equal to the length of the track
293+ */
294+ private static String getSingleUrlFromHlsManifest (final String hlsManifestUrl ) throws ParsingException {
295+ final Downloader dl = NewPipe .getDownloader ();
296+ final String hlsManifestResponse ;
297+
298+ try {
299+ hlsManifestResponse = dl .get (hlsManifestUrl ).responseBody ();
300+ } catch (final IOException | ReCaptchaException e ) {
301+ throw new ParsingException ("Could not get SoundCloud HLS Manifest" );
302+ }
303+
304+ final List <String > hlsRangesList = new ArrayList <>();
305+ final Matcher pattern_matches = PATTERN_WEB_URLS_IN_HLS_MANIFESTS .matcher (hlsManifestResponse );
306+
307+ while (pattern_matches .find ()) {
308+ hlsRangesList .add (hlsManifestResponse .substring (pattern_matches .start (0 ),
309+ pattern_matches .end (0 )));
310+ }
311+
312+ final String hlsLastRangeUrl = hlsRangesList .get (hlsRangesList .size () - 1 );
313+ final String [] hlsLastRangeUrlArray = hlsLastRangeUrl .split ("/" );
314+
315+ return HTTPS + hlsLastRangeUrlArray [2 ] + "/media/0/" + hlsLastRangeUrlArray [5 ] + "/" + hlsLastRangeUrlArray [6 ];
316+ }
317+
318+ private static String urlEncode (final String value ) {
298319 try {
299320 return URLEncoder .encode (value , UTF_8 );
300- } catch (UnsupportedEncodingException e ) {
321+ } catch (final UnsupportedEncodingException e ) {
301322 throw new IllegalStateException (e );
302323 }
303324 }
0 commit comments