diff --git a/build.gradle b/build.gradle index 5df11d9..db0afaf 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } diff --git a/config.properties b/config.properties index c7eb856..e16cf6b 100644 --- a/config.properties +++ b/config.properties @@ -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 diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index 3f1b2d7..e6792f5 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -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"; diff --git a/src/main/java/me/kavin/piped/server/handlers/StreamHandlers.java b/src/main/java/me/kavin/piped/server/handlers/StreamHandlers.java index e0d3478..23703bf 100644 --- a/src/main/java/me/kavin/piped/server/handlers/StreamHandlers.java +++ b/src/main/java/me/kavin/piped/server/handlers/StreamHandlers.java @@ -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 subtitles = new ObjectArrayList<>(); - final List 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 videoStreams = new ObjectArrayList<>(); - final List audioStreams = new ObjectArrayList<>(); + List 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 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); } diff --git a/src/main/java/me/kavin/piped/utils/CollectionUtils.java b/src/main/java/me/kavin/piped/utils/CollectionUtils.java index bdaf1e3..d8c7980 100644 --- a/src/main/java/me/kavin/piped/utils/CollectionUtils.java +++ b/src/main/java/me/kavin/piped/utils/CollectionUtils.java @@ -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 subtitles = new ObjectArrayList<>(); + final List 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 videoStreams = new ObjectArrayList<>(); + final List 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 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 collectRelatedItems(List items) { return items .stream() diff --git a/src/main/java/me/kavin/piped/utils/GeoRestrictionBypassHelper.java b/src/main/java/me/kavin/piped/utils/GeoRestrictionBypassHelper.java new file mode 100644 index 0000000..a5989cf --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/GeoRestrictionBypassHelper.java @@ -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 requestsMap = new Object2LongOpenHashMap<>(); + private static final Map responsesMap = new Object2ObjectOpenHashMap<>(); + private static final List 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); + } +} diff --git a/src/main/java/me/kavin/piped/utils/RequestUtils.java b/src/main/java/me/kavin/piped/utils/RequestUtils.java index 0687703..a5968f5 100644 --- a/src/main/java/me/kavin/piped/utils/RequestUtils.java +++ b/src/main/java/me/kavin/piped/utils/RequestUtils.java @@ -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); + } } diff --git a/src/main/java/me/kavin/piped/utils/WaitingListener.java b/src/main/java/me/kavin/piped/utils/WaitingListener.java new file mode 100644 index 0000000..9eeb529 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/WaitingListener.java @@ -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(); + } + } +} diff --git a/src/main/java/me/kavin/piped/utils/matrix/SyncRunner.java b/src/main/java/me/kavin/piped/utils/matrix/SyncRunner.java index 55ffe52..e4cf8ba 100644 --- a/src/main/java/me/kavin/piped/utils/matrix/SyncRunner.java +++ b/src/main/java/me/kavin/piped/utils/matrix/SyncRunner.java @@ -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" -> { diff --git a/src/main/java/me/kavin/piped/utils/obj/ChannelItem.java b/src/main/java/me/kavin/piped/utils/obj/ChannelItem.java index 4fbc957..4277a48 100644 --- a/src/main/java/me/kavin/piped/utils/obj/ChannelItem.java +++ b/src/main/java/me/kavin/piped/utils/obj/ChannelItem.java @@ -1,5 +1,8 @@ package me.kavin.piped.utils.obj; +import lombok.NoArgsConstructor; + +@NoArgsConstructor public class ChannelItem extends ContentItem { public final String type = "channel"; diff --git a/src/main/java/me/kavin/piped/utils/obj/ChapterSegment.java b/src/main/java/me/kavin/piped/utils/obj/ChapterSegment.java index 059eff1..67387a2 100644 --- a/src/main/java/me/kavin/piped/utils/obj/ChapterSegment.java +++ b/src/main/java/me/kavin/piped/utils/obj/ChapterSegment.java @@ -1,5 +1,8 @@ package me.kavin.piped.utils.obj; +import lombok.NoArgsConstructor; + +@NoArgsConstructor public class ChapterSegment { public String title, image; diff --git a/src/main/java/me/kavin/piped/utils/obj/ContentItem.java b/src/main/java/me/kavin/piped/utils/obj/ContentItem.java index 31cc3f3..0fe0d61 100644 --- a/src/main/java/me/kavin/piped/utils/obj/ContentItem.java +++ b/src/main/java/me/kavin/piped/utils/obj/ContentItem.java @@ -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; diff --git a/src/main/java/me/kavin/piped/utils/obj/PipedStream.java b/src/main/java/me/kavin/piped/utils/obj/PipedStream.java index 3290409..05fc012 100644 --- a/src/main/java/me/kavin/piped/utils/obj/PipedStream.java +++ b/src/main/java/me/kavin/piped/utils/obj/PipedStream.java @@ -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; diff --git a/src/main/java/me/kavin/piped/utils/obj/PlaylistItem.java b/src/main/java/me/kavin/piped/utils/obj/PlaylistItem.java index 31b6a16..d89528c 100644 --- a/src/main/java/me/kavin/piped/utils/obj/PlaylistItem.java +++ b/src/main/java/me/kavin/piped/utils/obj/PlaylistItem.java @@ -1,5 +1,8 @@ package me.kavin.piped.utils.obj; +import lombok.NoArgsConstructor; + +@NoArgsConstructor public class PlaylistItem extends ContentItem { public final String type = "playlist"; diff --git a/src/main/java/me/kavin/piped/utils/obj/StreamItem.java b/src/main/java/me/kavin/piped/utils/obj/StreamItem.java index e5ccf86..23e7804 100644 --- a/src/main/java/me/kavin/piped/utils/obj/StreamItem.java +++ b/src/main/java/me/kavin/piped/utils/obj/StreamItem.java @@ -1,5 +1,8 @@ package me.kavin.piped.utils.obj; +import lombok.NoArgsConstructor; + +@NoArgsConstructor public class StreamItem extends ContentItem { public final String type = "stream"; diff --git a/src/main/java/me/kavin/piped/utils/obj/Streams.java b/src/main/java/me/kavin/piped/utils/obj/Streams.java index c903058..7e4bcb9 100644 --- a/src/main/java/me/kavin/piped/utils/obj/Streams.java +++ b/src/main/java/me/kavin/piped/utils/obj/Streams.java @@ -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, diff --git a/src/main/java/me/kavin/piped/utils/obj/Subtitle.java b/src/main/java/me/kavin/piped/utils/obj/Subtitle.java index f2d2264..db5563a 100644 --- a/src/main/java/me/kavin/piped/utils/obj/Subtitle.java +++ b/src/main/java/me/kavin/piped/utils/obj/Subtitle.java @@ -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; diff --git a/src/main/java/me/kavin/piped/utils/obj/federation/FederatedGeoBypassRequest.java b/src/main/java/me/kavin/piped/utils/obj/federation/FederatedGeoBypassRequest.java new file mode 100644 index 0000000..e27efe5 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/federation/FederatedGeoBypassRequest.java @@ -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 allowedCountries; +} diff --git a/src/main/java/me/kavin/piped/utils/obj/federation/FederatedGeoBypassResponse.java b/src/main/java/me/kavin/piped/utils/obj/federation/FederatedGeoBypassResponse.java new file mode 100644 index 0000000..c95a61d --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/federation/FederatedGeoBypassResponse.java @@ -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; + } +}