11package org .schabi .newpipe .util ;
22
3+ import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
4+
35import android .content .Context ;
46import android .view .LayoutInflater ;
57import android .view .View ;
1618import org .schabi .newpipe .DownloaderImpl ;
1719import org .schabi .newpipe .R ;
1820import org .schabi .newpipe .extractor .MediaFormat ;
21+ import org .schabi .newpipe .extractor .downloader .Response ;
1922import org .schabi .newpipe .extractor .stream .AudioStream ;
2023import org .schabi .newpipe .extractor .stream .Stream ;
2124import org .schabi .newpipe .extractor .stream .SubtitlesStream ;
2225import org .schabi .newpipe .extractor .stream .VideoStream ;
26+ import org .schabi .newpipe .extractor .utils .Utils ;
2327
2428import java .io .Serializable ;
29+ import java .util .ArrayList ;
2530import java .util .Arrays ;
2631import java .util .Collections ;
2732import java .util .List ;
2833import java .util .concurrent .Callable ;
34+ import java .util .stream .Collectors ;
2935
3036import io .reactivex .rxjava3 .android .schedulers .AndroidSchedulers ;
3137import io .reactivex .rxjava3 .core .Single ;
@@ -228,6 +234,7 @@ public static class StreamInfoWrapper<T extends Stream> implements Serializable
228234
229235 private final List <T > streamsList ;
230236 private final long [] streamSizes ;
237+ private final MediaFormat [] streamFormats ;
231238 private final String unknownSize ;
232239
233240 public StreamInfoWrapper (@ NonNull final List <T > streamList ,
@@ -236,31 +243,42 @@ public StreamInfoWrapper(@NonNull final List<T> streamList,
236243 this .streamSizes = new long [streamsList .size ()];
237244 this .unknownSize = context == null
238245 ? "--.-" : context .getString (R .string .unknown_content );
239-
240- resetSizes ();
246+ this . streamFormats = new MediaFormat [ streamsList . size ()];
247+ resetInfo ();
241248 }
242249
243250 /**
244- * Helper method to fetch the sizes of all the streams in a wrapper.
251+ * Helper method to fetch the sizes and missing media formats
252+ * of all the streams in a wrapper.
245253 *
246254 * @param <X> the stream type's class extending {@link Stream}
247255 * @param streamsWrapper the wrapper
248256 * @return a {@link Single} that returns a boolean indicating if any elements were changed
249257 */
250258 @ NonNull
251- public static <X extends Stream > Single <Boolean > fetchSizeForWrapper (
259+ public static <X extends Stream > Single <Boolean > fetchMoreInfoForWrapper (
252260 final StreamInfoWrapper <X > streamsWrapper ) {
253261 final Callable <Boolean > fetchAndSet = () -> {
254262 boolean hasChanged = false ;
255263 for (final X stream : streamsWrapper .getStreamsList ()) {
256- if (streamsWrapper .getSizeInBytes (stream ) > SIZE_UNSET ) {
264+ final boolean changeSize = streamsWrapper .getSizeInBytes (stream ) <= SIZE_UNSET ;
265+ final boolean changeFormat = stream .getFormat () == null ;
266+ if (!changeSize && !changeFormat ) {
257267 continue ;
258268 }
259-
260- final long contentLength = DownloaderImpl .getInstance ().getContentLength (
261- stream .getContent ());
262- streamsWrapper .setSize (stream , contentLength );
263- hasChanged = true ;
269+ final Response response = DownloaderImpl .getInstance ()
270+ .head (stream .getContent ());
271+ if (changeSize ) {
272+ final String contentLength = response .getHeader ("Content-Length" );
273+ if (!isNullOrEmpty (contentLength )) {
274+ streamsWrapper .setSize (stream , Long .parseLong (contentLength ));
275+ hasChanged = true ;
276+ }
277+ }
278+ if (changeFormat ) {
279+ hasChanged = retrieveMediaFormat (stream , streamsWrapper , response )
280+ || hasChanged ;
281+ }
264282 }
265283 return hasChanged ;
266284 };
@@ -271,8 +289,135 @@ public static <X extends Stream> Single<Boolean> fetchSizeForWrapper(
271289 .onErrorReturnItem (true );
272290 }
273291
274- public void resetSizes () {
292+ /**
293+ * Try to retrieve the {@link MediaFormat} for a stream from the request headers.
294+ *
295+ * @param <X> the stream type to get the {@link MediaFormat} for
296+ * @param stream the stream to find the {@link MediaFormat} for
297+ * @param streamsWrapper the wrapper to store the found {@link MediaFormat} in
298+ * @param response the response of the head request for the given stream
299+ * @return {@code true} if the media format could be retrieved; {@code false} otherwise
300+ */
301+ private static <X extends Stream > boolean retrieveMediaFormat (
302+ @ NonNull final X stream ,
303+ @ NonNull final StreamInfoWrapper <X > streamsWrapper ,
304+ @ NonNull final Response response ) {
305+ return retrieveMediaFormatFromFileTypeHeaders (stream , streamsWrapper , response )
306+ || retrieveMediaFormatFromContentDispositionHeader (
307+ stream , streamsWrapper , response )
308+ || retrieveMediaFormatFromContentTypeHeader (stream , streamsWrapper , response );
309+ }
310+
311+ private static <X extends Stream > boolean retrieveMediaFormatFromFileTypeHeaders (
312+ @ NonNull final X stream ,
313+ @ NonNull final StreamInfoWrapper <X > streamsWrapper ,
314+ @ NonNull final Response response ) {
315+ // try to use additional headers from CDNs or servers,
316+ // e.g. x-amz-meta-file-type (e.g. for SoundCloud)
317+ final List <String > keys = response .responseHeaders ().keySet ().stream ()
318+ .filter (k -> k .endsWith ("file-type" )).collect (Collectors .toList ());
319+ if (!keys .isEmpty ()) {
320+ for (final String key : keys ) {
321+ final String suffix = response .getHeader (key );
322+ final MediaFormat format = MediaFormat .getFromSuffix (suffix );
323+ if (format != null ) {
324+ streamsWrapper .setFormat (stream , format );
325+ return true ;
326+ }
327+ }
328+ }
329+ return false ;
330+ }
331+
332+ /**
333+ * <p>Retrieve a {@link MediaFormat} from a HTTP Content-Disposition header
334+ * for a stream and store the info in a wrapper.</p>
335+ * @see
336+ * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">
337+ * mdn Web Docs for the HTTP Content-Disposition Header</a>
338+ * @param stream the stream to get the {@link MediaFormat} for
339+ * @param streamsWrapper the wrapper to store the {@link MediaFormat} in
340+ * @param response the response to get the Content-Disposition header from
341+ * @return {@code true} if the {@link MediaFormat} could be retrieved from the response;
342+ * otherwise {@code false}
343+ * @param <X>
344+ */
345+ public static <X extends Stream > boolean retrieveMediaFormatFromContentDispositionHeader (
346+ @ NonNull final X stream ,
347+ @ NonNull final StreamInfoWrapper <X > streamsWrapper ,
348+ @ NonNull final Response response ) {
349+ // parse the Content-Disposition header,
350+ // see
351+ // there can be two filename directives
352+ String contentDisposition = response .getHeader ("Content-Disposition" );
353+ if (contentDisposition == null ) {
354+ return false ;
355+ }
356+ try {
357+ contentDisposition = Utils .decodeUrlUtf8 (contentDisposition );
358+ final String [] parts = contentDisposition .split (";" );
359+ for (String part : parts ) {
360+ final String fileName ;
361+ part = part .trim ();
362+
363+ // extract the filename
364+ if (part .startsWith ("filename=" )) {
365+ // remove directive and decode
366+ fileName = Utils .decodeUrlUtf8 (part .substring (9 ));
367+ } else if (part .startsWith ("filename*=" )) {
368+ fileName = Utils .decodeUrlUtf8 (part .substring (10 ));
369+ } else {
370+ continue ;
371+ }
372+
373+ // extract the file extension / suffix
374+ final String [] p = fileName .split ("\\ ." );
375+ String suffix = p [p .length - 1 ];
376+ if (suffix .endsWith ("\" " ) || suffix .endsWith ("'" )) {
377+ // remove trailing quotes if present, end index is exclusive
378+ suffix = suffix .substring (0 , suffix .length () - 1 );
379+ }
380+
381+ // get the corresponding media format
382+ final MediaFormat format = MediaFormat .getFromSuffix (suffix );
383+ if (format != null ) {
384+ streamsWrapper .setFormat (stream , format );
385+ return true ;
386+ }
387+ }
388+ } catch (final Exception ignored ) {
389+ // fail silently
390+ }
391+ return false ;
392+ }
393+
394+ private static <X extends Stream > boolean retrieveMediaFormatFromContentTypeHeader (
395+ @ NonNull final X stream ,
396+ @ NonNull final StreamInfoWrapper <X > streamsWrapper ,
397+ @ NonNull final Response response ) {
398+ // try to get the format by content type
399+ // some mime types are not unique for every format, those are omitted
400+ final List <MediaFormat > formats = MediaFormat .getAllFromMimeType (
401+ response .getHeader ("Content-Type" ));
402+ final List <MediaFormat > uniqueFormats = new ArrayList <>(formats .size ());
403+ for (int i = 0 ; i < formats .size (); i ++) {
404+ final MediaFormat format = formats .get (i );
405+ if (uniqueFormats .stream ().filter (f -> f .id == format .id ).count () == 0 ) {
406+ uniqueFormats .add (format );
407+ }
408+ }
409+ if (uniqueFormats .size () == 1 ) {
410+ streamsWrapper .setFormat (stream , uniqueFormats .get (0 ));
411+ return true ;
412+ }
413+ return false ;
414+ }
415+
416+ public void resetInfo () {
275417 Arrays .fill (streamSizes , SIZE_UNSET );
418+ for (int i = 0 ; i < streamsList .size (); i ++) {
419+ streamFormats [i ] = streamsList .get (i ).getFormat ();
420+ }
276421 }
277422
278423 public static <X extends Stream > StreamInfoWrapper <X > empty () {
@@ -306,5 +451,13 @@ private String formatSize(final long size) {
306451 public void setSize (final T stream , final long sizeInBytes ) {
307452 streamSizes [streamsList .indexOf (stream )] = sizeInBytes ;
308453 }
454+
455+ public MediaFormat getFormat (final int streamIndex ) {
456+ return streamFormats [streamIndex ];
457+ }
458+
459+ public void setFormat (final T stream , final MediaFormat format ) {
460+ streamFormats [streamsList .indexOf (stream )] = format ;
461+ }
309462 }
310463}
0 commit comments