mirror of
https://github.com/TeamPiped/Piped-Backend.git
synced 2024-08-14 23:51:41 +00:00
Merge pull request #473 from TeamPiped/federated-geo-bypass
Implement geo restrictions bypass.
This commit is contained in:
commit
a9b16ee727
19 changed files with 416 additions and 63 deletions
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package me.kavin.piped.consts;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
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 +25,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,11 +83,16 @@ 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()
|
||||
.addMixIn(Page.class, PageMixin.class)
|
||||
.addMixIn(ListLinkHandler.class, ListLinkHandlerMixin.class)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.build();
|
||||
|
||||
public static final Object2ObjectOpenHashMap<String, String> hibernateProperties = new Object2ObjectOpenHashMap<>();
|
||||
|
@ -130,6 +138,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 +173,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";
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
21
src/main/java/me/kavin/piped/utils/WaitingListener.java
Normal file
21
src/main/java/me/kavin/piped/utils/WaitingListener.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" -> {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package me.kavin.piped.utils.obj;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class ChannelItem extends ContentItem {
|
||||
|
||||
public final String type = "channel";
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package me.kavin.piped.utils.obj;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class ChapterSegment {
|
||||
|
||||
public String title, image;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package me.kavin.piped.utils.obj;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class PlaylistItem extends ContentItem {
|
||||
|
||||
public final String type = "playlist";
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package me.kavin.piped.utils.obj;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class StreamItem extends ContentItem {
|
||||
|
||||
public final String type = "stream";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue