Merge branch 'dev' into pr2335

This commit is contained in:
Stypox 2022-03-19 22:12:58 +01:00
commit 6ead95b5d2
423 changed files with 3413 additions and 4301 deletions

View file

@ -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);
}
}

View file

@ -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));
}

View file

@ -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":

View file

@ -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;
}
}
}

View file

@ -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);
}
/**

View file

@ -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) {

View 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()
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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) {}
}

View 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)
));
}
}

View file

@ -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));
}
}
}

View file

@ -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);

View file

@ -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() { }