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