Merge branch 'dev' into pr2335
This commit is contained in:
commit
6ead95b5d2
423 changed files with 3413 additions and 4301 deletions
|
|
@ -1,44 +0,0 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class BitmapUtils {
|
||||
private BitmapUtils() { }
|
||||
|
||||
@Nullable
|
||||
public static Bitmap centerCrop(final Bitmap inputBitmap, final int newWidth,
|
||||
final int newHeight) {
|
||||
if (inputBitmap == null || inputBitmap.isRecycled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final float sourceWidth = inputBitmap.getWidth();
|
||||
final float sourceHeight = inputBitmap.getHeight();
|
||||
|
||||
final float xScale = newWidth / sourceWidth;
|
||||
final float yScale = newHeight / sourceHeight;
|
||||
|
||||
final float newXScale;
|
||||
final float newYScale;
|
||||
|
||||
if (yScale > xScale) {
|
||||
newXScale = xScale / yScale;
|
||||
newYScale = 1.0f;
|
||||
} else {
|
||||
newXScale = 1.0f;
|
||||
newYScale = yScale / xScale;
|
||||
}
|
||||
|
||||
final float scaledWidth = newXScale * sourceWidth;
|
||||
final float scaledHeight = newYScale * sourceHeight;
|
||||
|
||||
final int left = (int) ((sourceWidth - scaledWidth) / 2);
|
||||
final int top = (int) ((sourceHeight - scaledHeight) / 2);
|
||||
final int width = (int) scaledWidth;
|
||||
final int height = (int) scaledHeight;
|
||||
|
||||
return Bitmap.createBitmap(inputBitmap, left, top, width, height);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
|
@ -30,7 +32,6 @@ import androidx.preference.PreferenceManager;
|
|||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
|
|
@ -41,6 +42,7 @@ import org.schabi.newpipe.extractor.Page;
|
|||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
||||
import org.schabi.newpipe.extractor.feed.FeedInfo;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
|
|
@ -49,6 +51,7 @@ import org.schabi.newpipe.extractor.search.SearchInfo;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -57,8 +60,6 @@ import io.reactivex.rxjava3.core.Maybe;
|
|||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public final class ExtractorHelper {
|
||||
private static final String TAG = ExtractorHelper.class.getSimpleName();
|
||||
private static final InfoCache CACHE = InfoCache.getInstance();
|
||||
|
|
@ -84,11 +85,12 @@ public final class ExtractorHelper {
|
|||
.fromQuery(searchString, contentFilter, sortFilter)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreSearchItems(final int serviceId,
|
||||
final String searchString,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter,
|
||||
final Page page) {
|
||||
public static Single<InfoItemsPage<InfoItem>> getMoreSearchItems(
|
||||
final int serviceId,
|
||||
final String searchString,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter,
|
||||
final Page page) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
SearchInfo.getMoreItems(NewPipe.getService(serviceId),
|
||||
|
|
@ -124,8 +126,9 @@ public final class ExtractorHelper {
|
|||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreChannelItems(final int serviceId, final String url,
|
||||
final Page nextPage) {
|
||||
public static Single<InfoItemsPage<StreamInfoItem>> getMoreChannelItems(final int serviceId,
|
||||
final String url,
|
||||
final Page nextPage) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||
|
|
@ -155,15 +158,17 @@ public final class ExtractorHelper {
|
|||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
|
||||
final CommentsInfo info,
|
||||
final Page nextPage) {
|
||||
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
||||
final int serviceId,
|
||||
final CommentsInfo info,
|
||||
final Page nextPage) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
||||
}
|
||||
|
||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId, final String url,
|
||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||
final String url,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
||||
|
|
@ -171,8 +176,9 @@ public final class ExtractorHelper {
|
|||
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMorePlaylistItems(final int serviceId, final String url,
|
||||
final Page nextPage) {
|
||||
public static Single<InfoItemsPage<StreamInfoItem>> getMorePlaylistItems(final int serviceId,
|
||||
final String url,
|
||||
final Page nextPage) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||
|
|
@ -184,8 +190,9 @@ public final class ExtractorHelper {
|
|||
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreKioskItems(final int serviceId, final String url,
|
||||
final Page nextPage) {
|
||||
public static Single<InfoItemsPage<StreamInfoItem>> getMoreKioskItems(final int serviceId,
|
||||
final String url,
|
||||
final Page nextPage) {
|
||||
return Single.fromCallable(() ->
|
||||
KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public final class KioskTranslator {
|
|||
}
|
||||
}
|
||||
|
||||
public static int getKioskIcon(final String kioskId, final Context c) {
|
||||
public static int getKioskIcon(final String kioskId) {
|
||||
switch (kioskId) {
|
||||
case "Trending":
|
||||
case "Top 50":
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class LayoutManagerSmoothScroller extends LinearLayoutManager {
|
||||
public LayoutManagerSmoothScroller(final Context context) {
|
||||
super(context, VERTICAL, false);
|
||||
}
|
||||
|
||||
public LayoutManagerSmoothScroller(final Context context, final int orientation,
|
||||
final boolean reverseLayout) {
|
||||
super(context, orientation, reverseLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void smoothScrollToPosition(final RecyclerView recyclerView,
|
||||
final RecyclerView.State state, final int position) {
|
||||
final RecyclerView.SmoothScroller smoothScroller
|
||||
= new TopSnappedSmoothScroller(recyclerView.getContext());
|
||||
smoothScroller.setTargetPosition(position);
|
||||
startSmoothScroll(smoothScroller);
|
||||
}
|
||||
|
||||
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
|
||||
TopSnappedSmoothScroller(final Context context) {
|
||||
super(context);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PointF computeScrollVectorForPosition(final int targetPosition) {
|
||||
return LayoutManagerSmoothScroller.this
|
||||
.computeScrollVectorForPosition(targetPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getVerticalSnapPreference() {
|
||||
return SNAP_TO_START;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
|
@ -19,7 +20,11 @@ import java.util.Arrays;
|
|||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class ListHelper {
|
||||
// Video format in order of quality. 0=lowest quality, n=highest quality
|
||||
|
|
@ -33,8 +38,9 @@ public final class ListHelper {
|
|||
private static final List<MediaFormat> AUDIO_FORMAT_EFFICIENCY_RANKING =
|
||||
Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3);
|
||||
|
||||
private static final List<String> HIGH_RESOLUTION_LIST
|
||||
= Arrays.asList("1440p", "2160p", "1440p60", "2160p60");
|
||||
private static final Set<String> HIGH_RESOLUTION_LIST
|
||||
// Uses a HashSet for better performance
|
||||
= new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60"));
|
||||
|
||||
private ListHelper() { }
|
||||
|
||||
|
|
@ -108,17 +114,21 @@ public final class ListHelper {
|
|||
* Join the two lists of video streams (video_only and normal videos),
|
||||
* and sort them according with default format chosen by the user.
|
||||
*
|
||||
* @param context context to search for the format to give preference
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param context the context to search for the format to give preference
|
||||
* @param videoStreams the normal videos list
|
||||
* @param videoOnlyStreams the video-only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||
* streams and normal video streams are available
|
||||
* @return the sorted list
|
||||
*/
|
||||
public static List<VideoStream> getSortedStreamVideosList(final Context context,
|
||||
final List<VideoStream> videoStreams,
|
||||
final List<VideoStream>
|
||||
videoOnlyStreams,
|
||||
final boolean ascendingOrder) {
|
||||
@NonNull
|
||||
public static List<VideoStream> getSortedStreamVideosList(
|
||||
@NonNull final Context context,
|
||||
@Nullable final List<VideoStream> videoStreams,
|
||||
@Nullable final List<VideoStream> videoOnlyStreams,
|
||||
final boolean ascendingOrder,
|
||||
final boolean preferVideoOnlyStreams) {
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
|
|
@ -128,7 +138,7 @@ public final class ListHelper {
|
|||
R.string.default_video_format_key, R.string.default_video_format_value);
|
||||
|
||||
return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams,
|
||||
videoOnlyStreams, ascendingOrder);
|
||||
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -192,56 +202,55 @@ public final class ListHelper {
|
|||
* Join the two lists of video streams (video_only and normal videos),
|
||||
* and sort them according with default format chosen by the user.
|
||||
*
|
||||
* @param defaultFormat format to give preference
|
||||
* @param showHigherResolutions show >1080p resolutions
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param defaultFormat format to give preference
|
||||
* @param showHigherResolutions show >1080p resolutions
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||
* streams and normal video streams are available
|
||||
* @return the sorted list
|
||||
*/
|
||||
static List<VideoStream> getSortedStreamVideosList(final MediaFormat defaultFormat,
|
||||
final boolean showHigherResolutions,
|
||||
final List<VideoStream> videoStreams,
|
||||
final List<VideoStream> videoOnlyStreams,
|
||||
final boolean ascendingOrder) {
|
||||
final ArrayList<VideoStream> retList = new ArrayList<>();
|
||||
@NonNull
|
||||
static List<VideoStream> getSortedStreamVideosList(
|
||||
@Nullable final MediaFormat defaultFormat,
|
||||
final boolean showHigherResolutions,
|
||||
@Nullable final List<VideoStream> videoStreams,
|
||||
@Nullable final List<VideoStream> videoOnlyStreams,
|
||||
final boolean ascendingOrder,
|
||||
final boolean preferVideoOnlyStreams
|
||||
) {
|
||||
// Determine order of streams
|
||||
// The last added list is preferred
|
||||
final List<List<VideoStream>> videoStreamsOrdered =
|
||||
preferVideoOnlyStreams
|
||||
? Arrays.asList(videoStreams, videoOnlyStreams)
|
||||
: Arrays.asList(videoOnlyStreams, videoStreams);
|
||||
|
||||
final List<VideoStream> allInitialStreams = videoStreamsOrdered.stream()
|
||||
// Ignore lists that are null
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
// Filter out higher resolutions (or not if high resolutions should always be shown)
|
||||
.filter(stream -> showHigherResolutions
|
||||
|| !HIGH_RESOLUTION_LIST.contains(stream.getResolution()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final HashMap<String, VideoStream> hashMap = new HashMap<>();
|
||||
|
||||
if (videoOnlyStreams != null) {
|
||||
for (final VideoStream stream : videoOnlyStreams) {
|
||||
if (!showHigherResolutions
|
||||
&& HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
|
||||
continue;
|
||||
}
|
||||
retList.add(stream);
|
||||
}
|
||||
}
|
||||
if (videoStreams != null) {
|
||||
for (final VideoStream stream : videoStreams) {
|
||||
if (!showHigherResolutions
|
||||
&& HIGH_RESOLUTION_LIST.contains(stream.getResolution())) {
|
||||
continue;
|
||||
}
|
||||
retList.add(stream);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all to the hashmap
|
||||
for (final VideoStream videoStream : retList) {
|
||||
for (final VideoStream videoStream : allInitialStreams) {
|
||||
hashMap.put(videoStream.getResolution(), videoStream);
|
||||
}
|
||||
|
||||
// Override the values when the key == resolution, with the defaultFormat
|
||||
for (final VideoStream videoStream : retList) {
|
||||
for (final VideoStream videoStream : allInitialStreams) {
|
||||
if (videoStream.getFormat() == defaultFormat) {
|
||||
hashMap.put(videoStream.getResolution(), videoStream);
|
||||
}
|
||||
}
|
||||
|
||||
retList.clear();
|
||||
retList.addAll(hashMap.values());
|
||||
sortStreamList(retList, ascendingOrder);
|
||||
return retList;
|
||||
// Return the sorted list
|
||||
return sortStreamList(new ArrayList<>(hashMap.values()), ascendingOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -257,16 +266,18 @@ public final class ListHelper {
|
|||
* 1080p -> 1080
|
||||
* 1080p60 -> 1081
|
||||
* <br>
|
||||
* ascendingOrder ? 360 < 720 < 721 < 1080 < 1081
|
||||
* !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360</pre></blockquote>
|
||||
* ascendingOrder ? 360 < 720 < 721 < 1080 < 1081
|
||||
* !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360</pre></blockquote>
|
||||
*
|
||||
* @param videoStreams list that the sorting will be applied
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @return The sorted list (same reference as parameter videoStreams)
|
||||
*/
|
||||
private static void sortStreamList(final List<VideoStream> videoStreams,
|
||||
final boolean ascendingOrder) {
|
||||
private static List<VideoStream> sortStreamList(final List<VideoStream> videoStreams,
|
||||
final boolean ascendingOrder) {
|
||||
final Comparator<VideoStream> comparator = ListHelper::compareVideoStreamResolution;
|
||||
Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed());
|
||||
return videoStreams;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -277,28 +288,12 @@ public final class ListHelper {
|
|||
* @param audioStreams List of audio streams
|
||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
||||
*/
|
||||
static int getHighestQualityAudioIndex(@Nullable MediaFormat format,
|
||||
final List<AudioStream> audioStreams) {
|
||||
int result = -1;
|
||||
if (audioStreams != null) {
|
||||
while (result == -1) {
|
||||
AudioStream prevStream = null;
|
||||
for (int idx = 0; idx < audioStreams.size(); idx++) {
|
||||
final AudioStream stream = audioStreams.get(idx);
|
||||
if ((format == null || stream.getFormat() == format)
|
||||
&& (prevStream == null || compareAudioStreamBitrate(prevStream, stream,
|
||||
AUDIO_FORMAT_QUALITY_RANKING) < 0)) {
|
||||
prevStream = stream;
|
||||
result = idx;
|
||||
}
|
||||
}
|
||||
if (result == -1 && format == null) {
|
||||
break;
|
||||
}
|
||||
format = null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
static int getHighestQualityAudioIndex(@Nullable final MediaFormat format,
|
||||
@Nullable final List<AudioStream> audioStreams) {
|
||||
return getAudioIndexByHighestRank(format, audioStreams,
|
||||
// Compares descending (last = highest rank)
|
||||
(s1, s2) -> compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_QUALITY_RANKING)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -309,28 +304,47 @@ public final class ListHelper {
|
|||
* @param audioStreams List of audio streams
|
||||
* @return Index of audio stream that produces the most compact results or -1 if not found
|
||||
*/
|
||||
static int getMostCompactAudioIndex(@Nullable MediaFormat format,
|
||||
final List<AudioStream> audioStreams) {
|
||||
int result = -1;
|
||||
if (audioStreams != null) {
|
||||
while (result == -1) {
|
||||
AudioStream prevStream = null;
|
||||
for (int idx = 0; idx < audioStreams.size(); idx++) {
|
||||
final AudioStream stream = audioStreams.get(idx);
|
||||
if ((format == null || stream.getFormat() == format)
|
||||
&& (prevStream == null || compareAudioStreamBitrate(prevStream, stream,
|
||||
AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) {
|
||||
prevStream = stream;
|
||||
result = idx;
|
||||
}
|
||||
}
|
||||
if (result == -1 && format == null) {
|
||||
break;
|
||||
}
|
||||
format = null;
|
||||
}
|
||||
static int getMostCompactAudioIndex(@Nullable final MediaFormat format,
|
||||
@Nullable final List<AudioStream> audioStreams) {
|
||||
|
||||
return getAudioIndexByHighestRank(format, audioStreams,
|
||||
// The "-" is important -> Compares ascending (first = highest rank)
|
||||
(s1, s2) -> -compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_EFFICIENCY_RANKING)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio-stream from the list with the highest rank, depending on the comparator.
|
||||
* Format will be ignored if it yields no results.
|
||||
*
|
||||
* @param targetedFormat The target format type or null if it doesn't matter
|
||||
* @param audioStreams List of audio streams
|
||||
* @param comparator The comparator used for determining the max/best/highest ranked value
|
||||
* @return Index of audio stream that produces the highest ranked result or -1 if not found
|
||||
*/
|
||||
private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat,
|
||||
@Nullable final List<AudioStream> audioStreams,
|
||||
final Comparator<AudioStream> comparator) {
|
||||
if (audioStreams == null || audioStreams.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
|
||||
final AudioStream highestRankedAudioStream = audioStreams.stream()
|
||||
.filter(audioStream -> targetedFormat == null
|
||||
|| audioStream.getFormat() == targetedFormat)
|
||||
.max(comparator)
|
||||
.orElse(null);
|
||||
|
||||
if (highestRankedAudioStream == null) {
|
||||
// Fallback: Ignore targetedFormat if not null
|
||||
if (targetedFormat != null) {
|
||||
return getAudioIndexByHighestRank(null, audioStreams, comparator);
|
||||
}
|
||||
// targetedFormat is already null -> return -1
|
||||
return -1;
|
||||
}
|
||||
|
||||
return audioStreams.indexOf(highestRankedAudioStream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
|
|
@ -214,7 +215,8 @@ public final class NavigationHelper {
|
|||
// External Players
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static void playOnExternalAudioPlayer(final Context context, final StreamInfo info) {
|
||||
public static void playOnExternalAudioPlayer(@NonNull final Context context,
|
||||
@NonNull final StreamInfo info) {
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
||||
|
||||
if (index == -1) {
|
||||
|
|
@ -226,9 +228,11 @@ public final class NavigationHelper {
|
|||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream);
|
||||
}
|
||||
|
||||
public static void playOnExternalVideoPlayer(final Context context, final StreamInfo info) {
|
||||
public static void playOnExternalVideoPlayer(@NonNull final Context context,
|
||||
@NonNull final StreamInfo info) {
|
||||
final ArrayList<VideoStream> videoStreamsList = new ArrayList<>(
|
||||
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false));
|
||||
ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false,
|
||||
false));
|
||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
|
||||
|
||||
if (index == -1) {
|
||||
|
|
@ -240,8 +244,10 @@ public final class NavigationHelper {
|
|||
playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream);
|
||||
}
|
||||
|
||||
public static void playOnExternalPlayer(final Context context, final String name,
|
||||
final String artist, final Stream stream) {
|
||||
public static void playOnExternalPlayer(@NonNull final Context context,
|
||||
@Nullable final String name,
|
||||
@Nullable final String artist,
|
||||
@NonNull final Stream stream) {
|
||||
final Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType());
|
||||
|
|
@ -253,7 +259,8 @@ public final class NavigationHelper {
|
|||
resolveActivityOrAskToInstall(context, intent);
|
||||
}
|
||||
|
||||
public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) {
|
||||
public static void resolveActivityOrAskToInstall(@NonNull final Context context,
|
||||
@NonNull final Intent intent) {
|
||||
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||
ShareUtils.openIntentInApp(context, intent, false);
|
||||
} else {
|
||||
|
|
@ -402,6 +409,15 @@ public final class NavigationHelper {
|
|||
.commit();
|
||||
}
|
||||
|
||||
public static void openChannelFragment(@NonNull final Fragment fragment,
|
||||
@NonNull final StreamInfoItem item,
|
||||
final String uploaderUrl) {
|
||||
// For some reason `getParentFragmentManager()` doesn't work, but this does.
|
||||
openChannelFragment(
|
||||
fragment.requireActivity().getSupportFragmentManager(),
|
||||
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
||||
}
|
||||
|
||||
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
||||
final int serviceId, final String url,
|
||||
@NonNull final String name) {
|
||||
|
|
|
|||
116
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
116
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package org.schabi.newpipe.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.Signature
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object ReleaseVersionUtil {
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private const val RELEASE_CERT_PUBLIC_KEY_SHA1 =
|
||||
"B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"
|
||||
|
||||
@JvmStatic
|
||||
fun isReleaseApk(): Boolean {
|
||||
return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
private val certificateSHA1Fingerprint: String
|
||||
get() {
|
||||
val app = App.getApp()
|
||||
val signatures: List<Signature> = try {
|
||||
PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
showRequestError(app, e, "Could not find package info")
|
||||
return ""
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val x509cert = try {
|
||||
val cert = signatures[0].toByteArray()
|
||||
val input: InputStream = ByteArrayInputStream(cert)
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
cf.generateCertificate(input) as X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
showRequestError(app, e, "Certificate error")
|
||||
return ""
|
||||
}
|
||||
|
||||
return try {
|
||||
val md = MessageDigest.getInstance("SHA1")
|
||||
val publicKey = md.digest(x509cert.encoded)
|
||||
byte2HexFormatted(publicKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||
""
|
||||
} catch (e: CertificateEncodingException) {
|
||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun byte2HexFormatted(arr: ByteArray): String {
|
||||
val str = StringBuilder(arr.size * 2)
|
||||
for (i in arr.indices) {
|
||||
var h = Integer.toHexString(arr[i].toInt())
|
||||
val l = h.length
|
||||
if (l == 1) {
|
||||
h = "0$h"
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l)
|
||||
}
|
||||
str.append(h.uppercase())
|
||||
if (i < arr.size - 1) {
|
||||
str.append(':')
|
||||
}
|
||||
}
|
||||
return str.toString()
|
||||
}
|
||||
|
||||
private fun showRequestError(app: App, e: Exception, request: String) {
|
||||
createNotification(
|
||||
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
|
||||
)
|
||||
}
|
||||
|
||||
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
|
||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce expiry date time in between 6 hours and 72 hours from now
|
||||
*
|
||||
* @return Epoch second of expiry date time
|
||||
*/
|
||||
fun coerceUpdateCheckExpiry(expiryString: String?): Long {
|
||||
val now = ZonedDateTime.now()
|
||||
return expiryString?.let {
|
||||
var expiry =
|
||||
ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
||||
expiry = maxOf(expiry, now.plusHours(6))
|
||||
expiry = minOf(expiry, now.plusHours(72))
|
||||
expiry.toEpochSecond()
|
||||
} ?: now.plusHours(6).toEpochSecond()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Utility class for putting the uploader url into the database - when required.
|
||||
*/
|
||||
public final class SaveUploaderUrlHelper {
|
||||
private SaveUploaderUrlHelper() {
|
||||
}
|
||||
|
||||
// Public functions which call the function that does
|
||||
// the actual work with the correct parameters
|
||||
public static void saveUploaderUrlIfNeeded(@NonNull final Fragment fragment,
|
||||
@NonNull final StreamInfoItem infoItem,
|
||||
@NonNull final SaveUploaderUrlCallback callback) {
|
||||
saveUploaderUrlIfNeeded(fragment.requireContext(),
|
||||
infoItem.getServiceId(),
|
||||
infoItem.getUrl(),
|
||||
infoItem.getUploaderUrl(),
|
||||
callback);
|
||||
}
|
||||
public static void saveUploaderUrlIfNeeded(@NonNull final Context context,
|
||||
@NonNull final PlayQueueItem queueItem,
|
||||
@NonNull final SaveUploaderUrlCallback callback) {
|
||||
saveUploaderUrlIfNeeded(context,
|
||||
queueItem.getServiceId(),
|
||||
queueItem.getUrl(),
|
||||
queueItem.getUploaderUrl(),
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and saves the uploaderUrl if it is empty (meaning that it does
|
||||
* not exist in the video item). The callback is called with either the
|
||||
* fetched uploaderUrl, or the already saved uploaderUrl, but it is always
|
||||
* called with a valid uploaderUrl that can be used to show channel details.
|
||||
*
|
||||
* @param context Context
|
||||
* @param serviceId The serviceId of the item
|
||||
* @param url The item url
|
||||
* @param uploaderUrl The uploaderUrl of the item, if null or empty, it
|
||||
* will be fetched using the item url.
|
||||
* @param callback The callback that returns the fetched or existing
|
||||
* uploaderUrl
|
||||
*/
|
||||
private static void saveUploaderUrlIfNeeded(@NonNull final Context context,
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
// Only used if not null or empty
|
||||
@Nullable final String uploaderUrl,
|
||||
@NonNull final SaveUploaderUrlCallback callback) {
|
||||
if (isNullOrEmpty(uploaderUrl)) {
|
||||
Toast.makeText(context, R.string.loading_channel_details,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
NewPipeDatabase.getInstance(context).streamDAO()
|
||||
.setUploaderUrl(serviceId, url, result.getUploaderUrl())
|
||||
.subscribeOn(Schedulers.io()).subscribe();
|
||||
callback.onCallback(result.getUploaderUrl());
|
||||
}, throwable -> ErrorUtil.createNotification(context,
|
||||
new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
||||
"Could not load channel details")
|
||||
));
|
||||
} else {
|
||||
callback.onCallback(uploaderUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public interface SaveUploaderUrlCallback {
|
||||
void onCallback(@NonNull String uploaderUrl);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ public final class SerializedCache {
|
|||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final SerializedCache INSTANCE = new SerializedCache();
|
||||
private static final int MAX_ITEMS_ON_CACHE = 5;
|
||||
private static final LruCache<String, CacheData> LRU_CACHE =
|
||||
private static final LruCache<String, CacheData<?>> LRU_CACHE =
|
||||
new LruCache<>(MAX_ITEMS_ON_CACHE);
|
||||
private static final String TAG = "SerializedCache";
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ public final class SerializedCache {
|
|||
Log.d(TAG, "get() called with: key = [" + key + "]");
|
||||
}
|
||||
synchronized (LRU_CACHE) {
|
||||
final CacheData data = LRU_CACHE.get(key);
|
||||
final CacheData<?> data = LRU_CACHE.get(key);
|
||||
return data != null ? getItem(data, type) : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ public final class SerializedCache {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T getItem(@NonNull final CacheData data, @NonNull final Class<T> type) {
|
||||
private <T> T getItem(@NonNull final CacheData<?> data, @NonNull final Class<T> type) {
|
||||
return type.isAssignableFrom(data.type) ? type.cast(data.item) : null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package org.schabi.newpipe.util
|
||||
|
||||
import android.widget.SeekBar
|
||||
|
||||
/**
|
||||
* Why the hell didn't they make a stub implementation for this?
|
||||
*/
|
||||
abstract class SimpleOnSeekBarChangeListener : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
}
|
||||
128
app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
Normal file
128
app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Utility class for fetching additional data for stream items when needed.
|
||||
*/
|
||||
public final class SparseItemUtil {
|
||||
private SparseItemUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to certainly obtain an single play queue with all of the data filled in when the
|
||||
* stream info item you are handling might be sparse, e.g. because it was fetched via a {@link
|
||||
* org.schabi.newpipe.extractor.feed.FeedExtractor}. FeedExtractors provide a fast and
|
||||
* lightweight method to fetch info, but the info might be incomplete (see
|
||||
* {@link org.schabi.newpipe.local.feed.service.FeedLoadService} for more details).
|
||||
*
|
||||
* @param context Android context
|
||||
* @param item item which is checked and eventually loaded completely
|
||||
* @param callback callback to call with the single play queue built from the original item if
|
||||
* all info was available, otherwise from the fetched {@link
|
||||
* org.schabi.newpipe.extractor.stream.StreamInfo}
|
||||
*/
|
||||
public static void fetchItemInfoIfSparse(@NonNull final Context context,
|
||||
@NonNull final StreamInfoItem item,
|
||||
@NonNull final Consumer<SinglePlayQueue> callback) {
|
||||
if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM)
|
||||
|| item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) {
|
||||
// if the duration is >= 0 (provided that the item is not a livestream) and there is an
|
||||
// uploader url, probably all info is already there, so there is no need to fetch it
|
||||
callback.accept(new SinglePlayQueue(item));
|
||||
}
|
||||
|
||||
// either the duration or the uploader url are not available, so fetch more info
|
||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||
streamInfo -> callback.accept(new SinglePlayQueue(streamInfo)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to certainly obtain an uploader url when the stream info item or play queue item you
|
||||
* are handling might not have the uploader url (e.g. because it was fetched with {@link
|
||||
* org.schabi.newpipe.extractor.feed.FeedExtractor}). A toast is shown if loading details is
|
||||
* required.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param serviceId serviceId of the item
|
||||
* @param url item url
|
||||
* @param uploaderUrl uploaderUrl of the item; if null or empty will be fetched
|
||||
* @param callback callback to be called with either the original uploaderUrl, if it was a
|
||||
* valid url, otherwise with the uploader url obtained by fetching the {@link
|
||||
* org.schabi.newpipe.extractor.stream.StreamInfo} corresponding to the item
|
||||
*/
|
||||
public static void fetchUploaderUrlIfSparse(@NonNull final Context context,
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
@Nullable final String uploaderUrl,
|
||||
@NonNull final Consumer<String> callback) {
|
||||
if (isNullOrEmpty(uploaderUrl)) {
|
||||
fetchStreamInfoAndSaveToDatabase(context, serviceId, url,
|
||||
streamInfo -> callback.accept(streamInfo.getUploaderUrl()));
|
||||
} else {
|
||||
callback.accept(uploaderUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the stream info corresponding to the given data on an I/O thread, stores the result in
|
||||
* the database and calls the callback on the main thread with the result. A toast will be shown
|
||||
* to the user about loading stream details, so this needs to be called on the main thread.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param serviceId service id of the stream to load
|
||||
* @param url url of the stream to load
|
||||
* @param callback callback to be called with the result
|
||||
*/
|
||||
private static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context context,
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
final Consumer<StreamInfo> callback) {
|
||||
Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show();
|
||||
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
// save to database in the background (not on main thread)
|
||||
Completable.fromAction(() -> NewPipeDatabase.getInstance(context)
|
||||
.streamDAO().upsert(new StreamEntity(result)))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.doOnError(throwable ->
|
||||
ErrorUtil.createNotification(context,
|
||||
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
"Saving stream info to database", result)))
|
||||
.subscribe();
|
||||
|
||||
// call callback on main thread with the obtained result
|
||||
callback.accept(result);
|
||||
}, throwable -> ErrorUtil.createNotification(context,
|
||||
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
"Loading stream info: " + url, serviceId)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public enum StreamDialogEntry {
|
||||
//////////////////////////////////////
|
||||
// enum values with DEFAULT actions //
|
||||
//////////////////////////////////////
|
||||
|
||||
show_channel_details(R.string.show_channel_details, (fragment, item) -> {
|
||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(fragment, item,
|
||||
uploaderUrl -> openChannelFragment(fragment, item, uploaderUrl));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Enqueues the stream automatically to the current PlayerType.<br>
|
||||
* <br>
|
||||
* Info: Add this entry within showStreamDialog.
|
||||
*/
|
||||
enqueue(R.string.enqueue_stream, (fragment, item) -> {
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.enqueueOnPlayer(fragment.getContext(), fullItem));
|
||||
}),
|
||||
|
||||
enqueue_next(R.string.enqueue_next_stream, (fragment, item) -> {
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), fullItem));
|
||||
}),
|
||||
|
||||
start_here_on_background(R.string.start_here_on_background, (fragment, item) -> {
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), fullItem, true));
|
||||
}),
|
||||
|
||||
start_here_on_popup(R.string.start_here_on_popup, (fragment, item) -> {
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.playOnPopupPlayer(fragment.getContext(), fullItem, true));
|
||||
}),
|
||||
|
||||
set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
|
||||
}), // has to be set manually
|
||||
|
||||
delete(R.string.delete, (fragment, item) -> {
|
||||
}), // has to be set manually
|
||||
|
||||
append_playlist(R.string.add_to_playlist, (fragment, item) -> {
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
fragment.getContext(),
|
||||
Collections.singletonList(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragment.getParentFragmentManager(),
|
||||
"StreamDialogEntry@"
|
||||
+ (dialog instanceof PlaylistAppendDialog ? "append" : "create")
|
||||
+ "_playlist"
|
||||
)
|
||||
);
|
||||
}),
|
||||
|
||||
play_with_kodi(R.string.play_with_kodi_title, (fragment, item) -> {
|
||||
final Uri videoUrl = Uri.parse(item.getUrl());
|
||||
try {
|
||||
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
|
||||
} catch (final Exception e) {
|
||||
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
|
||||
}
|
||||
}),
|
||||
|
||||
share(R.string.share, (fragment, item) ->
|
||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||
item.getThumbnailUrl())),
|
||||
|
||||
open_in_browser(R.string.open_in_browser, (fragment, item) ->
|
||||
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
|
||||
|
||||
|
||||
mark_as_watched(R.string.mark_as_watched, (fragment, item) -> {
|
||||
new HistoryRecordManager(fragment.getContext())
|
||||
.markAsWatched(item)
|
||||
.onErrorComplete()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe();
|
||||
});
|
||||
|
||||
///////////////
|
||||
// variables //
|
||||
///////////////
|
||||
|
||||
private static StreamDialogEntry[] enabledEntries;
|
||||
private final int resource;
|
||||
private final StreamDialogEntryAction defaultAction;
|
||||
private StreamDialogEntryAction customAction;
|
||||
|
||||
StreamDialogEntry(final int resource, final StreamDialogEntryAction defaultAction) {
|
||||
this.resource = resource;
|
||||
this.defaultAction = defaultAction;
|
||||
this.customAction = null;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// non-static methods to initialize and edit entries //
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
public static void setEnabledEntries(final List<StreamDialogEntry> entries) {
|
||||
setEnabledEntries(entries.toArray(new StreamDialogEntry[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called before using {@link #setCustomAction(StreamDialogEntryAction)}.
|
||||
*
|
||||
* @param entries the entries to be enabled
|
||||
*/
|
||||
public static void setEnabledEntries(final StreamDialogEntry... entries) {
|
||||
// cleanup from last time StreamDialogEntry was used
|
||||
for (final StreamDialogEntry streamDialogEntry : values()) {
|
||||
streamDialogEntry.customAction = null;
|
||||
}
|
||||
|
||||
enabledEntries = entries;
|
||||
}
|
||||
|
||||
public static String[] getCommands(final Context context) {
|
||||
final String[] commands = new String[enabledEntries.length];
|
||||
for (int i = 0; i != enabledEntries.length; ++i) {
|
||||
commands[i] = context.getResources().getString(enabledEntries[i].resource);
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// static methods that act on enabled entries //
|
||||
////////////////////////////////////////////////
|
||||
|
||||
public static void clickOn(final int which, final Fragment fragment,
|
||||
final StreamInfoItem infoItem) {
|
||||
if (enabledEntries[which].customAction == null) {
|
||||
enabledEntries[which].defaultAction.onClick(fragment, infoItem);
|
||||
} else {
|
||||
enabledEntries[which].customAction.onClick(fragment, infoItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called.
|
||||
*
|
||||
* @param action the action to be set
|
||||
*/
|
||||
public void setCustomAction(final StreamDialogEntryAction action) {
|
||||
this.customAction = action;
|
||||
}
|
||||
|
||||
public interface StreamDialogEntryAction {
|
||||
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
||||
}
|
||||
|
||||
public static boolean shouldAddMarkAsWatched(final StreamType streamType,
|
||||
final Context context) {
|
||||
final boolean isWatchHistoryEnabled = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
||||
return streamType != StreamType.AUDIO_LIVE_STREAM
|
||||
&& streamType != StreamType.LIVE_STREAM
|
||||
&& isWatchHistoryEnabled;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// private method to open channel fragment //
|
||||
/////////////////////////////////////////////
|
||||
|
||||
private static void openChannelFragment(final Fragment fragment,
|
||||
final StreamInfoItem item,
|
||||
final String uploaderUrl) {
|
||||
// For some reason `getParentFragmentManager()` doesn't work, but this does.
|
||||
NavigationHelper.openChannelFragment(
|
||||
fragment.requireActivity().getSupportFragmentManager(),
|
||||
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// helper functions //
|
||||
/////////////////////////////////////////////
|
||||
|
||||
private static void fetchItemInfoIfSparse(final Fragment fragment,
|
||||
final StreamInfoItem item,
|
||||
final Consumer<SinglePlayQueue> callback) {
|
||||
if (!(item.getStreamType() == StreamType.LIVE_STREAM
|
||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM)
|
||||
&& item.getDuration() < 0) {
|
||||
// Sparse item: fetched by fast fetch
|
||||
ExtractorHelper.getStreamInfo(
|
||||
item.getServiceId(),
|
||||
item.getUrl(),
|
||||
false
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
final HistoryRecordManager recordManager =
|
||||
new HistoryRecordManager(fragment.getContext());
|
||||
recordManager.saveStreamState(result, 0)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Log.e("StreamDialogEntry",
|
||||
throwable.toString()))
|
||||
.subscribe();
|
||||
|
||||
callback.accept(new SinglePlayQueue(result));
|
||||
}, throwable -> Log.e("StreamDialogEntry", throwable.toString()));
|
||||
} else {
|
||||
callback.accept(new SinglePlayQueue(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import android.widget.TextView;
|
|||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
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.SubtitlesStream;
|
||||
|
|
@ -137,7 +138,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
}
|
||||
|
||||
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
||||
final SecondaryStreamHelper secondary = secondaryStreams == null ? null
|
||||
final SecondaryStreamHelper<U> secondary = secondaryStreams == null ? null
|
||||
: secondaryStreams.get(position);
|
||||
if (secondary != null) {
|
||||
final long size
|
||||
|
|
@ -153,16 +154,11 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
|
||||
if (stream instanceof SubtitlesStream) {
|
||||
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
||||
} else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
// noinspection AndroidLintSetTextI18n
|
||||
formatNameView.setText("opus");
|
||||
} else {
|
||||
switch (stream.getFormat()) {
|
||||
case WEBMA_OPUS:
|
||||
// noinspection AndroidLintSetTextI18n
|
||||
formatNameView.setText("opus");
|
||||
break;
|
||||
default:
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
break;
|
||||
}
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
}
|
||||
|
||||
qualityView.setText(qualityString);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import org.schabi.newpipe.R;
|
|||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
/**
|
||||
* Util class that provides methods which are related to the Kodi Media Center and its Kore app.
|
||||
* @see <a href="https://kodi.tv/">Kodi website</a>
|
||||
*/
|
||||
public final class KoreUtils {
|
||||
private KoreUtils() { }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue