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