Merge pull request #1142 from TeamNewPipe/peertube-v6

[PeerTube] Add support for PeerTube v6 features
This commit is contained in:
Stypox 2024-03-29 12:25:38 +01:00 committed by GitHub
commit fb468a23f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 157 additions and 2 deletions

View File

@ -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 {

View File

@ -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;

View File

@ -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 {