diff --git a/app/src/main/java/org/schabi/newpipe/SponsorBlockApiTask.java b/app/src/main/java/org/schabi/newpipe/SponsorBlockApiTask.java
new file mode 100644
index 000000000..a99a9df6d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/SponsorBlockApiTask.java
@@ -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 {
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index 61c5d9e68..8c9043d70 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -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;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index 56744d858..94e4ad771 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -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 customMarkers = new ArrayList<>();
+ ArrayList 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()) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
index 576d42a00..a4716dcb7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -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();
diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
index b0bb30aa7..f83892470 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -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
diff --git a/app/src/main/java/org/schabi/newpipe/util/SponsorTimeInfo.java b/app/src/main/java/org/schabi/newpipe/util/SponsorTimeInfo.java
new file mode 100644
index 000000000..07f232ac4
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/SponsorTimeInfo.java
@@ -0,0 +1,27 @@
+package org.schabi.newpipe.util;
+
+import java.util.ArrayList;
+
+public class SponsorTimeInfo {
+ public ArrayList 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;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/util/TimeFrame.java b/app/src/main/java/org/schabi/newpipe/util/TimeFrame.java
new file mode 100644
index 000000000..d67f1faad
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/TimeFrame.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java
new file mode 100644
index 000000000..48f8847c8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java
@@ -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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java b/app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java
new file mode 100644
index 000000000..0c51b720f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/ic_sponsor_block.png b/app/src/main/res/drawable-hdpi/ic_sponsor_block.png
new file mode 100644
index 000000000..1b7227c44
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_sponsor_block.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_sponsor_block_stop.png b/app/src/main/res/drawable-hdpi/ic_sponsor_block_stop.png
new file mode 100644
index 000000000..303ac5435
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_sponsor_block_stop.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_sponsor_block.png b/app/src/main/res/drawable-mdpi/ic_sponsor_block.png
new file mode 100644
index 000000000..45ce1f618
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_sponsor_block.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_sponsor_block_stop.png b/app/src/main/res/drawable-mdpi/ic_sponsor_block_stop.png
new file mode 100644
index 000000000..3c67a85e6
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_sponsor_block_stop.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_sponsor_block.png b/app/src/main/res/drawable-xhdpi/ic_sponsor_block.png
new file mode 100644
index 000000000..3378a55bf
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_sponsor_block.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_sponsor_block_stop.png b/app/src/main/res/drawable-xhdpi/ic_sponsor_block_stop.png
new file mode 100644
index 000000000..bd9267b4c
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_sponsor_block_stop.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_sponsor_block.png b/app/src/main/res/drawable-xxhdpi/ic_sponsor_block.png
new file mode 100644
index 000000000..8f70b6550
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_sponsor_block.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_sponsor_block_stop.png b/app/src/main/res/drawable-xxhdpi/ic_sponsor_block_stop.png
new file mode 100644
index 000000000..682ce80cf
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_sponsor_block_stop.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_sponsor_block.png b/app/src/main/res/drawable-xxxhdpi/ic_sponsor_block.png
new file mode 100644
index 000000000..9840dbc91
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_sponsor_block.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_sponsor_block_stop.png b/app/src/main/res/drawable-xxxhdpi/ic_sponsor_block_stop.png
new file mode 100644
index 000000000..62afbe65e
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_sponsor_block_stop.png differ
diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml
index 16dcff639..a5d10d6fa 100644
--- a/app/src/main/res/layout-large-land/activity_main_player.xml
+++ b/app/src/main/res/layout-large-land/activity_main_player.xml
@@ -413,6 +413,23 @@
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_background"
tools:ignore="RtlHardcoded" />
+
+
-
+
- downloads_storage_ask
storage_use_saf
+ skip_sponsors
+ Skip Sponsors
+ Use the SponsorBlock API to automatically skip sponsors in videos.
+ sponsorblock_notifications
+ Notify when sponsors are skipped
+ Send a notification to your device when a sponsor is automatically skipped.
+ sponsorblock_username
+ Set Username
+ Set a username used when submitting sponsor times. Leave empty to use a default randomized username.
+ sponsorblock_status
+ View Online Status
+ View the online status of the SponsorBlock servers.
+ sponsorblock_leaderboards
+ View Leaderboards
+ View the online leaderboards for SponsorBlock.
+
file_rename_charset
file_replacement_character
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3625e67f4..f390eb29d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -129,6 +129,7 @@
Other
Debug
Updates
+ SponsorBlock (beta)
Playing in background
Playing in popup mode
Queued on background player
@@ -181,6 +182,7 @@
Switch to Background
Switch to Popup
Switch to Main
+ Submit Sponsor Times
Import database
Export database
Overrides your current history and subscriptions
@@ -609,6 +611,7 @@
Videos that have been watched before and after being added to the playlist will be removed.\nAre you sure? This cannot be undone!
Yes, and partially watched videos
Due to ExoPlayer constraints the seek duration was set to %d seconds
+ Sponsor skipped
- %d second
diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml
index bf9c3d115..5c6ad3977 100644
--- a/app/src/main/res/xml/content_settings.xml
+++ b/app/src/main/res/xml/content_settings.xml
@@ -117,4 +117,43 @@
android:summary="@string/feed_use_dedicated_fetch_method_summary"/>
+
+
+
+
+
+
+
+
+
+
+
+
+