NewPipeExtractor/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeParsingHelper.java

318 lines
14 KiB
Java

package org.schabi.newpipe.extractor.services.peertube;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.Image.ResolutionLevel;
import org.schabi.newpipe.extractor.InfoItemExtractor;
import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeChannelInfoItemExtractor;
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubePlaylistInfoItemExtractor;
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeSepiaStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Parser;
import javax.annotation.Nonnull;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN;
import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public final class PeertubeParsingHelper {
public static final String START_KEY = "start";
public static final String COUNT_KEY = "count";
public static final int ITEMS_PER_PAGE = 12;
public static final String START_PATTERN = "start=(\\d*)";
private PeertubeParsingHelper() {
}
public static void validate(final JsonObject json) throws ContentNotAvailableException {
final String error = json.getString("error");
if (!isBlank(error)) {
throw new ContentNotAvailableException(error);
}
}
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
throws ParsingException {
try {
return OffsetDateTime.ofInstant(Instant.parse(textualUploadDate), ZoneOffset.UTC);
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"", e);
}
}
public static Page getNextPage(final String prevPageUrl, final long total) {
final String prevStart;
try {
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
} catch (final Parser.RegexException e) {
return null;
}
if (isBlank(prevStart)) {
return null;
}
final long nextStart;
try {
nextStart = Long.parseLong(prevStart) + ITEMS_PER_PAGE;
} catch (final NumberFormatException e) {
return null;
}
if (nextStart >= total) {
return null;
} else {
return new Page(prevPageUrl.replace(
START_KEY + "=" + prevStart, START_KEY + "=" + nextStart));
}
}
public static void collectItemsFrom(final InfoItemsCollector collector,
final JsonObject json,
final String baseUrl) throws ParsingException {
collectItemsFrom(collector, json, baseUrl, false);
}
/**
* Collect items from the given JSON object with the given collector.
*
* <p>
* Supported info item types are streams with their Sepia variant, channels and playlists.
* </p>
*
* @param collector the collector used to collect information
* @param json the JSOn response to retrieve data from
* @param baseUrl the base URL of the instance
* @param sepia if we should use {@code PeertubeSepiaStreamInfoItemExtractor} to extract
* streams or {@code PeertubeStreamInfoItemExtractor} otherwise
*/
public static void collectItemsFrom(final InfoItemsCollector collector,
final JsonObject json,
final String baseUrl,
final boolean sepia) throws ParsingException {
final JsonArray contents;
try {
contents = (JsonArray) JsonUtils.getValue(json, "data");
} catch (final Exception e) {
throw new ParsingException("Unable to extract list info", e);
}
for (final Object c : contents) {
if (c instanceof JsonObject) {
JsonObject item = (JsonObject) c;
// PeerTube playlists have the stream info encapsulated in an "video" object
if (item.has("video")) {
item = item.getObject("video");
}
final boolean isPlaylistInfoItem = item.has("videosLength");
final boolean isChannelInfoItem = item.has("followersCount");
final InfoItemExtractor extractor;
if (sepia) {
extractor = new PeertubeSepiaStreamInfoItemExtractor(item, baseUrl);
} else if (isPlaylistInfoItem) {
extractor = new PeertubePlaylistInfoItemExtractor(item, baseUrl);
} else if (isChannelInfoItem) {
extractor = new PeertubeChannelInfoItemExtractor(item, baseUrl);
} else {
extractor = new PeertubeStreamInfoItemExtractor(item, baseUrl);
}
collector.commit(extractor);
}
}
}
/**
* Get avatars from a {@code ownerAccount} or a {@code videoChannel} {@link JsonObject}.
*
* <p>
* If the {@code avatars} {@link JsonArray} is present and non null or empty, avatars will be
* extracted from this array using {@link #getImagesFromAvatarOrBannerArray(String, JsonArray)}.
* </p>
*
* <p>
* If that's not the case, an avatar will extracted using the {@code avatar} {@link JsonObject}.
* </p>
*
* <p>
* Note that only images for which paths are not null and not empty will be added to the
* unmodifiable {@link Image} list returned.
* </p>
*
* @param baseUrl the base URL of the PeerTube instance
* @param ownerAccountOrVideoChannelObject the {@code ownerAccount} or {@code videoChannel}
* {@link JsonObject}
* @return an unmodifiable list of {@link Image}s, which may be empty but never null
*/
@Nonnull
public static List<Image> getAvatarsFromOwnerAccountOrVideoChannelObject(
@Nonnull final String baseUrl,
@Nonnull final JsonObject ownerAccountOrVideoChannelObject) {
return getImagesFromAvatarsOrBanners(baseUrl, ownerAccountOrVideoChannelObject,
"avatars", "avatar");
}
/**
* Get banners from a {@code ownerAccount} or a {@code videoChannel} {@link JsonObject}.
*
* <p>
* If the {@code banners} {@link JsonArray} is present and non null or empty, banners will be
* extracted from this array using {@link #getImagesFromAvatarOrBannerArray(String, JsonArray)}.
* </p>
*
* <p>
* If that's not the case, a banner will extracted using the {@code banner} {@link JsonObject}.
* </p>
*
* <p>
* Note that only images for which paths are not null and not empty will be added to the
* unmodifiable {@link Image} list returned.
* </p>
*
* @param baseUrl the base URL of the PeerTube instance
* @param ownerAccountOrVideoChannelObject the {@code ownerAccount} or {@code videoChannel}
* {@link JsonObject}
* @return an unmodifiable list of {@link Image}s, which may be empty but never null
*/
@Nonnull
public static List<Image> getBannersFromAccountOrVideoChannelObject(
@Nonnull final String baseUrl,
@Nonnull final JsonObject ownerAccountOrVideoChannelObject) {
return getImagesFromAvatarsOrBanners(baseUrl, ownerAccountOrVideoChannelObject,
"banners", "banner");
}
/**
* Get thumbnails from a playlist or a video item {@link JsonObject}.
*
* <p>
* PeerTube provides two thumbnails in its API: a low one, represented by the value of the
* {@code thumbnailPath} key, and a medium one, represented by the value of the
* {@code previewPath} key.
* </p>
*
* <p>
* If a value is not null or empty, an {@link Image} will be added to the list returned with
* the URL to the thumbnail ({@code baseUrl + value}), a height and a width unknown and the
* corresponding resolution level (see above).
* </p>
*
* @param baseUrl the base URL of the PeerTube instance
* @param playlistOrVideoItemObject the playlist or the video item {@link JsonObject}, which
* must not be null
* @return an unmodifiable list of {@link Image}s, which is never null but can be empty
*/
@Nonnull
public static List<Image> getThumbnailsFromPlaylistOrVideoItem(
@Nonnull final String baseUrl,
@Nonnull final JsonObject playlistOrVideoItemObject) {
final List<Image> imageList = new ArrayList<>(2);
final String thumbnailPath = playlistOrVideoItemObject.getString("thumbnailPath");
if (!isNullOrEmpty(thumbnailPath)) {
imageList.add(new Image(baseUrl + thumbnailPath, HEIGHT_UNKNOWN, WIDTH_UNKNOWN,
ResolutionLevel.LOW));
}
final String previewPath = playlistOrVideoItemObject.getString("previewPath");
if (!isNullOrEmpty(previewPath)) {
imageList.add(new Image(baseUrl + previewPath, HEIGHT_UNKNOWN, WIDTH_UNKNOWN,
ResolutionLevel.MEDIUM));
}
return Collections.unmodifiableList(imageList);
}
/**
* Utility method to get avatars and banners from video channels and accounts from given name
* keys.
*
* <p>
* Only images for which paths are not null and not empty will be added to the unmodifiable
* {@link Image} list returned and only the width of avatars or banners is provided by the API,
* and so is the only image dimension known.
* </p>
*
* @param baseUrl the base URL of the PeerTube instance
* @param ownerAccountOrVideoChannelObject the {@code ownerAccount} or {@code videoChannel}
* {@link JsonObject}
* @param jsonArrayName the key name of the {@link JsonArray} to which
* extract all images available ({@code avatars} or
* {@code banners})
* @param jsonObjectName the key name of the {@link JsonObject} to which
* extract a single image ({@code avatar} or
* {@code banner}), used as a fallback if the images
* {@link JsonArray} is null or empty
* @return an unmodifiable list of {@link Image}s, which may be empty but never null
*/
@Nonnull
private static List<Image> getImagesFromAvatarsOrBanners(
@Nonnull final String baseUrl,
@Nonnull final JsonObject ownerAccountOrVideoChannelObject,
@Nonnull final String jsonArrayName,
@Nonnull final String jsonObjectName) {
final JsonArray images = ownerAccountOrVideoChannelObject.getArray(jsonArrayName);
if (!isNullOrEmpty(images)) {
return getImagesFromAvatarOrBannerArray(baseUrl, images);
}
final JsonObject image = ownerAccountOrVideoChannelObject.getObject(jsonObjectName);
final String path = image.getString("path");
if (!isNullOrEmpty(path)) {
return List.of(new Image(baseUrl + path, HEIGHT_UNKNOWN,
image.getInt("width", WIDTH_UNKNOWN), ResolutionLevel.UNKNOWN));
}
return List.of();
}
/**
* Get {@link Image}s from an {@code avatars} or a {@code banners} {@link JsonArray}.
*
* <p>
* Only images for which paths are not null and not empty will be added to the
* unmodifiable {@link Image} list returned.
* </p>
*
* <p>
* Note that only the width of avatars or banners is provided by the API, and so only is the
* only dimension known of images.
* </p>
*
* @param baseUrl the base URL of the PeerTube instance from which the
* {@code avatarsOrBannersArray} {@link JsonArray} comes from
* @param avatarsOrBannersArray an {@code avatars} or {@code banners} {@link JsonArray}
* @return an unmodifiable list of {@link Image}s, which may be empty but never null
*/
@Nonnull
private static List<Image> getImagesFromAvatarOrBannerArray(
@Nonnull final String baseUrl,
@Nonnull final JsonArray avatarsOrBannersArray) {
return avatarsOrBannersArray.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(image -> !isNullOrEmpty(image.getString("path")))
.map(image -> new Image(baseUrl + image.getString("path"), HEIGHT_UNKNOWN,
image.getInt("width", WIDTH_UNKNOWN), ResolutionLevel.UNKNOWN))
.collect(Collectors.toUnmodifiableList());
}
}