Implement geo restrictions bypass.

This commit is contained in:
Kavin 2022-11-27 00:03:16 +00:00
parent a654d22635
commit 12c41980dd
No known key found for this signature in database
GPG Key ID: 49451E4482CC5BCD
19 changed files with 414 additions and 63 deletions

View File

@ -1,6 +1,7 @@
plugins {
id "com.github.johnrengelman.shadow" version "7.1.2"
id "java"
id "io.freefair.lombok" version "6.5.1"
id "eclipse"
}

View File

@ -60,6 +60,9 @@ MATRIX_SERVER:https://matrix-client.matrix.org
# If not present, will work in anon mode
#MATRIX_TOKEN:INSERT_HERE
# Geo Restriction Checker for federated bypassing of Geo Restrictions
#GEO_RESTRICTION_CHECKER_URL:INSERT_HERE
# Hibernate properties
hibernate.connection.url:jdbc:postgresql://postgres:5432/piped
hibernate.connection.driver_class:org.postgresql.Driver

View File

@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import me.kavin.piped.utils.PageMixin;
import me.kavin.piped.utils.RequestUtils;
import me.kavin.piped.utils.resp.ListLinkHandlerMixin;
import okhttp3.OkHttpClient;
import okhttp3.brotli.BrotliInterceptor;
@ -23,6 +24,7 @@ import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
public class Constants {
@ -80,6 +82,10 @@ public class Constants {
public static final String MATRIX_TOKEN;
public static final String GEO_RESTRICTION_CHECKER_URL;
public static final String YOUTUBE_COUNTRY;
public static final String VERSION;
public static final ObjectMapper mapper = JsonMapper.builder()
@ -130,6 +136,7 @@ public class Constants {
});
MATRIX_SERVER = getProperty(prop, "MATRIX_SERVER", "https://matrix-client.matrix.org");
MATRIX_TOKEN = getProperty(prop, "MATRIX_TOKEN");
GEO_RESTRICTION_CHECKER_URL = getProperty(prop, "GEO_RESTRICTION_CHECKER_URL");
prop.forEach((_key, _value) -> {
String key = String.valueOf(_key), value = String.valueOf(_value);
if (key.startsWith("hibernate"))
@ -164,6 +171,18 @@ public class Constants {
}
h2client = builder.build();
h2_no_redir_client = builder_noredir.build();
String temp = null;
try {
var html = RequestUtils.sendGet("https://www.youtube.com/");
var regex = Pattern.compile("GL\":\"([A-Z]{2})\"", Pattern.MULTILINE);
var matcher = regex.matcher(html);
if (matcher.find()) {
temp = matcher.group(1);
}
} catch (Exception ignored) {
System.err.println("Failed to get country from YouTube!");
}
YOUTUBE_COUNTRY = temp;
VERSION = new File("VERSION").exists() ?
IOUtils.toString(new FileReader("VERSION")) :
"unknown";

View File

@ -9,6 +9,8 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.kavin.piped.consts.Constants;
import me.kavin.piped.utils.*;
import me.kavin.piped.utils.obj.*;
import me.kavin.piped.utils.obj.federation.FederatedGeoBypassRequest;
import me.kavin.piped.utils.obj.federation.FederatedGeoBypassResponse;
import me.kavin.piped.utils.obj.federation.FederatedVideoInfo;
import me.kavin.piped.utils.resp.InvalidRequestResponse;
import me.kavin.piped.utils.resp.VideoResolvedResponse;
@ -18,20 +20,21 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static java.nio.charset.StandardCharsets.UTF_8;
import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE;
import static me.kavin.piped.consts.Constants.mapper;
import static me.kavin.piped.utils.CollectionUtils.collectRelatedItems;
import static me.kavin.piped.utils.URLUtils.*;
import static me.kavin.piped.utils.URLUtils.rewriteURL;
import static me.kavin.piped.utils.URLUtils.substringYouTube;
import static org.schabi.newpipe.extractor.NewPipe.getPreferredContentCountry;
import static org.schabi.newpipe.extractor.NewPipe.getPreferredLocalization;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
@ -48,6 +51,9 @@ public class StreamHandlers {
try {
return StreamInfo.getInfo("https://www.youtube.com/watch?v=" + videoId);
} catch (Exception e) {
if (e instanceof GeographicRestrictionException) {
return null;
}
transaction.setThrowable(e);
ExceptionUtils.rethrow(e);
} finally {
@ -98,21 +104,93 @@ public class StreamHandlers {
return null;
});
final List<Subtitle> subtitles = new ObjectArrayList<>();
final List<ChapterSegment> chapters = new ObjectArrayList<>();
StreamInfo info = null;
final StreamInfo info = futureStream.get(10, TimeUnit.SECONDS);
try {
info = futureStream.get(10, TimeUnit.SECONDS);
} catch (ExecutionException e) {
if (!(e.getCause() instanceof GeographicRestrictionException)) {
ExceptionUtils.rethrow(e);
}
}
info.getStreamSegments().forEach(segment -> chapters.add(new ChapterSegment(segment.getTitle(), rewriteURL(segment.getPreviewUrl()),
segment.getStartTimeSeconds())));
if (info == null) {
// We're geo restricted
info.getSubtitles()
.forEach(subtitle -> subtitles.add(new Subtitle(rewriteURL(subtitle.getContent()),
subtitle.getFormat().getMimeType(), subtitle.getDisplayLanguageName(),
subtitle.getLanguageTag(), subtitle.isAutoGenerated())));
if (Constants.MATRIX_TOKEN != null && Constants.GEO_RESTRICTION_CHECKER_URL != null) {
final List<PipedStream> videoStreams = new ObjectArrayList<>();
final List<PipedStream> audioStreams = new ObjectArrayList<>();
List<String> allowedCountries = new ObjectArrayList<>();
{
var restrictedTree = RequestUtils.sendGetJson(Constants.GEO_RESTRICTION_CHECKER_URL + "/api/region/check?video_id=" + videoId);
var it = restrictedTree.get("regions").elements();
while (it.hasNext()) {
var region = it.next();
allowedCountries.add(region.textValue());
}
}
if (allowedCountries.isEmpty())
throw new GeographicRestrictionException("Federated bypass failed, video not available in any region");
MatrixHelper.sendEvent("video.piped.stream.bypass.request", new FederatedGeoBypassRequest(videoId, allowedCountries));
var listener = new WaitingListener(10_000);
GeoRestrictionBypassHelper.makeRequest(videoId, listener);
listener.waitFor();
FederatedGeoBypassResponse federatedGeoBypassResponse = GeoRestrictionBypassHelper.getResponse(videoId);
if (federatedGeoBypassResponse == null)
throw new GeographicRestrictionException("Federated bypass failed, likely not authorized or no suitable instances found for country");
Streams streams = federatedGeoBypassResponse.getData();
String lbryId;
try {
lbryId = futureLbryId.get(2, TimeUnit.SECONDS);
} catch (Exception e) {
lbryId = null;
}
if (lbryId != null) {
streams.lbryId = lbryId;
}
String lbryURL;
try {
lbryURL = futureLBRY.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
lbryURL = null;
}
if (lbryURL != null)
streams.videoStreams.add(0, new PipedStream(lbryURL, "MP4", "LBRY", "video/mp4", false));
// Attempt to get dislikes calculating with the RYD API rating
if (streams.dislikes < 0 && streams.likes >= 0) {
double rating;
try {
rating = futureDislikeRating.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
rating = -1;
}
if (rating > 1 && rating <= 5) {
streams.dislikes = Math.round(streams.likes * ((5 - rating) / (rating - 1)));
}
}
return mapper.writeValueAsBytes(streams);
} else if (Constants.GEO_RESTRICTION_CHECKER_URL == null) {
throw new GeographicRestrictionException("This instance does not have a geo restriction checker set in its configuration");
}
throw new GeographicRestrictionException("Geo restricted content, this instance is not part of the Matrix Federation protocol");
}
Streams streams = CollectionUtils.collectStreamInfo(info);
String lbryURL = null;
@ -123,40 +201,20 @@ public class StreamHandlers {
}
if (lbryURL != null)
videoStreams.add(new PipedStream(lbryURL, "MP4", "LBRY", "video/mp4", false));
boolean livestream = info.getStreamType() == StreamType.LIVE_STREAM;
if (!livestream) {
info.getVideoOnlyStreams().forEach(stream -> videoStreams.add(new PipedStream(rewriteVideoURL(stream.getContent()),
String.valueOf(stream.getFormat()), stream.getResolution(), stream.getFormat().getMimeType(), true,
stream.getBitrate(), stream.getInitStart(), stream.getInitEnd(), stream.getIndexStart(),
stream.getIndexEnd(), stream.getCodec(), stream.getWidth(), stream.getHeight(), 30)));
info.getVideoStreams()
.forEach(stream -> videoStreams
.add(new PipedStream(rewriteVideoURL(stream.getContent()), String.valueOf(stream.getFormat()),
stream.getResolution(), stream.getFormat().getMimeType(), false)));
info.getAudioStreams()
.forEach(stream -> audioStreams.add(new PipedStream(rewriteVideoURL(stream.getContent()),
String.valueOf(stream.getFormat()), stream.getAverageBitrate() + " kbps",
stream.getFormat().getMimeType(), false, stream.getBitrate(), stream.getInitStart(),
stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec(), stream.getAudioTrackId(), stream.getAudioTrackName())));
}
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
streams.videoStreams.add(0, new PipedStream(lbryURL, "MP4", "LBRY", "video/mp4", false));
long time = info.getUploadDate() != null ? info.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
: System.currentTimeMillis();
if (info.getUploadDate() != null && System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) {
VideoHelpers.updateVideo(info.getId(), info, time);
StreamInfo finalInfo = info;
Multithreading.runAsync(() -> {
try {
MatrixHelper.sendEvent("video.piped.stream.info", new FederatedVideoInfo(
info.getId(), StringUtils.substring(info.getUploaderUrl(), -24),
info.getName(),
info.getDuration(), info.getViewCount())
finalInfo.getId(), StringUtils.substring(finalInfo.getUploaderUrl(), -24),
finalInfo.getName(),
finalInfo.getDuration(), finalInfo.getViewCount())
);
} catch (IOException e) {
throw new RuntimeException(e);
@ -172,8 +230,10 @@ public class StreamHandlers {
lbryId = null;
}
streams.lbryId = lbryId;
// Attempt to get dislikes calculating with the RYD API rating
if (info.getDislikeCount() < 0 && info.getLikeCount() >= 0) {
if (streams.dislikes < 0 && streams.likes >= 0) {
double rating;
try {
rating = futureDislikeRating.get(3, TimeUnit.SECONDS);
@ -182,17 +242,10 @@ public class StreamHandlers {
}
if (rating > 1 && rating <= 5) {
info.setDislikeCount(Math.round(info.getLikeCount() * ((5 - rating) / (rating - 1))));
streams.dislikes = Math.round(streams.likes * ((5 - rating) / (rating - 1)));
}
}
final Streams streams = new Streams(info.getName(), info.getDescription().getContent(),
info.getTextualUploadDate(), info.getUploaderName(), substringYouTube(info.getUploaderUrl()),
rewriteURL(info.getUploaderAvatarUrl()), rewriteURL(info.getThumbnailUrl()), info.getDuration(),
info.getViewCount(), info.getLikeCount(), info.getDislikeCount(), info.getUploaderSubscriberCount(), info.isUploaderVerified(),
audioStreams, videoStreams, relatedStreams, subtitles, livestream, rewriteVideoURL(info.getHlsUrl()),
rewriteVideoURL(info.getDashMpdUrl()), lbryId, chapters);
return mapper.writeValueAsBytes(streams);
}

View File

@ -1,21 +1,66 @@
package me.kavin.piped.utils;
import me.kavin.piped.utils.obj.ChannelItem;
import me.kavin.piped.utils.obj.ContentItem;
import me.kavin.piped.utils.obj.PlaylistItem;
import me.kavin.piped.utils.obj.StreamItem;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.kavin.piped.utils.obj.*;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.List;
import static me.kavin.piped.utils.URLUtils.rewriteURL;
import static me.kavin.piped.utils.URLUtils.substringYouTube;
import static me.kavin.piped.utils.URLUtils.*;
public class CollectionUtils {
public static Streams collectStreamInfo(StreamInfo info) {
final List<Subtitle> subtitles = new ObjectArrayList<>();
final List<ChapterSegment> chapters = new ObjectArrayList<>();
info.getStreamSegments().forEach(segment -> chapters.add(new ChapterSegment(segment.getTitle(), rewriteURL(segment.getPreviewUrl()),
segment.getStartTimeSeconds())));
info.getSubtitles()
.forEach(subtitle -> subtitles.add(new Subtitle(rewriteURL(subtitle.getContent()),
subtitle.getFormat().getMimeType(), subtitle.getDisplayLanguageName(),
subtitle.getLanguageTag(), subtitle.isAutoGenerated())));
final List<PipedStream> videoStreams = new ObjectArrayList<>();
final List<PipedStream> audioStreams = new ObjectArrayList<>();
boolean livestream = info.getStreamType() == StreamType.LIVE_STREAM;
if (!livestream) {
info.getVideoOnlyStreams().forEach(stream -> videoStreams.add(new PipedStream(rewriteVideoURL(stream.getContent()),
String.valueOf(stream.getFormat()), stream.getResolution(), stream.getFormat().getMimeType(), true,
stream.getBitrate(), stream.getInitStart(), stream.getInitEnd(), stream.getIndexStart(),
stream.getIndexEnd(), stream.getCodec(), stream.getWidth(), stream.getHeight(), 30)));
info.getVideoStreams()
.forEach(stream -> videoStreams
.add(new PipedStream(rewriteVideoURL(stream.getContent()), String.valueOf(stream.getFormat()),
stream.getResolution(), stream.getFormat().getMimeType(), false)));
info.getAudioStreams()
.forEach(stream -> audioStreams.add(new PipedStream(rewriteVideoURL(stream.getContent()),
String.valueOf(stream.getFormat()), stream.getAverageBitrate() + " kbps",
stream.getFormat().getMimeType(), false, stream.getBitrate(), stream.getInitStart(),
stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec(), stream.getAudioTrackId(), stream.getAudioTrackName())));
}
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
final Streams streams = new Streams(info.getName(), info.getDescription().getContent(),
info.getTextualUploadDate(), info.getUploaderName(), substringYouTube(info.getUploaderUrl()),
rewriteURL(info.getUploaderAvatarUrl()), rewriteURL(info.getThumbnailUrl()), info.getDuration(),
info.getViewCount(), info.getLikeCount(), info.getDislikeCount(), info.getUploaderSubscriberCount(), info.isUploaderVerified(),
audioStreams, videoStreams, relatedStreams, subtitles, livestream, rewriteVideoURL(info.getHlsUrl()),
rewriteVideoURL(info.getDashMpdUrl()), null, chapters);
return streams;
}
public static List<ContentItem> collectRelatedItems(List<? extends InfoItem> items) {
return items
.stream()

View File

@ -0,0 +1,104 @@
package me.kavin.piped.utils;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.kavin.piped.utils.obj.federation.FederatedGeoBypassResponse;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class GeoRestrictionBypassHelper {
private static final Map<String, Long> requestsMap = new Object2LongOpenHashMap<>();
private static final Map<String, Response> responsesMap = new Object2ObjectOpenHashMap<>();
private static final List<ListenerRequest> waitingListeners = new ObjectArrayList<>();
public static void makeRequest(String id, WaitingListener listener) {
synchronized (requestsMap) {
if (!requestsMap.containsKey(id))
requestsMap.put(id, System.currentTimeMillis());
else {
synchronized (responsesMap) {
if (responsesMap.containsKey(id)) {
listener.done();
return;
}
}
}
}
synchronized (waitingListeners) {
waitingListeners.add(new ListenerRequest(id, listener));
}
}
public static void addResponse(FederatedGeoBypassResponse response) {
String id = response.getVideoId();
synchronized (requestsMap) {
if (requestsMap.containsKey(id)) {
synchronized (responsesMap) {
responsesMap.put(id, new Response(response));
}
synchronized (waitingListeners) {
for (ListenerRequest waitingListener : waitingListeners) {
if (waitingListener.id.equals(id)) {
waitingListener.listener.done();
}
}
}
}
}
}
public static FederatedGeoBypassResponse getResponse(String id) {
synchronized (responsesMap) {
if (responsesMap.containsKey(id)) {
return responsesMap.get(id).response;
}
}
return null;
}
private static final class ListenerRequest {
private final String id;
private final long creationTime = System.currentTimeMillis();
private final WaitingListener listener;
public ListenerRequest(String id, WaitingListener listener) {
this.id = id;
this.listener = listener;
}
}
private static final class Response {
private final FederatedGeoBypassResponse response;
private final long time = System.currentTimeMillis();
public Response(FederatedGeoBypassResponse response) {
this.response = response;
}
}
static {
long time = TimeUnit.SECONDS.toMillis(60);
// Start evictor timer to remove old requests, responses and listeners
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
synchronized (requestsMap) {
requestsMap.entrySet().removeIf(e -> e.getValue() < System.currentTimeMillis() - time);
}
synchronized (responsesMap) {
responsesMap.entrySet().removeIf(e -> e.getValue().time < System.currentTimeMillis() - time);
}
synchronized (waitingListeners) {
waitingListeners.removeIf(e -> e.creationTime < System.currentTimeMillis() - time);
}
}
}, time, time);
}
}

View File

@ -43,4 +43,12 @@ public class RequestUtils {
}
}
}
public static JsonNode sendGetJson(String url, String ua) throws IOException {
return getJsonNode(Constants.h2client, new Request.Builder().header("User-Agent", ua).url(url).build());
}
public static JsonNode sendGetJson(String url) throws IOException {
return sendGetJson(url, Constants.USER_AGENT);
}
}

View File

@ -0,0 +1,21 @@
package me.kavin.piped.utils;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class WaitingListener {
private final long maxWaitTime;
public void waitFor() throws InterruptedException {
synchronized (this) {
this.wait(maxWaitTime);
}
}
public void done() {
synchronized (this) {
this.notifyAll();
}
}
}

View File

@ -1,14 +1,20 @@
package me.kavin.piped.utils.matrix;
import com.fasterxml.jackson.databind.JsonNode;
import me.kavin.piped.consts.Constants;
import me.kavin.piped.utils.*;
import me.kavin.piped.utils.obj.MatrixHelper;
import me.kavin.piped.utils.obj.Streams;
import me.kavin.piped.utils.obj.federation.FederatedChannelInfo;
import me.kavin.piped.utils.obj.federation.FederatedGeoBypassRequest;
import me.kavin.piped.utils.obj.federation.FederatedGeoBypassResponse;
import me.kavin.piped.utils.obj.federation.FederatedVideoInfo;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.hibernate.StatelessSession;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.io.IOException;
import java.util.Set;
@ -121,17 +127,40 @@ public class SyncRunner implements Runnable {
for (var event : events) {
var type = event.get("type").asText();
var content = event.at("/content/content");
if (type.startsWith("video.piped.stream.bypass.")) {
switch (type) {
case "video.piped.stream.bypass.request" -> {
FederatedGeoBypassRequest bypassRequest = mapper.treeToValue(content, FederatedGeoBypassRequest.class);
if (bypassRequest.getAllowedCountries().contains(Constants.YOUTUBE_COUNTRY)) {
// We're capable of helping another instance!
Multithreading.runAsync(() -> {
try {
StreamInfo info = StreamInfo.getInfo("https://www.youtube.com/watch?v=" + bypassRequest.getVideoId());
Streams streams = CollectionUtils.collectStreamInfo(info);
FederatedGeoBypassResponse bypassResponse = new FederatedGeoBypassResponse(bypassRequest.getVideoId(), Constants.YOUTUBE_COUNTRY, streams);
MatrixHelper.sendEvent("video.piped.stream.bypass.response", bypassResponse);
} catch (Exception ignored) {
}
});
}
}
case "video.piped.stream.bypass.response" -> {
FederatedGeoBypassResponse bypassResponse = mapper.treeToValue(content, FederatedGeoBypassResponse.class);
GeoRestrictionBypassHelper.addResponse(bypassResponse);
}
}
}
if (event.get("sender").asText().equals(user_id)) {
if (type.startsWith("video.piped.stream.bypass.")) {
// TODO: Implement geo-restriction bypassing
}
continue;
}
var content = event.at("/content/content");
switch (type) {
case "video.piped.stream.info" -> {

View File

@ -1,5 +1,8 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class ChannelItem extends ContentItem {
public final String type = "channel";

View File

@ -1,5 +1,8 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class ChapterSegment {
public String title, image;

View File

@ -1,6 +1,17 @@
package me.kavin.piped.utils.obj;
public class ContentItem {
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(name = "stream", value = StreamItem.class),
@JsonSubTypes.Type(name = "channel", value = ChannelItem.class),
@JsonSubTypes.Type(name = "playlist", value = PlaylistItem.class),
})
public abstract class ContentItem {
public String url;

View File

@ -1,5 +1,8 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class PipedStream {
public String url, format, quality, mimeType, codec, audioTrackId, audioTrackName;

View File

@ -1,5 +1,8 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class PlaylistItem extends ContentItem {
public final String type = "playlist";

View File

@ -1,5 +1,8 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class StreamItem extends ContentItem {
public final String type = "stream";

View File

@ -1,9 +1,11 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
import me.kavin.piped.consts.Constants;
import java.util.List;
@NoArgsConstructor
public class Streams {
public String title, description, uploadDate, uploader, uploaderUrl, uploaderAvatar, thumbnailUrl, hls, dash,

View File

@ -1,9 +1,12 @@
package me.kavin.piped.utils.obj;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class Subtitle {
public final String url, mimeType, name, code;
public final boolean autoGenerated;
public String url, mimeType, name, code;
public boolean autoGenerated;
public Subtitle(String url, String mimeType, String name, String code, boolean autoGenerated) {
this.url = url;

View File

@ -0,0 +1,15 @@
package me.kavin.piped.utils.obj.federation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class FederatedGeoBypassRequest {
private String videoId;
private List<String> allowedCountries;
}

View File

@ -0,0 +1,22 @@
package me.kavin.piped.utils.obj.federation;
import lombok.Getter;
import lombok.NoArgsConstructor;
import me.kavin.piped.consts.Constants;
import me.kavin.piped.utils.obj.Streams;
@NoArgsConstructor
@Getter
public class FederatedGeoBypassResponse {
private String videoId;
private String country;
private String videoProxyUrl;
private Streams data;
public FederatedGeoBypassResponse(String videoId, String country, Streams data) {
this.videoId = videoId;
this.country = country;
this.data = data;
this.videoProxyUrl = Constants.PROXY_PART;
}
}