mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07: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,46 +225,58 @@ 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); | ||||
|                     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,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
	 Stypox
					Stypox