diff --git a/integrations/java/app/revanced/integrations/patches/SpoofSignaturePatch.java b/integrations/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java similarity index 78% rename from integrations/java/app/revanced/integrations/patches/SpoofSignaturePatch.java rename to integrations/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java index ca523a4d4..09dbfd34a 100644 --- a/integrations/java/app/revanced/integrations/patches/SpoofSignaturePatch.java +++ b/integrations/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java @@ -1,12 +1,11 @@ -package app.revanced.integrations.patches; +package app.revanced.integrations.patches.spoof; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; +import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.utils.LogHelper; +import static app.revanced.integrations.patches.spoof.requests.StoryBoardRendererRequester.fetchStoryboardsRenderer; import static app.revanced.integrations.utils.ReVancedUtils.containsAny; /** @noinspection unused*/ @@ -40,6 +39,8 @@ public class SpoofSignaturePatch { private static boolean isPlayingShorts; + private static String storyboardRendererSpec = ""; + /** * Injection point. * @@ -63,6 +64,7 @@ public class SpoofSignaturePatch { // This will cause playback issues in the feed, but it's better than manipulating the history. parameters; + fetchStoryboardsRenderer(VideoInformation.getVideoId()); return INCOGNITO_PARAMETERS; } @@ -75,16 +77,15 @@ public class SpoofSignaturePatch { /** * Injection point. - * - * @param view seekbar thumbnail view. Includes both shorts and regular videos. */ - public static void seekbarImageViewCreated(ImageView view) { - if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return; - if (isPlayingShorts) return; + public static String getStoryboardRendererSpec() { + return storyboardRendererSpec; + } - view.setVisibility(View.GONE); - // Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible). - ViewGroup parentLayout = (ViewGroup) view.getParent(); - parentLayout.setPadding(0, 0, 0, 0); + public static void setStoryboardRendererSpec(String newlyLoadedStoryboardRendererSpec) { + if (storyboardRendererSpec.equals(newlyLoadedStoryboardRendererSpec)) + return; + + storyboardRendererSpec = newlyLoadedStoryboardRendererSpec; } } diff --git a/integrations/java/app/revanced/integrations/patches/spoof/requests/StoryBoardRendererRequester.java b/integrations/java/app/revanced/integrations/patches/spoof/requests/StoryBoardRendererRequester.java new file mode 100644 index 000000000..44f10f5e1 --- /dev/null +++ b/integrations/java/app/revanced/integrations/patches/spoof/requests/StoryBoardRendererRequester.java @@ -0,0 +1,79 @@ +package app.revanced.integrations.patches.spoof.requests; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.integrations.patches.spoof.SpoofSignaturePatch; +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; + +public class StoryBoardRendererRequester { + private static final String INNER_TUBE_BODY = + "{" + + "\"context\": " + + "{" + + "\"client\": " + + "{ " + + "\"clientName\": \"ANDROID\", \"clientVersion\": \"18.37.36\", \"platform\": \"MOBILE\", " + + "\"osName\": \"Android\", \"osVersion\": \"12\", \"androidSdkVersion\": 31 " + + "} " + + "}, " + + "\"videoId\": \"%s\"" + + "}"; + + private StoryBoardRendererRequester() { + } + + // TODO: Find a way to increase the quality of SeekBar thumbnail previews + public static void fetchStoryboardsRenderer(@NonNull String videoId) { + ReVancedUtils.verifyOffMainThread(); + + try { + final byte[] innerTubeBody = String.format(INNER_TUBE_BODY, videoId).getBytes(StandardCharsets.UTF_8); + + HttpURLConnection connection = StoryBoardRendererRoutes.getPlayerResponseConnectionFromRoute(); + connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); + + final int responseCode = connection.getResponseCode(); + + if (responseCode == 200) { + final JSONObject playerResponse = Requester.parseJSONObject(connection); + + final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); + final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer") + ? "playerLiveStoryboardSpecRenderer" + : "playerStoryboardSpecRenderer"; + final JSONObject storyboardsRenderer = storyboards.getJSONObject(storyboardsRendererTag); + final String storyboardsRendererSpec = storyboardsRenderer.getString("spec"); + + SpoofSignaturePatch.setStoryboardRendererSpec(storyboardsRendererSpec); + LogHelper.printDebug(() -> "StoryBoard renderer spec: " + storyboardsRendererSpec); + + } else { + handleConnectionError("API not available: " + responseCode, null); + } + connection.disconnect(); + } catch (SocketTimeoutException ex) { + handleConnectionError("API timed out", ex); + } catch (IOException ex) { + handleConnectionError(String.format("Failed to fetch StoryBoard URL (%s)", ex.getMessage()), ex); + } catch (Exception ex) { + handleConnectionError("Failed to fetch StoryBoard URL", ex); + } + } + + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (ex != null) + LogHelper.printException(() -> toastMessage, ex); + else + LogHelper.printException(() -> toastMessage); + + SpoofSignaturePatch.setStoryboardRendererSpec(""); + } +} \ No newline at end of file diff --git a/integrations/java/app/revanced/integrations/patches/spoof/requests/StoryBoardRendererRoutes.java b/integrations/java/app/revanced/integrations/patches/spoof/requests/StoryBoardRendererRoutes.java new file mode 100644 index 000000000..f5fb11b6c --- /dev/null +++ b/integrations/java/app/revanced/integrations/patches/spoof/requests/StoryBoardRendererRoutes.java @@ -0,0 +1,34 @@ +package app.revanced.integrations.patches.spoof.requests; + +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.requests.Route; + +import java.io.IOException; +import java.net.HttpURLConnection; + +/** @noinspection unused*/ +public final class StoryBoardRendererRoutes { + private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/"; + private static final String YT_API_KEY = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w"; + + static final Route GET_PLAYER_RESPONSE_BODY = new Route(Route.Method.POST, "player?key={api_key}"); + + private StoryBoardRendererRoutes() { + } + + public static HttpURLConnection getPlayerResponseConnectionFromRoute() throws IOException { + var connection = Requester.getConnectionFromRoute(YT_API_URL, GET_PLAYER_RESPONSE_BODY, YT_API_KEY); + connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip"); + connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept-Language", "en-GB, en;q=0.9"); + connection.setRequestProperty("Pragma", "no-cache"); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setUseCaches(false); + connection.setDoOutput(true); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + return connection; + } + +} \ No newline at end of file