SponsorBlock: Init
Added a setting which, when enabled, skips parts of the video according to the sponsor times SponsorBlock's API returns for the video.
174
app/src/main/java/org/schabi/newpipe/SponsorBlockApiTask.java
Normal file
|
@ -0,0 +1,174 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.schabi.newpipe.util.SponsorTimeInfo;
|
||||
import org.schabi.newpipe.util.TimeFrame;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class SponsorBlockApiTask extends AsyncTask<String, Void, JSONObject> {
|
||||
private static final Application app = App.getApp();
|
||||
private static final String sponsorBlockApiUrl = "https://api.sponsor.ajay.app/api/";
|
||||
private static final int timeoutPeriod = 30;
|
||||
private static final String TAG = SponsorBlockApiTask.class.getSimpleName();
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private OkHttpClient client;
|
||||
|
||||
// api methods
|
||||
public SponsorTimeInfo getVideoSponsorTimes(String url) throws ExecutionException, InterruptedException, JSONException {
|
||||
String videoId = parseIdFromUrl(url);
|
||||
String apiSuffix = "getVideoSponsorTimes?videoID=" + videoId;
|
||||
|
||||
JSONObject obj = execute(apiSuffix).get();
|
||||
JSONArray arrayObj = obj.getJSONArray("sponsorTimes");
|
||||
|
||||
SponsorTimeInfo result = new SponsorTimeInfo();
|
||||
|
||||
for (int i = 0; i < arrayObj.length(); i++) {
|
||||
JSONArray subArrayObj = arrayObj.getJSONArray(i);
|
||||
|
||||
double startTime = subArrayObj.getDouble(0) * 1000;
|
||||
double endTime = subArrayObj.getDouble(1) * 1000;
|
||||
|
||||
TimeFrame timeFrame = new TimeFrame(startTime, endTime);
|
||||
|
||||
result.timeFrames.add(timeFrame);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void postVideoSponsorTimes(String url, double startTime, double endTime, String userId) {
|
||||
if (userId == null) {
|
||||
userId = getRandomUserId();
|
||||
}
|
||||
|
||||
double dStartTime = startTime / 1000.0;
|
||||
double dEndTime = endTime / 1000.0;
|
||||
|
||||
String videoId = parseIdFromUrl(url);
|
||||
String apiSuffix = "postVideoSponsorTimes?videoID=" + videoId + "&startTime=" + dStartTime + "&endTime=" + dEndTime + "&userID=" + userId;
|
||||
|
||||
execute(apiSuffix);
|
||||
}
|
||||
|
||||
// task methods
|
||||
@Override
|
||||
protected JSONObject doInBackground(String... strings) {
|
||||
if (isCancelled() || !isConnected()) return null;
|
||||
|
||||
try {
|
||||
if (client == null) {
|
||||
client = getUnsafeOkHttpClient()
|
||||
.newBuilder()
|
||||
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(sponsorBlockApiUrl + strings[0])
|
||||
.build();
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
ResponseBody responseBody = response.body();
|
||||
|
||||
return responseBody == null
|
||||
? null
|
||||
: new JSONObject(responseBody.string());
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// helper methods
|
||||
private boolean isConnected() {
|
||||
ConnectivityManager cm = (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
private OkHttpClient getUnsafeOkHttpClient() throws NoSuchAlgorithmException, KeyManagementException {
|
||||
final TrustManager[] trustAllCerts = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@Override
|
||||
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@Override
|
||||
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||
return new java.security.cert.X509Certificate[]{};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||
|
||||
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||
|
||||
return new OkHttpClient
|
||||
.Builder()
|
||||
.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0])
|
||||
.hostnameVerifier((hostname, session) -> true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String parseIdFromUrl(String youTubeUrl) {
|
||||
String pattern = "(?<=youtu.be/|watch\\?v=|/videos/|embed/)[^#&?]*";
|
||||
Pattern compiledPattern = Pattern.compile(pattern);
|
||||
Matcher matcher = compiledPattern.matcher(youTubeUrl);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getRandomUserId() {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder salt = new StringBuilder();
|
||||
Random random = new Random();
|
||||
|
||||
while (salt.length() < 36) {
|
||||
int index = (int) (random.nextFloat() * chars.length());
|
||||
salt.append(chars.charAt(index));
|
||||
}
|
||||
|
||||
return salt.toString();
|
||||
}
|
||||
}
|
|
@ -34,6 +34,8 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
|
@ -54,9 +56,11 @@ import com.nostra13.universalimageloader.core.ImageLoader;
|
|||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.SponsorBlockApiTask;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||
|
@ -74,6 +78,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.SponsorTimeInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -109,6 +114,11 @@ public abstract class BasePlayer implements
|
|||
public static final int STATE_PAUSED_SEEK = 127;
|
||||
public static final int STATE_COMPLETED = 128;
|
||||
|
||||
@NonNull
|
||||
private final SharedPreferences mPrefs;
|
||||
|
||||
private SponsorTimeInfo sponsorTimeInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -219,6 +229,8 @@ public abstract class BasePlayer implements
|
|||
|
||||
this.loadControl = new LoadController();
|
||||
this.renderFactory = new DefaultRenderersFactory(context);
|
||||
|
||||
this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
|
@ -648,11 +660,37 @@ public abstract class BasePlayer implements
|
|||
if (simpleExoPlayer == null) {
|
||||
return;
|
||||
}
|
||||
int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0);
|
||||
onUpdateProgress(
|
||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
currentProgress,
|
||||
(int) simpleExoPlayer.getDuration(),
|
||||
simpleExoPlayer.getBufferedPercentage()
|
||||
);
|
||||
|
||||
if (mPrefs.getBoolean(context.getString(R.string.sponsorblock_enable), false) && sponsorTimeInfo != null) {
|
||||
int skipTo = sponsorTimeInfo.getSponsorEndTimeFromProgress(currentProgress);
|
||||
|
||||
if (skipTo == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
seekTo(skipTo);
|
||||
|
||||
if (mPrefs.getBoolean(context.getString(R.string.sponsorblock_notifications), false)) {
|
||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, context.getString(R.string.notification_channel_id))
|
||||
.setOngoing(false)
|
||||
.setSmallIcon(R.drawable.ic_sponsor_block)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentTitle(context.getString(R.string.settings_category_sponsorblock))
|
||||
.setContentText(context.getString(R.string.sponsorblock_skipped_sponsor) + " \uD83D\uDC4D");
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(App.getApp());
|
||||
notificationManager.notify(0, notificationBuilder.build());
|
||||
}
|
||||
|
||||
if (DEBUG)
|
||||
Log.d("SPONSOR_BLOCK", "Skipped sponsor: currentProgress = [" + currentProgress + "], skipped to = [" + skipTo + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private Disposable getProgressReactor() {
|
||||
|
@ -1015,6 +1053,15 @@ public abstract class BasePlayer implements
|
|||
|
||||
initThumbnail(info.getThumbnailUrl());
|
||||
registerView();
|
||||
|
||||
if (mPrefs.getBoolean(context.getString(R.string.sponsorblock_enable), false)) {
|
||||
try {
|
||||
sponsorTimeInfo = new SponsorBlockApiTask().getVideoSponsorTimes(getVideoUrl());
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e("SPONSOR_BLOCK", "Error getting video sponsor times.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1546,4 +1593,12 @@ public abstract class BasePlayer implements
|
|||
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
|
||||
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true);
|
||||
}
|
||||
|
||||
public SponsorTimeInfo getSponsorTimeInfo() {
|
||||
return sponsorTimeInfo;
|
||||
}
|
||||
|
||||
public void setSponsorTimeInfo(SponsorTimeInfo sponsorTimeInfo) {
|
||||
this.sponsorTimeInfo = sponsorTimeInfo;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -70,6 +71,7 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
|||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.SponsorBlockApiTask;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
|
@ -87,10 +89,15 @@ import org.schabi.newpipe.util.ListHelper;
|
|||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.SponsorTimeInfo;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
import org.schabi.newpipe.util.TimeFrame;
|
||||
import org.schabi.newpipe.views.MarkableSeekBar;
|
||||
import org.schabi.newpipe.views.SeekBarMarker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
|
@ -129,6 +136,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
|
||||
private ContentObserver rotationObserver;
|
||||
|
||||
private int customSponsorStartTime;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -548,6 +557,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
private ImageButton switchPopupButton;
|
||||
private ImageButton switchBackgroundButton;
|
||||
private ImageButton muteButton;
|
||||
private ImageButton submitSponsorTimesButton;
|
||||
|
||||
private RelativeLayout windowRootLayout;
|
||||
private View secondaryControls;
|
||||
|
@ -585,6 +595,17 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
this.toggleOrientationButton = view.findViewById(R.id.toggleOrientation);
|
||||
this.switchBackgroundButton = view.findViewById(R.id.switchBackground);
|
||||
this.muteButton = view.findViewById(R.id.switchMute);
|
||||
|
||||
this.submitSponsorTimesButton = rootView.findViewById(R.id.submitSponsorTimes);
|
||||
this.submitSponsorTimesButton.setTag(false);
|
||||
this.submitSponsorTimesButton.setOnLongClickListener(v -> {
|
||||
onSponsorBlockButtonLongClicked();
|
||||
return true;
|
||||
});
|
||||
if (!defaultPreferences.getBoolean(getString(R.string.sponsorblock_enable), false)) {
|
||||
this.submitSponsorTimesButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.switchPopupButton = view.findViewById(R.id.switchPopup);
|
||||
|
||||
this.queueLayout = findViewById(R.id.playQueuePanel);
|
||||
|
@ -634,6 +655,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
toggleOrientationButton.setOnClickListener(this);
|
||||
switchBackgroundButton.setOnClickListener(this);
|
||||
muteButton.setOnClickListener(this);
|
||||
submitSponsorTimesButton.setOnClickListener(this);
|
||||
switchPopupButton.setOnClickListener(this);
|
||||
|
||||
getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> {
|
||||
|
@ -814,6 +836,97 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
setMuteButton(muteButton, playerImpl.isMuted());
|
||||
}
|
||||
|
||||
private void onSponsorBlockButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onSponsorBlockButtonClicked() called");
|
||||
if (playerImpl.getPlayer() == null) return;
|
||||
|
||||
if ((boolean) submitSponsorTimesButton.getTag()) {
|
||||
TimeFrame customTimeFrame = new TimeFrame(customSponsorStartTime, simpleExoPlayer.getCurrentPosition());
|
||||
customTimeFrame.tag = "custom";
|
||||
|
||||
SponsorTimeInfo sponsorTimeInfo = getSponsorTimeInfo();
|
||||
if (sponsorTimeInfo == null) {
|
||||
sponsorTimeInfo = new SponsorTimeInfo();
|
||||
|
||||
setSponsorTimeInfo(sponsorTimeInfo);
|
||||
}
|
||||
|
||||
sponsorTimeInfo.timeFrames.add(customTimeFrame);
|
||||
|
||||
SeekBarMarker marker = new SeekBarMarker(customTimeFrame.startTime, customTimeFrame.endTime, (int) simpleExoPlayer.getDuration(), Color.BLUE);
|
||||
marker.tag = "custom";
|
||||
|
||||
MarkableSeekBar markableSeekBar = (MarkableSeekBar) getPlaybackSeekBar();
|
||||
markableSeekBar.seekBarMarkers.add(marker);
|
||||
markableSeekBar.invalidate();
|
||||
|
||||
submitSponsorTimesButton.setTag(false);
|
||||
submitSponsorTimesButton.setImageResource(R.drawable.ic_sponsor_block);
|
||||
}
|
||||
else {
|
||||
customSponsorStartTime = (int) simpleExoPlayer.getCurrentPosition();
|
||||
|
||||
submitSponsorTimesButton.setTag(true);
|
||||
submitSponsorTimesButton.setImageResource(R.drawable.ic_sponsor_block_stop);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSponsorBlockButtonLongClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onSponsorBlockButtonLongClicked() called");
|
||||
if (playerImpl.getPlayer() == null) return;
|
||||
|
||||
ArrayList<SeekBarMarker> customMarkers = new ArrayList<>();
|
||||
ArrayList<TimeFrame> customTimeFrames = new ArrayList<>();
|
||||
|
||||
for (SeekBarMarker marker : ((MarkableSeekBar) getPlaybackSeekBar()).seekBarMarkers) {
|
||||
if (marker.tag == "custom") {
|
||||
customMarkers.add(marker);
|
||||
}
|
||||
}
|
||||
|
||||
if (customMarkers.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
SponsorTimeInfo sponsorTimeInfo = getSponsorTimeInfo();
|
||||
if (sponsorTimeInfo != null) {
|
||||
for (TimeFrame timeFrame : sponsorTimeInfo.timeFrames) {
|
||||
if (timeFrame.tag == "custom") {
|
||||
customTimeFrames.add(timeFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new AlertDialog
|
||||
.Builder(context)
|
||||
.setMessage("Submit " + customMarkers.size() + " sponsor time segment(s)?")
|
||||
.setPositiveButton("Yes", (dialog, id) -> {
|
||||
String username = defaultPreferences.getString(getString(R.string.sponsorblock_username), null);
|
||||
for (TimeFrame timeFrame : customTimeFrames) {
|
||||
try {
|
||||
new SponsorBlockApiTask().postVideoSponsorTimes(getVideoUrl(), timeFrame.startTime, timeFrame.endTime, username);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e("SPONSOR_BLOCK", "Error getting video sponsor times.", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton("No", null)
|
||||
.setNeutralButton("Clear", (dialog, id) -> {
|
||||
for (SeekBarMarker marker : customMarkers) {
|
||||
((MarkableSeekBar) getPlaybackSeekBar()).seekBarMarkers.remove(marker);
|
||||
}
|
||||
|
||||
if (sponsorTimeInfo != null) {
|
||||
for (TimeFrame timeFrame : customTimeFrames) {
|
||||
sponsorTimeInfo.timeFrames.remove(timeFrame);
|
||||
}
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
|
@ -845,7 +958,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
onPlayBackgroundButtonClicked();
|
||||
} else if (v.getId() == muteButton.getId()) {
|
||||
onMuteUnmuteButtonClicked();
|
||||
} else if (v.getId() == closeButton.getId()) {
|
||||
|
||||
} else if (v.getId() == submitSponsorTimesButton.getId()) {
|
||||
onSponsorBlockButtonClicked();
|
||||
|
||||
}else if (v.getId() == closeButton.getId()) {
|
||||
onPlaybackShutdown();
|
||||
return;
|
||||
} else if (v.getId() == kodiButton.getId()) {
|
||||
|
|
|
@ -69,6 +69,10 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.SponsorTimeInfo;
|
||||
import org.schabi.newpipe.util.TimeFrame;
|
||||
import org.schabi.newpipe.views.MarkableSeekBar;
|
||||
import org.schabi.newpipe.views.SeekBarMarker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -621,6 +625,8 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
|
||||
super.onPrepared(playWhenReady);
|
||||
|
||||
tryMarkSponsorTimes();
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler
|
||||
|
@ -628,6 +634,28 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
}
|
||||
}
|
||||
|
||||
private void tryMarkSponsorTimes() {
|
||||
SponsorTimeInfo sponsorTimeInfo = getSponsorTimeInfo();
|
||||
|
||||
if (sponsorTimeInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(playbackSeekBar instanceof MarkableSeekBar)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MarkableSeekBar markableSeekBar = (MarkableSeekBar) playbackSeekBar;
|
||||
|
||||
for (TimeFrame timeFrame : sponsorTimeInfo.timeFrames) {
|
||||
SeekBarMarker seekBarMarker = new SeekBarMarker(timeFrame.startTime, timeFrame.endTime, (int) simpleExoPlayer.getDuration(), Color.GREEN);
|
||||
markableSeekBar.seekBarMarkers.add(seekBarMarker);
|
||||
markableSeekBar.invalidate();
|
||||
|
||||
Log.d("SPONSOR_BLOCK", "Progress bar marker: " + seekBarMarker.percentStart + ", " + seekBarMarker.percentEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
|||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
@ -138,6 +139,20 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||
startActivityForResult(i, REQUEST_EXPORT_PATH);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference sponsorblockStatusPreference = findPreference(getString(R.string.sponsorblock_status));
|
||||
sponsorblockStatusPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://status.sponsor.ajay.app/"));
|
||||
startActivity(i);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference sponsorblockLeaderboardsPreference = findPreference(getString(R.string.sponsorblock_leaderboards));
|
||||
sponsorblockLeaderboardsPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://api.sponsor.ajay.app/stats"));
|
||||
startActivity(i);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class SponsorTimeInfo {
|
||||
public ArrayList<TimeFrame> timeFrames = new ArrayList<>();
|
||||
|
||||
public int getSponsorEndTimeFromProgress(int progress) {
|
||||
if (timeFrames == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (TimeFrame timeFrames : timeFrames) {
|
||||
if (progress < timeFrames.startTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (progress > timeFrames.endTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return (int) Math.ceil((timeFrames.endTime));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
12
app/src/main/java/org/schabi/newpipe/util/TimeFrame.java
Normal file
|
@ -0,0 +1,12 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
public class TimeFrame {
|
||||
public double startTime;
|
||||
public double endTime;
|
||||
public Object tag;
|
||||
|
||||
public TimeFrame(double startTime, double endTime) {
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.schabi.newpipe.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatSeekBar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MarkableSeekBar extends AppCompatSeekBar {
|
||||
public ArrayList<SeekBarMarker> seekBarMarkers = new ArrayList<>();
|
||||
private RectF markerRect = new RectF();
|
||||
|
||||
public MarkableSeekBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MarkableSeekBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MarkableSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
protected synchronized void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
Drawable progressDrawable = getProgressDrawable();
|
||||
Rect progressDrawableBounds = progressDrawable.getBounds();
|
||||
|
||||
int width = getMeasuredWidth() - (getPaddingStart() + getPaddingEnd());
|
||||
int height = progressDrawable.getIntrinsicHeight();
|
||||
|
||||
for (int i = 0; i < seekBarMarkers.size(); i++) {
|
||||
SeekBarMarker marker = seekBarMarkers.get(i);
|
||||
|
||||
markerRect.left = width - (float) Math.floor(width * (1.0 - marker.percentStart)) + getPaddingStart();
|
||||
markerRect.top = progressDrawableBounds.bottom - height - 1;
|
||||
markerRect.right = width - (float) Math.ceil(width * (1.0 - marker.percentEnd)) + getPaddingStart();
|
||||
markerRect.bottom = progressDrawableBounds.bottom;
|
||||
|
||||
canvas.drawRect(markerRect, marker.paint);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.schabi.newpipe.views;
|
||||
|
||||
import android.graphics.Paint;
|
||||
|
||||
public class SeekBarMarker {
|
||||
public double startTime;
|
||||
public double endTime;
|
||||
public double percentStart;
|
||||
public double percentEnd;
|
||||
public Paint paint;
|
||||
public Object tag;
|
||||
|
||||
public SeekBarMarker(double startTime, double endTime, int maxTime, int color) {
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.percentStart = Math.round((startTime / maxTime) * 100.0) / 100.0;
|
||||
this.percentEnd = Math.round((endTime / maxTime) * 100.0) / 100.0;
|
||||
|
||||
initPaint(color);
|
||||
}
|
||||
|
||||
public SeekBarMarker(double percentStart, double percentEnd, int color) {
|
||||
this.percentStart = percentStart;
|
||||
this.percentEnd = percentEnd;
|
||||
|
||||
initPaint(color);
|
||||
}
|
||||
|
||||
private void initPaint(int color) {
|
||||
this.paint = new Paint();
|
||||
this.paint.setColor(color);
|
||||
}
|
||||
}
|
BIN
app/src/main/res/drawable-hdpi/ic_sponsor_block.png
Normal file
After Width: | Height: | Size: 911 B |
BIN
app/src/main/res/drawable-hdpi/ic_sponsor_block_stop.png
Normal file
After Width: | Height: | Size: 818 B |
BIN
app/src/main/res/drawable-mdpi/ic_sponsor_block.png
Normal file
After Width: | Height: | Size: 548 B |
BIN
app/src/main/res/drawable-mdpi/ic_sponsor_block_stop.png
Normal file
After Width: | Height: | Size: 475 B |
BIN
app/src/main/res/drawable-xhdpi/ic_sponsor_block.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_sponsor_block_stop.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_sponsor_block.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_sponsor_block_stop.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_sponsor_block.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_sponsor_block_stop.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
|
@ -413,6 +413,23 @@
|
|||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/switch_to_background"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/submitSponsorTimes"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_toLeftOf="@id/switchMute"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_sponsor_block"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/submit_sponsor_times"
|
||||
tools:ignore="RtlHardcoded"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
@ -438,7 +455,7 @@
|
|||
tools:text="1:06:29"/>
|
||||
|
||||
|
||||
<org.schabi.newpipe.views.FocusAwareSeekBar
|
||||
<org.schabi.newpipe.views.MarkableSeekBar
|
||||
android:id="@+id/playbackSeekBar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -406,6 +406,22 @@
|
|||
android:contentDescription="@string/switch_to_background"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/submitSponsorTimes"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_toLeftOf="@id/switchMute"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_sponsor_block"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/submit_sponsor_times"
|
||||
tools:ignore="RtlHardcoded"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
@ -431,7 +447,7 @@
|
|||
tools:text="1:06:29"/>
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
<org.schabi.newpipe.views.MarkableSeekBar
|
||||
android:id="@+id/playbackSeekBar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -223,6 +223,22 @@
|
|||
<string name="downloads_storage_ask" translatable="false">downloads_storage_ask</string>
|
||||
<string name="storage_use_saf" translatable="false">storage_use_saf</string>
|
||||
|
||||
<string name="sponsorblock_enable">skip_sponsors</string>
|
||||
<string name="sponsorblock_enable_title">Skip Sponsors</string>
|
||||
<string name="sponsorblock_enable_summary">Use the SponsorBlock API to automatically skip sponsors in videos.</string>
|
||||
<string name="sponsorblock_notifications">sponsorblock_notifications</string>
|
||||
<string name="sponsorblock_notifications_title">Notify when sponsors are skipped</string>
|
||||
<string name="sponsorblock_notifications_summary">Send a notification to your device when a sponsor is automatically skipped.</string>
|
||||
<string name="sponsorblock_username">sponsorblock_username</string>
|
||||
<string name="sponsorblock_username_title">Set Username</string>
|
||||
<string name="sponsorblock_username_summary">Set a username used when submitting sponsor times. Leave empty to use a default randomized username.</string>
|
||||
<string name="sponsorblock_status">sponsorblock_status</string>
|
||||
<string name="sponsorblock_status_title">View Online Status</string>
|
||||
<string name="sponsorblock_status_summary">View the online status of the SponsorBlock servers.</string>
|
||||
<string name="sponsorblock_leaderboards">sponsorblock_leaderboards</string>
|
||||
<string name="sponsorblock_leaderboards_title">View Leaderboards</string>
|
||||
<string name="sponsorblock_leaderboards_summary">View the online leaderboards for SponsorBlock.</string>
|
||||
|
||||
<!-- FileName Downloads -->
|
||||
<string name="settings_file_charset_key" translatable="false">file_rename_charset</string>
|
||||
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>
|
||||
|
|
|
@ -129,6 +129,7 @@
|
|||
<string name="settings_category_other_title">Other</string>
|
||||
<string name="settings_category_debug_title">Debug</string>
|
||||
<string name="settings_category_updates_title">Updates</string>
|
||||
<string name="settings_category_sponsorblock">SponsorBlock (beta)</string>
|
||||
<string name="background_player_playing_toast">Playing in background</string>
|
||||
<string name="popup_playing_toast">Playing in popup mode</string>
|
||||
<string name="background_player_append">Queued on background player</string>
|
||||
|
@ -181,6 +182,7 @@
|
|||
<string name="switch_to_background">Switch to Background</string>
|
||||
<string name="switch_to_popup">Switch to Popup</string>
|
||||
<string name="switch_to_main">Switch to Main</string>
|
||||
<string name="submit_sponsor_times">Submit Sponsor Times</string>
|
||||
<string name="import_data_title">Import database</string>
|
||||
<string name="export_data_title">Export database</string>
|
||||
<string name="import_data_summary">Overrides your current history and subscriptions</string>
|
||||
|
@ -609,6 +611,7 @@
|
|||
<string name="remove_watched_popup_warning">Videos that have been watched before and after being added to the playlist will be removed.\nAre you sure? This cannot be undone!</string>
|
||||
<string name="remove_watched_popup_yes_and_partially_watched_videos">Yes, and partially watched videos</string>
|
||||
<string name="new_seek_duration_toast">Due to ExoPlayer constraints the seek duration was set to %d seconds</string>
|
||||
<string name="sponsorblock_skipped_sponsor">Sponsor skipped</string>
|
||||
<!-- Time duration plurals -->
|
||||
<plurals name="seconds">
|
||||
<item quantity="one">%d second</item>
|
||||
|
|
|
@ -117,4 +117,43 @@
|
|||
android:summary="@string/feed_use_dedicated_fetch_method_summary"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:layout="@layout/settings_category_header_layout"
|
||||
android:title="@string/settings_category_sponsorblock">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/sponsorblock_enable"
|
||||
android:summary="@string/sponsorblock_enable_summary"
|
||||
android:title="@string/sponsorblock_enable_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:dependency="@string/sponsorblock_enable"
|
||||
android:key="@string/sponsorblock_notifications"
|
||||
android:summary="@string/sponsorblock_notifications_summary"
|
||||
android:title="@string/sponsorblock_notifications_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<EditTextPreference
|
||||
android:dependency="@string/sponsorblock_enable"
|
||||
android:key="@string/sponsorblock_username"
|
||||
android:summary="@string/sponsorblock_username_summary"
|
||||
android:title="@string/sponsorblock_username_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/sponsorblock_status"
|
||||
android:summary="@string/sponsorblock_status_summary"
|
||||
android:title="@string/sponsorblock_status_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/sponsorblock_leaderboards"
|
||||
android:summary="@string/sponsorblock_leaderboards_summary"
|
||||
android:title="@string/sponsorblock_leaderboards_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
|