1111
1212import com .grack .nanojson .JsonArray ;
1313import com .grack .nanojson .JsonObject ;
14- import com .grack .nanojson .JsonParser ;
15- import com .grack .nanojson .JsonParserException ;
1614
1715import org .schabi .newpipe .extractor .Image ;
1816import org .schabi .newpipe .extractor .MediaFormat ;
19- import org .schabi .newpipe .extractor .NewPipe ;
2017import org .schabi .newpipe .extractor .StreamingService ;
2118import org .schabi .newpipe .extractor .downloader .Downloader ;
2219import org .schabi .newpipe .extractor .exceptions .ContentNotAvailableException ;
3027import org .schabi .newpipe .extractor .stream .AudioStream ;
3128import org .schabi .newpipe .extractor .stream .DeliveryMethod ;
3229import org .schabi .newpipe .extractor .stream .Description ;
30+ import org .schabi .newpipe .extractor .stream .HlsAudioStream ;
31+ import org .schabi .newpipe .extractor .stream .SoundcloudHlsUtils ;
3332import org .schabi .newpipe .extractor .stream .Stream ;
3433import org .schabi .newpipe .extractor .stream .StreamExtractor ;
3534import org .schabi .newpipe .extractor .stream .StreamInfoItemsCollector ;
@@ -188,25 +187,106 @@ public List<AudioStream> getAudioStreams() throws ExtractionException {
188187 return audioStreams ;
189188 }
190189
191- @ Nonnull
192- private String getTranscodingUrl (final String endpointUrl )
193- throws IOException , ExtractionException {
194- String apiStreamUrl = endpointUrl + "?client_id=" + clientId ();
190+ // TODO: put this somewhere better
191+ /**
192+ * Constructs the API endpoint url for this track that will be called to get the url
193+ * for retrieving the actual byte data for playback
194+ * (e.g. the actual url could be an m3u8 playlist)
195+ * @param baseUrl The baseUrl needed to construct the full url
196+ * @return The full API endpoint url to call to get the actual playback url
197+ * @throws IOException If there is a problem getting clientId
198+ * @throws ExtractionException For the same reason
199+ */
200+ private String getApiStreamUrl (final String baseUrl ) throws ExtractionException , IOException {
201+ String apiStreamUrl = baseUrl + "?client_id=" + clientId ();
195202
196203 final String trackAuthorization = track .getString ("track_authorization" );
197204 if (!isNullOrEmpty (trackAuthorization )) {
198205 apiStreamUrl += "&track_authorization=" + trackAuthorization ;
199206 }
207+ return apiStreamUrl ;
208+ }
200209
201- final String response = NewPipe .getDownloader ().get (apiStreamUrl ).responseBody ();
202- final JsonObject urlObject ;
203- try {
204- urlObject = JsonParser .object ().from (response );
205- } catch (final JsonParserException e ) {
206- throw new ParsingException ("Could not parse streamable URL" , e );
210+ public static final class StreamBuildResult {
211+ public final String contentUrl ;
212+ public final MediaFormat mediaFormat ;
213+
214+ public StreamBuildResult (final String contentUrl , final MediaFormat mediaFormat ) {
215+ this .contentUrl = contentUrl ;
216+ this .mediaFormat = mediaFormat ;
217+ }
218+
219+ @ Override
220+ public String toString () {
221+ return "StreamBuildResult{"
222+ + "contentUrl='" + contentUrl + '\''
223+ + ", mediaFormat=" + mediaFormat
224+ + '}' ;
225+ }
226+ }
227+
228+ /**
229+ * Builds the common audio stream components for all SoundCloud audio streams<p>
230+ * Returns the stream content url if we support this type of transcoding, {@code null} otherwise
231+ * @param transcoding The SoundCloud JSON transcoding object for this stream
232+ * @param builder AudioStream builder to set the common values
233+ * @return the stream content url if this transcoding is supported and common values were built
234+ * {@code null} otherwise
235+ */
236+ @ Nullable
237+ private StreamBuildResult buildBaseAudioStream (final JsonObject transcoding ,
238+ final AudioStream .Builder builder )
239+ throws ExtractionException , IOException {
240+ ExtractorLogger .d (TAG , getName () + " Building base audio stream info" );
241+ final var preset = transcoding .getString ("preset" , ID_UNKNOWN );
242+ final MediaFormat mediaFormat ;
243+ if (preset .contains ("mp3" )) {
244+ mediaFormat = MediaFormat .MP3 ;
245+ builder .setAverageBitrate (128 );
246+ } else if (preset .contains ("opus" )) {
247+ mediaFormat = MediaFormat .OPUS ;
248+ builder .setAverageBitrate (64 );
249+ builder .setDeliveryMethod (DeliveryMethod .HLS );
250+ } else if (preset .contains ("aac_160k" )) {
251+ mediaFormat = MediaFormat .M4A ;
252+ builder .setAverageBitrate (160 );
253+ } else {
254+ // Unknown format, return null to skip to the next audio stream
255+ return null ;
207256 }
208257
209- return urlObject .getString ("url" );
258+ builder .setMediaFormat (mediaFormat );
259+
260+ builder .setId (preset );
261+ final var url = transcoding .getString ("url" );
262+ final var hlsPlaylistUrl = SoundcloudHlsUtils .getStreamContentUrl (getApiStreamUrl (url ));
263+ builder .setContent (hlsPlaylistUrl , true );
264+ return new StreamBuildResult (hlsPlaylistUrl , mediaFormat );
265+ }
266+
267+ private HlsAudioStream buildHlsAudioStream (final JsonObject transcoding )
268+ throws ExtractionException , IOException {
269+ ExtractorLogger .d (TAG , getName () + "Extracting hls audio stream" );
270+ final var builder = new HlsAudioStream .Builder ();
271+ final StreamBuildResult buildResult = buildBaseAudioStream (transcoding , builder );
272+ if (buildResult == null ) {
273+ return null ;
274+ }
275+
276+ builder .setApiStreamUrl (getApiStreamUrl (transcoding .getString ("url" )));
277+ builder .setPlaylistId (SoundcloudHlsUtils .extractHlsPlaylistId (buildResult .contentUrl ,
278+ buildResult .mediaFormat ));
279+
280+ return builder .build ();
281+ }
282+
283+ private AudioStream buildProgressiveAudioStream (final JsonObject transcoding )
284+ throws ExtractionException , IOException {
285+ ExtractorLogger .d (TAG , getName () + "Extracting progressive audio stream" );
286+ final var builder = new AudioStream .Builder ();
287+ final StreamBuildResult buildResult = buildBaseAudioStream (transcoding , builder );
288+ return buildResult == null ? null : builder .build ();
289+ // TODO: anything else?
210290 }
211291
212292 private void extractAudioStreams (@ Nonnull final JsonArray transcodings ,
@@ -220,47 +300,35 @@ private void extractAudioStreams(@Nonnull final JsonArray transcodings,
220300 return ;
221301 }
222302
223- try {
224- final String preset = transcoding .getString ("preset" , ID_UNKNOWN );
225- final String protocol = transcoding .getObject ("format" )
226- .getString ("protocol" );
227-
228- if (protocol .contains ("encrypted" )) {
229- // Skip DRM-protected streams, which have encrypted in their protocol
230- // name
231- return ;
232- }
233-
234- final AudioStream .Builder builder = new AudioStream .Builder ()
235- .setId (preset );
303+ final String protocol = transcoding .getObject ("format" )
304+ .getString ("protocol" );
236305
237- if (protocol .equals ("hls" )) {
238- builder .setDeliveryMethod (DeliveryMethod .HLS );
239- }
240-
241- builder .setContent (getTranscodingUrl (url ), true );
242-
243- if (preset .contains ("mp3" )) {
244- builder .setMediaFormat (MediaFormat .MP3 );
245- builder .setAverageBitrate (128 );
246- } else if (preset .contains ("opus" )) {
247- builder .setMediaFormat (MediaFormat .OPUS );
248- builder .setAverageBitrate (64 );
249- } else if (preset .contains ("aac_160k" )) {
250- builder .setMediaFormat (MediaFormat .M4A );
251- builder .setAverageBitrate (160 );
252- } else {
253- // Unknown format, skip to the next audio stream
254- return ;
255- }
306+ if (protocol .contains ("encrypted" )) {
307+ // Skip DRM-protected streams, which have encrypted in their protocol
308+ // name
309+ return ;
310+ }
256311
257- final AudioStream audioStream = builder .build ();
258- if (!Stream .containSimilarStream (audioStream , audioStreams )) {
312+ final AudioStream audioStream ;
313+ try {
314+ // SoundCloud only has one progressive stream, the rest are HLS
315+ audioStream = protocol .equals ("hls" )
316+ ? buildHlsAudioStream (transcoding )
317+ : buildProgressiveAudioStream (transcoding );
318+ if (audioStream != null
319+ && !Stream .containSimilarStream (audioStream , audioStreams )) {
320+ ExtractorLogger .d (TAG , audioStream .getFormat ().getName () + " "
321+ + getName () + " " + audioStream .getContent ());
259322 audioStreams .add (audioStream );
260323 }
261- } catch (final ExtractionException | IOException ignored ) {
324+ } catch (final ExtractionException | IOException e ) {
262325 // Something went wrong when trying to get and add this audio stream,
263326 // skip to the next one
327+ final var preset = transcoding .getString ("preset" , "unknown" );
328+ ExtractorLogger .e (TAG ,
329+ getName () + " Failed to extract audio stream for transcoding "
330+ + '[' + protocol + '/' + preset + "] " + url ,
331+ e );
264332 }
265333 });
266334 }
0 commit comments