From c69ce3253fcc42474e68e3319a176dc8610f9286 Mon Sep 17 00:00:00 2001 From: polymorphicshade Date: Sun, 8 Mar 2020 15:00:13 -0600 Subject: [PATCH] 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. --- .../schabi/newpipe/SponsorBlockApiTask.java | 174 ++++++++++++++++++ .../org/schabi/newpipe/player/BasePlayer.java | 57 +++++- .../newpipe/player/MainVideoPlayer.java | 119 +++++++++++- .../schabi/newpipe/player/VideoPlayer.java | 28 +++ .../settings/ContentSettingsFragment.java | 15 ++ .../schabi/newpipe/util/SponsorTimeInfo.java | 27 +++ .../org/schabi/newpipe/util/TimeFrame.java | 12 ++ .../schabi/newpipe/views/MarkableSeekBar.java | 50 +++++ .../schabi/newpipe/views/SeekBarMarker.java | 33 ++++ .../res/drawable-hdpi/ic_sponsor_block.png | Bin 0 -> 911 bytes .../drawable-hdpi/ic_sponsor_block_stop.png | Bin 0 -> 818 bytes .../res/drawable-mdpi/ic_sponsor_block.png | Bin 0 -> 548 bytes .../drawable-mdpi/ic_sponsor_block_stop.png | Bin 0 -> 475 bytes .../res/drawable-xhdpi/ic_sponsor_block.png | Bin 0 -> 1326 bytes .../drawable-xhdpi/ic_sponsor_block_stop.png | Bin 0 -> 1173 bytes .../res/drawable-xxhdpi/ic_sponsor_block.png | Bin 0 -> 2146 bytes .../drawable-xxhdpi/ic_sponsor_block_stop.png | Bin 0 -> 1984 bytes .../res/drawable-xxxhdpi/ic_sponsor_block.png | Bin 0 -> 3007 bytes .../ic_sponsor_block_stop.png | Bin 0 -> 2788 bytes .../activity_main_player.xml | 19 +- .../main/res/layout/activity_main_player.xml | 18 +- app/src/main/res/values/settings_keys.xml | 16 ++ app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/content_settings.xml | 39 ++++ 24 files changed, 606 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/SponsorBlockApiTask.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/SponsorTimeInfo.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/TimeFrame.java create mode 100644 app/src/main/java/org/schabi/newpipe/views/MarkableSeekBar.java create mode 100644 app/src/main/java/org/schabi/newpipe/views/SeekBarMarker.java create mode 100644 app/src/main/res/drawable-hdpi/ic_sponsor_block.png create mode 100644 app/src/main/res/drawable-hdpi/ic_sponsor_block_stop.png create mode 100644 app/src/main/res/drawable-mdpi/ic_sponsor_block.png create mode 100644 app/src/main/res/drawable-mdpi/ic_sponsor_block_stop.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_sponsor_block.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_sponsor_block_stop.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_sponsor_block.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_sponsor_block_stop.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_sponsor_block.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_sponsor_block_stop.png 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 0000000000000000000000000000000000000000..1b7227c449982d9900cbeef1fbac33c6bcd91f48 GIT binary patch literal 911 zcmV;A191F_P)SKP5n*q~xC~!w@su^>*jw`2O_$<2v1Y?(^Jpc9lH!>2}Y(&+qx3=ltHDla}^>I5Z5y z=nV=$DL4gggH{kR{u~irm)}o=?O-NwIU%AySPia#PVfVSKphBz09XtbfZ6<%?DeOHMYZi1J+yQ;TdOrJ}9M8B@*`NtDm<4^}bM_ml9b3Rk@YO8nCnID}iUl3y zx`tU$JAI~1(U9!u0|x|^gC%0mEJ%E28}KO_dJB3n5?|X8AW)!xMW8V?Pby@-&xqGWfhQGH_L1w3w!_@6myZKpeVb8SQldrDZ;B*+g!0G z*O9^Cn_WTjyT#*^GBOm@NR^0ZK-7L@)ZDQ7$nsc@;yJj~$bPK;KCAwxlUrGoj38Va z$n+6OFn<}?NI^MP+g!~p7N>loDv1Bzte7N2^x6C1WFjZb=kqOMcc+pV=W3P_KkJvQ zGeMa_ee&-wRkk}ui(^m)C z0l3*BI5Dai>ax3)x{baIJZAXIqq}QUOf$RDYYdj`WR5{oS;2R)L4T2?m^PNAa@w0= z=WG^WG)PoT1|6DWq#R8Hh4i~JyNAVep0}h!oI@w?&=6BVWd1Rg`lljFT7+|4U>=wR zo+l9V5=>!K$-IkV$H}w3h&l_tu=jTJ{2psGv8`Cj)No$f-JW>iAfhUnPxdhq_kwN; z>1Mi+>$~ZDRgUgV9AQ5Cg!JK~=?X)cpRVjH{Ka# l@%>LIs*3NF>;KG)v0vWa^t>M1cIE&8002ovPDHLkV1jP6vq}H} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..303ac5435defa4adfebe332b1a86a3c6200deffd GIT binary patch literal 818 zcmV-21I_%2P)JJ_brgh&DwJBBCIQ zS}92(6k(yb6lyb$rch=zx9<H`sVDLR5)U;<;E5-^IANE&4>8Xc9X_gx4mX_t9^haSP~} zUR9w%Y7zS)K8u^8Rjd~)#8%NP&WL^y|BZg0H`A|#f29X8N7@neT`@kflE?E;_+B^J_GJcOYwZ;#$Cg%x|Z?Nf)SzRg$gQ5{`*0 zc@U$2wsslczXBzh#9cWAY%3_vbNeW!f40_z@3edH2X@t#fCIJQ``$hHA)D2z$`jJBF>H+bIjWX_0pg4M_J5Xq$zm~~&hvhl8%rIlr=XcRvvy^L) z)z&{myVxueOAM1>d^>v4<=oAB*emK;f%651nP*%Sou1`h7K?VksXX;kGRTfHP0967Y+r{m>-i&*7IVw wxWIgw%y~xn6sm%IRg$kP$-S=PpZQ_z7eVqP$D&KtrvLx|07*qoM6N<$f~Y!?F#rGn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..45ce1f618abcb6a8b76fce3ccb039b178914acfb GIT binary patch literal 548 zcmV+<0^9wGP)@9iHj+x*iE_igv7MLo#U6ge+ z6J&JW^R;`jcv$h7;Iy4!700|1Xxsr@!ghfKGFIA5FpfCBcqK66L%4yTF0o&AJtxc# zvhu@t>?eWIuLcjT*eAOAuA6~Xc!iE46Exz572Bn+YqcG`q#d$8rsxQs;;$9=LwESH z+ug|GJ;H#;B3I%Te&u=6+qf@J;g%qXH>krmtD|(ff?ba{=Q~d57{PqSPKww(u34Rb z`*`=I2+pFjP(_`QHvm&b?A)YQ)j2;UJFK<79&0`Zf2pxjZVvlBa2oJV=k&eD;BxJ_ z#>R?0)#7>{NRsC+gQxVGJV9HjM`x=(db1utIW>kedhOi5p3k+qGjHl9is@dFJCBs> mPe28R^kDj=b*nP4AKD&2VQfa6z7G`u%+kh5jO3*A0Vdun8`~3#fyNsB7#BY)KpL#CQ0_f!LmqejNfLZWzGReco|2*8Mp>1Fb=lC6KJ{iXi2l9oRnrouc%po%a8+$ zpz8J>5As-GA+tbDQw=KURWl1bgL#X%tSNg@w&Y~#X%=XJDT_GS=1?RpIrM6nLLR+D zfCYHEDyEP}a{{~tP=Fqj^fZNeum+9;C~ySUd0$Ln9Ng%r`Yn*t=Er}EOJE3m_$cr$ zkCK;|7o-R_d=y~rE@$B-9?*SoK+{MnfF%nBL)TO;J^F3Gh!^Bgo8E2Ym78t%n)ZPU zFau7zFh9`?drptvp6r;WndtE0t4hn9Sf>q#Pw$=9wBE-2d(ZBf(`{GFfLj@qEsvlh zO{Uk@$p-QWhINELMNwn?mh#)59j)^!7zJZsMPp7-e*z+Ep|651G7|a^@dX&G60ftC R^`HO%002ovPDHLkV1lY3&HexY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3378a55bff1e2db1203c39b8dd407f4240d9fe73 GIT binary patch literal 1326 zcmV+}1=0G6P)2sX^qZ9-|U!hKzB|IXm6FwE%gs$M<)Wy5c_}2Q|Ae?EuiY4?&VZN|lXcv-> zfZf7AVVCfhuvu6OHuKCB{E2TMNL2+x?Ahdmaq~QiVub+Ip1=pb6ZVEFu!-3l$Zi`;0f(O*6+ZN+ zzydZ=3#IReDX@YMPZKW5R{?mIv!}FqP~ZyT6n?J`Q{XOMUL_3kp@74?T6j25|EOgj zFlr1N38^lj=WAI9`4I$VG=7fgXP|9CPB-!sS`jF$Jr zY$_iuWvO=-QUS)emYgFK?dH2Bw6E!jVHIccJ#=a)pb4F8=bN;=I*cAbmR)3M)!7)Y zhUxjjC?_-X<;25DExRJ{OAxv0k!fv|nVC%I7aY&eTLHZrfHUTj(KXZeS=v*gC}7^c zm+b5$VPb@3&Zq#(KP^U9mt-;rQCKiP=^xcTZ%wnmtuc=hXHgG|L)#epe%g2E8rs@% z=)w3yBB}!Vdm<&7BQb^VCb1uk$^B+4k!yTTrInA-UK3&9ONHAQFds_oybsZCh*Q|A z!tTr$#&MYH%rfvk52wxp6wVBXgdb7`usBN0faxaeac&V($2%FTnfB~_%8SP6O&Hxo z`}G0}{{|W1Y@*QH8P7dAmKr-*+B!mOTLFZ(ban_ER|n&b@l~O|kr~-R%2`jD&lery z09i>IYGcjuSzwv}PcVSZ#pprDsq?8mBq$&luf;sG zRjw-34b91vylsgW@@!tjgITEX4v!?hHN4Q*Ok>H6On}>&z3JF-RQk)R&G4HKFLUC$ zV%?S1QOQd>=5!&?qC}fX@VO7+Ev-De*te8p$xj0=J}Rinwe%o-E47^prw(OiGp@I; zT8DDYWb_gDT;*I39ZtJSYD92Ll0I-8c4Tkg7Y`)-B{{^+z@)|#4qxF|UB;DFyHg0R zE|p8~R7gF5B34rx<-8xkeQ`I{xm&!)c~6Sisr6ka(wM2d=R_#mN5+@>zaXRvy4#5` k34YrO^*jDg8vi%?8|@c^GO3(MdH?_b07*qoM6N<$f`nRl?EnA( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bd9267b4ce5a67c971adf58f3bc3f0302525e41d GIT binary patch literal 1173 zcmV;G1Zw+-G%8De+%(6&xxP2e|9$ex+-@Cr)79RM+J?EbD|Nh^Z&$%}&Y%Khv!7z+T zf~A5Df)j#9!COI(;Xd?`r;#$nf;8m{hTw2PqM%f8Ptg09xIRILpheImI43v`&XLwa zp1!}z{)Dp>8{z&G9w*o-crF;iV87s*;D}(YV3A;oAkquINb)YCta7x%X<*|yHfQ@% z+e-wO1iwtRbVIOC5G#lP)942If^xxWK?9=9Xy9ErdDAExfxTF`)y5BOs_pZFB(GZq z;nxL&kh~CV7R=;*v|yE>M$l%_9qfcYIFxI(U@|slFg{+8ZxBsOJc%zfHRWA|YLib8 zWC^ZX^nHJZUTkDxGm_^!rmkD)PW&?ZUkAdqX{QQy3EmmO5AOwg&=8Iz9cWx`<$%r* z1ASLHqs3|l$ySr;6hYi$sp@nMxeW5bTCd+Vau zX+`#0UkJtv${grG8QQd>Ut5vYGezktnBzbP)}T!_-n1e+M8;(H9!_&WHKp)eX+@UH z%R=@$y$*7q7fl<8=US01(9x|sM$>3_iUY3M@hln_Sdr=G>5JeEo2Ua0a$p~t+8M;- z+!0=5OOeQN(J2ljqe-=08;P)h0$XX3j}CC)BU-i*FYt7H>^CjjhaKQR6`Q!nXp8ei zdYco_e8FM{O{g3gMnfW+ZhJ%OnocWB=Y&u>aG!h?#MgO48qG-W5=05IL+C&@db-gR z?Je;^2#PpbwuZogR%{m$-*1OFo@7#ApVn{DA7ncRWfii4e zBwp;xy{H}_p0b3mB>pgf;vZsTB{sF2jra9(H;F62Pc-WsPHz4Vop8rtOKm3k>xR12 zG`O^eGJL!fwYnqCfV-KrQh#2tD{-A`Z$et0=>pi7pd5K{R*_ccbJwwOB1Z6%IeH^$ z+r8K;dD`o2hpULV+K36%SDFO+=Wh=F5nTQbjY7PZ*#bunhjryn2=u2WJzRWuIYC;! z&F-Xm^C_FdHd#57gY|(g9fMkH>zTJ#c=*8Q3g5^ZPT2vt(u4h#GKC%DFxy=n=yN@6 z{Pf*C@>Wn*IZ{IPsa40sE5zMmorpr*fJwMMSMqKKc@4@YgoQFNJJlmdcaE8)ry^95 zN?Im)49diWIU$(tQ~Md9C$*+c?`H~*@lNyX_WL~V$8phS`YL}Nd?T$bWc}TS6BF3S nx0}X*KJU=)Tw_0J{BPk8_1VK5M={vy00000NkvXXu0mjfN~9v_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8f70b6550b00ed5a0dc612178ffa4b4da842873b GIT binary patch literal 2146 zcmV-o2%YzdP)P7L-UfLM{gKv&R zDf@51X2EE|U4l}HA|*|5hhU6g_eEyNIdp2C;5k7XL3KffAYIFdvrhe{EXrtuGUp1m z3I4js^4u?&AZV=L2SH3lK@W`nd6x$5f}Y3}TodGHPos4 zZKunCoyY9%EvV!NF;`>iA9bmCi(sgrp-l*8S{XqtK}SJ{hgCvrE~>1g{I~dO=JT!Bi+(mg9oA1qK}HQ_#+DHT~My6fF?0i26vJAEPK6xFZ$)~hZ9Ahyn5<$a} zNnM#$kT%&*g2?P|K}$R+nNG*2<4BtliJ$@WaXq9x={G@^7omHA`ssO^#f}V)M9}?| z@lm9;4vHYlb2%meFTum8?*U57jMT6?bXXlk-o(5h38MBiP(4BRnb%Ss_4FkZq+v8i znKNn&C~a>d2qOK|8a`#OT_B~cE&hy&7Hc!%pq@&$<7kqbR7e+cq$LGG)J_dO ze^Chi*wm%JkHu1PRT`aHNW0X-t=AKR$VIGi`XA$LgPu9dpp5ZIJLjyJSSoeQZjDKm zLi(I!BZ%6pUe6J6v|%Ax&|8tFF|-n~Ryc(GC5@2QE*S|j;lN}s6TSr!t8~P-H<^(!kG%pB2O2&E=@~6SI5&8R6fM+pnLo0IR6;__1!>|*r(GXY3 zg06RVb-aYMa)JYX6C`=ASA9`HJ>w>Skc0klO7B~kouWK#pV_3WhxtViMt7(Wu8z^P z`#EPXy@^t>u-oY{m!n73LTvf21fk(wd~%lQ?5T!&I#K#WFAT97CO>$%(^;=3eAmV+ zm#ey4*Z3F?8cA1@w^2;2wpZ5bM5?SS1r@UkLl7Kv@A~KvGw@|qaG5<*ygcjhEIL3t zTKfWw`gS<^&E!KdIpcjI=^l!JoF@OZn)DwANX}OjCB5B*??bET+qJfdeo)tk zmefJVLdB_kWdP)?AViDaPThR9{&4u@bRwnJTia?UST%1dt&{^{|%>aD| zGT%i1&kK45uL{jTJ$&*raD7!+E>si+yW4Pz^J|A^k%CP}2sV*yR3bDN z(chAxqAYNcbg;rWdex+{<M$dUgDn@jscPu|D!2xxMC~DB_8_fgs91`nSW;R3 Y1=;jlrk}>3r2qf`07*qoM6N<$f|m~hc>n+a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..682ce80cf717767992f2c498759aaf21aec70e76 GIT binary patch literal 1984 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 Bz)=7I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..62afbe65e0e1b55fde9ac2b7fb5a71076a778243 GIT binary patch literal 2788 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 + + - + - 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"/> + + + + + + + + + + + + +