Update extractor and refactored NewPipe

This commit is contained in:
Mauricio Colli 2017-09-03 03:04:18 -03:00
parent bddd9b3409
commit 146d4a8365
254 changed files with 7836 additions and 7302 deletions

View file

@ -2,17 +2,24 @@ package org.schabi.newpipe.util;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.res.ColorStateList;
import android.support.annotation.ColorInt;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.MainActivity;
public class AnimationUtils {
private static final String TAG = "AnimationUtils";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final boolean DEBUG = MainActivity.DEBUG;
public enum Type {
ALPHA, SCALE_AND_ALPHA
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA
}
public static void animateView(View view, boolean enterOrExit, long duration) {
@ -47,7 +54,16 @@ public class AnimationUtils {
*/
public static void animateView(final View view, Type animationType, boolean enterOrExit, long duration, long delay, Runnable execOnEnd) {
if (DEBUG) {
Log.d(TAG, "animateView() called with: view = [" + view + "], animationType = [" + animationType + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "], execOnEnd = [" + execOnEnd + "]");
String id;
try {
id = view.getResources().getResourceEntryName(view.getId());
} catch (Exception e) {
id = view.getId() + "";
}
String msg = String.format("%8s → [%s:%s] [%s %s:%s] execOnEnd=%s",
enterOrExit, view.getClass().getSimpleName(), id, animationType, duration, delay, execOnEnd);
Log.d(TAG, "animateView()" + msg);
}
if (view.getVisibility() == View.VISIBLE && enterOrExit) {
@ -76,15 +92,132 @@ public class AnimationUtils {
case SCALE_AND_ALPHA:
animateScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
break;
case LIGHT_SCALE_AND_ALPHA:
animateLightScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
break;
}
}
/**
* Animate the background color of a view
*/
public static void animateBackgroundColor(final View view, long duration, @ColorInt final int colorStart, @ColorInt final int colorEnd) {
if (DEBUG) {
Log.d(TAG, "animateBackgroundColor() called with: view = [" + view + "], duration = [" + duration + "], colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]");
}
final int[][] EMPTY = new int[][]{new int[0]};
ValueAnimator viewPropertyAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), colorStart, colorEnd);
viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator());
viewPropertyAnimator.setDuration(duration);
viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ViewCompat.setBackgroundTintList(view, new ColorStateList(EMPTY, new int[]{(int) animation.getAnimatedValue()}));
}
});
viewPropertyAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
ViewCompat.setBackgroundTintList(view, new ColorStateList(EMPTY, new int[]{colorEnd}));
}
@Override
public void onAnimationCancel(Animator animation) {
onAnimationEnd(animation);
}
});
viewPropertyAnimator.start();
}
/**
* Animate the text color of any view that extends {@link TextView} (Buttons, EditText...)
*/
public static void animateTextColor(final TextView view, long duration, @ColorInt final int colorStart, @ColorInt final int colorEnd) {
if (DEBUG) {
Log.d(TAG, "animateTextColor() called with: view = [" + view + "], duration = [" + duration + "], colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]");
}
ValueAnimator viewPropertyAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), colorStart, colorEnd);
viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator());
viewPropertyAnimator.setDuration(duration);
viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
view.setTextColor((int) animation.getAnimatedValue());
}
});
viewPropertyAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setTextColor(colorEnd);
}
@Override
public void onAnimationCancel(Animator animation) {
view.setTextColor(colorEnd);
}
});
viewPropertyAnimator.start();
}
/*//////////////////////////////////////////////////////////////////////////
// Internals
//////////////////////////////////////////////////////////////////////////*/
private static void animateAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
if (enterOrExit) {
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (execOnEnd != null) execOnEnd.run();
}
}).start();
} else {
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
if (execOnEnd != null) execOnEnd.run();
}
}).start();
}
}
private static void animateScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
if (enterOrExit) {
view.setAlpha(0f);
view.setScaleX(.8f);
view.setScaleY(.8f);
view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (execOnEnd != null) execOnEnd.run();
}
}).start();
} else {
view.setScaleX(1f);
view.setScaleY(1f);
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
if (execOnEnd != null) execOnEnd.run();
}
}).start();
}
}
private static void animateLightScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
if (enterOrExit) {
view.setAlpha(.5f);
view.setScaleX(.95f);
view.setScaleY(.95f);
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (execOnEnd != null) execOnEnd.run();
@ -94,27 +227,8 @@ public class AnimationUtils {
view.setAlpha(1f);
view.setScaleX(1f);
view.setScaleY(1f);
view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
if (execOnEnd != null) execOnEnd.run();
}
}).start();
}
}
private static void animateAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
if (enterOrExit) {
view.animate().alpha(1f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (execOnEnd != null) execOnEnd.run();
}
}).start();
} else {
view.animate().alpha(0f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).scaleX(.95f).scaleY(.95f)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);

View file

@ -0,0 +1,236 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Extractors.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.util;
import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.ListExtractor.NextItemsResult;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.io.InterruptedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import io.reactivex.Maybe;
import io.reactivex.MaybeSource;
import io.reactivex.Single;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
public final class ExtractorHelper {
private static final String TAG = ExtractorHelper.class.getSimpleName();
private static final InfoCache cache = InfoCache.getInstance();
private ExtractorHelper() {
//no instance
}
public static Single<SearchResult> searchFor(final int serviceId, final String query, final int pageNumber, final String searchLanguage, final SearchEngine.Filter filter) {
return Single.fromCallable(new Callable<SearchResult>() {
@Override
public SearchResult call() throws Exception {
return SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(),
query, pageNumber, searchLanguage, filter);
}
});
}
public static Single<NextItemsResult> getMoreSearchItems(final int serviceId, final String query, final int nextPageNumber, final String searchLanguage, final SearchEngine.Filter filter) {
return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter)
.map(new Function<SearchResult, NextItemsResult>() {
@Override
public NextItemsResult apply(@NonNull SearchResult searchResult) throws Exception {
return new NextItemsResult(searchResult.resultList, nextPageNumber + "", searchResult.errors);
}
});
}
public static Single<List<String>> suggestionsFor(final int serviceId, final String query, final String searchLanguage) {
return Single.fromCallable(new Callable<List<String>>() {
@Override
public List<String> call() throws Exception {
return NewPipe.getService(serviceId).getSuggestionExtractor().suggestionList(query, searchLanguage);
}
});
}
public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url, boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<StreamInfo>() {
@Override
public StreamInfo call() throws Exception {
return StreamInfo.getInfo(NewPipe.getService(serviceId), url);
}
}));
}
public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url, boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<ChannelInfo>() {
@Override
public ChannelInfo call() throws Exception {
return ChannelInfo.getInfo(NewPipe.getService(serviceId), url);
}
}));
}
public static Single<NextItemsResult> getMoreChannelItems(final int serviceId, final String nextStreamsUrl) {
return Single.fromCallable(new Callable<NextItemsResult>() {
@Override
public NextItemsResult call() throws Exception {
return ChannelInfo.getMoreItems(NewPipe.getService(serviceId), nextStreamsUrl);
}
});
}
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId, final String url, boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<PlaylistInfo>() {
@Override
public PlaylistInfo call() throws Exception {
return PlaylistInfo.getInfo(NewPipe.getService(serviceId), url);
}
}));
}
public static Single<NextItemsResult> getMorePlaylistItems(final int serviceId, final String nextStreamsUrl) {
return Single.fromCallable(new Callable<NextItemsResult>() {
@Override
public NextItemsResult call() throws Exception {
return PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), nextStreamsUrl);
}
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/**
* Check if we can load it from the cache (forceLoad parameter), if we can't, load from the network (Single loadFromNetwork)
* and put the results in the cache.
*/
private static <I extends Info> Single<I> checkCache(boolean forceLoad, int serviceId, String url, Single<I> loadFromNetwork) {
loadFromNetwork = loadFromNetwork.doOnSuccess(new Consumer<I>() {
@Override
public void accept(@NonNull I i) throws Exception {
cache.putInfo(i);
}
});
Single<I> load;
if (forceLoad) {
cache.removeInfo(serviceId, url);
load = loadFromNetwork;
} else {
load = Maybe.concat(ExtractorHelper.<I>loadFromCache(serviceId, url), loadFromNetwork.toMaybe())
.firstElement() //Take the first valid
.toSingle();
}
return load;
}
/**
* Default implementation uses the {@link InfoCache} to get cached results
*/
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) {
return Maybe.defer(new Callable<MaybeSource<? extends I>>() {
@Override
public MaybeSource<? extends I> call() throws Exception {
//noinspection unchecked
I info = (I) cache.getFromKey(serviceId, url);
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
// Only return info if it's not null (it is cached)
if (info != null) {
return Maybe.just(info);
}
return Maybe.empty();
}
});
}
/**
* Check if throwable have the cause that can be assignable from the causes to check.
*
* @see Class#isAssignableFrom(Class)
*/
public static boolean hasAssignableCauseThrowable(Throwable throwable, Class<?>... causesToCheck) {
// Check if getCause is not the same as cause (the getCause is already the root),
// as it will cause a infinite loop if it is
Throwable cause, getCause = throwable;
for (Class<?> causesEl : causesToCheck) {
if (throwable.getClass().isAssignableFrom(causesEl)) {
return true;
}
}
while ((cause = throwable.getCause()) != null && getCause != cause) {
getCause = cause;
for (Class<?> causesEl : causesToCheck) {
if (cause.getClass().isAssignableFrom(causesEl)) {
return true;
}
}
}
return false;
}
/**
* Check if throwable have the exact cause from one of the causes to check.
*/
public static boolean hasExactCauseThrowable(Throwable throwable, Class<?>... causesToCheck) {
// Check if getCause is not the same as cause (the getCause is already the root),
// as it will cause a infinite loop if it is
Throwable cause, getCause = throwable;
for (Class<?> causesEl : causesToCheck) {
if (throwable.getClass().equals(causesEl)) {
return true;
}
}
while ((cause = throwable.getCause()) != null && getCause != cause) {
getCause = cause;
for (Class<?> causesEl : causesToCheck) {
if (cause.getClass().equals(causesEl)) {
return true;
}
}
}
return false;
}
/**
* Check if throwable have Interrupted* exception as one of its causes.
*/
public static boolean isInterruptedCaused(Throwable throwable) {
return ExtractorHelper.hasExactCauseThrowable(throwable, InterruptedIOException.class, InterruptedException.class);
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* InfoCache.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.util;
import android.support.annotation.NonNull;
import android.support.v4.util.LruCache;
import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info;
public final class InfoCache {
private static final boolean DEBUG = MainActivity.DEBUG;
private final String TAG = getClass().getSimpleName();
private static final InfoCache instance = new InfoCache();
private static final int MAX_ITEMS_ON_CACHE = 60;
/**
* Trim the cache to this size
*/
private static final int TRIM_CACHE_TO = 30;
// TODO: Replace to one with timeout (like the one from guava)
private static final LruCache<String, Info> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE);
private InfoCache() {
//no instance
}
public static InfoCache getInstance() {
return instance;
}
public Info getFromKey(int serviceId, @NonNull String url) {
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) {
return lruCache.get(serviceId + url);
}
}
public void putInfo(@NonNull Info info) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
synchronized (lruCache) {
lruCache.put(info.service_id + info.url, info);
}
}
public void removeInfo(@NonNull Info info) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
synchronized (lruCache) {
lruCache.remove(info.service_id + info.url);
}
}
public void removeInfo(int serviceId, @NonNull String url) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) {
lruCache.remove(serviceId + url);
}
}
public void clearCache() {
if (DEBUG) Log.d(TAG, "clearCache() called");
synchronized (lruCache) {
lruCache.evictAll();
}
}
public void trimCache() {
if (DEBUG) Log.d(TAG, "trimCache() called");
synchronized (lruCache) {
lruCache.trimToSize(TRIM_CACHE_TO);
}
}
public long getSize() {
synchronized (lruCache) {
return lruCache.size();
}
}
}

View file

@ -0,0 +1,269 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.annotation.StringRes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@SuppressWarnings("WeakerAccess")
public final class ListHelper {
private static final List<String> HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60");
/**
* Return the index of the default stream in the list, based on the parameters
* defaultResolution and defaultFormat
*
* @return index of the default resolution&format
*/
public static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey, MediaFormat defaultFormat, List<VideoStream> videoStreams) {
if (videoStreams == null || videoStreams.isEmpty()) return -1;
sortStreamList(videoStreams, false);
if (defaultResolution.equals(bestResolutionKey)) {
return 0;
}
int defaultStreamIndex = getDefaultStreamIndex(defaultResolution, defaultFormat, videoStreams);
if (defaultStreamIndex == -1 && defaultResolution.contains("p60")) {
defaultStreamIndex = getDefaultStreamIndex(defaultResolution.replace("p60", "p"), defaultFormat, videoStreams);
}
// this is actually an error,
// but maybe there is really no stream fitting to the default value.
if (defaultStreamIndex == -1) return 0;
return defaultStreamIndex;
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getDefaultResolutionIndex(Context context, List<VideoStream> videoStreams) {
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value));
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getPopupDefaultResolutionIndex(Context context, List<VideoStream> videoStreams) {
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value));
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
public static int getDefaultAudioFormat(Context context, List<AudioStream> audioStreams) {
MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, R.string.default_audio_format_value);
return getHighestQualityAudioIndex(defaultFormat, audioStreams);
}
public static int getHighestQualityAudioIndex(List<AudioStream> audioStreams) {
if (audioStreams == null || audioStreams.isEmpty()) return -1;
int highestQualityIndex = 0;
if (audioStreams.size() > 1) for (int i = 1; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
if (audioStream.average_bitrate >= audioStreams.get(highestQualityIndex).average_bitrate) highestQualityIndex = i;
}
return highestQualityIndex;
}
/**
* Get the audio from the list with the highest bitrate
*
* @param audioStreams list the audio streams
* @return audio with highest average bitrate
*/
public static AudioStream getHighestQualityAudio(List<AudioStream> audioStreams) {
if (audioStreams == null || audioStreams.isEmpty()) return null;
return audioStreams.get(getHighestQualityAudioIndex(audioStreams));
}
/**
* Get the audio from the list with the highest bitrate
*
* @param audioStreams list the audio streams
* @return index of the audio with the highest average bitrate of the default format
*/
public static int getHighestQualityAudioIndex(MediaFormat defaultFormat, List<AudioStream> audioStreams) {
if (audioStreams == null || audioStreams.isEmpty() || defaultFormat == null) return -1;
int highestQualityIndex = -1;
for (int i = 0; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
if (highestQualityIndex == -1 && audioStream.format == defaultFormat.id) highestQualityIndex = i;
if (highestQualityIndex != -1 && audioStream.format == defaultFormat.id
&& audioStream.average_bitrate > audioStreams.get(highestQualityIndex).average_bitrate) {
highestQualityIndex = i;
}
}
if (highestQualityIndex == -1) highestQualityIndex = getHighestQualityAudioIndex(audioStreams);
return highestQualityIndex;
}
/**
* 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
* @return the sorted list
*/
public static List<VideoStream> getSortedStreamVideosList(Context context, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean showHigherResolutions = preferences.getBoolean(context.getString(R.string.show_higher_resolutions_key), false);
MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value);
return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder);
}
/**
* 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 @return the sorted list
* @return the sorted list
*/
public static List<VideoStream> getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
ArrayList<VideoStream> retList = new ArrayList<>();
HashMap<String, VideoStream> hashMap = new HashMap<>();
if (videoOnlyStreams != null) {
for (VideoStream stream : videoOnlyStreams) {
if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue;
retList.add(stream);
}
}
if (videoStreams != null) {
for (VideoStream stream : videoStreams) {
if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue;
retList.add(stream);
}
}
// Add all to the hashmap
for (VideoStream videoStream : retList) hashMap.put(videoStream.resolution, videoStream);
// Override the values when the key == resolution, with the defaultFormat
for (VideoStream videoStream : retList) {
if (videoStream.format == defaultFormat.id) hashMap.put(videoStream.resolution, videoStream);
}
retList.clear();
retList.addAll(hashMap.values());
sortStreamList(retList, ascendingOrder);
return retList;
}
/**
* Sort the streams list depending on the parameter ascendingOrder;
* <p>
* It works like that:<br>
* - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1"
* and sort by the greatest:<br>
* <blockquote><pre>
* 720p -> 720
* 720p60 -> 721
* 360p -> 360
* 1080p -> 1080
* 1080p60 -> 1081
* <br>
* 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
*/
public static void sortStreamList(List<VideoStream> videoStreams, final boolean ascendingOrder) {
Collections.sort(videoStreams, new Comparator<VideoStream>() {
@Override
public int compare(VideoStream o1, VideoStream o2) {
int res1 = Integer.parseInt(o1.resolution.replace("0p60", "1").replaceAll("[^\\d.]", ""));
int res2 = Integer.parseInt(o2.resolution.replace("0p60", "1").replaceAll("[^\\d.]", ""));
return ascendingOrder ? res1 - res2 : res2 - res1;
}
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private static int getDefaultStreamIndex(String defaultResolution, MediaFormat defaultFormat, List<VideoStream> videoStreams) {
int defaultStreamIndex = -1;
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream stream = videoStreams.get(i);
if (defaultStreamIndex == -1 && stream.resolution.equals(defaultResolution)) defaultStreamIndex = i;
if (stream.format == defaultFormat.id && stream.resolution.equals(defaultResolution)) {
return i;
}
}
return defaultStreamIndex;
}
private static int getDefaultResolutionWithDefaultFormat(Context context, String defaultResolution, List<VideoStream> videoStreams) {
MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value);
return getDefaultResolutionIndex(defaultResolution, context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
}
private static MediaFormat getDefaultFormat(Context context, @StringRes int defaultFormatKey, @StringRes int defaultFormatValueKey) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String defaultFormat = context.getString(defaultFormatValueKey);
String defaultFormatString = preferences.getString(context.getString(defaultFormatKey), defaultFormat);
MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString);
if (defaultMediaFormat == null) {
preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat).apply();
defaultMediaFormat = getMediaFormatFromKey(context, defaultFormat);
}
return defaultMediaFormat;
}
private static MediaFormat getMediaFormatFromKey(Context context, String formatKey) {
MediaFormat format = null;
if (formatKey.equals(context.getString(R.string.video_webm_key))) {
format = MediaFormat.WEBM;
} else if (formatKey.equals(context.getString(R.string.video_mp4_key))) {
format = MediaFormat.MPEG_4;
} else if (formatKey.equals(context.getString(R.string.video_3gp_key))) {
format = MediaFormat.v3GPP;
} else if (formatKey.equals(context.getString(R.string.audio_webm_key))) {
format = MediaFormat.WEBMA;
} else if (formatKey.equals(context.getString(R.string.audio_m4a_key))) {
format = MediaFormat.M4A;
}
return format;
}
}

View file

@ -4,6 +4,8 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.support.annotation.PluralsRes;
import android.support.annotation.StringRes;
import org.schabi.newpipe.R;
@ -14,7 +16,7 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
/*
* Created by chschtsch on 12/29/15.
*
* Copyright (C) Gregory Arkhipov 2015
@ -42,38 +44,28 @@ public class Localization {
public static Locale getPreferredLocale(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
String languageCode = sp.getString(String.valueOf(R.string.search_language_key),
context.getString(R.string.default_language_value));
String languageCode = sp.getString(context.getString(R.string.search_language_key), context.getString(R.string.default_language_value));
if(languageCode.length() == 2) {
return new Locale(languageCode);
}
else if(languageCode.contains("_")) {
String country = languageCode
.substring(languageCode.indexOf("_"), languageCode.length());
return new Locale(languageCode.substring(0, 2), country);
try {
if (languageCode.length() == 2) {
return new Locale(languageCode);
} else if (languageCode.contains("_")) {
String country = languageCode.substring(languageCode.indexOf("_"), languageCode.length());
return new Locale(languageCode.substring(0, 2), country);
}
} catch (Exception ignored) {
}
return Locale.getDefault();
}
public static String localizeViewCount(long viewCount, Context context) {
Locale locale = getPreferredLocale(context);
Resources res = context.getResources();
String viewsString = res.getString(R.string.view_count_text);
NumberFormat nf = NumberFormat.getInstance(locale);
String formattedViewCount = nf.format(viewCount);
return String.format(viewsString, formattedViewCount);
}
public static String localizeNumber(long number, Context context) {
public static String localizeNumber(Context context, long number) {
Locale locale = getPreferredLocale(context);
NumberFormat nf = NumberFormat.getInstance(locale);
return nf.format(number);
}
private static String formatDate(String date, Context context) {
private static String formatDate(Context context, String date) {
Locale locale = getPreferredLocale(context);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
Date datum = null;
@ -88,11 +80,75 @@ public class Localization {
return df.format(datum);
}
public static String localizeDate(String date, Context context) {
public static String localizeDate(Context context, String date) {
Resources res = context.getResources();
String dateString = res.getString(R.string.upload_date_text);
String formattedDate = formatDate(date, context);
String formattedDate = formatDate(context, date);
return String.format(dateString, formattedDate);
}
public static String localizeViewCount(Context context, long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, localizeNumber(context, viewCount));
}
public static String localizeSubscribersCount(Context context, long subscriberCount) {
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, localizeNumber(context, subscriberCount));
}
public static String localizeStreamCount(Context context, long streamCount) {
return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, localizeNumber(context, streamCount));
}
public static String shortCount(Context context, long count) {
if (count >= 1000000000) {
return Long.toString(count / 1000000000) + context.getString(R.string.short_billion);
} else if (count >= 1000000) {
return Long.toString(count / 1000000) + context.getString(R.string.short_million);
} else if (count >= 1000) {
return Long.toString(count / 1000) + context.getString(R.string.short_thousand);
} else {
return Long.toString(count);
}
}
public static String shortViewCount(Context context, long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, shortCount(context, viewCount));
}
public static String shortSubscriberCount(Context context, long subscriberCount) {
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, shortCount(context, subscriberCount));
}
private static String getQuantity(Context context, @PluralsRes int pluralId, @StringRes int zeroCaseStringId, long count, String formattedCount) {
if (count == 0) return context.getString(zeroCaseStringId);
// As we use the already formatted count, is not the responsibility of this method handle long numbers
// (it probably will fall in the "other" category, or some language have some specific rule... then we have to change it)
int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE : count < Integer.MIN_VALUE ? Integer.MIN_VALUE : (int) count;
return context.getResources().getQuantityString(pluralId, safeCount, formattedCount);
}
public static String getDurationString(long duration) {
if (duration < 0) {
duration = 0;
}
String output;
long days = duration / (24 * 60 * 60L); /* greater than a day */
duration %= (24 * 60 * 60L);
long hours = duration / (60 * 60L); /* greater than an hour */
duration %= (60 * 60L);
long minutes = duration / 60L;
long seconds = duration % 60L;
//handle days
if (days > 0) {
output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds);
} else if (hours > 0) {
output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
} else {
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
}
return output;
}
}

View file

@ -4,30 +4,33 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.preference.PreferenceManager;
import android.support.annotation.CheckResult;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.about.AboutActivity;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.about.AboutActivity;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.fragments.FeedFragment;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.channel.ChannelFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.search.SearchFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.history.HistoryActivity;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@SuppressWarnings({"unused", "WeakerAccess"})
public class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@ -38,14 +41,14 @@ public class NavigationHelper {
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, StreamInfo info, int selectedStreamIndex) {
Intent mIntent = new Intent(context, targetClazz)
.putExtra(BasePlayer.VIDEO_TITLE, info.title)
.putExtra(BasePlayer.VIDEO_URL, info.webpage_url)
.putExtra(BasePlayer.VIDEO_TITLE, info.name)
.putExtra(BasePlayer.VIDEO_URL, info.url)
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name)
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamIndex)
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false))
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, Utils.getHighestQualityAudio(info.audio_streams));
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000);
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)))
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, ListHelper.getHighestQualityAudio(info.audio_streams));
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L);
return mIntent;
}
@ -54,27 +57,26 @@ public class NavigationHelper {
.putExtra(BasePlayer.VIDEO_TITLE, instance.getVideoTitle())
.putExtra(BasePlayer.VIDEO_URL, instance.getVideoUrl())
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, instance.getVideoThumbnailUrl())
.putExtra(BasePlayer.CHANNEL_NAME, instance.getChannelName())
.putExtra(BasePlayer.CHANNEL_NAME, instance.getUploaderName())
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, instance.getSelectedStreamIndex())
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, instance.getVideoStreamsList())
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, instance.getAudioStream())
.putExtra(BasePlayer.START_POSITION, ((int) instance.getPlayer().getCurrentPosition()))
.putExtra(BasePlayer.START_POSITION, instance.getPlayer().getCurrentPosition())
.putExtra(BasePlayer.PLAYBACK_SPEED, instance.getPlaybackSpeed());
}
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info) {
return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(Utils.getPreferredAudioFormat(context, info.audio_streams)));
return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(ListHelper.getDefaultAudioFormat(context, info.audio_streams)));
}
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info, AudioStream audioStream) {
Intent mIntent = new Intent(context, BackgroundPlayer.class)
.putExtra(BasePlayer.VIDEO_TITLE, info.title)
.putExtra(BasePlayer.VIDEO_URL, info.webpage_url)
.putExtra(BasePlayer.VIDEO_TITLE, info.name)
.putExtra(BasePlayer.VIDEO_URL, info.url)
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name)
.putExtra(BackgroundPlayer.AUDIO_STREAM, audioStream);
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000);
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L);
return mIntent;
}
@ -90,9 +92,11 @@ public class NavigationHelper {
}
private static void openMainFragment(FragmentManager fragmentManager) {
InfoCache.getInstance().trimCache();
fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out)
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, new MainFragment())
.addToBackStack(MAIN_FRAGMENT_TAG)
.commit();
@ -100,7 +104,7 @@ public class NavigationHelper {
public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out)
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query))
.addToBackStack(null)
.commit();
@ -125,7 +129,7 @@ public class NavigationHelper {
instance.setAutoplay(autoPlay);
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out)
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, instance)
.addToBackStack(null)
.commit();
@ -134,15 +138,24 @@ public class NavigationHelper {
public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) {
if (name == null) name = "";
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out)
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name))
.addToBackStack(null)
.commit();
}
public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) {
if (name == null) name = "";
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name))
.addToBackStack(null)
.commit();
}
public static void openWhatsNewFragment(FragmentManager fragmentManager) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out)
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, new FeedFragment())
.addToBackStack(null)
.commit();
@ -182,48 +195,21 @@ public class NavigationHelper {
public static void openMainActivity(Context context) {
Intent mIntent = new Intent(context, MainActivity.class);
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(mIntent);
}
public static void openByLink(Context context, String url) throws Exception {
Intent intentByLink = getIntentByLink(context, url);
if (intentByLink == null) throw new NullPointerException("getIntentByLink(context = [" + context + "], url = [" + url + "]) returned null");
intentByLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentByLink.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intentByLink);
}
private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) {
Intent mIntent = new Intent(context, MainActivity.class);
mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId);
mIntent.putExtra(Constants.KEY_URL, url);
mIntent.putExtra(Constants.KEY_LINK_TYPE, type);
return mIntent;
}
private static Intent getIntentByLink(Context context, String url) throws Exception {
StreamingService service = NewPipe.getServiceByUrl(url);
if (service == null) throw new Exception("NewPipe.getServiceByUrl returned null for url > \"" + url + "\"");
int serviceId = service.getServiceId();
switch (service.getLinkTypeByUrl(url)) {
case STREAM:
Intent sIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM);
sIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.autoplay_through_intent_key), false));
return sIntent;
case CHANNEL:
return getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL);
case NONE:
throw new Exception("Url not known to service. service=" + serviceId + " url=" + url);
}
return null;
}
public static void openAbout(Context context) {
Intent intent = new Intent(context, AboutActivity.class);
context.startActivity(intent);
}
public static void openHistory(Context context) {
Intent intent = new Intent(context, HistoryActivity.class);
context.startActivity(intent);
}
public static void openSettings(Context context) {
Intent intent = new Intent(context, SettingsActivity.class);
context.startActivity(intent);
@ -237,4 +223,62 @@ public class NavigationHelper {
activity.startActivity(intent);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Link handling
//////////////////////////////////////////////////////////////////////////*/
public static void openByLink(Context context, String url) throws Exception {
Intent intentByLink = getIntentByLink(context, url);
if (intentByLink == null)
throw new NullPointerException("getIntentByLink(context = [" + context + "], url = [" + url + "]) returned null");
intentByLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentByLink.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intentByLink);
}
private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) {
Intent mIntent = new Intent(context, MainActivity.class);
mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId);
mIntent.putExtra(Constants.KEY_URL, url);
mIntent.putExtra(Constants.KEY_LINK_TYPE, type);
return mIntent;
}
private static Intent getIntentByLink(Context context, String url) throws Exception {
StreamingService service = NewPipe.getServiceByUrl(url);
int serviceId = service.getServiceId();
StreamingService.LinkType linkType = service.getLinkTypeByUrl(url);
if (linkType == StreamingService.LinkType.NONE) {
throw new Exception("Url not known to service. service=" + serviceId + " url=" + url);
}
url = getCleanUrl(service, url, linkType);
Intent rIntent = getOpenIntent(context, url, serviceId, linkType);
switch (linkType) {
case STREAM:
rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.autoplay_through_intent_key), false));
break;
}
return rIntent;
}
private static String getCleanUrl(StreamingService service, String dirtyUrl, StreamingService.LinkType linkType) throws Exception {
switch (linkType) {
case STREAM:
return service.getStreamUrlIdHandler().cleanUrl(dirtyUrl);
case CHANNEL:
return service.getChannelUrlIdHandler().cleanUrl(dirtyUrl);
case PLAYLIST:
return service.getPlaylistUrlIdHandler().cleanUrl(dirtyUrl);
case NONE:
break;
}
return null;
}
}

View file

@ -0,0 +1,319 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* StateSaver.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.util;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.schabi.newpipe.MainActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
/**
* A way to save state to disk or in a in-memory map if it's just changing configurations (i.e. rotating the phone).
*/
public class StateSaver {
private static final ConcurrentHashMap<String, Queue<Object>> stateObjectsHolder = new ConcurrentHashMap<>();
private static final String TAG = "StateSaver";
private static final String CACHE_DIR_NAME = "state_cache";
public static final String KEY_SAVED_STATE = "key_saved_state";
private static String cacheDirPath;
private StateSaver() {
//no instance
}
/**
* Initialize the StateSaver, usually you want to call this in the Application class
*
* @param context used to get the available cache dir
*/
public static void init(Context context) {
File externalCacheDir = context.getExternalCacheDir();
if (externalCacheDir != null) cacheDirPath = externalCacheDir.getAbsolutePath();
if (TextUtils.isEmpty(cacheDirPath)) cacheDirPath = context.getCacheDir().getAbsolutePath();
}
/**
* Used for describe how to save/read the objects.
* <p>
* Queue was chosen by its FIFO property.
*/
public interface WriteRead {
/**
* Generate a changing suffix that will name the cache file,
* and be used to identify if it changed (thus reducing useless reading/saving).
*
* @return a unique value
*/
String generateSuffix();
/**
* Add to this queue objects that you want to save.
*/
void writeTo(Queue<Object> objectsToSave);
/**
* Poll saved objects from the queue in the order they were written.
*
* @param savedObjects queue of objects returned by {@link #writeTo(Queue)}
*/
void readFrom(@NonNull Queue<Object> savedObjects) throws Exception;
}
/**
* @see #tryToRestore(SavedState, WriteRead)
*/
public static SavedState tryToRestore(Bundle outState, WriteRead writeRead) {
if (outState == null || writeRead == null) return null;
SavedState savedState = outState.getParcelable(KEY_SAVED_STATE);
if (savedState == null) return null;
return tryToRestore(savedState, writeRead);
}
/**
* Try to restore the state from memory and disk, using the {@link StateSaver.WriteRead#readFrom(Queue)} from the writeRead.
*/
private static SavedState tryToRestore(@NonNull SavedState savedState, @NonNull WriteRead writeRead) {
if (MainActivity.DEBUG) {
Log.d(TAG, "tryToRestore() called with: savedState = [" + savedState + "], writeRead = [" + writeRead + "]");
}
FileInputStream fileInputStream = null;
try {
Queue<Object> savedObjects = stateObjectsHolder.remove(savedState.prefixFileSaved);
if (savedObjects != null) {
writeRead.readFrom(savedObjects);
if (MainActivity.DEBUG) {
Log.d(TAG, "tryToSave: reading objects from holder > " + savedObjects + ", stateObjectsHolder > " + stateObjectsHolder);
}
return savedState;
}
File file = new File(savedState.pathFileSaved);
if (!file.exists()) return null;
fileInputStream = new FileInputStream(file);
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
//noinspection unchecked
savedObjects = (Queue<Object>) inputStream.readObject();
if (savedObjects != null) {
writeRead.readFrom(savedObjects);
}
return savedState;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException ignored) {
}
}
}
return null;
}
/**
* @see #tryToSave(boolean, String, String, WriteRead)
*/
public static SavedState tryToSave(boolean isChangingConfig, @Nullable SavedState savedState, Bundle outState, WriteRead writeRead) {
String currentSavedPrefix = savedState == null || TextUtils.isEmpty(savedState.prefixFileSaved)
? System.nanoTime() - writeRead.hashCode() + ""
: savedState.prefixFileSaved;
savedState = tryToSave(isChangingConfig, currentSavedPrefix, writeRead.generateSuffix(), writeRead);
if (savedState != null) {
outState.putParcelable(StateSaver.KEY_SAVED_STATE, savedState);
return savedState;
}
return null;
}
/**
* If it's not changing configuration (i.e. rotating screen), try to write the state from {@link StateSaver.WriteRead#writeTo(Queue)}
* to the file with the name of prefixFileName + suffixFileName, in a cache folder got from the {@link #init(Context)}.
* <p>
* It checks if the file already exists and if it does, just return the path, so a good way to save is:
* <li> A fixed prefix for the file
* <li> A changing suffix
*/
private static SavedState tryToSave(boolean isChangingConfig, final String prefixFileName, String suffixFileName, WriteRead writeRead) {
if (MainActivity.DEBUG) {
Log.d(TAG, "tryToSave() called with: isChangingConfig = [" + isChangingConfig + "], prefixFileName = [" + prefixFileName + "], suffixFileName = [" + suffixFileName + "], writeRead = [" + writeRead + "]");
}
Queue<Object> savedObjects = new LinkedList<>();
writeRead.writeTo(savedObjects);
if (isChangingConfig) {
if (savedObjects.size() > 0) {
stateObjectsHolder.put(prefixFileName, savedObjects);
return new SavedState(prefixFileName, "");
} else return null;
}
FileOutputStream fileOutputStream = null;
try {
File cacheDir = new File(cacheDirPath);
if (!cacheDir.exists()) throw new RuntimeException("Cache dir does not exist > " + cacheDirPath);
cacheDir = new File(cacheDir, CACHE_DIR_NAME);
if (!cacheDir.exists()) {
boolean mkdirResult = cacheDir.mkdir();
if (!mkdirResult) return null;
}
if (TextUtils.isEmpty(suffixFileName)) suffixFileName = ".cache";
File file = new File(cacheDir, prefixFileName + suffixFileName);
if (file.exists() && file.length() > 0) {
// If the file already exists, just return it
return new SavedState(prefixFileName, file.getAbsolutePath());
} else {
// Delete any file that contains the prefix
File[] files = cacheDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.contains(prefixFileName);
}
});
for (File file1 : files) file1.delete();
}
fileOutputStream = new FileOutputStream(file);
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(savedObjects);
return new SavedState(prefixFileName, file.getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException ignored) {
}
}
}
return null;
}
/**
* Delete the cache file contained in the savedState and remove any possible-existing value in the memory-cache.
*/
public static void onDestroy(SavedState savedState) {
if (MainActivity.DEBUG) Log.d(TAG, "onDestroy() called with: savedState = [" + savedState + "]");
if (savedState != null && !TextUtils.isEmpty(savedState.pathFileSaved)) {
stateObjectsHolder.remove(savedState.prefixFileSaved);
try {
//noinspection ResultOfMethodCallIgnored
new File(savedState.pathFileSaved).delete();
} catch (Exception ignored) {
}
}
}
/**
* Clear all the files in cache (in memory and disk).
*/
public static void clearStateFiles() {
if (MainActivity.DEBUG) Log.d(TAG, "clearStateFiles() called");
stateObjectsHolder.clear();
File cacheDir = new File(cacheDirPath);
if (!cacheDir.exists()) return;
cacheDir = new File(cacheDir, CACHE_DIR_NAME);
if (cacheDir.exists()) {
for (File file : cacheDir.listFiles()) file.delete();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Inner
//////////////////////////////////////////////////////////////////////////*/
public static class SavedState implements Parcelable {
public String prefixFileSaved;
public String pathFileSaved;
public SavedState(String prefixFileSaved, String pathFileSaved) {
this.prefixFileSaved = prefixFileSaved;
this.pathFileSaved = pathFileSaved;
}
protected SavedState(Parcel in) {
prefixFileSaved = in.readString();
pathFileSaved = in.readString();
}
@Override
public String toString() {
return prefixFileSaved + " > " + pathFileSaved;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(prefixFileSaved);
dest.writeString(pathFileSaved);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

View file

@ -13,16 +13,17 @@ public class ThemeHelper {
* @param context context that the theme will be applied
*/
public static void setTheme(Context context) {
String lightTheme = context.getResources().getString(R.string.light_theme_key);
String darkTheme = context.getResources().getString(R.string.dark_theme_key);
String blackTheme = context.getResources().getString(R.string.black_theme_key);
String themeKey = context.getString(R.string.theme_key);
String darkTheme = context.getResources().getString(R.string.dark_theme_title);
String blackTheme = context.getResources().getString(R.string.black_theme_title);
String selectedTheme = getSelectedTheme(context);
String sp = PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, context.getResources().getString(R.string.light_theme_title));
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme);
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme);
else context.setTheme(R.style.AppTheme);
if (selectedTheme.equals(lightTheme)) context.setTheme(R.style.LightTheme);
else if (selectedTheme.equals(blackTheme)) context.setTheme(R.style.BlackTheme);
else if (selectedTheme.equals(darkTheme)) context.setTheme(R.style.DarkTheme);
// Fallback
else context.setTheme(R.style.DarkTheme);
}
/**
@ -31,12 +32,12 @@ public class ThemeHelper {
* @param context context to get the preference
*/
public static boolean isLightThemeSelected(Context context) {
return getSelectedTheme(context).equals(context.getResources().getString(R.string.light_theme_key));
}
public static String getSelectedTheme(Context context) {
String themeKey = context.getString(R.string.theme_key);
String darkTheme = context.getResources().getString(R.string.dark_theme_title);
String blackTheme = context.getResources().getString(R.string.black_theme_title);
String sp = PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, context.getResources().getString(R.string.light_theme_title));
return !(sp.equals(darkTheme) || sp.equals(blackTheme));
String defaultTheme = context.getResources().getString(R.string.default_theme_value);
return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme);
}
}

View file

@ -1,273 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@SuppressWarnings("WeakerAccess")
public class Utils {
private static final List<String> HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60");
/**
* Return the index of the default stream in the list, based on the parameters
* defaultResolution and preferredFormat
*
* @param videoStreams the list that will be extracted the index
*
* @return index of the preferred resolution&format
*/
public static int getDefaultResolution(String defaultResolution, String preferredFormat, List<VideoStream> videoStreams) {
if (defaultResolution.equals("Best resolution")) {
return 0;
}
// first try to find the one with the right resolution
int selectedFormat = 0;
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream item = videoStreams.get(i);
if (defaultResolution.equals(item.resolution)) {
selectedFormat = i;
}
}
// than try to find the one with the right resolution and format
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream item = videoStreams.get(i);
if (defaultResolution.equals(item.resolution)
&& preferredFormat.equals(MediaFormat.getNameById(item.format))) {
selectedFormat = i;
}
}
if (selectedFormat == 0 && !videoStreams.get(selectedFormat).resolution.contains(defaultResolution.replace("p60", "p"))) {
// Maybe there's no 60 fps variant available, so fallback to the normal version
String replace = defaultResolution.replace("p60", "p");
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream item = videoStreams.get(i);
if (replace.equals(item.resolution)) selectedFormat = i;
}
// than try to find the one with the right resolution and format
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream item = videoStreams.get(i);
if (replace.equals(item.resolution)
&& preferredFormat.equals(MediaFormat.getNameById(item.format))) {
selectedFormat = i;
}
}
}
// this is actually an error,
// but maybe there is really no stream fitting to the default value.
return selectedFormat;
}
/**
* @see #getDefaultResolution(String, String, List)
*/
public static int getDefaultResolution(Context context, List<VideoStream> videoStreams) {
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences
.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value));
String preferredFormat = defaultPreferences
.getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default));
return getDefaultResolution(defaultResolution, preferredFormat, videoStreams);
}
/**
* @see #getDefaultResolution(String, String, List)
*/
public static int getPopupDefaultResolution(Context context, List<VideoStream> videoStreams) {
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences
.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value));
String preferredFormat = defaultPreferences
.getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default));
return getDefaultResolution(defaultResolution, preferredFormat, videoStreams);
}
/**
* Return the index of the default stream in the list, based on the
* preferred audio format chosen in the settings
*
* @param context context to get the preferred audio format
* @param audioStreams the list that will be extracted the index
*
* @return index of the preferred format
*/
public static int getPreferredAudioFormat(Context context, List<AudioStream> audioStreams) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
if (sharedPreferences == null) return 0;
String preferredFormatString = sharedPreferences.getString(context.getString(R.string.default_audio_format_key), "webm");
int preferredFormat = MediaFormat.WEBMA.id;
switch (preferredFormatString) {
case "webm":
preferredFormat = MediaFormat.WEBMA.id;
break;
case "m4a":
preferredFormat = MediaFormat.M4A.id;
break;
default:
break;
}
int highestQualityIndex = 0;
// Try to find a audio stream with the preferred format
for (int i = 0; i < audioStreams.size(); i++) if (audioStreams.get(i).format == preferredFormat) highestQualityIndex = i;
// Try to find a audio stream with the highest bitrate and preferred format
for (int i = 0; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate
&& audioStream.format == preferredFormat) highestQualityIndex = i;
}
return highestQualityIndex;
}
/**
* Get the audio from the list with the highest bitrate
*
* @param audioStreams list the audio streams
* @return audio with highest average bitrate
*/
public static AudioStream getHighestQualityAudio(List<AudioStream> audioStreams) {
int highestQualityIndex = 0;
for (int i = 1; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate) highestQualityIndex = i;
}
return audioStreams.get(highestQualityIndex);
}
/**
* Join the two lists of video streams (video_only and normal videos), and sort them according with preferred 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
* @return the sorted list
*/
public static ArrayList<VideoStream> getSortedStreamVideosList(Context context, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
boolean showHigherResolutions = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.show_higher_resolutions_key), false);
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default));
MediaFormat preferredFormat = MediaFormat.WEBM;
switch (preferredFormatString) {
case "WebM":
preferredFormat = MediaFormat.WEBM;
break;
case "MPEG-4":
preferredFormat = MediaFormat.MPEG_4;
break;
case "3GPP":
preferredFormat = MediaFormat.v3GPP;
break;
default:
break;
}
return getSortedStreamVideosList(preferredFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder);
}
/**
* Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format
* chosen by the user
*
* @param preferredFormat 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 @return the sorted list
* @return the sorted list
*/
public static ArrayList<VideoStream> getSortedStreamVideosList(MediaFormat preferredFormat, boolean showHigherResolutions, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) {
ArrayList<VideoStream> retList = new ArrayList<>();
HashMap<String, VideoStream> hashMap = new HashMap<>();
if (videoOnlyStreams != null) {
for (VideoStream stream : videoOnlyStreams) {
if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue;
retList.add(stream);
}
}
if (videoStreams != null) {
for (VideoStream stream : videoStreams) {
if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue;
retList.add(stream);
}
}
// Add all to the hashmap
for (VideoStream videoStream : retList) hashMap.put(videoStream.resolution, videoStream);
// Override the values when the key == resolution, with the preferredFormat
for (VideoStream videoStream : retList) {
if (videoStream.format == preferredFormat.id) hashMap.put(videoStream.resolution, videoStream);
}
retList.clear();
retList.addAll(hashMap.values());
sortStreamList(retList, ascendingOrder);
return retList;
}
/**
* Sort the streams list depending on the parameter ascendingOrder;
* <p>
* It works like that:<br>
* - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1"
* and sort by the greatest:<br>
* <blockquote><pre>
* 720p -> 720
* 720p60 -> 721
* 360p -> 360
* 1080p -> 1080
* 1080p60 -> 1081
* <p>
* ascendingOrder ? 360 < 720 < 721 < 1080 < 1081
* !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360/pre></blockquote>
* <p>
* @param videoStreams list that the sorting will be applied
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
*/
public static void sortStreamList(List<VideoStream> videoStreams, final boolean ascendingOrder) {
Collections.sort(videoStreams, new Comparator<VideoStream>() {
@Override
public int compare(VideoStream o1, VideoStream o2) {
int res1 = Integer.parseInt(o1.resolution.replace("0p60", "1").replaceAll("[^\\d.]", ""));
int res2 = Integer.parseInt(o2.resolution.replace("0p60", "1").replaceAll("[^\\d.]", ""));
return ascendingOrder ? res1 - res2 : res2 - res1;
}
});
}
}