Skip to content

Commit 832dfda

Browse files
added extractor components for SponsorBlock
1 parent f276caf commit 832dfda

File tree

7 files changed

+397
-6
lines changed

7 files changed

+397
-6
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.schabi.newpipe.extractor.sponsorblock;
2+
3+
public enum SponsorBlockAction {
4+
SKIP("skip"),
5+
POI("poi");
6+
7+
private final String apiName;
8+
9+
SponsorBlockAction(final String apiName) {
10+
this.apiName = apiName;
11+
}
12+
13+
public static SponsorBlockAction fromApiName(final String apiName) {
14+
switch (apiName) {
15+
case "skip":
16+
return SponsorBlockAction.SKIP;
17+
case "poi":
18+
return SponsorBlockAction.POI;
19+
default:
20+
throw new IllegalArgumentException("Invalid API name");
21+
}
22+
}
23+
24+
public String getApiName() {
25+
return apiName;
26+
}
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.schabi.newpipe.extractor.sponsorblock;
2+
3+
public class SponsorBlockApiSettings {
4+
public String apiUrl;
5+
public boolean includeSponsorCategory;
6+
public boolean includeIntroCategory;
7+
public boolean includeOutroCategory;
8+
public boolean includeInteractionCategory;
9+
public boolean includeHighlightCategory;
10+
public boolean includeSelfPromoCategory;
11+
public boolean includeMusicCategory;
12+
public boolean includePreviewCategory;
13+
public boolean includeFillerCategory;
14+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.schabi.newpipe.extractor.sponsorblock;
2+
3+
public enum SponsorBlockCategory {
4+
SPONSOR("sponsor"),
5+
INTRO("intro"),
6+
OUTRO("outro"),
7+
INTERACTION("interaction"),
8+
HIGHLIGHT("poi_highlight"),
9+
SELF_PROMO("selfpromo"),
10+
NON_MUSIC("music_offtopic"),
11+
PREVIEW("preview"),
12+
FILLER("filler"),
13+
PENDING("pending");
14+
15+
private final String apiName;
16+
17+
SponsorBlockCategory(final String apiName) {
18+
this.apiName = apiName;
19+
}
20+
21+
public static SponsorBlockCategory fromApiName(final String apiName) {
22+
switch (apiName) {
23+
case "sponsor":
24+
return SponsorBlockCategory.SPONSOR;
25+
case "intro":
26+
return SponsorBlockCategory.INTRO;
27+
case "outro":
28+
return SponsorBlockCategory.OUTRO;
29+
case "interaction":
30+
return SponsorBlockCategory.INTERACTION;
31+
case "poi_highlight":
32+
return SponsorBlockCategory.HIGHLIGHT;
33+
case "selfpromo":
34+
return SponsorBlockCategory.SELF_PROMO;
35+
case "music_offtopic":
36+
return SponsorBlockCategory.NON_MUSIC;
37+
case "preview":
38+
return SponsorBlockCategory.PREVIEW;
39+
case "filler":
40+
return SponsorBlockCategory.FILLER;
41+
default:
42+
throw new IllegalArgumentException("Invalid API name");
43+
}
44+
}
45+
46+
public String getApiName() {
47+
return apiName;
48+
}
49+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package org.schabi.newpipe.extractor.sponsorblock;
2+
3+
import com.grack.nanojson.JsonArray;
4+
import com.grack.nanojson.JsonObject;
5+
import com.grack.nanojson.JsonParser;
6+
import com.grack.nanojson.JsonParserException;
7+
8+
import org.schabi.newpipe.extractor.NewPipe;
9+
import org.schabi.newpipe.extractor.downloader.Response;
10+
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
11+
import org.schabi.newpipe.extractor.stream.StreamInfo;
12+
import org.schabi.newpipe.extractor.utils.RandomStringFromAlphabetGenerator;
13+
import org.schabi.newpipe.extractor.utils.Utils;
14+
15+
import java.io.IOException;
16+
import java.io.UnsupportedEncodingException;
17+
import java.security.NoSuchAlgorithmException;
18+
import java.security.SecureRandom;
19+
import java.util.ArrayList;
20+
import java.util.Random;
21+
22+
public final class SponsorBlockExtractorHelper {
23+
private static final String ALPHABET =
24+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
25+
private static final Random NUMBER_GENERATOR = new SecureRandom();
26+
27+
private SponsorBlockExtractorHelper() {
28+
}
29+
30+
public static SponsorBlockSegment[] getSegments(final StreamInfo streamInfo,
31+
final SponsorBlockApiSettings apiSettings)
32+
throws UnsupportedEncodingException {
33+
if (!streamInfo.getUrl().startsWith("https://www.youtube.com")
34+
|| apiSettings.apiUrl == null
35+
|| apiSettings.apiUrl.isEmpty()) {
36+
return new SponsorBlockSegment[0];
37+
}
38+
39+
final String videoId = streamInfo.getId();
40+
41+
final ArrayList<String> categoryParamList = new ArrayList<>();
42+
43+
if (apiSettings.includeSponsorCategory) {
44+
categoryParamList.add(SponsorBlockCategory.SPONSOR.getApiName());
45+
}
46+
if (apiSettings.includeIntroCategory) {
47+
categoryParamList.add(SponsorBlockCategory.INTRO.getApiName());
48+
}
49+
if (apiSettings.includeOutroCategory) {
50+
categoryParamList.add(SponsorBlockCategory.OUTRO.getApiName());
51+
}
52+
if (apiSettings.includeInteractionCategory) {
53+
categoryParamList.add(SponsorBlockCategory.INTERACTION.getApiName());
54+
}
55+
if (apiSettings.includeHighlightCategory) {
56+
categoryParamList.add(SponsorBlockCategory.HIGHLIGHT.getApiName());
57+
}
58+
if (apiSettings.includeSelfPromoCategory) {
59+
categoryParamList.add(SponsorBlockCategory.SELF_PROMO.getApiName());
60+
}
61+
if (apiSettings.includeMusicCategory) {
62+
categoryParamList.add(SponsorBlockCategory.NON_MUSIC.getApiName());
63+
}
64+
if (apiSettings.includePreviewCategory) {
65+
categoryParamList.add(SponsorBlockCategory.PREVIEW.getApiName());
66+
}
67+
68+
if (apiSettings.includeFillerCategory) {
69+
categoryParamList.add(SponsorBlockCategory.FILLER.getApiName());
70+
}
71+
72+
if (categoryParamList.size() == 0) {
73+
return new SponsorBlockSegment[0];
74+
}
75+
76+
final String categoryParams = Utils.encodeUrlUtf8(
77+
"[\"" + String.join("\",\"", categoryParamList) + "\"]");
78+
79+
final String actionParams = Utils.encodeUrlUtf8("[\"skip\",\"poi\"]");
80+
81+
final String videoIdHash;
82+
try {
83+
videoIdHash = Utils.toSha256(videoId);
84+
} catch (NoSuchAlgorithmException e) {
85+
return new SponsorBlockSegment[0];
86+
}
87+
88+
final String url = apiSettings.apiUrl + "skipSegments/" + videoIdHash.substring(0, 4)
89+
+ "?categories=" + categoryParams
90+
+ "&actionTypes=" + actionParams
91+
+ "&userAgent=Mozilla/5.0";
92+
93+
JsonArray responseArray = null;
94+
95+
try {
96+
final String responseBody = NewPipe.getDownloader().get(url).responseBody();
97+
98+
responseArray = JsonParser.array().from(responseBody);
99+
} catch (ReCaptchaException | IOException | JsonParserException e) {
100+
// ignored
101+
}
102+
103+
if (responseArray == null) {
104+
return new SponsorBlockSegment[0];
105+
}
106+
107+
final ArrayList<SponsorBlockSegment> result = new ArrayList<>();
108+
109+
for (final Object obj1 : responseArray) {
110+
final JsonObject jObj1 = (JsonObject) obj1;
111+
112+
final String responseVideoId = jObj1.getString("videoID");
113+
if (!responseVideoId.equals(videoId)) {
114+
continue;
115+
}
116+
117+
final JsonArray segmentArray = (JsonArray) jObj1.get("segments");
118+
if (segmentArray == null) {
119+
continue;
120+
}
121+
122+
for (final Object obj2 : segmentArray) {
123+
final JsonObject jObj2 = (JsonObject) obj2;
124+
125+
final JsonArray segmentInfo = (JsonArray) jObj2.get("segment");
126+
if (segmentInfo == null) {
127+
continue;
128+
}
129+
130+
final String uuid = jObj2.getString("UUID");
131+
final double startTime = segmentInfo.getDouble(0) * 1000;
132+
final double endTime = segmentInfo.getDouble(1) * 1000;
133+
final String category = jObj2.getString("category");
134+
final String action = jObj2.getString("actionType");
135+
136+
final SponsorBlockSegment sponsorBlockSegment =
137+
new SponsorBlockSegment(uuid, startTime, endTime,
138+
SponsorBlockCategory.fromApiName(category),
139+
SponsorBlockAction.fromApiName(action));
140+
result.add(sponsorBlockSegment);
141+
}
142+
}
143+
144+
return result.toArray(new SponsorBlockSegment[0]);
145+
}
146+
147+
public static Response submitSponsorBlockSegment(
148+
final StreamInfo streamInfo,
149+
final SponsorBlockSegment segment,
150+
final String apiUrl)
151+
throws IOException, ReCaptchaException {
152+
if (segment.category == SponsorBlockCategory.PENDING) {
153+
return null;
154+
}
155+
156+
if (!streamInfo.getUrl().startsWith("https://www.youtube.com")) {
157+
return null;
158+
}
159+
160+
final String videoId = streamInfo.getId();
161+
162+
final String localUserId =
163+
RandomStringFromAlphabetGenerator.generate(ALPHABET, 32, NUMBER_GENERATOR);
164+
165+
final String actionType = segment.category == SponsorBlockCategory.HIGHLIGHT
166+
? "poi"
167+
: "skip";
168+
169+
final double startInSeconds = segment.startTime / 1000.0;
170+
final double endInSeconds = segment.category == SponsorBlockCategory.HIGHLIGHT
171+
? startInSeconds
172+
: segment.endTime / 1000.0;
173+
174+
final String url = apiUrl + "skipSegments?"
175+
+ "videoID=" + videoId
176+
+ "&startTime=" + startInSeconds
177+
+ "&endTime=" + endInSeconds
178+
+ "&category=" + segment.category.getApiName()
179+
+ "&userID=" + localUserId
180+
+ "&userAgent=Mozilla/5.0"
181+
+ "&actionType=" + actionType;
182+
return NewPipe.getDownloader().post(url, null, new byte[0]);
183+
}
184+
185+
public static Response submitSponsorBlockSegmentVote(final String uuid,
186+
final String apiUrl,
187+
final int vote)
188+
throws IOException, ReCaptchaException {
189+
final String localUserId =
190+
RandomStringFromAlphabetGenerator.generate(ALPHABET, 32, NUMBER_GENERATOR);
191+
192+
final String url = apiUrl + "voteOnSponsorTime?"
193+
+ "UUID=" + uuid
194+
+ "&userID=" + localUserId
195+
+ "&type=" + vote;
196+
197+
return NewPipe.getDownloader().post(url, null, new byte[0]);
198+
}
199+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.schabi.newpipe.extractor.sponsorblock;
2+
3+
import java.io.Serializable;
4+
5+
public class SponsorBlockSegment implements Serializable {
6+
public String uuid;
7+
public double startTime;
8+
public double endTime;
9+
public SponsorBlockCategory category;
10+
public SponsorBlockAction action;
11+
12+
public SponsorBlockSegment(final String uuid, final double startTime, final double endTime,
13+
final SponsorBlockCategory category,
14+
final SponsorBlockAction action) {
15+
// NOTE: start/end times are in milliseconds
16+
17+
this.uuid = uuid;
18+
this.startTime = startTime;
19+
this.endTime = endTime;
20+
this.category = category;
21+
this.action = action;
22+
23+
// since "highlight" segments are marked with the same start time and end time,
24+
// increment the end time by 1 second (so it is actually visible on the seekbar)
25+
if (this.category == SponsorBlockCategory.HIGHLIGHT) {
26+
this.endTime = this.startTime + 1000;
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)