Skip to content

Commit 7f4e606

Browse files
committed
Add SponsorBlock support
1 parent d656c75 commit 7f4e606

10 files changed

Lines changed: 576 additions & 10 deletions

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import android.support.v4.media.session.MediaSessionCompat;
5959
import android.util.Log;
6060
import android.view.LayoutInflater;
61+
import android.widget.Toast;
6162

6263
import androidx.annotation.NonNull;
6364
import androidx.annotation.Nullable;
@@ -89,6 +90,9 @@
8990
import org.schabi.newpipe.error.ErrorUtil;
9091
import org.schabi.newpipe.error.UserAction;
9192
import org.schabi.newpipe.extractor.Image;
93+
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction;
94+
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory;
95+
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
9296
import org.schabi.newpipe.extractor.stream.AudioStream;
9397
import org.schabi.newpipe.extractor.stream.StreamInfo;
9498
import org.schabi.newpipe.extractor.stream.StreamType;
@@ -127,9 +131,12 @@
127131
import org.schabi.newpipe.util.StreamTypeUtil;
128132
import org.schabi.newpipe.util.image.CoilHelper;
129133

134+
import java.util.Collections;
135+
import java.util.HashSet;
130136
import java.util.List;
131137
import java.util.Objects;
132138
import java.util.Optional;
139+
import java.util.Set;
133140
import java.util.stream.IntStream;
134141

135142
import coil3.target.Target;
@@ -202,6 +209,14 @@ public final class Player implements PlaybackListener, Listener {
202209
@Nullable
203210
private coil3.request.Disposable thumbnailDisposable;
204211

212+
@NonNull
213+
private List<SponsorBlockSegment> sponsorBlockSegments = Collections.emptyList();
214+
@NonNull
215+
private final Set<String> skippedSponsorBlockSegments = new HashSet<>();
216+
@Nullable
217+
private String ignoredSponsorBlockSegment;
218+
private boolean sponsorBlockSkipInProgress = false;
219+
205220
/*//////////////////////////////////////////////////////////////////////////
206221
// Player
207222
//////////////////////////////////////////////////////////////////////////*/
@@ -1025,10 +1040,161 @@ public void triggerProgressUpdate() {
10251040
return;
10261041
}
10271042

1043+
maybeSkipSponsorBlockSegment();
10281044
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
10291045
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
10301046
}
10311047

1048+
private void maybeSkipSponsorBlockSegment() {
1049+
if (!isSponsorBlockEnabled() || sponsorBlockSegments.isEmpty() || !isPlaying()
1050+
|| simpleExoPlayer.getPlaybackState()
1051+
!= com.google.android.exoplayer2.Player.STATE_READY) {
1052+
return;
1053+
}
1054+
1055+
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
1056+
SponsorBlockSegment activeSegment = null;
1057+
for (final SponsorBlockSegment segment : sponsorBlockSegments) {
1058+
if (isSegmentActive(segment, currentPositionMillis)) {
1059+
activeSegment = segment;
1060+
break;
1061+
}
1062+
}
1063+
1064+
if (activeSegment == null) {
1065+
ignoredSponsorBlockSegment = null;
1066+
return;
1067+
}
1068+
1069+
final String segmentKey = getSegmentKey(activeSegment);
1070+
if (segmentKey.equals(ignoredSponsorBlockSegment)
1071+
|| skippedSponsorBlockSegments.contains(segmentKey)
1072+
|| activeSegment.getActionType() != SponsorBlockAction.SKIP
1073+
|| activeSegment.getCategory() == SponsorBlockCategory.HIGHLIGHT) {
1074+
return;
1075+
}
1076+
1077+
final long segmentEndMillis = Math.round(activeSegment.getEndTimeSeconds() * 1000.0d);
1078+
final long durationMillis = simpleExoPlayer.getDuration();
1079+
final long targetPositionMillis = durationMillis > 0 && durationMillis != C.TIME_UNSET
1080+
? MathUtils.clamp(segmentEndMillis, 0, durationMillis)
1081+
: Math.max(segmentEndMillis, 0);
1082+
if (targetPositionMillis <= currentPositionMillis) {
1083+
skippedSponsorBlockSegments.add(segmentKey);
1084+
return;
1085+
}
1086+
1087+
sponsorBlockSkipInProgress = true;
1088+
skippedSponsorBlockSegments.add(segmentKey);
1089+
simpleExoPlayer.seekTo(targetPositionMillis);
1090+
if (prefs.getBoolean(context.getString(R.string.sponsor_block_notifications_key), true)) {
1091+
Toast.makeText(context,
1092+
context.getString(R.string.sponsor_block_skipped_segment,
1093+
getSegmentCategoryName(activeSegment)),
1094+
Toast.LENGTH_SHORT).show();
1095+
}
1096+
}
1097+
1098+
private boolean isSponsorBlockEnabled() {
1099+
return prefs.getBoolean(context.getString(R.string.sponsor_block_enable_key), false);
1100+
}
1101+
1102+
private boolean isSegmentActive(@NonNull final SponsorBlockSegment segment,
1103+
final long currentPositionMillis) {
1104+
if (!segment.isValid() || segment.getActionType() == SponsorBlockAction.UNKNOWN
1105+
|| segment.getActionType() == SponsorBlockAction.POI
1106+
|| segment.getActionType() == SponsorBlockAction.CHAPTER
1107+
|| segment.getActionType() == SponsorBlockAction.FULL
1108+
|| segment.getActionType() == SponsorBlockAction.MUTE
1109+
|| !isCategoryEnabled(segment.getCategory())) {
1110+
return false;
1111+
}
1112+
final long segmentStartMillis = Math.round(segment.getStartTimeSeconds() * 1000.0d);
1113+
final long segmentEndMillis = Math.round(segment.getEndTimeSeconds() * 1000.0d);
1114+
return currentPositionMillis >= segmentStartMillis
1115+
&& currentPositionMillis < segmentEndMillis;
1116+
}
1117+
1118+
private boolean isCategoryEnabled(@Nullable final SponsorBlockCategory category) {
1119+
if (category == null) {
1120+
return false;
1121+
}
1122+
switch (category) {
1123+
case SPONSOR:
1124+
return prefs.getBoolean(
1125+
context.getString(R.string.sponsor_block_category_sponsor_key), true);
1126+
case INTRO:
1127+
return prefs.getBoolean(
1128+
context.getString(R.string.sponsor_block_category_intro_key), false);
1129+
case OUTRO:
1130+
return prefs.getBoolean(
1131+
context.getString(R.string.sponsor_block_category_outro_key), false);
1132+
case INTERACTION:
1133+
return prefs.getBoolean(
1134+
context.getString(R.string.sponsor_block_category_interaction_key),
1135+
false);
1136+
case HIGHLIGHT:
1137+
return prefs.getBoolean(
1138+
context.getString(R.string.sponsor_block_category_highlight_key),
1139+
false);
1140+
case SELF_PROMO:
1141+
return prefs.getBoolean(
1142+
context.getString(R.string.sponsor_block_category_self_promo_key),
1143+
false);
1144+
case MUSIC_OFFTOPIC:
1145+
return prefs.getBoolean(
1146+
context.getString(R.string.sponsor_block_category_non_music_key),
1147+
false);
1148+
case PREVIEW:
1149+
return prefs.getBoolean(
1150+
context.getString(R.string.sponsor_block_category_preview_key), false);
1151+
case FILLER:
1152+
return prefs.getBoolean(
1153+
context.getString(R.string.sponsor_block_category_filler_key), false);
1154+
default:
1155+
return false;
1156+
}
1157+
}
1158+
1159+
@NonNull
1160+
private String getSegmentKey(@NonNull final SponsorBlockSegment segment) {
1161+
if (!isNullOrEmpty(segment.getUuid())) {
1162+
return segment.getUuid();
1163+
}
1164+
return segment.getCategory() + ":" + segment.getActionType() + ":"
1165+
+ segment.getStartTimeSeconds() + ":" + segment.getEndTimeSeconds();
1166+
}
1167+
1168+
@NonNull
1169+
private String getSegmentCategoryName(@NonNull final SponsorBlockSegment segment) {
1170+
final SponsorBlockCategory category = segment.getCategory();
1171+
if (category == null) {
1172+
return context.getString(R.string.sponsor_block_skipped_segment_fallback);
1173+
}
1174+
switch (category) {
1175+
case SPONSOR:
1176+
return context.getString(R.string.sponsor_block_category_sponsor_title);
1177+
case INTRO:
1178+
return context.getString(R.string.sponsor_block_category_intro_title);
1179+
case OUTRO:
1180+
return context.getString(R.string.sponsor_block_category_outro_title);
1181+
case INTERACTION:
1182+
return context.getString(R.string.sponsor_block_category_interaction_title);
1183+
case HIGHLIGHT:
1184+
return context.getString(R.string.sponsor_block_category_highlight_title);
1185+
case SELF_PROMO:
1186+
return context.getString(R.string.sponsor_block_category_self_promo_title);
1187+
case MUSIC_OFFTOPIC:
1188+
return context.getString(R.string.sponsor_block_category_non_music_title);
1189+
case PREVIEW:
1190+
return context.getString(R.string.sponsor_block_category_preview_title);
1191+
case FILLER:
1192+
return context.getString(R.string.sponsor_block_category_filler_title);
1193+
default:
1194+
return context.getString(R.string.sponsor_block_skipped_segment_fallback);
1195+
}
1196+
}
1197+
10321198
private Disposable getProgressUpdateDisposable() {
10331199
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
10341200
AndroidSchedulers.mainThread())
@@ -1444,6 +1610,20 @@ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
14441610
return;
14451611
}
14461612

1613+
if (discontinuityReason == DISCONTINUITY_REASON_SEEK && !sponsorBlockSkipInProgress) {
1614+
final String seekTargetSegmentKey = getActiveSponsorBlockSegmentKey(
1615+
newPosition.positionMs);
1616+
if (prefs.getBoolean(context.getString(R.string.sponsor_block_graced_rewind_key),
1617+
true)) {
1618+
ignoredSponsorBlockSegment = seekTargetSegmentKey;
1619+
} else if (seekTargetSegmentKey != null) {
1620+
skippedSponsorBlockSegments.remove(seekTargetSegmentKey);
1621+
}
1622+
}
1623+
if (sponsorBlockSkipInProgress) {
1624+
sponsorBlockSkipInProgress = false;
1625+
}
1626+
14471627
// Refresh the playback if there is a transition to the next video
14481628
final int newIndex = newPosition.mediaItemIndex;
14491629
switch (discontinuityReason) {
@@ -1919,6 +2099,7 @@ private void updateMetadataWith(@NonNull final StreamInfo info) {
19192099
return;
19202100
}
19212101

2102+
updateSponsorBlockSegments(info);
19222103
maybeAutoQueueNextStream(info);
19232104

19242105
loadCurrentThumbnail(info.getThumbnails());
@@ -1929,6 +2110,31 @@ private void updateMetadataWith(@NonNull final StreamInfo info) {
19292110
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
19302111
}
19312112

2113+
private void updateSponsorBlockSegments(@NonNull final StreamInfo info) {
2114+
skippedSponsorBlockSegments.clear();
2115+
ignoredSponsorBlockSegment = null;
2116+
sponsorBlockSkipInProgress = false;
2117+
if (!isSponsorBlockEnabled()) {
2118+
sponsorBlockSegments = Collections.emptyList();
2119+
return;
2120+
}
2121+
final List<SponsorBlockSegment> segments = info.getSponsorBlockSegments();
2122+
sponsorBlockSegments = segments == null ? Collections.emptyList() : segments;
2123+
}
2124+
2125+
@Nullable
2126+
private String getActiveSponsorBlockSegmentKey(final long positionMillis) {
2127+
if (!isSponsorBlockEnabled() || sponsorBlockSegments.isEmpty()) {
2128+
return null;
2129+
}
2130+
for (final SponsorBlockSegment segment : sponsorBlockSegments) {
2131+
if (isSegmentActive(segment, positionMillis)) {
2132+
return getSegmentKey(segment);
2133+
}
2134+
}
2135+
return null;
2136+
}
2137+
19322138
@NonNull
19332139
public String getVideoUrl() {
19342140
return currentMetadata == null

app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ private SettingsResourceRegistry() {
3939
add(NotificationSettingsFragment.class, R.xml.notifications_settings);
4040
add(PlayerNotificationSettingsFragment.class, R.xml.player_notification_settings);
4141
add(UpdateSettingsFragment.class, R.xml.update_settings);
42+
add(SponsorBlockSettingsFragment.class, R.xml.sponsor_block_settings);
43+
add(SponsorBlockCategoriesSettingsFragment.class, R.xml.sponsor_block_categories_settings);
4244
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
4345
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
4446
add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.schabi.newpipe.settings;
2+
3+
import android.content.SharedPreferences;
4+
import android.os.Bundle;
5+
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
9+
import org.schabi.newpipe.util.ServiceHelper;
10+
11+
public class SponsorBlockCategoriesSettingsFragment extends BasePreferenceFragment {
12+
private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener =
13+
this::onPreferenceChanged;
14+
15+
@Override
16+
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
17+
@Nullable final String rootKey) {
18+
addPreferencesFromResourceRegistry();
19+
}
20+
21+
@Override
22+
public void onCreate(@Nullable final Bundle savedInstanceState) {
23+
super.onCreate(savedInstanceState);
24+
getPreferenceManager().getSharedPreferences()
25+
.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
26+
}
27+
28+
@Override
29+
public void onDestroy() {
30+
getPreferenceManager().getSharedPreferences()
31+
.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
32+
super.onDestroy();
33+
}
34+
35+
private void onPreferenceChanged(@NonNull final SharedPreferences preferences,
36+
@Nullable final String key) {
37+
if (key != null && key.startsWith("sponsor_block_category_")) {
38+
ServiceHelper.initServices(requireContext());
39+
}
40+
}
41+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.schabi.newpipe.settings;
2+
3+
import android.content.SharedPreferences;
4+
import android.os.Bundle;
5+
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
9+
import org.schabi.newpipe.R;
10+
import org.schabi.newpipe.util.ServiceHelper;
11+
import org.schabi.newpipe.util.external_communication.ShareUtils;
12+
13+
public class SponsorBlockSettingsFragment extends BasePreferenceFragment {
14+
private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener =
15+
this::onPreferenceChanged;
16+
17+
@Override
18+
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
19+
@Nullable final String rootKey) {
20+
addPreferencesFromResourceRegistry();
21+
22+
requirePreference(R.string.sponsor_block_home_page_key).setOnPreferenceClickListener(
23+
preference -> {
24+
ShareUtils.openUrlInApp(requireContext(),
25+
getString(R.string.sponsor_block_homepage_url));
26+
return true;
27+
});
28+
requirePreference(R.string.sponsor_block_privacy_key).setOnPreferenceClickListener(
29+
preference -> {
30+
ShareUtils.openUrlInApp(requireContext(),
31+
getString(R.string.sponsor_block_privacy_policy_url));
32+
return true;
33+
});
34+
}
35+
36+
@Override
37+
public void onCreate(@Nullable final Bundle savedInstanceState) {
38+
super.onCreate(savedInstanceState);
39+
getPreferenceManager().getSharedPreferences()
40+
.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
41+
}
42+
43+
@Override
44+
public void onDestroy() {
45+
getPreferenceManager().getSharedPreferences()
46+
.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
47+
super.onDestroy();
48+
}
49+
50+
private void onPreferenceChanged(@NonNull final SharedPreferences preferences,
51+
@Nullable final String key) {
52+
if (key != null && key.startsWith("sponsor_block_")) {
53+
ServiceHelper.initServices(requireContext());
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)