Skip to content

Commit f3859ed

Browse files
committed
Retrieve MediaFormat for streams that could not be extracted by the extractor
1 parent 0db12e5 commit f3859ed

2 files changed

Lines changed: 169 additions & 16 deletions

File tree

app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ private String getNameEditText() {
766766
}
767767

768768
private void showFailedDialog(@StringRes final int msg) {
769-
assureCorrectAppLanguage(getContext());
769+
assureCorrectAppLanguage(requireContext());
770770
new AlertDialog.Builder(context)
771771
.setTitle(R.string.general_error)
772772
.setMessage(msg)
@@ -799,7 +799,7 @@ private void prepareSelectedDownload() {
799799
filenameTmp += "opus";
800800
} else if (format != null) {
801801
mimeTmp = format.mimeType;
802-
filenameTmp += format.suffix;
802+
filenameTmp += format.getSuffix();
803803
}
804804
break;
805805
case R.id.video_button:
@@ -808,7 +808,7 @@ private void prepareSelectedDownload() {
808808
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
809809
if (format != null) {
810810
mimeTmp = format.mimeType;
811-
filenameTmp += format.suffix;
811+
filenameTmp += format.getSuffix();
812812
}
813813
break;
814814
case R.id.subtitle_button:
@@ -820,9 +820,9 @@ private void prepareSelectedDownload() {
820820
}
821821

822822
if (format == MediaFormat.TTML) {
823-
filenameTmp += MediaFormat.SRT.suffix;
823+
filenameTmp += MediaFormat.SRT.getSuffix();
824824
} else if (format != null) {
825-
filenameTmp += format.suffix;
825+
filenameTmp += format.getSuffix();
826826
}
827827
break;
828828
default:

app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java

Lines changed: 164 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.schabi.newpipe.util;
22

3+
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
4+
35
import android.content.Context;
46
import android.view.LayoutInflater;
57
import android.view.View;
@@ -16,16 +18,20 @@
1618
import org.schabi.newpipe.DownloaderImpl;
1719
import org.schabi.newpipe.R;
1820
import org.schabi.newpipe.extractor.MediaFormat;
21+
import org.schabi.newpipe.extractor.downloader.Response;
1922
import org.schabi.newpipe.extractor.stream.AudioStream;
2023
import org.schabi.newpipe.extractor.stream.Stream;
2124
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
2225
import org.schabi.newpipe.extractor.stream.VideoStream;
26+
import org.schabi.newpipe.extractor.utils.Utils;
2327

2428
import java.io.Serializable;
29+
import java.util.ArrayList;
2530
import java.util.Arrays;
2631
import java.util.Collections;
2732
import java.util.List;
2833
import java.util.concurrent.Callable;
34+
import java.util.stream.Collectors;
2935

3036
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
3137
import 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

Comments
 (0)