Skip to content

Commit fb468a2

Browse files
authored
Merge pull request #1142 from TeamNewPipe/peertube-v6
[PeerTube] Add support for PeerTube v6 features
2 parents 6589e2c + fe47a43 commit fb468a2

3 files changed

Lines changed: 157 additions & 2 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import org.schabi.newpipe.extractor.stream.AudioStream;
2727
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
2828
import org.schabi.newpipe.extractor.stream.Description;
29+
import org.schabi.newpipe.extractor.stream.Frameset;
2930
import org.schabi.newpipe.extractor.stream.Stream;
3031
import org.schabi.newpipe.extractor.stream.StreamExtractor;
3132
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
33+
import org.schabi.newpipe.extractor.stream.StreamSegment;
3234
import org.schabi.newpipe.extractor.stream.StreamType;
3335
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
3436
import org.schabi.newpipe.extractor.stream.VideoStream;
@@ -316,6 +318,66 @@ public String getSupportInfo() {
316318
}
317319
}
318320

321+
@Nonnull
322+
@Override
323+
public List<StreamSegment> getStreamSegments() throws ParsingException {
324+
final List<StreamSegment> segments = new ArrayList<>();
325+
final JsonObject segmentsJson;
326+
try {
327+
segmentsJson = fetchSubApiContent("chapters");
328+
} catch (final IOException | ReCaptchaException e) {
329+
throw new ParsingException("Could not get stream segments", e);
330+
}
331+
if (segmentsJson != null && segmentsJson.has("chapters")) {
332+
final JsonArray segmentsArray = segmentsJson.getArray("chapters");
333+
for (int i = 0; i < segmentsArray.size(); i++) {
334+
final JsonObject segmentObject = segmentsArray.getObject(i);
335+
segments.add(new StreamSegment(
336+
segmentObject.getString("title"),
337+
segmentObject.getInt("timecode")));
338+
}
339+
}
340+
341+
return segments;
342+
}
343+
344+
@Nonnull
345+
@Override
346+
public List<Frameset> getFrames() throws ExtractionException {
347+
final List<Frameset> framesets = new ArrayList<>();
348+
final JsonObject storyboards;
349+
try {
350+
storyboards = fetchSubApiContent("storyboards");
351+
} catch (final IOException | ReCaptchaException e) {
352+
throw new ExtractionException("Could not get frames", e);
353+
}
354+
if (storyboards != null && storyboards.has("storyboards")) {
355+
final JsonArray storyboardsArray = storyboards.getArray("storyboards");
356+
for (final Object storyboard : storyboardsArray) {
357+
if (storyboard instanceof JsonObject) {
358+
final JsonObject storyboardObject = (JsonObject) storyboard;
359+
final String url = storyboardObject.getString("storyboardPath");
360+
final int width = storyboardObject.getInt("spriteWidth");
361+
final int height = storyboardObject.getInt("spriteHeight");
362+
final int totalWidth = storyboardObject.getInt("totalWidth");
363+
final int totalHeight = storyboardObject.getInt("totalHeight");
364+
final int framesPerPageX = totalWidth / width;
365+
final int framesPerPageY = totalHeight / height;
366+
final int count = framesPerPageX * framesPerPageY;
367+
final int durationPerFrame = storyboardObject.getInt("spriteDuration") * 1000;
368+
369+
framesets.add(new Frameset(
370+
// there is only one composite image per video containing all frames
371+
List.of(baseUrl + url),
372+
width, height, count,
373+
durationPerFrame, framesPerPageX, framesPerPageY));
374+
}
375+
}
376+
}
377+
378+
return framesets;
379+
}
380+
319381
@Nonnull
320382
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
321383
throws UnsupportedEncodingException {
@@ -636,6 +698,41 @@ private void addNewVideoStream(@Nonnull final JsonObject streamJsonObject,
636698
}
637699
}
638700

701+
/**
702+
* Fetch content from a sub-API of the video.
703+
* @param subPath the API subpath after the video id,
704+
* e.g. "storyboards" for "/api/v1/videos/{id}/storyboards"
705+
* @return the {@link JsonObject} of the sub-API or null if the API does not exist
706+
* which is the case if the instance has an outdated PeerTube version.
707+
* @throws ParsingException if the API response could not be parsed to a {@link JsonObject}
708+
* @throws IOException if the API response could not be fetched
709+
* @throws ReCaptchaException if the API response is a reCaptcha
710+
*/
711+
@Nullable
712+
private JsonObject fetchSubApiContent(@Nonnull final String subPath)
713+
throws ParsingException, IOException, ReCaptchaException {
714+
final String apiUrl = baseUrl + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT
715+
+ getId() + "/" + subPath;
716+
final Response response = getDownloader().get(apiUrl);
717+
if (response == null) {
718+
throw new ParsingException("Could not get segments from API.");
719+
}
720+
if (response.responseCode() == 400) {
721+
// Chapter or segments support was added with PeerTube v6.0.0
722+
// This instance does not support it yet.
723+
return null;
724+
}
725+
if (response.responseCode() != 200) {
726+
throw new ParsingException("Could not get segments from API. Response code: "
727+
+ response.responseCode());
728+
}
729+
try {
730+
return JsonParser.object().from(response.responseBody());
731+
} catch (final JsonParserException e) {
732+
throw new ParsingException("Could not parse json data for segments", e);
733+
}
734+
}
735+
639736
@Nonnull
640737
@Override
641738
public String getName() throws ParsingException {

extractor/src/main/java/org/schabi/newpipe/extractor/stream/Frameset.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import java.io.Serializable;
44
import java.util.List;
55

6+
/**
7+
* Class to handle framesets / storyboards which summarize the stream content.
8+
*/
69
public final class Frameset implements Serializable {
710

811
private final List<String> urls;
@@ -13,6 +16,17 @@ public final class Frameset implements Serializable {
1316
private final int framesPerPageX;
1417
private final int framesPerPageY;
1518

19+
/**
20+
* Creates a new Frameset or set of storyboards.
21+
* @param urls the URLs to the images with frames / storyboards
22+
* @param frameWidth the width of a single frame, in pixels
23+
* @param frameHeight the height of a single frame, in pixels
24+
* @param totalCount the total count of frames
25+
* @param durationPerFrame the duration per frame in milliseconds
26+
* @param framesPerPageX the maximum count of frames per page by x / over the width of the image
27+
* @param framesPerPageY the maximum count of frames per page by y / over the height
28+
* of the image
29+
*/
1630
public Frameset(
1731
final List<String> urls,
1832
final int frameWidth,
@@ -32,7 +46,7 @@ public Frameset(
3246
}
3347

3448
/**
35-
* @return list of urls to images with frames
49+
* @return list of URLs to images with frames
3650
*/
3751
public List<String> getUrls() {
3852
return urls;

extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
2626
private static final String BASE_URL = "/videos/watch/";
2727

2828
@Override public boolean expectedHasAudioStreams() { return false; }
29-
@Override public boolean expectedHasFrames() { return false; }
3029

3130
public static class WhatIsPeertube extends PeertubeStreamExtractorTest {
3231
private static final String ID = "9c9de5e8-0a1e-484a-b099-e80766180a6d";
@@ -138,6 +137,7 @@ public static void setUp() throws Exception {
138137
@Override public String expectedLicence() { return "Unknown"; }
139138
@Override public Locale expectedLanguageInfo() { return null; }
140139
@Override public List<String> expectedTags() { return Arrays.asList("Marinauts", "adobe flash", "adobe flash player", "flash games", "the marinauts"); }
140+
@Override public boolean expectedHasFrames() { return false; } // not yet supported by instance
141141
}
142142

143143
@Disabled("Test broken, SSL problem")
@@ -185,6 +185,50 @@ public static void setUp() throws Exception {
185185
@Override public List<String> expectedTags() { return Arrays.asList("Covid-19", "Gérôme-Mary trebor", "Horreur et beauté", "court-métrage", "nue artistique"); }
186186
}
187187

188+
public static class Segments extends PeertubeStreamExtractorTest {
189+
private static final String ID = "vqABGP97fEjo7RhPuDnSZk";
190+
private static final String INSTANCE = "https://tube.tchncs.de";
191+
192+
private static final String URL = INSTANCE + BASE_URL + ID;
193+
private static StreamExtractor extractor;
194+
195+
@BeforeAll
196+
public static void setUp() throws Exception {
197+
NewPipe.init(DownloaderTestImpl.getInstance());
198+
// setting instance might break test when running in parallel (!)
199+
PeerTube.setInstance(new PeertubeInstance(INSTANCE, "tchncs.de"));
200+
extractor = PeerTube.getStreamExtractor(URL);
201+
extractor.fetchPage();
202+
}
203+
204+
@Override public StreamExtractor extractor() { return extractor; }
205+
@Override public StreamingService expectedService() { return PeerTube; }
206+
@Override public String expectedName() { return "Bauinformatik 11 – Objekte und Methoden"; }
207+
@Override public String expectedId() { return ID; }
208+
@Override public String expectedUrlContains() { return INSTANCE + BASE_URL + ID; }
209+
@Override public String expectedOriginalUrlContains() { return URL; }
210+
211+
@Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; }
212+
@Override public String expectedUploaderName() { return "Martin Vogel"; }
213+
@Override public String expectedUploaderUrl() { return "https://tube.tchncs.de/accounts/martin_vogel@tube.tchncs.de"; }
214+
@Override public String expectedSubChannelName() { return "Bauinformatik mit Python"; }
215+
@Override public String expectedSubChannelUrl() { return "https://tube.tchncs.de/video-channels/python"; }
216+
@Override public List<String> expectedDescriptionContains() { // CRLF line ending
217+
return Arrays.asList("Um", "Programme", "Variablen", "Funktionen", "Objekte", "Skript", "Wiederholung", "Listen");
218+
}
219+
@Override public long expectedLength() { return 1017; }
220+
@Override public long expectedViewCountAtLeast() { return 20; }
221+
@Nullable @Override public String expectedUploadDate() { return "2023-12-08 15:57:04.142"; }
222+
@Nullable @Override public String expectedTextualUploadDate() { return "2023-12-08T15:57:04.142Z"; }
223+
@Override public long expectedLikeCountAtLeast() { return 0; }
224+
@Override public long expectedDislikeCountAtLeast() { return 0; }
225+
@Override public boolean expectedHasSubtitles() { return false; }
226+
@Override public String expectedHost() { return "tube.tchncs.de"; }
227+
@Override public String expectedCategory() { return "Unknown"; }
228+
@Override public String expectedLicence() { return "Unknown"; }
229+
@Override public Locale expectedLanguageInfo() { return null; }
230+
@Override public List<String> expectedTags() { return Arrays.asList("Attribute", "Bauinformatik", "Klassen", "Objekte", "Python"); }
231+
}
188232

189233
@BeforeAll
190234
public static void setUp() throws Exception {

0 commit comments

Comments
 (0)