diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index 4626c08..85e0eac 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -33,6 +33,7 @@ import org.xml.sax.InputSource; import java.io.ByteArrayInputStream; import java.net.InetSocketAddress; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -149,6 +150,23 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher { } catch (Exception e) { return getErrorResponse(e, request.getPath()); } + })).map(GET, "/dearrow", AsyncServlet.ofBlocking(executor, request -> { + try { + var videoIds = getArray(request.getQueryParameter("videoIds")); + + return getJsonResponse( + SponsorBlockUtils.getDeArrowedInfo(List.of(videoIds)) + .thenApplyAsync(json -> { + try { + return mapper.writeValueAsBytes(json); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }).get(), + "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } })).map(GET, "/streams/:videoId", AsyncServlet.ofBlocking(executor, request -> { try { return getJsonResponse(StreamHandlers.streamsResponse(request.getPathParameter("videoId")), diff --git a/src/main/java/me/kavin/piped/utils/SponsorBlockUtils.java b/src/main/java/me/kavin/piped/utils/SponsorBlockUtils.java index bfe580f..77ffbc6 100644 --- a/src/main/java/me/kavin/piped/utils/SponsorBlockUtils.java +++ b/src/main/java/me/kavin/piped/utils/SponsorBlockUtils.java @@ -1,5 +1,10 @@ package me.kavin.piped.utils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import me.kavin.piped.consts.Constants; import me.kavin.piped.utils.resp.InvalidRequestResponse; import me.kavin.piped.utils.resp.SimpleErrorMessage; @@ -7,6 +12,9 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; import static me.kavin.piped.consts.Constants.mapper; @@ -46,4 +54,46 @@ public class SponsorBlockUtils { return null; } + public static CompletableFuture getDeArrowedInfo(List videoIds) { + ObjectNode objectNode = mapper.createObjectNode(); + + var futures = videoIds.stream() + .map(id -> getDeArrowedInfo(id).thenAcceptAsync(jsonNode -> objectNode.set(id, jsonNode.orElse(NullNode.getInstance())))) + .toArray(CompletableFuture[]::new); + + return CompletableFuture.allOf(futures) + .thenApplyAsync(v -> objectNode, Multithreading.getCachedExecutor()); + } + + private static CompletableFuture> getDeArrowedInfo(String videoId) { + + String hash = DigestUtils.sha256Hex(videoId); + + CompletableFuture> future = new CompletableFuture<>(); + + Multithreading.runAsync(() -> { + for (String url : Constants.SPONSORBLOCK_SERVERS) + try { + Optional optional = RequestUtils.sendGetJson(url + "/api/branding/" + URLUtils.silentEncode(hash.substring(0, 4))) + .thenApplyAsync(json -> json.has(videoId) ? Optional.of(json.get(videoId)) : Optional.empty()) + .get(); + + optional.ifPresent(jsonNode -> { + ArrayNode nodes = (ArrayNode) jsonNode.get("thumbnails"); + for (JsonNode node : nodes) { + ((ObjectNode) node).set("thumbnail", new TextNode(URLUtils.rewriteURL("https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=" + videoId + "&time=" + node.get("timestamp").asText()))); + } + }); + + + future.complete(optional); + return; + } catch (Exception ignored) { + } + future.completeExceptionally(new Exception("All SponsorBlock servers are down")); + }); + + return future; + + } }