From 0db12e5561f66eb9a717998910c9cb064768af99 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Thu, 15 Jun 2023 12:38:39 +0200 Subject: [PATCH 1/5] Rename StreamSizeWrapper to StreamInfoWrapper --- .../newpipe/util/StreamItemAdapterTest.kt | 8 +++---- .../newpipe/download/DownloadDialog.java | 24 +++++++++---------- .../newpipe/util/AudioTrackAdapter.java | 8 +++---- .../newpipe/util/SecondaryStreamHelper.java | 6 ++--- .../newpipe/util/StreamItemAdapter.java | 22 ++++++++--------- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt index 0fe251c16..c67564232 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt @@ -84,7 +84,7 @@ class StreamItemAdapterTest { @Test fun subtitleStreams_noIcon() { val adapter = StreamItemAdapter( - StreamItemAdapter.StreamSizeWrapper( + StreamItemAdapter.StreamInfoWrapper( (0 until 5).map { SubtitlesStream.Builder() .setContent("https://example.com", true) @@ -105,7 +105,7 @@ class StreamItemAdapterTest { @Test fun audioStreams_noIcon() { val adapter = StreamItemAdapter( - StreamItemAdapter.StreamSizeWrapper( + StreamItemAdapter.StreamInfoWrapper( (0 until 5).map { AudioStream.Builder() .setId(Stream.ID_UNKNOWN) @@ -128,7 +128,7 @@ class StreamItemAdapterTest { * [videoOnly] vararg. */ private fun getVideoStreams(vararg videoOnly: Boolean) = - StreamItemAdapter.StreamSizeWrapper( + StreamItemAdapter.StreamInfoWrapper( videoOnly.map { VideoStream.Builder() .setId(Stream.ID_UNKNOWN) @@ -196,7 +196,7 @@ class StreamItemAdapterTest { streams.forEachIndexed { index, stream -> val secondaryStreamHelper: SecondaryStreamHelper? = stream?.let { SecondaryStreamHelper( - StreamItemAdapter.StreamSizeWrapper(streams, context), + StreamItemAdapter.StreamInfoWrapper(streams, context), it ) } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 9e05584da..e5bb7a7e1 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -67,7 +67,7 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.SecondaryStreamHelper; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.StreamItemAdapter; -import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper; import org.schabi.newpipe.util.AudioTrackAdapter; import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper; import org.schabi.newpipe.util.ThemeHelper; @@ -97,9 +97,9 @@ public class DownloadDialog extends DialogFragment @State StreamInfo currentInfo; @State - StreamSizeWrapper wrappedVideoStreams; + StreamInfoWrapper wrappedVideoStreams; @State - StreamSizeWrapper wrappedSubtitleStreams; + StreamInfoWrapper wrappedSubtitleStreams; @State AudioTracksWrapper wrappedAudioTracks; @State @@ -187,8 +187,8 @@ public class DownloadDialog extends DialogFragment wrappedAudioTracks.size() > 1 ); - this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context); - this.wrappedSubtitleStreams = new StreamSizeWrapper<>( + this.wrappedVideoStreams = new StreamInfoWrapper<>(videoStreams, context); + this.wrappedSubtitleStreams = new StreamInfoWrapper<>( getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context); this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams); @@ -258,10 +258,10 @@ public class DownloadDialog extends DialogFragment * Update the displayed video streams based on the selected audio track. */ private void updateSecondaryStreams() { - final StreamSizeWrapper audioStreams = getWrappedAudioStreams(); + final StreamInfoWrapper audioStreams = getWrappedAudioStreams(); final var secondaryStreams = new SparseArrayCompat>(4); final List videoStreams = wrappedVideoStreams.getStreamsList(); - wrappedVideoStreams.resetSizes(); + wrappedVideoStreams.resetInfo(); for (int i = 0; i < videoStreams.size(); i++) { if (!videoStreams.get(i).isVideoOnly()) { @@ -396,7 +396,7 @@ public class DownloadDialog extends DialogFragment private void fetchStreamsSize() { disposables.clear(); - disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams) + disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedVideoStreams) .subscribe(result -> { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) { @@ -406,7 +406,7 @@ public class DownloadDialog extends DialogFragment new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, "Downloading video stream size", currentInfo.getServiceId())))); - disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams()) + disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams()) .subscribe(result -> { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { @@ -416,7 +416,7 @@ public class DownloadDialog extends DialogFragment new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, "Downloading audio stream size", currentInfo.getServiceId())))); - disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams) + disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams) .subscribe(result -> { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { @@ -724,9 +724,9 @@ public class DownloadDialog extends DialogFragment dialogBinding.subtitleButton.setEnabled(enabled); } - private StreamSizeWrapper getWrappedAudioStreams() { + private StreamInfoWrapper getWrappedAudioStreams() { if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) { - return StreamSizeWrapper.empty(); + return StreamInfoWrapper.empty(); } return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex); } diff --git a/app/src/main/java/org/schabi/newpipe/util/AudioTrackAdapter.java b/app/src/main/java/org/schabi/newpipe/util/AudioTrackAdapter.java index 39a05acb3..90689052e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AudioTrackAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/AudioTrackAdapter.java @@ -13,7 +13,7 @@ import androidx.annotation.Nullable; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper; import java.io.Serializable; import java.util.List; @@ -75,15 +75,15 @@ public class AudioTrackAdapter extends BaseAdapter { } public static class AudioTracksWrapper implements Serializable { - private final List> tracksList; + private final List> tracksList; public AudioTracksWrapper(@NonNull final List> groupedAudioStreams, @Nullable final Context context) { this.tracksList = groupedAudioStreams.stream().map(streams -> - new StreamSizeWrapper<>(streams, context)).collect(Collectors.toList()); + new StreamInfoWrapper<>(streams, context)).collect(Collectors.toList()); } - public List> getTracksList() { + public List> getTracksList() { return tracksList; } diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index e7fd2d4a4..9415135cf 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -7,15 +7,15 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper; import java.util.List; public class SecondaryStreamHelper { private final int position; - private final StreamSizeWrapper streams; + private final StreamInfoWrapper streams; - public SecondaryStreamHelper(@NonNull final StreamSizeWrapper streams, + public SecondaryStreamHelper(@NonNull final StreamInfoWrapper streams, final T selectedStream) { this.streams = streams; this.position = streams.getStreamsList().indexOf(selectedStream); diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 2eb63ff41..48fb81c99 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -41,7 +41,7 @@ import us.shandian.giga.util.Utility; */ public class StreamItemAdapter extends BaseAdapter { @NonNull - private final StreamSizeWrapper streamsWrapper; + private final StreamInfoWrapper streamsWrapper; @NonNull private final SparseArrayCompat> secondaryStreams; @@ -53,7 +53,7 @@ public class StreamItemAdapter extends BaseA private final boolean hasAnyVideoOnlyStreamWithNoSecondaryStream; public StreamItemAdapter( - @NonNull final StreamSizeWrapper streamsWrapper, + @NonNull final StreamInfoWrapper streamsWrapper, @NonNull final SparseArrayCompat> secondaryStreams ) { this.streamsWrapper = streamsWrapper; @@ -63,7 +63,7 @@ public class StreamItemAdapter extends BaseA checkHasAnyVideoOnlyStreamWithNoSecondaryStream(); } - public StreamItemAdapter(final StreamSizeWrapper streamsWrapper) { + public StreamItemAdapter(final StreamInfoWrapper streamsWrapper) { this(streamsWrapper, new SparseArrayCompat<>(0)); } @@ -121,7 +121,7 @@ public class StreamItemAdapter extends BaseA final TextView sizeView = convertView.findViewById(R.id.stream_size); final T stream = getItem(position); - final MediaFormat mediaFormat = stream.getFormat(); + final MediaFormat mediaFormat = streamsWrapper.getFormat(position); int woSoundIconVisibility = View.GONE; String qualityString; @@ -221,16 +221,16 @@ public class StreamItemAdapter extends BaseA * * @param the stream type's class extending {@link Stream} */ - public static class StreamSizeWrapper implements Serializable { - private static final StreamSizeWrapper EMPTY = - new StreamSizeWrapper<>(Collections.emptyList(), null); + public static class StreamInfoWrapper implements Serializable { + private static final StreamInfoWrapper EMPTY = + new StreamInfoWrapper<>(Collections.emptyList(), null); private static final int SIZE_UNSET = -2; private final List streamsList; private final long[] streamSizes; private final String unknownSize; - public StreamSizeWrapper(@NonNull final List streamList, + public StreamInfoWrapper(@NonNull final List streamList, @Nullable final Context context) { this.streamsList = streamList; this.streamSizes = new long[streamsList.size()]; @@ -249,7 +249,7 @@ public class StreamItemAdapter extends BaseA */ @NonNull public static Single fetchSizeForWrapper( - final StreamSizeWrapper streamsWrapper) { + final StreamInfoWrapper streamsWrapper) { final Callable fetchAndSet = () -> { boolean hasChanged = false; for (final X stream : streamsWrapper.getStreamsList()) { @@ -275,9 +275,9 @@ public class StreamItemAdapter extends BaseA Arrays.fill(streamSizes, SIZE_UNSET); } - public static StreamSizeWrapper empty() { + public static StreamInfoWrapper empty() { //noinspection unchecked - return (StreamSizeWrapper) EMPTY; + return (StreamInfoWrapper) EMPTY; } public List getStreamsList() { From f3859ed710887d9549d7f6c35f214416f055fbdf Mon Sep 17 00:00:00 2001 From: TobiGr Date: Mon, 14 Aug 2023 23:05:30 +0200 Subject: [PATCH 2/5] Retrieve MediaFormat for streams that could not be extracted by the extractor --- .../newpipe/download/DownloadDialog.java | 10 +- .../newpipe/util/StreamItemAdapter.java | 175 ++++++++++++++++-- 2 files changed, 169 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index e5bb7a7e1..9e9909e85 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -766,7 +766,7 @@ public class DownloadDialog extends DialogFragment } private void showFailedDialog(@StringRes final int msg) { - assureCorrectAppLanguage(getContext()); + assureCorrectAppLanguage(requireContext()); new AlertDialog.Builder(context) .setTitle(R.string.general_error) .setMessage(msg) @@ -799,7 +799,7 @@ public class DownloadDialog extends DialogFragment filenameTmp += "opus"; } else if (format != null) { mimeTmp = format.mimeType; - filenameTmp += format.suffix; + filenameTmp += format.getSuffix(); } break; case R.id.video_button: @@ -808,7 +808,7 @@ public class DownloadDialog extends DialogFragment format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); if (format != null) { mimeTmp = format.mimeType; - filenameTmp += format.suffix; + filenameTmp += format.getSuffix(); } break; case R.id.subtitle_button: @@ -820,9 +820,9 @@ public class DownloadDialog extends DialogFragment } if (format == MediaFormat.TTML) { - filenameTmp += MediaFormat.SRT.suffix; + filenameTmp += MediaFormat.SRT.getSuffix(); } else if (format != null) { - filenameTmp += format.suffix; + filenameTmp += format.getSuffix(); } break; default: diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 48fb81c99..04cd737b6 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.util; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -16,16 +18,20 @@ import androidx.collection.SparseArrayCompat; import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; +import java.util.stream.Collectors; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; @@ -228,6 +234,7 @@ public class StreamItemAdapter extends BaseA private final List streamsList; private final long[] streamSizes; + private final MediaFormat[] streamFormats; private final String unknownSize; public StreamInfoWrapper(@NonNull final List streamList, @@ -236,31 +243,42 @@ public class StreamItemAdapter extends BaseA this.streamSizes = new long[streamsList.size()]; this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content); - - resetSizes(); + this.streamFormats = new MediaFormat[streamsList.size()]; + resetInfo(); } /** - * Helper method to fetch the sizes of all the streams in a wrapper. + * Helper method to fetch the sizes and missing media formats + * of all the streams in a wrapper. * * @param the stream type's class extending {@link Stream} * @param streamsWrapper the wrapper * @return a {@link Single} that returns a boolean indicating if any elements were changed */ @NonNull - public static Single fetchSizeForWrapper( + public static Single fetchMoreInfoForWrapper( final StreamInfoWrapper streamsWrapper) { final Callable fetchAndSet = () -> { boolean hasChanged = false; for (final X stream : streamsWrapper.getStreamsList()) { - if (streamsWrapper.getSizeInBytes(stream) > SIZE_UNSET) { + final boolean changeSize = streamsWrapper.getSizeInBytes(stream) <= SIZE_UNSET; + final boolean changeFormat = stream.getFormat() == null; + if (!changeSize && !changeFormat) { continue; } - - final long contentLength = DownloaderImpl.getInstance().getContentLength( - stream.getContent()); - streamsWrapper.setSize(stream, contentLength); - hasChanged = true; + final Response response = DownloaderImpl.getInstance() + .head(stream.getContent()); + if (changeSize) { + final String contentLength = response.getHeader("Content-Length"); + if (!isNullOrEmpty(contentLength)) { + streamsWrapper.setSize(stream, Long.parseLong(contentLength)); + hasChanged = true; + } + } + if (changeFormat) { + hasChanged = retrieveMediaFormat(stream, streamsWrapper, response) + || hasChanged; + } } return hasChanged; }; @@ -271,8 +289,135 @@ public class StreamItemAdapter extends BaseA .onErrorReturnItem(true); } - public void resetSizes() { + /** + * Try to retrieve the {@link MediaFormat} for a stream from the request headers. + * + * @param the stream type to get the {@link MediaFormat} for + * @param stream the stream to find the {@link MediaFormat} for + * @param streamsWrapper the wrapper to store the found {@link MediaFormat} in + * @param response the response of the head request for the given stream + * @return {@code true} if the media format could be retrieved; {@code false} otherwise + */ + private static boolean retrieveMediaFormat( + @NonNull final X stream, + @NonNull final StreamInfoWrapper streamsWrapper, + @NonNull final Response response) { + return retrieveMediaFormatFromFileTypeHeaders(stream, streamsWrapper, response) + || retrieveMediaFormatFromContentDispositionHeader( + stream, streamsWrapper, response) + || retrieveMediaFormatFromContentTypeHeader(stream, streamsWrapper, response); + } + + private static boolean retrieveMediaFormatFromFileTypeHeaders( + @NonNull final X stream, + @NonNull final StreamInfoWrapper streamsWrapper, + @NonNull final Response response) { + // try to use additional headers from CDNs or servers, + // e.g. x-amz-meta-file-type (e.g. for SoundCloud) + final List keys = response.responseHeaders().keySet().stream() + .filter(k -> k.endsWith("file-type")).collect(Collectors.toList()); + if (!keys.isEmpty()) { + for (final String key : keys) { + final String suffix = response.getHeader(key); + final MediaFormat format = MediaFormat.getFromSuffix(suffix); + if (format != null) { + streamsWrapper.setFormat(stream, format); + return true; + } + } + } + return false; + } + + /** + *

Retrieve a {@link MediaFormat} from a HTTP Content-Disposition header + * for a stream and store the info in a wrapper.

+ * @see + * + * mdn Web Docs for the HTTP Content-Disposition Header + * @param stream the stream to get the {@link MediaFormat} for + * @param streamsWrapper the wrapper to store the {@link MediaFormat} in + * @param response the response to get the Content-Disposition header from + * @return {@code true} if the {@link MediaFormat} could be retrieved from the response; + * otherwise {@code false} + * @param + */ + public static boolean retrieveMediaFormatFromContentDispositionHeader( + @NonNull final X stream, + @NonNull final StreamInfoWrapper streamsWrapper, + @NonNull final Response response) { + // parse the Content-Disposition header, + // see + // there can be two filename directives + String contentDisposition = response.getHeader("Content-Disposition"); + if (contentDisposition == null) { + return false; + } + try { + contentDisposition = Utils.decodeUrlUtf8(contentDisposition); + final String[] parts = contentDisposition.split(";"); + for (String part : parts) { + final String fileName; + part = part.trim(); + + // extract the filename + if (part.startsWith("filename=")) { + // remove directive and decode + fileName = Utils.decodeUrlUtf8(part.substring(9)); + } else if (part.startsWith("filename*=")) { + fileName = Utils.decodeUrlUtf8(part.substring(10)); + } else { + continue; + } + + // extract the file extension / suffix + final String[] p = fileName.split("\\."); + String suffix = p[p.length - 1]; + if (suffix.endsWith("\"") || suffix.endsWith("'")) { + // remove trailing quotes if present, end index is exclusive + suffix = suffix.substring(0, suffix.length() - 1); + } + + // get the corresponding media format + final MediaFormat format = MediaFormat.getFromSuffix(suffix); + if (format != null) { + streamsWrapper.setFormat(stream, format); + return true; + } + } + } catch (final Exception ignored) { + // fail silently + } + return false; + } + + private static boolean retrieveMediaFormatFromContentTypeHeader( + @NonNull final X stream, + @NonNull final StreamInfoWrapper streamsWrapper, + @NonNull final Response response) { + // try to get the format by content type + // some mime types are not unique for every format, those are omitted + final List formats = MediaFormat.getAllFromMimeType( + response.getHeader("Content-Type")); + final List uniqueFormats = new ArrayList<>(formats.size()); + for (int i = 0; i < formats.size(); i++) { + final MediaFormat format = formats.get(i); + if (uniqueFormats.stream().filter(f -> f.id == format.id).count() == 0) { + uniqueFormats.add(format); + } + } + if (uniqueFormats.size() == 1) { + streamsWrapper.setFormat(stream, uniqueFormats.get(0)); + return true; + } + return false; + } + + public void resetInfo() { Arrays.fill(streamSizes, SIZE_UNSET); + for (int i = 0; i < streamsList.size(); i++) { + streamFormats[i] = streamsList.get(i).getFormat(); + } } public static StreamInfoWrapper empty() { @@ -306,5 +451,13 @@ public class StreamItemAdapter extends BaseA public void setSize(final T stream, final long sizeInBytes) { streamSizes[streamsList.indexOf(stream)] = sizeInBytes; } + + public MediaFormat getFormat(final int streamIndex) { + return streamFormats[streamIndex]; + } + + public void setFormat(final T stream, final MediaFormat format) { + streamFormats[streamsList.indexOf(stream)] = format; + } } } From e51067177edf0ad4034c330f69c2dc7c92a4df15 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Mon, 14 Aug 2023 23:05:38 +0200 Subject: [PATCH 3/5] Add tests for new methods retrieving MediaFormats Fix failing tests --- .../newpipe/util/StreamItemAdapterTest.kt | 159 ++++++++++++++++++ .../newpipe/util/StreamItemAdapter.java | 14 +- 2 files changed, 169 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt index c67564232..13c27aec9 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt @@ -12,15 +12,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import androidx.test.internal.runner.junit4.statement.UiThreadStatement import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.schabi.newpipe.R import org.schabi.newpipe.extractor.MediaFormat +import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.Stream import org.schabi.newpipe.extractor.stream.SubtitlesStream import org.schabi.newpipe.extractor.stream.VideoStream +import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper @MediumTest @RunWith(AndroidJUnit4::class) @@ -123,6 +129,101 @@ class StreamItemAdapterTest { } } + @Test + fun retrieveMediaFormatFromFileTypeHeaders() { + val streams = getIncompleteAudioStreams(5) + val wrapper = StreamInfoWrapper(streams, context) + val retrieveMediaFormat = { stream: AudioStream, response: Response -> + StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response) + } + val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat) + + helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0) + helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1) + + helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF) + helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3) + } + + @Test + fun retrieveMediaFormatFromContentDispositionHeader() { + val streams = getIncompleteAudioStreams(11) + val wrapper = StreamInfoWrapper(streams, context) + val retrieveMediaFormat = { stream: AudioStream, response: Response -> + StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response) + } + val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat) + + helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0) + helper.assertInvalidResponse( + getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1 + ) + helper.assertInvalidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2 + ) + helper.assertInvalidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3 + ) + helper.assertInvalidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4 + ) + + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))), + 5, MediaFormat.OGG + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))), + 6, MediaFormat.FLAC + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))), + 7, MediaFormat.AIFF + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))), + 8, MediaFormat.M4A + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))), + 9, MediaFormat.OPUS + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))), + 10, MediaFormat.OPUS + ) + } + + @Test + fun retrieveMediaFormatFromContentTypeHeader() { + val streams = getIncompleteAudioStreams(10) + val wrapper = StreamInfoWrapper(streams, context) + val retrieveMediaFormat = { stream: AudioStream, response: Response -> + StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response) + } + val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat) + + helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0) + helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1) + helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2) + helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3) + helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4) + helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5) + + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 6, MediaFormat.FLAC + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 7, MediaFormat.WAV + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 8, MediaFormat.OPUS + ) + helper.assertValidResponse( + getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 9, MediaFormat.AIFF + ) + } + /** * @return a list of video streams, in which their video only property mirrors the provided * [videoOnly] vararg. @@ -161,6 +262,19 @@ class StreamItemAdapterTest { } ) + private fun getIncompleteAudioStreams(size: Int): List { + val list = ArrayList(size) + for (i in 1..size) { + list.add( + AudioStream.Builder() + .setId(Stream.ID_UNKNOWN) + .setContent("https://example.com/$i", true) + .build() + ) + } + return list + } + /** * Checks whether the item at [position] in the [spinner] has the correct icon visibility when * it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list). @@ -203,4 +317,49 @@ class StreamItemAdapterTest { put(index, secondaryStreamHelper) } } + + private fun getResponse(headers: Map): Response { + val listHeaders = HashMap>() + headers.forEach { entry -> + listHeaders[entry.key] = listOf(entry.value) + } + return Response(200, null, listHeaders, "", "") + } + + /** + * Helper class for assertion related to extractions of [MediaFormat]s. + */ + class AssertionHelper( + private val streams: List, + private val wrapper: StreamInfoWrapper, + private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean + ) { + + /** + * Assert that an invalid response does not result in wrongly extracted [MediaFormat]. + */ + fun assertInvalidResponse( + response: Response, + index: Int + ) { + assertFalse( + "invalid header returns valid value", retrieveMediaFormat(streams[index], response) + ) + assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index)) + } + + /** + * Assert that a valid response results in correctly extracted and handled [MediaFormat]. + */ + fun assertValidResponse( + response: Response, + index: Int, + format: MediaFormat + ) { + assertTrue( + "header was not recognized", retrieveMediaFormat(streams[index], response) + ) + assertEquals("Wrong media format extracted", format, wrapper.getFormat(index)) + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 04cd737b6..691b39403 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -13,6 +13,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.collection.SparseArrayCompat; import org.schabi.newpipe.DownloaderImpl; @@ -298,7 +299,8 @@ public class StreamItemAdapter extends BaseA * @param response the response of the head request for the given stream * @return {@code true} if the media format could be retrieved; {@code false} otherwise */ - private static boolean retrieveMediaFormat( + @VisibleForTesting + public static boolean retrieveMediaFormat( @NonNull final X stream, @NonNull final StreamInfoWrapper streamsWrapper, @NonNull final Response response) { @@ -308,7 +310,8 @@ public class StreamItemAdapter extends BaseA || retrieveMediaFormatFromContentTypeHeader(stream, streamsWrapper, response); } - private static boolean retrieveMediaFormatFromFileTypeHeaders( + @VisibleForTesting + public static boolean retrieveMediaFormatFromFileTypeHeaders( @NonNull final X stream, @NonNull final StreamInfoWrapper streamsWrapper, @NonNull final Response response) { @@ -342,6 +345,7 @@ public class StreamItemAdapter extends BaseA * otherwise {@code false} * @param */ + @VisibleForTesting public static boolean retrieveMediaFormatFromContentDispositionHeader( @NonNull final X stream, @NonNull final StreamInfoWrapper streamsWrapper, @@ -391,7 +395,8 @@ public class StreamItemAdapter extends BaseA return false; } - private static boolean retrieveMediaFormatFromContentTypeHeader( + @VisibleForTesting + public static boolean retrieveMediaFormatFromContentTypeHeader( @NonNull final X stream, @NonNull final StreamInfoWrapper streamsWrapper, @NonNull final Response response) { @@ -416,7 +421,8 @@ public class StreamItemAdapter extends BaseA public void resetInfo() { Arrays.fill(streamSizes, SIZE_UNSET); for (int i = 0; i < streamsList.size(); i++) { - streamFormats[i] = streamsList.get(i).getFormat(); + streamFormats[i] = streamsList.get(i) == null // test for invalid streams + ? null : streamsList.get(i).getFormat(); } } From ba84e7eeadafcbdd97c8a0cbd1dd52a1e2b2dc68 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Wed, 19 Jul 2023 23:36:38 +0200 Subject: [PATCH 4/5] Display "Unknown quality" if quality is unknown and not MediaFormat name --- .../main/java/org/schabi/newpipe/util/StreamItemAdapter.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 691b39403..d5b73d0b6 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -154,8 +154,6 @@ public class StreamItemAdapter extends BaseA final AudioStream audioStream = ((AudioStream) stream); if (audioStream.getAverageBitrate() > 0) { qualityString = audioStream.getAverageBitrate() + "kbps"; - } else if (mediaFormat != null) { - qualityString = mediaFormat.getName(); } else { qualityString = context.getString(R.string.unknown_quality); } From 992bb5d7be034e57a1daed1a9c398491fc5ddf33 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 19 Sep 2023 15:33:40 +0200 Subject: [PATCH 5/5] Simplify retrieveMediaFormatFromContentTypeHeader Also check for nullity --- .../newpipe/util/StreamItemAdapterTest.kt | 12 ++++++---- .../newpipe/util/StreamItemAdapter.java | 23 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt index 13c27aec9..9b8ee211e 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt @@ -196,7 +196,7 @@ class StreamItemAdapterTest { @Test fun retrieveMediaFormatFromContentTypeHeader() { - val streams = getIncompleteAudioStreams(10) + val streams = getIncompleteAudioStreams(12) val wrapper = StreamInfoWrapper(streams, context) val retrieveMediaFormat = { stream: AudioStream, response: Response -> StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response) @@ -209,18 +209,20 @@ class StreamItemAdapterTest { helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5) + helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6) + helper.assertInvalidResponse(getResponse(mapOf()), 7) helper.assertValidResponse( - getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 6, MediaFormat.FLAC + getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC ) helper.assertValidResponse( - getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 7, MediaFormat.WAV + getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV ) helper.assertValidResponse( - getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 8, MediaFormat.OPUS + getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS ) helper.assertValidResponse( - getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 9, MediaFormat.AIFF + getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF ) } diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index d5b73d0b6..2eeb14b1b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -27,7 +27,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.Utils; import java.io.Serializable; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -400,17 +399,21 @@ public class StreamItemAdapter extends BaseA @NonNull final Response response) { // try to get the format by content type // some mime types are not unique for every format, those are omitted - final List formats = MediaFormat.getAllFromMimeType( - response.getHeader("Content-Type")); - final List uniqueFormats = new ArrayList<>(formats.size()); - for (int i = 0; i < formats.size(); i++) { - final MediaFormat format = formats.get(i); - if (uniqueFormats.stream().filter(f -> f.id == format.id).count() == 0) { - uniqueFormats.add(format); + final String contentTypeHeader = response.getHeader("Content-Type"); + if (contentTypeHeader == null) { + return false; + } + + @Nullable MediaFormat foundFormat = null; + for (final MediaFormat format : MediaFormat.getAllFromMimeType(contentTypeHeader)) { + if (foundFormat == null) { + foundFormat = format; + } else if (foundFormat.id != format.id) { + return false; } } - if (uniqueFormats.size() == 1) { - streamsWrapper.setFormat(stream, uniqueFormats.get(0)); + if (foundFormat != null) { + streamsWrapper.setFormat(stream, foundFormat); return true; } return false;