mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-11-04 09:13:00 +00:00 
			
		
		
		
	Merge pull request #10165 from TeamNewPipe/fix/media-format
Fix downloads of streams with missing MediaFormat
This commit is contained in:
		@@ -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)
 | 
			
		||||
@@ -84,7 +90,7 @@ class StreamItemAdapterTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun subtitleStreams_noIcon() {
 | 
			
		||||
        val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
 | 
			
		||||
            StreamItemAdapter.StreamSizeWrapper(
 | 
			
		||||
            StreamItemAdapter.StreamInfoWrapper(
 | 
			
		||||
                (0 until 5).map {
 | 
			
		||||
                    SubtitlesStream.Builder()
 | 
			
		||||
                        .setContent("https://example.com", true)
 | 
			
		||||
@@ -105,7 +111,7 @@ class StreamItemAdapterTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun audioStreams_noIcon() {
 | 
			
		||||
        val adapter = StreamItemAdapter<AudioStream, Stream>(
 | 
			
		||||
            StreamItemAdapter.StreamSizeWrapper(
 | 
			
		||||
            StreamItemAdapter.StreamInfoWrapper(
 | 
			
		||||
                (0 until 5).map {
 | 
			
		||||
                    AudioStream.Builder()
 | 
			
		||||
                        .setId(Stream.ID_UNKNOWN)
 | 
			
		||||
@@ -123,12 +129,109 @@ 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(12)
 | 
			
		||||
        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.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
 | 
			
		||||
        helper.assertInvalidResponse(getResponse(mapOf()), 7)
 | 
			
		||||
 | 
			
		||||
        helper.assertValidResponse(
 | 
			
		||||
            getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
 | 
			
		||||
        )
 | 
			
		||||
        helper.assertValidResponse(
 | 
			
		||||
            getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
 | 
			
		||||
        )
 | 
			
		||||
        helper.assertValidResponse(
 | 
			
		||||
            getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
 | 
			
		||||
        )
 | 
			
		||||
        helper.assertValidResponse(
 | 
			
		||||
            getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return a list of video streams, in which their video only property mirrors the provided
 | 
			
		||||
     * [videoOnly] vararg.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getVideoStreams(vararg videoOnly: Boolean) =
 | 
			
		||||
        StreamItemAdapter.StreamSizeWrapper(
 | 
			
		||||
        StreamItemAdapter.StreamInfoWrapper(
 | 
			
		||||
            videoOnly.map {
 | 
			
		||||
                VideoStream.Builder()
 | 
			
		||||
                    .setId(Stream.ID_UNKNOWN)
 | 
			
		||||
@@ -161,6 +264,19 @@ class StreamItemAdapterTest {
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
 | 
			
		||||
        val list = ArrayList<AudioStream>(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).
 | 
			
		||||
@@ -196,11 +312,56 @@ class StreamItemAdapterTest {
 | 
			
		||||
            streams.forEachIndexed { index, stream ->
 | 
			
		||||
                val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
 | 
			
		||||
                    SecondaryStreamHelper(
 | 
			
		||||
                        StreamItemAdapter.StreamSizeWrapper(streams, context),
 | 
			
		||||
                        StreamItemAdapter.StreamInfoWrapper(streams, context),
 | 
			
		||||
                        it
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                put(index, secondaryStreamHelper)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private fun getResponse(headers: Map<String, String>): Response {
 | 
			
		||||
        val listHeaders = HashMap<String, List<String>>()
 | 
			
		||||
        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<T : Stream>(
 | 
			
		||||
        private val streams: List<T>,
 | 
			
		||||
        private val wrapper: StreamInfoWrapper<T>,
 | 
			
		||||
        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))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<VideoStream> wrappedVideoStreams;
 | 
			
		||||
    StreamInfoWrapper<VideoStream> wrappedVideoStreams;
 | 
			
		||||
    @State
 | 
			
		||||
    StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
 | 
			
		||||
    StreamInfoWrapper<SubtitlesStream> 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<AudioStream> audioStreams = getWrappedAudioStreams();
 | 
			
		||||
        final StreamInfoWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
 | 
			
		||||
        final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
 | 
			
		||||
        final List<VideoStream> 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<AudioStream> getWrappedAudioStreams() {
 | 
			
		||||
    private StreamInfoWrapper<AudioStream> getWrappedAudioStreams() {
 | 
			
		||||
        if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
 | 
			
		||||
            return StreamSizeWrapper.empty();
 | 
			
		||||
            return StreamInfoWrapper.empty();
 | 
			
		||||
        }
 | 
			
		||||
        return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
 | 
			
		||||
    }
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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<StreamSizeWrapper<AudioStream>> tracksList;
 | 
			
		||||
        private final List<StreamInfoWrapper<AudioStream>> tracksList;
 | 
			
		||||
 | 
			
		||||
        public AudioTracksWrapper(@NonNull final List<List<AudioStream>> 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<StreamSizeWrapper<AudioStream>> getTracksList() {
 | 
			
		||||
        public List<StreamInfoWrapper<AudioStream>> getTracksList() {
 | 
			
		||||
            return tracksList;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<T extends Stream> {
 | 
			
		||||
    private final int position;
 | 
			
		||||
    private final StreamSizeWrapper<T> streams;
 | 
			
		||||
    private final StreamInfoWrapper<T> streams;
 | 
			
		||||
 | 
			
		||||
    public SecondaryStreamHelper(@NonNull final StreamSizeWrapper<T> streams,
 | 
			
		||||
    public SecondaryStreamHelper(@NonNull final StreamInfoWrapper<T> streams,
 | 
			
		||||
                                 final T selectedStream) {
 | 
			
		||||
        this.streams = streams;
 | 
			
		||||
        this.position = streams.getStreamsList().indexOf(selectedStream);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -11,21 +13,25 @@ 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;
 | 
			
		||||
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.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;
 | 
			
		||||
@@ -41,7 +47,7 @@ import us.shandian.giga.util.Utility;
 | 
			
		||||
 */
 | 
			
		||||
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
 | 
			
		||||
    @NonNull
 | 
			
		||||
    private final StreamSizeWrapper<T> streamsWrapper;
 | 
			
		||||
    private final StreamInfoWrapper<T> streamsWrapper;
 | 
			
		||||
    @NonNull
 | 
			
		||||
    private final SparseArrayCompat<SecondaryStreamHelper<U>> secondaryStreams;
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +59,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
 | 
			
		||||
    private final boolean hasAnyVideoOnlyStreamWithNoSecondaryStream;
 | 
			
		||||
 | 
			
		||||
    public StreamItemAdapter(
 | 
			
		||||
            @NonNull final StreamSizeWrapper<T> streamsWrapper,
 | 
			
		||||
            @NonNull final StreamInfoWrapper<T> streamsWrapper,
 | 
			
		||||
            @NonNull final SparseArrayCompat<SecondaryStreamHelper<U>> secondaryStreams
 | 
			
		||||
    ) {
 | 
			
		||||
        this.streamsWrapper = streamsWrapper;
 | 
			
		||||
@@ -63,7 +69,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
 | 
			
		||||
                checkHasAnyVideoOnlyStreamWithNoSecondaryStream();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public StreamItemAdapter(final StreamSizeWrapper<T> streamsWrapper) {
 | 
			
		||||
    public StreamItemAdapter(final StreamInfoWrapper<T> streamsWrapper) {
 | 
			
		||||
        this(streamsWrapper, new SparseArrayCompat<>(0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -121,7 +127,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> 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;
 | 
			
		||||
@@ -147,8 +153,6 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> 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);
 | 
			
		||||
            }
 | 
			
		||||
@@ -221,47 +225,59 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
 | 
			
		||||
     *
 | 
			
		||||
     * @param <T> the stream type's class extending {@link Stream}
 | 
			
		||||
     */
 | 
			
		||||
    public static class StreamSizeWrapper<T extends Stream> implements Serializable {
 | 
			
		||||
        private static final StreamSizeWrapper<Stream> EMPTY =
 | 
			
		||||
                new StreamSizeWrapper<>(Collections.emptyList(), null);
 | 
			
		||||
    public static class StreamInfoWrapper<T extends Stream> implements Serializable {
 | 
			
		||||
        private static final StreamInfoWrapper<Stream> EMPTY =
 | 
			
		||||
                new StreamInfoWrapper<>(Collections.emptyList(), null);
 | 
			
		||||
        private static final int SIZE_UNSET = -2;
 | 
			
		||||
 | 
			
		||||
        private final List<T> streamsList;
 | 
			
		||||
        private final long[] streamSizes;
 | 
			
		||||
        private final MediaFormat[] streamFormats;
 | 
			
		||||
        private final String unknownSize;
 | 
			
		||||
 | 
			
		||||
        public StreamSizeWrapper(@NonNull final List<T> streamList,
 | 
			
		||||
        public StreamInfoWrapper(@NonNull final List<T> streamList,
 | 
			
		||||
                                 @Nullable final Context context) {
 | 
			
		||||
            this.streamsList = streamList;
 | 
			
		||||
            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 <X> 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 <X extends Stream> Single<Boolean> fetchSizeForWrapper(
 | 
			
		||||
                final StreamSizeWrapper<X> streamsWrapper) {
 | 
			
		||||
        public static <X extends Stream> Single<Boolean> fetchMoreInfoForWrapper(
 | 
			
		||||
                final StreamInfoWrapper<X> streamsWrapper) {
 | 
			
		||||
            final Callable<Boolean> 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);
 | 
			
		||||
                    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,13 +287,149 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
 | 
			
		||||
                    .onErrorReturnItem(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void resetSizes() {
 | 
			
		||||
            Arrays.fill(streamSizes, SIZE_UNSET);
 | 
			
		||||
        /**
 | 
			
		||||
         * Try to retrieve the {@link MediaFormat} for a stream from the request headers.
 | 
			
		||||
         *
 | 
			
		||||
         * @param <X>            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
 | 
			
		||||
         */
 | 
			
		||||
        @VisibleForTesting
 | 
			
		||||
        public static <X extends Stream> boolean retrieveMediaFormat(
 | 
			
		||||
                @NonNull final X stream,
 | 
			
		||||
                @NonNull final StreamInfoWrapper<X> streamsWrapper,
 | 
			
		||||
                @NonNull final Response response) {
 | 
			
		||||
            return retrieveMediaFormatFromFileTypeHeaders(stream, streamsWrapper, response)
 | 
			
		||||
                    || retrieveMediaFormatFromContentDispositionHeader(
 | 
			
		||||
                            stream, streamsWrapper, response)
 | 
			
		||||
                    || retrieveMediaFormatFromContentTypeHeader(stream, streamsWrapper, response);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static <X extends Stream> StreamSizeWrapper<X> empty() {
 | 
			
		||||
        @VisibleForTesting
 | 
			
		||||
        public static <X extends Stream> boolean retrieveMediaFormatFromFileTypeHeaders(
 | 
			
		||||
                @NonNull final X stream,
 | 
			
		||||
                @NonNull final StreamInfoWrapper<X> 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<String> 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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * <p>Retrieve a {@link MediaFormat} from a HTTP Content-Disposition header
 | 
			
		||||
         * for a stream and store the info in a wrapper.</p>
 | 
			
		||||
         * @see
 | 
			
		||||
         * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition">
 | 
			
		||||
         *     mdn Web Docs for the HTTP Content-Disposition Header</a>
 | 
			
		||||
         * @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 <X>
 | 
			
		||||
         */
 | 
			
		||||
        @VisibleForTesting
 | 
			
		||||
        public static <X extends Stream> boolean retrieveMediaFormatFromContentDispositionHeader(
 | 
			
		||||
                @NonNull final X stream,
 | 
			
		||||
                @NonNull final StreamInfoWrapper<X> 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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @VisibleForTesting
 | 
			
		||||
        public static <X extends Stream> boolean retrieveMediaFormatFromContentTypeHeader(
 | 
			
		||||
                @NonNull final X stream,
 | 
			
		||||
                @NonNull final StreamInfoWrapper<X> 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 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 (foundFormat != null) {
 | 
			
		||||
                streamsWrapper.setFormat(stream, foundFormat);
 | 
			
		||||
                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) == null // test for invalid streams
 | 
			
		||||
                        ? null : streamsList.get(i).getFormat();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static <X extends Stream> StreamInfoWrapper<X> empty() {
 | 
			
		||||
            //noinspection unchecked
 | 
			
		||||
            return (StreamSizeWrapper<X>) EMPTY;
 | 
			
		||||
            return (StreamInfoWrapper<X>) EMPTY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public List<T> getStreamsList() {
 | 
			
		||||
@@ -306,5 +458,13 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> 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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user