Merge pull request #1142 from TeamNewPipe/peertube-v6
[PeerTube] Add support for PeerTube v6 features
This commit is contained in:
commit
fb468a23f4
|
@ -26,9 +26,11 @@ import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeStream
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
@ -316,6 +318,66 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<StreamSegment> getStreamSegments() throws ParsingException {
|
||||||
|
final List<StreamSegment> segments = new ArrayList<>();
|
||||||
|
final JsonObject segmentsJson;
|
||||||
|
try {
|
||||||
|
segmentsJson = fetchSubApiContent("chapters");
|
||||||
|
} catch (final IOException | ReCaptchaException e) {
|
||||||
|
throw new ParsingException("Could not get stream segments", e);
|
||||||
|
}
|
||||||
|
if (segmentsJson != null && segmentsJson.has("chapters")) {
|
||||||
|
final JsonArray segmentsArray = segmentsJson.getArray("chapters");
|
||||||
|
for (int i = 0; i < segmentsArray.size(); i++) {
|
||||||
|
final JsonObject segmentObject = segmentsArray.getObject(i);
|
||||||
|
segments.add(new StreamSegment(
|
||||||
|
segmentObject.getString("title"),
|
||||||
|
segmentObject.getInt("timecode")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Frameset> getFrames() throws ExtractionException {
|
||||||
|
final List<Frameset> framesets = new ArrayList<>();
|
||||||
|
final JsonObject storyboards;
|
||||||
|
try {
|
||||||
|
storyboards = fetchSubApiContent("storyboards");
|
||||||
|
} catch (final IOException | ReCaptchaException e) {
|
||||||
|
throw new ExtractionException("Could not get frames", e);
|
||||||
|
}
|
||||||
|
if (storyboards != null && storyboards.has("storyboards")) {
|
||||||
|
final JsonArray storyboardsArray = storyboards.getArray("storyboards");
|
||||||
|
for (final Object storyboard : storyboardsArray) {
|
||||||
|
if (storyboard instanceof JsonObject) {
|
||||||
|
final JsonObject storyboardObject = (JsonObject) storyboard;
|
||||||
|
final String url = storyboardObject.getString("storyboardPath");
|
||||||
|
final int width = storyboardObject.getInt("spriteWidth");
|
||||||
|
final int height = storyboardObject.getInt("spriteHeight");
|
||||||
|
final int totalWidth = storyboardObject.getInt("totalWidth");
|
||||||
|
final int totalHeight = storyboardObject.getInt("totalHeight");
|
||||||
|
final int framesPerPageX = totalWidth / width;
|
||||||
|
final int framesPerPageY = totalHeight / height;
|
||||||
|
final int count = framesPerPageX * framesPerPageY;
|
||||||
|
final int durationPerFrame = storyboardObject.getInt("spriteDuration") * 1000;
|
||||||
|
|
||||||
|
framesets.add(new Frameset(
|
||||||
|
// there is only one composite image per video containing all frames
|
||||||
|
List.of(baseUrl + url),
|
||||||
|
width, height, count,
|
||||||
|
durationPerFrame, framesPerPageX, framesPerPageY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return framesets;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
|
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
|
||||||
throws UnsupportedEncodingException {
|
throws UnsupportedEncodingException {
|
||||||
|
@ -636,6 +698,41 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch content from a sub-API of the video.
|
||||||
|
* @param subPath the API subpath after the video id,
|
||||||
|
* e.g. "storyboards" for "/api/v1/videos/{id}/storyboards"
|
||||||
|
* @return the {@link JsonObject} of the sub-API or null if the API does not exist
|
||||||
|
* which is the case if the instance has an outdated PeerTube version.
|
||||||
|
* @throws ParsingException if the API response could not be parsed to a {@link JsonObject}
|
||||||
|
* @throws IOException if the API response could not be fetched
|
||||||
|
* @throws ReCaptchaException if the API response is a reCaptcha
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private JsonObject fetchSubApiContent(@Nonnull final String subPath)
|
||||||
|
throws ParsingException, IOException, ReCaptchaException {
|
||||||
|
final String apiUrl = baseUrl + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT
|
||||||
|
+ getId() + "/" + subPath;
|
||||||
|
final Response response = getDownloader().get(apiUrl);
|
||||||
|
if (response == null) {
|
||||||
|
throw new ParsingException("Could not get segments from API.");
|
||||||
|
}
|
||||||
|
if (response.responseCode() == 400) {
|
||||||
|
// Chapter or segments support was added with PeerTube v6.0.0
|
||||||
|
// This instance does not support it yet.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (response.responseCode() != 200) {
|
||||||
|
throw new ParsingException("Could not get segments from API. Response code: "
|
||||||
|
+ response.responseCode());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JsonParser.object().from(response.responseBody());
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ParsingException("Could not parse json data for segments", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
|
|
|
@ -3,6 +3,9 @@ package org.schabi.newpipe.extractor.stream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle framesets / storyboards which summarize the stream content.
|
||||||
|
*/
|
||||||
public final class Frameset implements Serializable {
|
public final class Frameset implements Serializable {
|
||||||
|
|
||||||
private final List<String> urls;
|
private final List<String> urls;
|
||||||
|
@ -13,6 +16,17 @@ public final class Frameset implements Serializable {
|
||||||
private final int framesPerPageX;
|
private final int framesPerPageX;
|
||||||
private final int framesPerPageY;
|
private final int framesPerPageY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Frameset or set of storyboards.
|
||||||
|
* @param urls the URLs to the images with frames / storyboards
|
||||||
|
* @param frameWidth the width of a single frame, in pixels
|
||||||
|
* @param frameHeight the height of a single frame, in pixels
|
||||||
|
* @param totalCount the total count of frames
|
||||||
|
* @param durationPerFrame the duration per frame in milliseconds
|
||||||
|
* @param framesPerPageX the maximum count of frames per page by x / over the width of the image
|
||||||
|
* @param framesPerPageY the maximum count of frames per page by y / over the height
|
||||||
|
* of the image
|
||||||
|
*/
|
||||||
public Frameset(
|
public Frameset(
|
||||||
final List<String> urls,
|
final List<String> urls,
|
||||||
final int frameWidth,
|
final int frameWidth,
|
||||||
|
@ -32,7 +46,7 @@ public final class Frameset implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list of urls to images with frames
|
* @return list of URLs to images with frames
|
||||||
*/
|
*/
|
||||||
public List<String> getUrls() {
|
public List<String> getUrls() {
|
||||||
return urls;
|
return urls;
|
||||||
|
|
|
@ -26,7 +26,6 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
|
||||||
private static final String BASE_URL = "/videos/watch/";
|
private static final String BASE_URL = "/videos/watch/";
|
||||||
|
|
||||||
@Override public boolean expectedHasAudioStreams() { return false; }
|
@Override public boolean expectedHasAudioStreams() { return false; }
|
||||||
@Override public boolean expectedHasFrames() { return false; }
|
|
||||||
|
|
||||||
public static class WhatIsPeertube extends PeertubeStreamExtractorTest {
|
public static class WhatIsPeertube extends PeertubeStreamExtractorTest {
|
||||||
private static final String ID = "9c9de5e8-0a1e-484a-b099-e80766180a6d";
|
private static final String ID = "9c9de5e8-0a1e-484a-b099-e80766180a6d";
|
||||||
|
@ -138,6 +137,7 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
|
||||||
@Override public String expectedLicence() { return "Unknown"; }
|
@Override public String expectedLicence() { return "Unknown"; }
|
||||||
@Override public Locale expectedLanguageInfo() { return null; }
|
@Override public Locale expectedLanguageInfo() { return null; }
|
||||||
@Override public List<String> expectedTags() { return Arrays.asList("Marinauts", "adobe flash", "adobe flash player", "flash games", "the marinauts"); }
|
@Override public List<String> expectedTags() { return Arrays.asList("Marinauts", "adobe flash", "adobe flash player", "flash games", "the marinauts"); }
|
||||||
|
@Override public boolean expectedHasFrames() { return false; } // not yet supported by instance
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled("Test broken, SSL problem")
|
@Disabled("Test broken, SSL problem")
|
||||||
|
@ -185,6 +185,50 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
|
||||||
@Override public List<String> expectedTags() { return Arrays.asList("Covid-19", "Gérôme-Mary trebor", "Horreur et beauté", "court-métrage", "nue artistique"); }
|
@Override public List<String> expectedTags() { return Arrays.asList("Covid-19", "Gérôme-Mary trebor", "Horreur et beauté", "court-métrage", "nue artistique"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Segments extends PeertubeStreamExtractorTest {
|
||||||
|
private static final String ID = "vqABGP97fEjo7RhPuDnSZk";
|
||||||
|
private static final String INSTANCE = "https://tube.tchncs.de";
|
||||||
|
|
||||||
|
private static final String URL = INSTANCE + BASE_URL + ID;
|
||||||
|
private static StreamExtractor extractor;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
// setting instance might break test when running in parallel (!)
|
||||||
|
PeerTube.setInstance(new PeertubeInstance(INSTANCE, "tchncs.de"));
|
||||||
|
extractor = PeerTube.getStreamExtractor(URL);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public StreamExtractor extractor() { return extractor; }
|
||||||
|
@Override public StreamingService expectedService() { return PeerTube; }
|
||||||
|
@Override public String expectedName() { return "Bauinformatik 11 – Objekte und Methoden"; }
|
||||||
|
@Override public String expectedId() { return ID; }
|
||||||
|
@Override public String expectedUrlContains() { return INSTANCE + BASE_URL + ID; }
|
||||||
|
@Override public String expectedOriginalUrlContains() { return URL; }
|
||||||
|
|
||||||
|
@Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; }
|
||||||
|
@Override public String expectedUploaderName() { return "Martin Vogel"; }
|
||||||
|
@Override public String expectedUploaderUrl() { return "https://tube.tchncs.de/accounts/martin_vogel@tube.tchncs.de"; }
|
||||||
|
@Override public String expectedSubChannelName() { return "Bauinformatik mit Python"; }
|
||||||
|
@Override public String expectedSubChannelUrl() { return "https://tube.tchncs.de/video-channels/python"; }
|
||||||
|
@Override public List<String> expectedDescriptionContains() { // CRLF line ending
|
||||||
|
return Arrays.asList("Um", "Programme", "Variablen", "Funktionen", "Objekte", "Skript", "Wiederholung", "Listen");
|
||||||
|
}
|
||||||
|
@Override public long expectedLength() { return 1017; }
|
||||||
|
@Override public long expectedViewCountAtLeast() { return 20; }
|
||||||
|
@Nullable @Override public String expectedUploadDate() { return "2023-12-08 15:57:04.142"; }
|
||||||
|
@Nullable @Override public String expectedTextualUploadDate() { return "2023-12-08T15:57:04.142Z"; }
|
||||||
|
@Override public long expectedLikeCountAtLeast() { return 0; }
|
||||||
|
@Override public long expectedDislikeCountAtLeast() { return 0; }
|
||||||
|
@Override public boolean expectedHasSubtitles() { return false; }
|
||||||
|
@Override public String expectedHost() { return "tube.tchncs.de"; }
|
||||||
|
@Override public String expectedCategory() { return "Unknown"; }
|
||||||
|
@Override public String expectedLicence() { return "Unknown"; }
|
||||||
|
@Override public Locale expectedLanguageInfo() { return null; }
|
||||||
|
@Override public List<String> expectedTags() { return Arrays.asList("Attribute", "Bauinformatik", "Klassen", "Objekte", "Python"); }
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
|
|
Loading…
Reference in New Issue