storyboardRendererFuture;
-
- /**
- * Injection point.
- * Blocks /get_watch requests by returning a localhost URI.
- *
- * @param playerRequestUri The URI of the player request.
- * @return Localhost URI if the request is a /get_watch request, otherwise the original URI.
- */
- public static Uri blockGetWatchRequest(Uri playerRequestUri) {
- if (SPOOF_CLIENT_ENABLED) {
- try {
- String path = playerRequestUri.getPath();
-
- if (path != null && path.contains("get_watch")) {
- Logger.printDebug(() -> "Blocking: " + playerRequestUri + " by returning: " + UNREACHABLE_HOST_URI_STRING);
-
- return UNREACHABLE_HOST_URI;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "blockGetWatchRequest failure", ex);
- }
- }
-
- return playerRequestUri;
- }
-
- /**
- * Injection point.
- *
- * Blocks /initplayback requests.
- */
- public static String blockInitPlaybackRequest(String originalUrlString) {
- if (SPOOF_CLIENT_ENABLED) {
- try {
- Uri originalUri = Uri.parse(originalUrlString);
- String path = originalUri.getPath();
-
- if (path != null && path.contains("initplayback")) {
- String replacementUriString = (getSpoofClientType() != ClientType.ANDROID_TESTSUITE)
- ? UNREACHABLE_HOST_URI_STRING
- // TODO: Ideally, a local proxy could be setup and block
- // the request the same way as Burp Suite is capable of
- // because that way the request is never sent to YouTube unnecessarily.
- // Just using localhost unfortunately does not work.
- : originalUri.buildUpon().clearQuery().build().toString();
-
- Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning: " + replacementUriString);
-
- return replacementUriString;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
- }
- }
-
- return originalUrlString;
- }
-
- private static ClientType getSpoofClientType() {
- if (isShortsOrClips) {
- lastSpoofedClientType = Settings.SPOOF_CLIENT_SHORTS.get();
- return lastSpoofedClientType;
- }
- LiveStreamRenderer renderer = getLiveStreamRenderer(false);
- if (renderer != null) {
- if (renderer.isLiveStream) {
- lastSpoofedClientType = Settings.SPOOF_CLIENT_LIVESTREAM.get();
- return lastSpoofedClientType;
- }
- if (!renderer.playabilityOk) {
- lastSpoofedClientType = Settings.SPOOF_CLIENT_FALLBACK.get();
- return lastSpoofedClientType;
- }
- }
- lastSpoofedClientType = Settings.SPOOF_CLIENT_GENERAL.get();
- return lastSpoofedClientType;
- }
-
- /**
- * Injection point.
- */
- public static int getClientTypeId(int originalClientTypeId) {
- if (SPOOF_CLIENT_ENABLED) {
- return getSpoofClientType().id;
- }
-
- return originalClientTypeId;
- }
-
- /**
- * Injection point.
- */
- public static String getClientVersion(String originalClientVersion) {
- if (SPOOF_CLIENT_ENABLED) {
- return getSpoofClientType().appVersion;
- }
-
- return originalClientVersion;
- }
-
- /**
- * Injection point.
- */
- public static String getClientModel(String originalClientModel) {
- if (SPOOF_CLIENT_ENABLED) {
- return getSpoofClientType().model;
- }
-
- return originalClientModel;
- }
-
- /**
- * Injection point.
- */
- public static String getOsVersion(String originalOsVersion) {
- if (SPOOF_CLIENT_ENABLED) {
- return getSpoofClientType().osVersion;
- }
-
- return originalOsVersion;
- }
-
- /**
- * Injection point.
- */
- public static String getUserAgent(String originalUserAgent) {
- if (SPOOF_CLIENT_ENABLED) {
- ClientType clientType = getSpoofClientType();
- if (clientType == ClientType.IOS) {
- Logger.printDebug(() -> "Replaced: '" + originalUserAgent + "' with: '"
- + clientType.userAgent + "'");
- return clientType.userAgent;
- }
- }
-
- return originalUserAgent;
- }
-
- /**
- * Injection point.
- */
- public static boolean isClientSpoofingEnabled() {
- return SPOOF_CLIENT_ENABLED;
- }
-
- /**
- * Injection point.
- */
- public static boolean enablePlayerGesture(boolean original) {
- return SPOOF_CLIENT_ENABLED || original;
- }
-
- /**
- * Injection point.
- * When spoofing the client to iOS or Android Testsuite the playback speed menu is missing from the player response.
- * Return true to force create the playback speed menu.
- */
- public static boolean forceCreatePlaybackSpeedMenu(boolean original) {
- if (SPOOF_CLIENT_ENABLED) {
- return true;
- }
-
- return original;
- }
-
- /**
- * Injection point.
- * When spoofing the client to iOS, background audio only playback of livestreams fails.
- * Return true to force enable audio background play.
- */
- public static boolean overrideBackgroundAudioPlayback() {
- return SPOOF_CLIENT_ENABLED &&
- BackgroundPlaybackPatch.playbackIsNotShort();
- }
-
- /**
- * Injection point.
- * When spoofing the client to Android TV the playback speed menu is missing from the player response.
- * Return false to force create the playback speed menu.
- */
- public static boolean forceCreatePlaybackSpeedMenuReversed(boolean original) {
- if (SPOOF_CLIENT_ENABLED) {
- return false;
- }
-
- return original;
- }
-
- private static final Uri VIDEO_STATS_PLAYBACK_URI = Uri.parse("https://www.youtube.com/api/stats/playback?ns=yt&ver=2&final=1");
-
- private static final String PARAM_DOC_ID = "docid";
- private static final String PARAM_LEN = "len";
- private static final String PARAM_CPN = "cpn";
-
- private static final String PARAM_EVENT_ID = "ei";
- private static final String PARAM_VM = "vm";
- private static final String PARAM_OF = "of";
-
- private static String mDocId;
- private static String mLen;
- private static String mCpn;
- private static String mEventId;
- private static String mVisitorMonitoringData;
- private static String mOfParam;
-
- /**
- * Injection point.
- */
- public static void setCpn(String cpn) {
- if (SPOOF_CLIENT_ENABLED && !Objects.equals(mCpn, cpn)) {
- mCpn = cpn;
- }
- }
-
- /**
- * Injection point.
- *
- * Parse parameters from the Tracking URL.
- * See yuliskov/MediaServiceCore.
- */
- public static void setTrackingUriParameter(Uri trackingUri) {
- try {
- if (SPOOF_CLIENT_ENABLED) {
- String path = trackingUri.getPath();
-
- if (path == null || (!path.contains("playback") && !path.contains("watchtime"))) {
- return;
- }
-
- mDocId = getQueryParameter(trackingUri, PARAM_DOC_ID);
- mLen = getQueryParameter(trackingUri, PARAM_LEN);
- mEventId = getQueryParameter(trackingUri, PARAM_EVENT_ID);
- mVisitorMonitoringData = getQueryParameter(trackingUri, PARAM_VM);
- mOfParam = getQueryParameter(trackingUri, PARAM_OF);
-
- Logger.printDebug(() -> "docId: " + mDocId + ", len: " + mLen + ", eventId: " + mEventId + ", visitorMonitoringData: " + mVisitorMonitoringData + ", of: " + mOfParam);
- }
- } catch (Exception ex) {
- Logger.printException(() -> "setTrackingUriParameter failure", ex);
- }
- }
-
- /**
- * Injection point.
- * This only works on YouTube 18.38.45 or earlier.
- *
- * Build a Tracking URL.
- * This does not include the last watched time.
- * See yuliskov/MediaServiceCore.
- */
- public static Uri overrideTrackingUrl(Uri trackingUrl) {
- try {
- if (SPOOF_CLIENT_ENABLED &&
- getSpoofClientType() == ClientType.IOS &&
- trackingUrl.toString().contains("youtube.com/csi") &&
- !StringUtils.isAnyEmpty(mDocId, mLen, mCpn, mEventId, mVisitorMonitoringData, mOfParam)
- ) {
- final Uri videoStatsPlaybackUri = VIDEO_STATS_PLAYBACK_URI
- .buildUpon()
- .appendQueryParameter(PARAM_DOC_ID, mDocId)
- .appendQueryParameter(PARAM_LEN, mLen)
- .appendQueryParameter(PARAM_CPN, mCpn)
- .appendQueryParameter(PARAM_EVENT_ID, mEventId)
- .appendQueryParameter(PARAM_VM, mVisitorMonitoringData)
- .appendQueryParameter(PARAM_OF, mOfParam)
- .build();
-
- Logger.printDebug(() -> "Replaced: '" + trackingUrl + "' with: '" + videoStatsPlaybackUri + "'");
- return videoStatsPlaybackUri;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "overrideTrackingUrl failure", ex);
- }
-
- return trackingUrl;
- }
-
- private static String getQueryParameter(Uri uri, String key) {
- List queryParams = uri.getQueryParameters(key);
- if (queryParams == null || queryParams.isEmpty()) {
- return "";
- } else {
- return queryParams.get(0);
- }
- }
-
- /**
- * Injection point.
- */
- public static String appendSpoofedClient(String videoFormat) {
- try {
- if (SPOOF_CLIENT_ENABLED && Settings.SPOOF_CLIENT_STATS_FOR_NERDS.get()
- && !TextUtils.isEmpty(videoFormat)) {
- // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
- return "\u202D" + videoFormat + String.format("\u2009(%s)", lastSpoofedClientType.friendlyName); // u202D = left to right override
- }
- } catch (Exception ex) {
- Logger.printException(() -> "appendSpoofedClient failure", ex);
- }
-
- return videoFormat;
- }
-
- /**
- * Injection point.
- */
- public static String setPlayerResponseVideoId(@NonNull String videoId, @Nullable String parameters, boolean isShortAndOpeningOrPlaying) {
- if (SPOOF_CLIENT_ENABLED) {
- isShortsOrClips = playerParameterIsClipsOrShorts(parameters, isShortAndOpeningOrPlaying);
-
- if (!isShortsOrClips) {
- fetchPlayerResponseRenderer(videoId, Settings.SPOOF_CLIENT_GENERAL.get());
- }
- }
-
- return parameters; // Return the original value since we are observing and not modifying.
- }
-
- /**
- * @return If the player parameters are for a Short or Clips.
- */
- private static boolean playerParameterIsClipsOrShorts(@Nullable String playerParameter, boolean isShortAndOpeningOrPlaying) {
- if (isShortAndOpeningOrPlaying) {
- return true;
- }
-
- return playerParameter != null && StringUtils.startsWithAny(playerParameter, CLIPS_OR_SHORTS_PARAMETERS);
- }
-
- private static void fetchPlayerResponseRenderer(@NonNull String videoId, @NonNull ClientType clientType) {
- if (!videoId.equals(lastPlayerResponseVideoId)) {
- lastPlayerResponseVideoId = videoId;
- liveStreamRendererFuture = submitOnBackgroundThread(() -> LiveStreamRendererRequester.getLiveStreamRenderer(videoId, clientType));
- storyboardRendererFuture = submitOnBackgroundThread(() -> StoryboardRendererRequester.getStoryboardRenderer(videoId));
- }
- // Block until the renderer fetch completes.
- // This is desired because if this returns without finishing the fetch
- // then video will start playback but the storyboard is not ready yet.
- getLiveStreamRenderer(true);
- }
-
- @Nullable
- private static LiveStreamRenderer getLiveStreamRenderer(boolean waitForCompletion) {
- Future future = liveStreamRendererFuture;
- if (future != null) {
- try {
- if (waitForCompletion || future.isDone()) {
- return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
- } // else, return null.
- } catch (TimeoutException ex) {
- Logger.printDebug(() -> "Could not get renderer (get timed out)");
- } catch (ExecutionException | InterruptedException ex) {
- // Should never happen.
- Logger.printException(() -> "Could not get renderer", ex);
- }
- }
- return null;
- }
-
- @Nullable
- private static StoryboardRenderer getStoryboardRenderer(boolean waitForCompletion) {
- Future future = storyboardRendererFuture;
- if (future != null) {
- try {
- if (waitForCompletion || future.isDone()) {
- return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
- } // else, return null.
- } catch (TimeoutException ex) {
- Logger.printDebug(() -> "Could not get renderer (get timed out)");
- } catch (ExecutionException | InterruptedException ex) {
- // Should never happen.
- Logger.printException(() -> "Could not get renderer", ex);
- }
- }
- return null;
- }
-
- private static boolean useFetchedStoryboardRenderer() {
- if (!SPOOF_CLIENT_ENABLED || isShortsOrClips) {
- return false;
- }
-
- // No seekbar thumbnail or low quality seekbar thumbnail.
- final ClientType clientType = getSpoofClientType();
- return clientType == ClientType.ANDROID_TESTSUITE || clientType == ClientType.ANDROID_UNPLUGGED;
- }
-
- private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
- boolean returnNullIfLiveStream) {
- if (useFetchedStoryboardRenderer()) {
- final StoryboardRenderer renderer = getStoryboardRenderer(false);
- if (renderer != null) {
- if (returnNullIfLiveStream && renderer.isLiveStream) {
- return null;
- }
- String spec = renderer.spec;
- if (spec != null) {
- return spec;
- }
- }
- }
-
- return originalStoryboardRendererSpec;
- }
-
- /**
- * Injection point.
- * Called from background threads and from the main thread.
- */
- @Nullable
- public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
- return getStoryboardRendererSpec(originalStoryboardRendererSpec, false);
- }
-
- /**
- * Injection point.
- * Uses additional check to handle live streams.
- * Called from background threads and from the main thread.
- */
- @Nullable
- public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) {
- return getStoryboardRendererSpec(originalStoryboardRendererSpec, true);
- }
-
- /**
- * Injection point.
- */
- public static int getStoryboardRecommendedLevel(int originalLevel) {
- if (useFetchedStoryboardRenderer()) {
- final StoryboardRenderer renderer = getStoryboardRenderer(false);
- if (renderer != null) {
- Integer recommendedLevel = renderer.recommendedLevel;
- if (recommendedLevel != null) return recommendedLevel;
- }
- }
-
- return originalLevel;
- }
-
- /**
- * Injection point. Forces seekbar to be shown for paid videos or
- * if {@link Settings#SPOOF_PLAYER_PARAMETER} is not enabled.
- */
- public static boolean getSeekbarThumbnailOverrideValue() {
- if (!useFetchedStoryboardRenderer()) {
- return false;
- }
- final StoryboardRenderer renderer = getStoryboardRenderer(false);
- if (renderer == null) {
- // Spoof storyboard renderer is turned off,
- // video is paid, or the storyboard fetch timed out.
- // Show empty thumbnails so the seek time and chapters still show up.
- return true;
- }
- return renderer.spec != null;
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java
deleted file mode 100644
index 153ac4efb1..0000000000
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java
+++ /dev/null
@@ -1,220 +0,0 @@
-package app.revanced.integrations.youtube.patches.misc;
-
-import static app.revanced.integrations.shared.utils.Utils.containsAny;
-import static app.revanced.integrations.shared.utils.Utils.submitOnBackgroundThread;
-import static app.revanced.integrations.youtube.patches.misc.requests.StoryboardRendererRequester.getStoryboardRenderer;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import app.revanced.integrations.shared.utils.Logger;
-import app.revanced.integrations.youtube.settings.Settings;
-import app.revanced.integrations.youtube.shared.PlayerType;
-import app.revanced.integrations.youtube.shared.VideoInformation;
-
-/**
- * @noinspection ALL
- *
- * Even if user spoof any player parameters with the client name "ANDROID", if a valid DroidGuard result is not sent,
- * user always receive a response with video id 'aQvGIIdgFDM' (the following content is not available on this app).
- * YouTube.js#623
- * Therefore, this patch is no longer valid.
- *
- * Currently, the only client name available on Android without DroidGuard results is "ANDROID_TESTSUITE".
- * invidious#4650
- */
-@Deprecated
-public class SpoofPlayerParameterPatch {
- private static final boolean spoofParameter = Settings.SPOOF_PLAYER_PARAMETER.get();
- private static final boolean spoofParameterInFeed = Settings.SPOOF_PLAYER_PARAMETER_IN_FEED.get();
-
- /**
- * Parameter (also used by
- * YouTube.js)
- * to fix playback issues.
- */
- private static final String INCOGNITO_PARAMETERS = "CgIIAQ%3D%3D";
-
- /**
- * Parameters used when playing clips.
- */
- private static final String CLIPS_PARAMETERS = "kAIB";
-
- /**
- * Parameters causing playback issues.
- */
- private static final String[] AUTOPLAY_PARAMETERS = {
- "YAHI", // Autoplay in feed.
- "SAFg" // Autoplay in scrim.
- };
-
- /**
- * Parameter used for autoplay in scrim.
- * Prepend this parameter to mute video playback (for autoplay in feed).
- */
- private static final String SCRIM_PARAMETER = "SAFgAXgB";
-
- /**
- * Last video id loaded. Used to prevent reloading the same spec multiple times.
- */
- @Nullable
- private static volatile String lastPlayerResponseVideoId;
-
- @Nullable
- private static volatile Future rendererFuture;
-
- private static volatile boolean useOriginalStoryboardRenderer;
-
- @Nullable
- private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
- Future future = rendererFuture;
- if (future != null) {
- try {
- if (waitForCompletion || future.isDone()) {
- return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
- } // else, return null.
- } catch (TimeoutException ex) {
- Logger.printDebug(() -> "Could not get renderer (get timed out)");
- } catch (ExecutionException | InterruptedException ex) {
- // Should never happen.
- Logger.printException(() -> "Could not get renderer", ex);
- }
- }
- return null;
- }
-
- /**
- * Injection point.
- *
- * {@link VideoInformation#getVideoId()} cannot be used because it is injected after PlayerResponse.
- * Therefore, we use the videoId called from PlaybackStartDescriptor.
- *
- * @param videoId Original video id value.
- * @param parameters Original player parameter value.
- */
- public static String spoofParameter(@NonNull String videoId, @Nullable String parameters, boolean isShortAndOpeningOrPlaying) {
- try {
- Logger.printDebug(() -> "Original player parameter value: " + parameters);
-
- if (!spoofParameter) {
- return parameters;
- }
-
- // Shorts do not need to be spoofed.
- if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
- return parameters;
- }
-
- // Clip's player parameters contain important information such as where the video starts, where it ends, and whether it loops.
- // Clips are 60 seconds or less in length, so no spoofing.
- if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) {
- return parameters;
- }
-
- final boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
- && containsAny(parameters, AUTOPLAY_PARAMETERS);
- if (isPlayingFeed) {
- //noinspection AssignmentUsedAsCondition
- if (useOriginalStoryboardRenderer = !spoofParameterInFeed) {
- // Don't spoof the feed video playback. This will cause video playback issues,
- // but only if user continues watching for more than 1 minute.
- return parameters;
- }
- // Spoof the feed video. Video will show up in watch history and video subtitles are missing.
- fetchStoryboardRenderer(videoId);
- return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
- }
-
- fetchStoryboardRenderer(videoId);
- } catch (Exception ex) {
- Logger.printException(() -> "spoofParameter failure", ex);
- }
- return INCOGNITO_PARAMETERS;
- }
-
- private static void fetchStoryboardRenderer(@NonNull String videoId) {
- if (!videoId.equals(lastPlayerResponseVideoId)) {
- rendererFuture = submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
- lastPlayerResponseVideoId = videoId;
- }
- // Block until the renderer fetch completes.
- // This is desired because if this returns without finishing the fetch
- // then video will start playback but the storyboard is not ready yet.
- getRenderer(true);
- }
-
- private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
- boolean returnNullIfLiveStream) {
- if (spoofParameter && !useOriginalStoryboardRenderer) {
- final StoryboardRenderer renderer = getRenderer(false);
- if (renderer != null) {
- if (returnNullIfLiveStream && renderer.isLiveStream) {
- return null;
- }
- String spec = renderer.spec;
- if (spec != null) {
- return spec;
- }
- }
- }
-
- return originalStoryboardRendererSpec;
- }
-
- /**
- * Injection point.
- * Called from background threads and from the main thread.
- */
- @Nullable
- public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
- return getStoryboardRendererSpec(originalStoryboardRendererSpec, false);
- }
-
- /**
- * Injection point.
- * Uses additional check to handle live streams.
- * Called from background threads and from the main thread.
- */
- @Nullable
- public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) {
- return getStoryboardRendererSpec(originalStoryboardRendererSpec, true);
- }
-
- /**
- * Injection point.
- */
- public static int getStoryboardRecommendedLevel(int originalLevel) {
- if (spoofParameter && !useOriginalStoryboardRenderer) {
- final StoryboardRenderer renderer = getRenderer(false);
- if (renderer != null) {
- Integer recommendedLevel = renderer.recommendedLevel;
- if (recommendedLevel != null) return recommendedLevel;
- }
- }
-
- return originalLevel;
- }
-
- /**
- * Injection point. Forces seekbar to be shown for paid videos or
- * if {@link Settings#SPOOF_PLAYER_PARAMETER} is not enabled.
- */
- public static boolean getSeekbarThumbnailOverrideValue() {
- if (!spoofParameter) {
- return false;
- }
- final StoryboardRenderer renderer = getRenderer(false);
- if (renderer == null) {
- // Spoof storyboard renderer is turned off,
- // video is paid, or the storyboard fetch timed out.
- // Show empty thumbnails so the seek time and chapters still show up.
- return true;
- }
- return renderer.spec != null;
- }
-}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java
deleted file mode 100644
index e5b626312c..0000000000
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package app.revanced.integrations.youtube.patches.misc;
-
-import androidx.annotation.Nullable;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * @noinspection ALL
- */
-public final class StoryboardRenderer {
- public final String videoId;
- @Nullable
- public final String spec;
- public final boolean isLiveStream;
- /**
- * Recommended image quality level, or NULL if no recommendation exists.
- */
- @Nullable
- public final Integer recommendedLevel;
-
- public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
- this.videoId = videoId;
- this.spec = spec;
- this.isLiveStream = isLiveStream;
- this.recommendedLevel = recommendedLevel;
- }
-
- @NotNull
- @Override
- public String toString() {
- return "StoryboardRenderer{" +
- "videoId=" + videoId +
- ", isLiveStream=" + isLiveStream +
- ", spec='" + spec + '\'' +
- ", recommendedLevel=" + recommendedLevel +
- '}';
- }
-}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java
deleted file mode 100644
index eba2749fdf..0000000000
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package app.revanced.integrations.youtube.patches.misc.requests;
-
-import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_LIVE_STREAM_RENDERER;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-import app.revanced.integrations.shared.requests.Requester;
-import app.revanced.integrations.shared.utils.Logger;
-import app.revanced.integrations.shared.utils.Utils;
-import app.revanced.integrations.youtube.patches.misc.LiveStreamRenderer;
-import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType;
-
-public class LiveStreamRendererRequester {
-
- private LiveStreamRendererRequester() {
- }
-
- private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
- Logger.printInfo(() -> toastMessage, ex);
- }
-
- @Nullable
- private static JSONObject fetchPlayerResponse(@NonNull String requestBody,
- @NonNull String userAgent) {
- final long startTime = System.currentTimeMillis();
- try {
- Utils.verifyOffMainThread();
- Objects.requireNonNull(requestBody);
-
- final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8);
-
- HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_LIVE_STREAM_RENDERER, userAgent);
- connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
-
- final int responseCode = connection.getResponseCode();
- if (responseCode == 200) return Requester.parseJSONObject(connection);
-
- // Always show a toast for this, as a non 200 response means something is broken.
- handleConnectionError("Fetch livestreams not available: " + responseCode, null);
- connection.disconnect();
- } catch (SocketTimeoutException ex) {
- handleConnectionError("Fetch livestreams temporarily not available (API timed out)", ex);
- } catch (IOException ex) {
- handleConnectionError("Fetch livestreams temporarily not available: " + ex.getMessage(), ex);
- } catch (Exception ex) {
- Logger.printException(() -> "Fetch livestreams failed", ex); // Should never happen.
- } finally {
- Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
- }
-
- return null;
- }
-
- private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) {
- try {
- return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK");
- } catch (JSONException e) {
- Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse);
- }
-
- return false;
- }
-
- private static boolean isLive(@NonNull JSONObject playerResponse) {
- try {
- return playerResponse.getJSONObject("videoDetails").getBoolean("isLive");
- } catch (JSONException e) {
- Logger.printDebug(() -> "Failed to get videoDetails for response: " + playerResponse);
- }
-
- return false;
- }
-
- private static boolean isLiveContent(@NonNull JSONObject playerResponse) {
- try {
- return playerResponse.getJSONObject("videoDetails").getBoolean("isLiveContent");
- } catch (JSONException e) {
- Logger.printDebug(() -> "Failed to get videoDetails for response: " + playerResponse);
- }
-
- return false;
- }
-
- /**
- * Fetches the liveStreamRenderer from the innerTubeBody.
- *
- * @return LiveStreamRenderer or null if playabilityStatus is not OK.
- */
- @Nullable
- private static LiveStreamRenderer getLiveStreamRendererUsingBody(@NonNull String videoId,
- @NonNull ClientType clientType) {
- final JSONObject playerResponse = fetchPlayerResponse(
- String.format(clientType.innerTubeBody, videoId),
- clientType.userAgent
- );
- if (playerResponse != null)
- return getLiveStreamRendererUsingResponse(videoId, playerResponse, clientType);
-
- return null;
- }
-
- @Nullable
- private static LiveStreamRenderer getLiveStreamRendererUsingResponse(@NonNull String videoId,
- @NonNull JSONObject playerResponse,
- @NonNull ClientType clientType) {
- try {
- Logger.printDebug(() -> "Parsing liveStreamRenderer from response: " + playerResponse);
-
- final String clientName = clientType.name();
- final boolean isPlayabilityOk = isPlayabilityStatusOk(playerResponse);
- final boolean isLiveStream = isLive(playerResponse) || isLiveContent(playerResponse);
-
- LiveStreamRenderer renderer = new LiveStreamRenderer(
- videoId,
- clientName,
- isPlayabilityOk,
- isLiveStream
- );
- Logger.printDebug(() -> "Fetched: " + renderer);
-
- return renderer;
- } catch (Exception e) {
- Logger.printException(() -> "Failed to get liveStreamRenderer", e);
- }
-
- return null;
- }
-
- @Nullable
- public static LiveStreamRenderer getLiveStreamRenderer(@NonNull String videoId, @NonNull ClientType clientType) {
- Objects.requireNonNull(videoId);
-
- LiveStreamRenderer renderer = getLiveStreamRendererUsingBody(videoId, clientType);
- if (renderer == null) {
- String finalClientName1 = clientType.name();
- Logger.printDebug(() -> videoId + " not available using " + finalClientName1 + " client");
-
- clientType = ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER;
- renderer = getLiveStreamRendererUsingBody(videoId, clientType);
- if (renderer == null) {
- String finalClientName2 = clientType.name();
- Logger.printDebug(() -> videoId + " not available using " + finalClientName2 + " client");
- }
- }
-
- return renderer;
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
deleted file mode 100644
index 2f20413c4b..0000000000
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
+++ /dev/null
@@ -1,486 +0,0 @@
-package app.revanced.integrations.youtube.patches.misc.requests;
-
-import static app.revanced.integrations.shared.utils.StringRef.str;
-
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
-import android.os.Build;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-
-import app.revanced.integrations.shared.requests.Requester;
-import app.revanced.integrations.shared.requests.Route;
-import app.revanced.integrations.shared.settings.Setting;
-import app.revanced.integrations.shared.utils.Logger;
-import app.revanced.integrations.shared.utils.PackageUtils;
-import app.revanced.integrations.youtube.settings.Settings;
-
-public final class PlayerRoutes {
- public static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
- Route.Method.POST,
- "player" +
- "?fields=storyboards.playerStoryboardSpecRenderer," +
- "storyboards.playerLiveStoryboardSpecRenderer," +
- "playabilityStatus.status," +
- "playabilityStatus.errorScreen"
- ).compile();
-
- public static final Route.CompiledRoute GET_LIVE_STREAM_RENDERER = new Route(
- Route.Method.POST,
- "player" +
- "?fields=playabilityStatus.status," +
- "videoDetails.isLive," +
- "videoDetails.isLiveContent"
- ).compile();
-
-
- private static final String ANDROID_CLIENT_VERSION = PackageUtils.getVersionName();
- private static final String ANDROID_DEVICE_MODEL = Build.MODEL;
- private static final String ANDROID_OS_RELEASE_VERSION = Build.VERSION.RELEASE;
- private static final int ANDROID_OS_SDK_VERSION = Build.VERSION.SDK_INT;
- private static final String ANDROID_USER_AGENT = "com.google.android.youtube/" +
- ANDROID_CLIENT_VERSION +
- " (Linux; U; Android " +
- ANDROID_OS_RELEASE_VERSION +
- "; GB) gzip";
-
- private static final String ANDROID_TESTSUITE_CLIENT_VERSION = "1.9";
-
-
- private static final String ANDROID_UNPLUGGED_CLIENT_VERSION = "8.31.0";
- /**
- * The device machine id for the Chromecast with Google TV 4K.
- *
- *
- * See this GitLab for more
- * information.
- *
- */
- private static final String ANDROID_UNPLUGGED_DEVICE_MODEL = "Chromecast";
- private static final String ANDROID_UNPLUGGED_OS_RELEASE_VERSION = "12";
- private static final int ANDROID_UNPLUGGED_OS_SDK_VERSION = 31;
- private static final String ANDROID_UNPLUGGED_USER_AGENT = "com.google.android.apps.youtube.unplugged/" +
- ANDROID_UNPLUGGED_CLIENT_VERSION +
- " (Linux; U; Android " +
- ANDROID_UNPLUGGED_OS_RELEASE_VERSION +
- "; GB) gzip";
-
-
- /**
- * The hardcoded client version of the Android VR app used for InnerTube requests with this client.
- *
- *
- * It can be extracted by getting the latest release version of the app on
- * the App
- * Store page of the YouTube app, in the {@code Additional details} section.
- *
- */
- private static final String ANDROID_VR_CLIENT_VERSION = "1.58.14";
-
- /**
- * The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
- *
- *
- * See this GitLab for more
- * information.
- *
- */
- private static final String ANDROID_VR_DEVICE_MODEL = "Quest 3";
-
- private static final String ANDROID_VR_OS_RELEASE_VERSION = "12";
- /**
- * The SDK version for Android 12 is 31,
- * but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
- */
- private static final int ANDROID_VR_OS_SDK_VERSION = 32;
-
- /**
- * Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
- * Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
- * Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
- */
- private static final String ANDROID_VR_USER_AGENT = "com.google.android.apps.youtube.vr.oculus/" +
- ANDROID_VR_CLIENT_VERSION +
- " (Linux; U; Android 12; GB) gzip";
-
-
- /**
- * The hardcoded client version of the iOS app used for InnerTube requests with this client.
- *
- *
- * It can be extracted by getting the latest release version of the app on
- * the App
- * Store page of the YouTube app, in the {@code What’s New} section.
- *
- */
- private static final String IOS_CLIENT_VERSION = "19.16.3";
- /**
- * The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps.
- * The device machine id for the iPhone 15 Pro Max (iPhone16,2), used to get HDR with AV1 hardware decoding.
- *
- *
- * See this GitHub Gist for more
- * information.
- *
- */
- private static final String IOS_DEVICE_MODEL = DeviceHardwareSupport.allowAV1()
- ? "iPhone16,2"
- : "iPhone11,4";
-
- /**
- * The minimum supported OS version for the iOS YouTube client is iOS 14.0.
- * Using an invalid OS version will use the AVC codec.
- */
- private static final String IOS_OS_VERSION = DeviceHardwareSupport.allowVP9()
- ? "17.6.1.21G101"
- : "13.7.17H35";
- private static final String IOS_USER_AGENT_VERSION = DeviceHardwareSupport.allowVP9()
- ? "17_6_1"
- : "13_7";
- private static final String IOS_USER_AGENT = "com.google.ios.youtube/" +
- IOS_CLIENT_VERSION +
- "(" +
- IOS_DEVICE_MODEL +
- "; U; CPU iOS " +
- IOS_USER_AGENT_VERSION +
- " like Mac OS X)";
-
- private static final String TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION = "2.0";
- private static final String TVHTML5_SIMPLY_EMBEDDED_PLAYER_USER_AGENT = "Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5)" +
- " AppleWebKit/537.36 (KHTML, like Gecko)" +
- " 85.0.4183.93/6.5 TV Safari/537.36";
- private static final String WEB_CLIENT_VERSION = "2.20240726.00.00";
- private static final String WEB_OS_VERSION = "10";
- private static final String WEB_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" +
- " AppleWebKit/537.36 (KHTML, like Gecko)" +
- " Chrome/124.0.0.0 Mobile Safari/537.36";
-
- private static final String ANDROID_INNER_TUBE_BODY;
- private static final String ANDROID_EMBED_INNER_TUBE_BODY;
- private static final String ANDROID_TESTSUITE_INNER_TUBE_BODY;
- private static final String ANDROID_UNPLUGGED_INNER_TUBE_BODY;
- private static final String ANDROID_VR_INNER_TUBE_BODY;
- private static final String IOS_INNER_TUBE_BODY;
- private static final String TVHTML5_SIMPLY_EMBED_INNER_TUBE_BODY;
- private static final String WEB_INNER_TUBE_BODY;
-
- private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
-
- /**
- * TCP connection and HTTP read timeout
- */
- private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
-
- static {
- JSONObject androidInnerTubeBody = new JSONObject();
- JSONObject androidEmbedInnerTubeBody = new JSONObject();
- JSONObject androidTestsuiteInnerTubeBody = new JSONObject();
- JSONObject androidUnpluggedInnerTubeBody = new JSONObject();
- JSONObject androidVRInnerTubeBody = new JSONObject();
- JSONObject iOSInnerTubeBody = new JSONObject();
- JSONObject tvEmbedInnerTubeBody = new JSONObject();
- JSONObject webInnerTubeBody = new JSONObject();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "ANDROID");
- client.put("clientVersion", ANDROID_CLIENT_VERSION);
- client.put("platform", "MOBILE");
- client.put("androidSdkVersion", ANDROID_OS_SDK_VERSION);
- client.put("osName", "Android");
- client.put("osVersion", ANDROID_OS_RELEASE_VERSION);
-
- context.put("client", client);
-
- androidInnerTubeBody.put("context", context);
- androidInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create Android innerTubeBody", e);
- }
-
- ANDROID_INNER_TUBE_BODY = androidInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "ANDROID_EMBEDDED_PLAYER");
- client.put("clientVersion", ANDROID_CLIENT_VERSION);
- client.put("clientScreen", "EMBED");
- client.put("platform", "MOBILE");
- client.put("androidSdkVersion", ANDROID_OS_SDK_VERSION);
- client.put("osName", "Android");
- client.put("osVersion", ANDROID_OS_RELEASE_VERSION);
-
- JSONObject thirdParty = new JSONObject();
- thirdParty.put("embedUrl", "https://www.youtube.com/embed/%s");
-
- context.put("thirdParty", thirdParty);
- context.put("client", client);
-
- androidEmbedInnerTubeBody.put("context", context);
- androidEmbedInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create Android Embed innerTubeBody", e);
- }
-
- ANDROID_EMBED_INNER_TUBE_BODY = androidEmbedInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "ANDROID_TESTSUITE");
- client.put("clientVersion", ANDROID_TESTSUITE_CLIENT_VERSION);
- client.put("platform", "MOBILE");
- client.put("androidSdkVersion", ANDROID_OS_SDK_VERSION);
- client.put("osName", "Android");
- client.put("osVersion", ANDROID_OS_RELEASE_VERSION);
-
- context.put("client", client);
-
- androidTestsuiteInnerTubeBody.put("context", context);
- androidTestsuiteInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create Android Testsuite innerTubeBody", e);
- }
-
- ANDROID_TESTSUITE_INNER_TUBE_BODY = androidTestsuiteInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "ANDROID_UNPLUGGED");
- client.put("clientVersion", ANDROID_UNPLUGGED_CLIENT_VERSION);
- client.put("platform", "MOBILE");
- client.put("androidSdkVersion", ANDROID_UNPLUGGED_OS_SDK_VERSION);
- client.put("osName", "Android");
- client.put("osVersion", ANDROID_UNPLUGGED_OS_RELEASE_VERSION);
-
- context.put("client", client);
-
- androidUnpluggedInnerTubeBody.put("context", context);
- androidUnpluggedInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create Android Unplugged innerTubeBody", e);
- }
-
- ANDROID_UNPLUGGED_INNER_TUBE_BODY = androidUnpluggedInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "ANDROID_VR");
- client.put("clientVersion", ANDROID_VR_CLIENT_VERSION);
- client.put("platform", "MOBILE");
- client.put("androidSdkVersion", ANDROID_VR_OS_SDK_VERSION);
- client.put("osName", "Android");
- client.put("osVersion", ANDROID_VR_OS_RELEASE_VERSION);
-
- context.put("client", client);
-
- androidVRInnerTubeBody.put("context", context);
- androidVRInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create Android VR innerTubeBody", e);
- }
-
- ANDROID_VR_INNER_TUBE_BODY = androidVRInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "IOS");
- client.put("clientVersion", IOS_CLIENT_VERSION);
- client.put("deviceMake", "Apple");
- client.put("deviceModel", IOS_DEVICE_MODEL);
- client.put("platform", "MOBILE");
- client.put("osName", "iOS");
- client.put("osVersion", IOS_OS_VERSION);
-
- context.put("client", client);
-
- iOSInnerTubeBody.put("context", context);
- iOSInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create iOS innerTubeBody", e);
- }
-
- IOS_INNER_TUBE_BODY = iOSInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER");
- client.put("clientVersion", TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION);
- client.put("platform", "TV");
- client.put("clientScreen", "EMBED");
-
- JSONObject thirdParty = new JSONObject();
- thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s");
-
- context.put("thirdParty", thirdParty);
- context.put("client", client);
-
- tvEmbedInnerTubeBody.put("context", context);
- tvEmbedInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create TV Embed innerTubeBody", e);
- }
-
- TVHTML5_SIMPLY_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString();
-
- try {
- JSONObject context = new JSONObject();
-
- JSONObject client = new JSONObject();
- client.put("clientName", "WEB");
- client.put("clientVersion", WEB_CLIENT_VERSION);
- client.put("clientScreen", "WATCH");
-
- context.put("client", client);
-
- webInnerTubeBody.put("context", context);
- webInnerTubeBody.put("videoId", "%s");
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create Web innerTubeBody", e);
- }
-
- WEB_INNER_TUBE_BODY = webInnerTubeBody.toString();
- }
-
- // Must check for device features in a separate class and cannot place this code inside
- // the Patch or ClientType enum due to cyclic Setting references.
- static class DeviceHardwareSupport {
- private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding();
- private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding();
-
- private static boolean deviceHasVP9HardwareDecoding() {
- MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
-
- for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
- final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- ? codecInfo.isHardwareAccelerated()
- : !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
- if (isHardwareAccelerated && !codecInfo.isEncoder()) {
- for (String type : codecInfo.getSupportedTypes()) {
- if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
- Logger.printDebug(() -> "Device supports VP9 hardware decoding.");
- return true;
- }
- }
- }
- }
-
- Logger.printDebug(() -> "Device does not support VP9 hardware decoding.");
- return false;
- }
-
- private static boolean deviceHasAV1HardwareDecoding() {
- // It appears all devices with hardware AV1 are also Android 10 or newer.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
-
- for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
- if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) {
- for (String type : codecInfo.getSupportedTypes()) {
- if (type.equalsIgnoreCase("video/av01")) {
- Logger.printDebug(() -> "Device supports AV1 hardware decoding.");
- return true;
- }
- }
- }
- }
- }
-
- Logger.printDebug(() -> "Device does not support AV1 hardware decoding.");
- return false;
- }
-
- static boolean allowVP9() {
- return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get();
- }
-
- static boolean allowAV1() {
- return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
- }
- }
-
- private PlayerRoutes() {
- }
-
- /**
- * @noinspection SameParameterValue
- */
- public static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, String userAgent) throws IOException {
- var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
-
- connection.setRequestProperty("User-Agent", userAgent);
- connection.setRequestProperty("X-Goog-Api-Format-Version", "2");
- connection.setRequestProperty("Content-Type", "application/json");
-
- connection.setUseCaches(false);
- connection.setDoOutput(true);
-
- connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
- connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
- return connection;
- }
-
- public static final class SpoofingToIOSAvailability implements Setting.Availability {
- public static boolean clientTypeIOSEnabled() {
- final ClientType clientTypeIOS = ClientType.IOS;
- return Settings.SPOOF_CLIENT_GENERAL.get() == clientTypeIOS ||
- Settings.SPOOF_CLIENT_LIVESTREAM.get() == clientTypeIOS ||
- Settings.SPOOF_CLIENT_SHORTS.get() == clientTypeIOS ||
- Settings.SPOOF_CLIENT_FALLBACK.get() == clientTypeIOS;
- }
-
- @Override
- public boolean isAvailable() {
- return clientTypeIOSEnabled();
- }
- }
-
- public enum ClientType {
- ANDROID(3, ANDROID_DEVICE_MODEL, ANDROID_CLIENT_VERSION, ANDROID_INNER_TUBE_BODY, ANDROID_OS_RELEASE_VERSION, ANDROID_USER_AGENT),
- ANDROID_EMBEDDED_PLAYER(55, ANDROID_DEVICE_MODEL, ANDROID_CLIENT_VERSION, ANDROID_EMBED_INNER_TUBE_BODY, ANDROID_OS_RELEASE_VERSION, ANDROID_USER_AGENT),
- ANDROID_TESTSUITE(30, ANDROID_DEVICE_MODEL, ANDROID_TESTSUITE_CLIENT_VERSION, ANDROID_TESTSUITE_INNER_TUBE_BODY, ANDROID_OS_RELEASE_VERSION, ANDROID_USER_AGENT),
- ANDROID_UNPLUGGED(29, ANDROID_UNPLUGGED_DEVICE_MODEL, ANDROID_UNPLUGGED_CLIENT_VERSION, ANDROID_UNPLUGGED_INNER_TUBE_BODY, ANDROID_UNPLUGGED_OS_RELEASE_VERSION, ANDROID_UNPLUGGED_USER_AGENT),
- ANDROID_VR(28, ANDROID_VR_DEVICE_MODEL, ANDROID_VR_CLIENT_VERSION, ANDROID_VR_INNER_TUBE_BODY, ANDROID_VR_OS_RELEASE_VERSION, ANDROID_VR_USER_AGENT),
- IOS(5, IOS_DEVICE_MODEL, IOS_CLIENT_VERSION, IOS_INNER_TUBE_BODY, IOS_OS_VERSION, IOS_USER_AGENT),
- // No suitable model name was found for TVHTML5_SIMPLY_EMBEDDED_PLAYER. Use the model name of ANDROID.
- TVHTML5_SIMPLY_EMBEDDED_PLAYER(85, ANDROID_DEVICE_MODEL, TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION, TVHTML5_SIMPLY_EMBED_INNER_TUBE_BODY, TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION, TVHTML5_SIMPLY_EMBEDDED_PLAYER_USER_AGENT),
- // No suitable model name was found for WEB. Use the model name of ANDROID.
- WEB(1, ANDROID_DEVICE_MODEL, WEB_CLIENT_VERSION, WEB_INNER_TUBE_BODY, WEB_OS_VERSION, WEB_USER_AGENT);
-
- public final String friendlyName;
- public final int id;
- public final String model;
- public final String appVersion;
- public final String innerTubeBody;
- public final String osVersion;
- public final String userAgent;
-
- ClientType(int id, String model, String appVersion, String innerTubeBody,
- String osVersion, String userAgent) {
- this.friendlyName = str("revanced_spoof_client_options_entry_" + name().toLowerCase());
- this.id = id;
- this.model = model;
- this.appVersion = appVersion;
- this.innerTubeBody = innerTubeBody;
- this.osVersion = osVersion;
- this.userAgent = userAgent;
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java
deleted file mode 100644
index 75b1b28aa6..0000000000
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package app.revanced.integrations.youtube.patches.misc.requests;
-
-import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STORYBOARD_SPEC_RENDERER;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-import app.revanced.integrations.shared.requests.Requester;
-import app.revanced.integrations.shared.utils.Logger;
-import app.revanced.integrations.shared.utils.Utils;
-import app.revanced.integrations.youtube.patches.misc.StoryboardRenderer;
-import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType;
-
-public class StoryboardRendererRequester {
-
- private StoryboardRendererRequester() {
- }
-
- private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
- Logger.printInfo(() -> toastMessage, ex);
- }
-
- @Nullable
- private static JSONObject fetchPlayerResponse(@NonNull String requestBody,
- @NonNull String userAgent) {
- final long startTime = System.currentTimeMillis();
- try {
- Utils.verifyOffMainThread();
- Objects.requireNonNull(requestBody);
-
- final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8);
-
- HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER, userAgent);
- connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
-
- final int responseCode = connection.getResponseCode();
- if (responseCode == 200) return Requester.parseJSONObject(connection);
-
- // Always show a toast for this, as a non 200 response means something is broken.
- handleConnectionError("Spoof storyboard not available: " + responseCode, null);
- connection.disconnect();
- } catch (SocketTimeoutException ex) {
- handleConnectionError("Spoof storyboard temporarily not available (API timed out)", ex);
- } catch (IOException ex) {
- handleConnectionError("Spoof storyboard temporarily not available: " + ex.getMessage(), ex);
- } catch (Exception ex) {
- Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
- } finally {
- Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
- }
-
- return null;
- }
-
- private static String getPlayabilityStatus(@NonNull JSONObject playerResponse) {
- try {
- return playerResponse.getJSONObject("playabilityStatus").getString("status");
- } catch (JSONException e) {
- Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse);
- }
-
- // Prevent NullPointerException
- return "";
- }
-
- /**
- * Fetches the storyboardRenderer from the innerTubeBody.
- *
- * @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer.
- * @return StoryboardRenderer or null if playabilityStatus is not OK.
- */
- @Nullable
- private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String videoId,
- @NonNull String innerTubeBody,
- @NonNull String userAgent) {
- final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, userAgent);
- if (playerResponse == null)
- return null;
-
- final String playabilityStatus = getPlayabilityStatus(playerResponse);
- if (playabilityStatus.equals("OK"))
- return getStoryboardRendererUsingResponse(videoId, playerResponse);
-
- // Get the StoryboardRenderer from Premieres Video.
- // In Android client, YouTube used weird base64-like encoding for PlayerResponse.
- // So additional fetching with WEB client is required for getting unSerialized ones.
- if (playabilityStatus.equals("LIVE_STREAM_OFFLINE"))
- return getTrailerStoryboardRenderer(videoId);
- return null;
- }
-
- @Nullable
- private static StoryboardRenderer getTrailerStoryboardRenderer(@NonNull String videoId) {
- try {
- final ClientType requestClient = ClientType.WEB;
- final JSONObject playerResponse = fetchPlayerResponse(String.format(requestClient.innerTubeBody, videoId), requestClient.userAgent);
-
- if (playerResponse == null)
- return null;
-
- JSONObject unSerializedPlayerResponse = playerResponse.getJSONObject("playabilityStatus")
- .getJSONObject("errorScreen").getJSONObject("ypcTrailerRenderer").getJSONObject("unserializedPlayerResponse");
-
- if (getPlayabilityStatus(unSerializedPlayerResponse).equals("OK"))
- return getStoryboardRendererUsingResponse(videoId, unSerializedPlayerResponse);
- return null;
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to get unserializedPlayerResponse", e);
- }
-
- return null;
- }
-
- @Nullable
- private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) {
- try {
- Logger.printDebug(() -> "Parsing storyboardRenderer from response: " + playerResponse);
- if (!playerResponse.has("storyboards")) {
- Logger.printDebug(() -> "Using empty storyboard");
- return new StoryboardRenderer(videoId, null, false, null);
- }
- final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
- final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer");
- final String storyboardsRendererTag = isLiveStream
- ? "playerLiveStoryboardSpecRenderer"
- : "playerStoryboardSpecRenderer";
-
- final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
- StoryboardRenderer renderer = new StoryboardRenderer(
- videoId,
- rendererElement.getString("spec"),
- isLiveStream,
- rendererElement.has("recommendedLevel")
- ? rendererElement.getInt("recommendedLevel")
- : null
- );
-
- Logger.printDebug(() -> "Fetched: " + renderer);
-
- return renderer;
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to get storyboardRenderer", e);
- }
-
- return null;
- }
-
- @Nullable
- public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
- Objects.requireNonNull(videoId);
-
- // Fetch with iOS.
- ClientType clientType = ClientType.IOS;
- StoryboardRenderer renderer = getStoryboardRendererUsingBody(
- videoId,
- String.format(clientType.innerTubeBody, videoId),
- clientType.userAgent
- );
- if (renderer == null) {
- Logger.printDebug(() -> videoId + " not available using iOS client");
-
- clientType = ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER;
- renderer = getStoryboardRendererUsingBody(
- videoId,
- String.format(clientType.innerTubeBody, videoId, videoId),
- clientType.userAgent
- );
- if (renderer == null) {
- Logger.printDebug(() -> videoId + " not available using TV html5 embedded client");
- }
- }
-
- return renderer;
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java
index 9d951380a1..5e73a29db1 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java
@@ -27,11 +27,6 @@ public static boolean SponsorBlock() {
return false;
}
- public static boolean SpoofClient() {
- // Replace this with true if the Spoof client patch succeeds
- return false;
- }
-
public static boolean ToolBarComponents() {
// Replace this with true if the Toolbar components patch succeeds
return false;
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 45bb7f9d4d..4c1bc0f898 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -32,8 +32,6 @@
import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.integrations.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
-import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes;
-import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType;
import app.revanced.integrations.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
@@ -456,20 +454,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true);
public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE);
public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true);
- public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true, "revanced_spoof_client_user_dialog_message");
- public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, "revanced_spoof_client_ios_force_avc_user_dialog_message", new PlayerRoutes.SpoofingToIOSAvailability());
- public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT));
- public static final BooleanSetting SPOOF_CLIENT_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_client_stats_for_nerds", TRUE, parent(SPOOF_CLIENT));
- public static final EnumSetting SPOOF_CLIENT_GENERAL = new EnumSetting<>("revanced_spoof_client_general",
- ClientType.IOS);
- public static final EnumSetting SPOOF_CLIENT_LIVESTREAM = new EnumSetting<>("revanced_spoof_client_livestream",
- ClientType.IOS);
- public static final EnumSetting SPOOF_CLIENT_SHORTS = new EnumSetting<>("revanced_spoof_client_shorts",
- ClientType.IOS);
- public static final EnumSetting SPOOF_CLIENT_FALLBACK = new EnumSetting<>("revanced_spoof_client_fallback",
- // Some private videos cannot be played with {@code ClientType.IOS}.
- // Use {@code ClientType.ANDROID_TESTSUITE}.
- ClientType.ANDROID_TESTSUITE);
public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java
index bea2adf2ba..f789f2d73d 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java
@@ -1,7 +1,6 @@
package app.revanced.integrations.youtube.settings.preference;
import static app.revanced.integrations.shared.utils.StringRef.str;
-import static app.revanced.integrations.youtube.patches.utils.PatchStatus.SpoofClient;
import android.content.Context;
import android.content.SharedPreferences;
@@ -12,18 +11,17 @@
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.shared.utils.Utils;
import app.revanced.integrations.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
-import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.SpoofingToIOSAvailability;
import app.revanced.integrations.youtube.settings.Settings;
-@SuppressWarnings("unused")
+@SuppressWarnings({"deprecation", "unused"})
public class WatchHistoryStatusPreference extends Preference {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
- // Because this listener may run before the ReVanced settings fragment updates SettingsEnum,
+ // Because this listener may run before the ReVanced settings fragment updates Settings,
// this could show the prior config and not the current.
//
// Push this call to the end of the main run queue,
- // so all other listeners are done and SettingsEnum is up to date.
+ // so all other listeners are done and Settings is up to date.
Utils.runOnMainThread(this::updateUI);
};
@@ -65,9 +63,6 @@ protected void onPrepareForRemoval() {
}
private void updateUI() {
- final boolean spoofClientEnabled = SpoofClient() && Settings.SPOOF_CLIENT.get();
- final boolean containsClientTypeIOS = SpoofingToIOSAvailability.clientTypeIOSEnabled();
-
final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get();
final boolean blockWatchHistory = watchHistoryType == WatchHistoryType.BLOCK;
final boolean replaceWatchHistory = watchHistoryType == WatchHistoryType.REPLACE;
@@ -75,14 +70,10 @@ private void updateUI() {
final String summaryTextKey;
if (blockWatchHistory) {
summaryTextKey = "revanced_watch_history_about_status_blocked";
- } else if (spoofClientEnabled && containsClientTypeIOS) {
- summaryTextKey = replaceWatchHistory
- ? "revanced_watch_history_about_status_ios_replaced"
- : "revanced_watch_history_about_status_ios_original";
+ } else if (replaceWatchHistory) {
+ summaryTextKey = "revanced_watch_history_about_status_replaced";
} else {
- summaryTextKey = replaceWatchHistory
- ? "revanced_watch_history_about_status_android_replaced"
- : "revanced_watch_history_about_status_android_original";
+ summaryTextKey = "revanced_watch_history_about_status_original";
}
setSummary(str(summaryTextKey));
From b1f53f941f7864f2cc98b39cb71a94794e43a5ba Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 02:11:15 +0900
Subject: [PATCH 11/23] feat(YouTube): Add `Spoof streaming data` patch
---
.../patches/misc/SpoofStreamingDataPatch.java | 209 +++++++++++++
.../patches/misc/client/AppClient.java | 279 ++++++++++++++++++
.../misc/client/DeviceHardwareSupport.java | 63 ++++
.../patches/misc/requests/PlayerRoutes.java | 83 ++++++
.../misc/requests/StreamingDataRequester.java | 150 ++++++++++
.../youtube/settings/Settings.java | 7 +
...oofStreamingDataSideEffectsPreference.java | 86 ++++++
.../java/org/chromium/net/UrlRequest.java | 4 +
8 files changed, 881 insertions(+)
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
new file mode 100644
index 0000000000..3108f76ee1
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
@@ -0,0 +1,209 @@
+package app.revanced.integrations.youtube.patches.misc;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+import org.chromium.net.UrlRequest;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import app.revanced.integrations.shared.settings.Setting;
+import app.revanced.integrations.shared.utils.Logger;
+import app.revanced.integrations.shared.utils.Utils;
+import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
+import app.revanced.integrations.youtube.patches.misc.requests.StreamingDataRequester;
+import app.revanced.integrations.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class SpoofStreamingDataPatch {
+ private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_STREAMING_DATA.get();
+
+ /**
+ * Any unreachable ip address. Used to intentionally fail requests.
+ */
+ private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
+ private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
+
+ private static volatile Future currentVideoStream;
+
+ private static String url;
+ private static Map playerHeaders;
+
+ /**
+ * Injection point.
+ * Blocks /get_watch requests by returning an unreachable URI.
+ *
+ * @param playerRequestUri The URI of the player request.
+ * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
+ */
+ public static Uri blockGetWatchRequest(Uri playerRequestUri) {
+ if (SPOOF_STREAMING_DATA) {
+ try {
+ String path = playerRequestUri.getPath();
+
+ if (path != null && path.contains("get_watch")) {
+ Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
+
+ return UNREACHABLE_HOST_URI;
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "blockGetWatchRequest failure", ex);
+ }
+ }
+
+ return playerRequestUri;
+ }
+
+ /**
+ * Injection point.
+ *
+ * Blocks /initplayback requests.
+ */
+ public static String blockInitPlaybackRequest(String originalUrlString) {
+ if (SPOOF_STREAMING_DATA) {
+ try {
+ var originalUri = Uri.parse(originalUrlString);
+ String path = originalUri.getPath();
+
+ if (path != null && path.contains("initplayback")) {
+ Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
+
+ return UNREACHABLE_HOST_URI_STRING;
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
+ }
+ }
+
+ return originalUrlString;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static boolean isSpoofingEnabled() {
+ return SPOOF_STREAMING_DATA;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void setHeader(String url, Map playerHeaders) {
+ if (SPOOF_STREAMING_DATA) {
+ SpoofStreamingDataPatch.url = url;
+ SpoofStreamingDataPatch.playerHeaders = playerHeaders;
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static UrlRequest buildRequest(UrlRequest.Builder builder) {
+ if (SPOOF_STREAMING_DATA) {
+ try {
+ Uri uri = Uri.parse(url);
+ String path = uri.getPath();
+ if (path != null && path.contains("player") && !path.contains("heartbeat")) {
+ String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
+ currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "buildRequest failure", ex);
+ }
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Injection point.
+ * Fix playback by replace the streaming data.
+ * Called after {@link #buildRequest(UrlRequest.Builder)}.
+ */
+ @Nullable
+ public static ByteBuffer getStreamingData(String videoId) {
+ if (SPOOF_STREAMING_DATA) {
+ try {
+ Utils.verifyOffMainThread();
+
+ var future = currentVideoStream;
+ if (future != null) {
+ final long maxSecondsToWait = 20;
+ var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS);
+ if (stream != null) {
+ Logger.printDebug(() -> "Overriding video stream");
+ return stream;
+ }
+
+ Logger.printDebug(() -> "Not overriding streaming data (video stream is null)");
+ }
+ } catch (TimeoutException ex) {
+ Logger.printInfo(() -> "getStreamingData timed out", ex);
+ } catch (InterruptedException ex) {
+ Logger.printException(() -> "getStreamingData interrupted", ex);
+ Thread.currentThread().interrupt(); // Restore interrupt status flag.
+ } catch (ExecutionException ex) {
+ Logger.printException(() -> "getStreamingData failure", ex);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Injection point.
+ * Called after {@link #getStreamingData(String)}.
+ */
+ @Nullable
+ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
+ if (SPOOF_STREAMING_DATA) {
+ try {
+ final int methodPost = 2;
+ if (method == methodPost) {
+ String path = uri.getPath();
+ String clientName = "c";
+ final boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter(clientName));
+ if (iosClient && path != null && path.contains("videoplayback")) {
+ return null;
+ }
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
+ }
+ }
+
+ return postData;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static String appendSpoofedClient(String videoFormat) {
+ try {
+ if (SPOOF_STREAMING_DATA && Settings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
+ && !TextUtils.isEmpty(videoFormat)) {
+ // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
+ return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequester.getLastSpoofedClientName()); // u202D = left to right override
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "appendSpoofedClient failure", ex);
+ }
+
+ return videoFormat;
+ }
+
+ public static final class ForceiOSAVCAvailability implements Setting.Availability {
+ @Override
+ public boolean isAvailable() {
+ return Settings.SPOOF_STREAMING_DATA.get() && Settings.SPOOF_STREAMING_DATA_TYPE.get() == ClientType.IOS;
+ }
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java
new file mode 100644
index 0000000000..f19fd8ef9b
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java
@@ -0,0 +1,279 @@
+package app.revanced.integrations.youtube.patches.misc.client;
+
+import static app.revanced.integrations.shared.utils.StringRef.str;
+
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.integrations.shared.utils.PackageUtils;
+
+public class AppClient {
+
+ // WEB
+ private static final String CLIENT_VERSION_WEB = "2.20240726.00.00";
+ private static final String DEVICE_MODEL_WEB = "Surface Book 3";
+ private static final String OS_VERSION_WEB = "10";
+ private static final String USER_AGENT_WEB = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0)" +
+ " Gecko/20100101" +
+ " Firefox/129.0";
+
+ // ANDROID
+ private static final String CLIENT_VERSION_ANDROID = PackageUtils.getVersionName();
+ private static final String DEVICE_MODEL_ANDROID = Build.MODEL;
+ private static final String OS_NAME_ANDROID = "Android";
+ private static final String OS_VERSION_ANDROID = Build.VERSION.RELEASE;
+ private static final int ANDROID_SDK_VERSION_ANDROID = Build.VERSION.SDK_INT;
+ private static final String USER_AGENT_ANDROID = "com.google.android.youtube/" +
+ CLIENT_VERSION_ANDROID +
+ " (Linux; U; Android " +
+ OS_VERSION_ANDROID +
+ "; GB) gzip";
+
+ // IOS
+ /**
+ * The hardcoded client version of the iOS app used for InnerTube requests with this client.
+ *
+ *
+ * It can be extracted by getting the latest release version of the app on
+ * the App
+ * Store page of the YouTube app, in the {@code What’s New} section.
+ *
+ */
+ private static final String CLIENT_VERSION_IOS = "19.16.3";
+ private static final String DEVICE_MAKE_IOS = "Apple";
+ /**
+ * The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps.
+ * The device machine id for the iPhone 15 Pro Max (iPhone16,2), used to get HDR with AV1 hardware decoding.
+ *
+ *
+ * See this GitHub Gist for more
+ * information.
+ *
+ */
+ private static final String DEVICE_MODEL_IOS = DeviceHardwareSupport.allowAV1()
+ ? "iPhone16,2"
+ : "iPhone11,4";
+ private static final String OS_NAME_IOS = "iOS";
+ /**
+ * The minimum supported OS version for the iOS YouTube client is iOS 14.0.
+ * Using an invalid OS version will use the AVC codec.
+ */
+ private static final String OS_VERSION_IOS = DeviceHardwareSupport.allowVP9()
+ ? "17.6.1.21G101"
+ : "13.7.17H35";
+ private static final String USER_AGENT_VERSION_IOS = DeviceHardwareSupport.allowVP9()
+ ? "17_6_1"
+ : "13_7";
+ private static final String USER_AGENT_IOS = "com.google.ios.youtube/" +
+ CLIENT_VERSION_IOS +
+ "(" +
+ DEVICE_MODEL_IOS +
+ "; U; CPU iOS " +
+ USER_AGENT_VERSION_IOS +
+ " like Mac OS X)";
+
+ // ANDROID VR
+ /**
+ * The hardcoded client version of the Android VR app used for InnerTube requests with this client.
+ *
+ *
+ * It can be extracted by getting the latest release version of the app on
+ * the App
+ * Store page of the YouTube app, in the {@code Additional details} section.
+ *
+ */
+ private static final String CLIENT_VERSION_ANDROID_VR = "1.56.21";
+ /**
+ * The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
+ *
+ *
+ * See this GitLab for more
+ * information.
+ *
+ */
+ private static final String DEVICE_MODEL_ANDROID_VR = "Quest 3";
+ private static final String OS_VERSION_ANDROID_VR = "12";
+ /**
+ * The SDK version for Android 12 is 31,
+ * but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
+ */
+ private static final int ANDROID_SDK_VERSION_ANDROID_VR = 32;
+ /**
+ * Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
+ * Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
+ * Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
+ */
+ private static final String USER_AGENT_ANDROID_VR = "com.google.android.youtube/" +
+ CLIENT_VERSION_ANDROID_VR +
+ " (Linux; U; Android " +
+ OS_VERSION_ANDROID_VR +
+ "; GB) gzip";
+
+ // ANDROID UNPLUGGED
+ private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.16.0";
+ /**
+ * The device machine id for the Chromecast with Google TV 4K.
+ *
+ *
+ * See this GitLab for more
+ * information.
+ *
+ */
+ private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Chromecast";
+ private static final String OS_VERSION_ANDROID_UNPLUGGED = "12";
+ private static final int ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = 31;
+ private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
+ CLIENT_VERSION_ANDROID_UNPLUGGED +
+ " (Linux; U; Android " +
+ OS_VERSION_ANDROID_UNPLUGGED +
+ "; GB) gzip";
+
+ // ANDROID TESTSUITE
+ private static final String CLIENT_VERSION_ANDROID_TESTSUITE = "1.9";
+ private static final String USER_AGENT_ANDROID_TESTSUITE = "com.google.android.youtube/" +
+ CLIENT_VERSION_ANDROID_TESTSUITE +
+ " (Linux; U; Android " +
+ OS_VERSION_ANDROID +
+ "; GB) gzip";
+
+ // TVHTML5 SIMPLY EMBEDDED PLAYER
+ private static final String CLIENT_VERSION_TVHTML5_SIMPLY_EMBEDDED_PLAYER = "2.0";
+
+ private AppClient() {
+ }
+
+ public enum ClientType {
+ WEB(1,
+ null,
+ DEVICE_MODEL_WEB,
+ CLIENT_VERSION_WEB,
+ null,
+ OS_VERSION_WEB,
+ null,
+ USER_AGENT_WEB
+ ),
+ ANDROID(3,
+ null,
+ DEVICE_MODEL_ANDROID,
+ CLIENT_VERSION_ANDROID,
+ OS_NAME_ANDROID,
+ OS_VERSION_ANDROID,
+ ANDROID_SDK_VERSION_ANDROID,
+ USER_AGENT_ANDROID
+ ),
+ IOS(5,
+ DEVICE_MAKE_IOS,
+ DEVICE_MODEL_IOS,
+ CLIENT_VERSION_IOS,
+ OS_NAME_IOS,
+ OS_VERSION_IOS,
+ null,
+ USER_AGENT_IOS
+ ),
+ ANDROID_VR(28,
+ null,
+ DEVICE_MODEL_ANDROID_VR,
+ CLIENT_VERSION_ANDROID_VR,
+ OS_NAME_ANDROID,
+ OS_VERSION_ANDROID_VR,
+ ANDROID_SDK_VERSION_ANDROID_VR,
+ USER_AGENT_ANDROID_VR
+ ),
+ ANDROID_UNPLUGGED(29,
+ null,
+ DEVICE_MODEL_ANDROID_UNPLUGGED,
+ CLIENT_VERSION_ANDROID_UNPLUGGED,
+ OS_NAME_ANDROID,
+ OS_VERSION_ANDROID_UNPLUGGED,
+ ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
+ USER_AGENT_ANDROID_UNPLUGGED
+ ),
+ ANDROID_TESTSUITE(30,
+ null,
+ DEVICE_MODEL_ANDROID,
+ CLIENT_VERSION_ANDROID_TESTSUITE,
+ OS_NAME_ANDROID,
+ OS_VERSION_ANDROID,
+ ANDROID_SDK_VERSION_ANDROID,
+ USER_AGENT_ANDROID_TESTSUITE
+ ),
+ ANDROID_EMBEDDED_PLAYER(55,
+ null,
+ DEVICE_MODEL_ANDROID,
+ CLIENT_VERSION_ANDROID,
+ OS_NAME_ANDROID,
+ OS_VERSION_ANDROID,
+ ANDROID_SDK_VERSION_ANDROID,
+ USER_AGENT_ANDROID
+ ),
+ TVHTML5_SIMPLY_EMBEDDED_PLAYER(85,
+ null,
+ DEVICE_MODEL_WEB,
+ CLIENT_VERSION_TVHTML5_SIMPLY_EMBEDDED_PLAYER,
+ null,
+ OS_VERSION_WEB,
+ null,
+ USER_AGENT_WEB
+ );
+
+ public final String friendlyName;
+
+ /**
+ * YouTube
+ * client type
+ */
+ public final int id;
+
+ /**
+ * Device manufacturer.
+ */
+ @Nullable
+ public final String make;
+
+ /**
+ * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
+ */
+ public final String model;
+
+ /**
+ * Device OS name.
+ */
+ @Nullable
+ public final String osName;
+
+ /**
+ * Device OS version.
+ */
+ public final String osVersion;
+
+ /**
+ * Player user-agent.
+ */
+ public final String userAgent;
+
+ /**
+ * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
+ * Field is null if not applicable.
+ */
+ public final Integer androidSdkVersion;
+
+ /**
+ * App version.
+ */
+ public final String appVersion;
+
+ ClientType(int id, @Nullable String make, String model, String appVersion, @Nullable String osName,
+ String osVersion, Integer androidSdkVersion, String userAgent) {
+ this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
+ this.id = id;
+ this.make = make;
+ this.model = model;
+ this.appVersion = appVersion;
+ this.osName = osName;
+ this.osVersion = osVersion;
+ this.androidSdkVersion = androidSdkVersion;
+ this.userAgent = userAgent;
+ }
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java
new file mode 100644
index 0000000000..d17f9525e4
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java
@@ -0,0 +1,63 @@
+package app.revanced.integrations.youtube.patches.misc.client;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.os.Build;
+
+import app.revanced.integrations.shared.utils.Logger;
+import app.revanced.integrations.youtube.settings.Settings;
+
+public class DeviceHardwareSupport {
+ private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding();
+ private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding();
+
+ private static boolean deviceHasVP9HardwareDecoding() {
+ MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
+
+ for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
+ final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ ? codecInfo.isHardwareAccelerated()
+ : !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
+ if (isHardwareAccelerated && !codecInfo.isEncoder()) {
+ for (String type : codecInfo.getSupportedTypes()) {
+ if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
+ Logger.printDebug(() -> "Device supports VP9 hardware decoding.");
+ return true;
+ }
+ }
+ }
+ }
+
+ Logger.printDebug(() -> "Device does not support VP9 hardware decoding.");
+ return false;
+ }
+
+ private static boolean deviceHasAV1HardwareDecoding() {
+ // It appears all devices with hardware AV1 are also Android 10 or newer.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
+
+ for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
+ if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) {
+ for (String type : codecInfo.getSupportedTypes()) {
+ if (type.equalsIgnoreCase("video/av01")) {
+ Logger.printDebug(() -> "Device supports AV1 hardware decoding.");
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ Logger.printDebug(() -> "Device does not support AV1 hardware decoding.");
+ return false;
+ }
+
+ public static boolean allowVP9() {
+ return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get();
+ }
+
+ public static boolean allowAV1() {
+ return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
new file mode 100644
index 0000000000..a1a9181b3e
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
@@ -0,0 +1,83 @@
+package app.revanced.integrations.youtube.patches.misc.requests;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import app.revanced.integrations.shared.requests.Requester;
+import app.revanced.integrations.shared.requests.Route;
+import app.revanced.integrations.shared.utils.Logger;
+import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
+
+public final class PlayerRoutes {
+ /**
+ * The base URL of requests of non-web clients to the InnerTube internal API.
+ */
+ private static final String YOUTUBEI_V1_GAPIS_URL = "https://youtubei.googleapis.com/youtubei/v1/";
+
+ static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
+ Route.Method.POST,
+ "player" +
+ "?fields=streamingData" +
+ "&alt=proto"
+ ).compile();
+
+ /**
+ * TCP connection and HTTP read timeout
+ */
+ private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
+
+ private PlayerRoutes() {
+ }
+
+ static String createInnertubeBody(ClientType clientType) {
+ JSONObject innerTubeBody = new JSONObject();
+
+ try {
+ JSONObject context = new JSONObject();
+
+ JSONObject client = new JSONObject();
+ client.put("clientName", clientType.name());
+ client.put("clientVersion", clientType.appVersion);
+ client.put("deviceModel", clientType.model);
+ client.put("osVersion", clientType.osVersion);
+ if (clientType.make != null) {
+ client.put("deviceMake", clientType.make);
+ }
+ if (clientType.osName != null) {
+ client.put("osName", clientType.osName);
+ }
+ if (clientType.androidSdkVersion != null) {
+ client.put("androidSdkVersion", clientType.androidSdkVersion.toString());
+ }
+
+ context.put("client", client);
+
+ innerTubeBody.put("context", context);
+ innerTubeBody.put("contentCheckOk", true);
+ innerTubeBody.put("racyCheckOk", true);
+ innerTubeBody.put("videoId", "%s");
+ } catch (JSONException e) {
+ Logger.printException(() -> "Failed to create innerTubeBody", e);
+ }
+
+ return innerTubeBody.toString();
+ }
+
+ /** @noinspection SameParameterValue*/
+ static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
+ var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_URL, route);
+
+ connection.setRequestProperty("Content-Type", "application/json");
+ connection.setRequestProperty("User-Agent", clientType.userAgent);
+
+ connection.setUseCaches(false);
+ connection.setDoOutput(true);
+
+ connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
+ connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
+ return connection;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java
new file mode 100644
index 0000000000..f9af4cde1c
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java
@@ -0,0 +1,150 @@
+package app.revanced.integrations.youtube.patches.misc.requests;
+
+import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Future;
+
+import app.revanced.integrations.shared.settings.BaseSettings;
+import app.revanced.integrations.shared.utils.Logger;
+import app.revanced.integrations.shared.utils.Utils;
+import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
+import app.revanced.integrations.youtube.settings.Settings;
+
+public class StreamingDataRequester {
+ private static final ClientType[] allClientTypes = {
+ ClientType.IOS,
+ ClientType.ANDROID_VR,
+ ClientType.ANDROID_UNPLUGGED,
+ ClientType.ANDROID_TESTSUITE,
+ ClientType.ANDROID_EMBEDDED_PLAYER,
+ ClientType.WEB,
+ ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER
+ };
+
+ private static ClientType[] clientTypesToUse;
+
+ static {
+ final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
+ clientTypesToUse = new ClientType[allClientTypes.length + 1];
+ clientTypesToUse[0] = clientType;
+ int i = 1;
+ for (ClientType c : allClientTypes) {
+ clientTypesToUse[i] = c;
+ i++;
+ }
+ clientTypesToUse = Arrays.stream(clientTypesToUse)
+ .distinct()
+ .toArray(ClientType[]::new);
+ }
+
+ private static String lastSpoofedClientName = "Unknown";
+
+ public static String getLastSpoofedClientName() {
+ return lastSpoofedClientName;
+ }
+
+ private StreamingDataRequester() {
+ }
+
+ private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) {
+ if (showToast) Utils.showToastShort(toastMessage);
+ Logger.printInfo(() -> toastMessage, ex);
+ }
+
+ @Nullable
+ private static HttpURLConnection send(ClientType clientType, String videoId,
+ Map playerHeaders,
+ boolean showErrorToasts) {
+ final long startTime = System.currentTimeMillis();
+ String clientTypeName = clientType.name();
+ Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name());
+
+ try {
+ HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
+
+ String authHeader = playerHeaders.get("Authorization");
+ String visitorId = playerHeaders.get("X-Goog-Visitor-Id");
+ connection.setRequestProperty("Authorization", authHeader);
+ connection.setRequestProperty("X-Goog-Visitor-Id", visitorId);
+
+ String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
+ byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
+ connection.setFixedLengthStreamingMode(requestBody.length);
+ connection.getOutputStream().write(requestBody);
+
+ final int responseCode = connection.getResponseCode();
+ if (responseCode == 200) return connection;
+
+ handleConnectionError(clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.getResponseMessage(),
+ null, showErrorToasts);
+ } catch (SocketTimeoutException ex) {
+ handleConnectionError("Connection timeout", ex, showErrorToasts);
+ } catch (IOException ex) {
+ handleConnectionError("Network error", ex, showErrorToasts);
+ } catch (Exception ex) {
+ Logger.printException(() -> "send failed", ex);
+ } finally {
+ Logger.printDebug(() -> clientTypeName + " took: " + (System.currentTimeMillis() - startTime) + "ms");
+ }
+
+ return null;
+ }
+
+ public static Future fetch(@NonNull String videoId, Map playerHeaders) {
+ Objects.requireNonNull(videoId);
+
+ return Utils.submitOnBackgroundThread(() -> {
+ final boolean debugEnabled = BaseSettings.ENABLE_DEBUG_LOGGING.get();
+
+ // Retry with different client if empty response body is received.
+ int i = 0;
+ for (ClientType clientType : clientTypesToUse) {
+ // Show an error if the last client type fails, or if the debug is enabled then show for all attempts.
+ final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled;
+
+ HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
+ if (connection != null) {
+ try {
+ // gzip encoding doesn't response with content length (-1),
+ // but empty response body does.
+ if (connection.getContentLength() != 0) {
+ try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) >= 0) {
+ baos.write(buffer, 0, bytesRead);
+ }
+
+ lastSpoofedClientName = clientType.friendlyName;
+
+ return ByteBuffer.wrap(baos.toByteArray());
+ }
+ }
+ }
+ } catch (IOException ex) {
+ Logger.printException(() -> "Fetch failed while processing response data", ex);
+ }
+ }
+ }
+
+ handleConnectionError("Could not fetch any client streams", null, debugEnabled);
+ return null;
+ });
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 4c1bc0f898..855b131aec 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -31,7 +31,9 @@
import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
+import app.revanced.integrations.youtube.patches.misc.SpoofStreamingDataPatch;
import app.revanced.integrations.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
+import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.integrations.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
@@ -454,6 +456,11 @@ public class Settings extends BaseSettings {
public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true);
public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE);
public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true);
+ public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true,"revanced_spoof_streaming_data_user_dialog_message");
+ public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
+ "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.ForceiOSAVCAvailability());
+ public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true, parent(SPOOF_STREAMING_DATA));
+ public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_STREAMING_DATA));
public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
new file mode 100644
index 0000000000..bb839b8428
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
@@ -0,0 +1,86 @@
+package app.revanced.integrations.youtube.settings.preference;
+
+import static app.revanced.integrations.shared.utils.StringRef.str;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.util.AttributeSet;
+
+import java.util.Arrays;
+import java.util.List;
+
+import app.revanced.integrations.shared.settings.Setting;
+import app.revanced.integrations.shared.utils.Utils;
+import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
+import app.revanced.integrations.youtube.settings.Settings;
+
+@SuppressWarnings({"deprecation", "unused"})
+public class SpoofStreamingDataSideEffectsPreference extends Preference {
+
+ private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
+ // Because this listener may run before the ReVanced settings fragment updates Settings,
+ // this could show the prior config and not the current.
+ //
+ // Push this call to the end of the main run queue,
+ // so all other listeners are done and Settings is up to date.
+ Utils.runOnMainThread(this::updateUI);
+ };
+
+ public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SpoofStreamingDataSideEffectsPreference(Context context) {
+ super(context);
+ }
+
+ private void addChangeListener() {
+ Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener);
+ }
+
+ private void removeChangeListener() {
+ Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener);
+ }
+
+ @Override
+ protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
+ super.onAttachedToHierarchy(preferenceManager);
+ updateUI();
+ addChangeListener();
+ }
+
+ @Override
+ protected void onPrepareForRemoval() {
+ super.onPrepareForRemoval();
+ removeChangeListener();
+ }
+
+ private static final List selectableClientTypes = Arrays.asList(
+ ClientType.IOS,
+ ClientType.ANDROID_VR,
+ ClientType.ANDROID_UNPLUGGED
+ );
+
+ private void updateUI() {
+ final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
+
+ final String summaryTextKey;
+ if (selectableClientTypes.contains(clientType)) {
+ summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
+ } else {
+ summaryTextKey = "revanced_spoof_streaming_data_side_effects_unknown";
+ }
+
+ setSummary(str(summaryTextKey));
+ }
+}
\ No newline at end of file
diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java
index 565fc22274..4c02f1a400 100644
--- a/stub/src/main/java/org/chromium/net/UrlRequest.java
+++ b/stub/src/main/java/org/chromium/net/UrlRequest.java
@@ -1,4 +1,8 @@
package org.chromium.net;
public abstract class UrlRequest {
+ public abstract class Builder {
+ public abstract Builder addHeader(String name, String value);
+ public abstract UrlRequest build();
+ }
}
From da31e2293bff53973c23241ba73b942e79ab5e2d Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sat, 31 Aug 2024 19:16:28 +0200
Subject: [PATCH 12/23] feat(YouTube - Hide feed components): Add syntax to
match whole keywords and not substrings
---
.../components/KeywordContentFilter.java | 294 +++++++++++++++---
.../settings/preference/HtmlPreference.java | 31 ++
2 files changed, 274 insertions(+), 51 deletions(-)
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java
index 35945d61c5..2f313706d6 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java
@@ -1,5 +1,13 @@
package app.revanced.integrations.youtube.patches.components;
+import static java.lang.Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS;
+import static java.lang.Character.UnicodeBlock.HIRAGANA;
+import static java.lang.Character.UnicodeBlock.KATAKANA;
+import static java.lang.Character.UnicodeBlock.KHMER;
+import static java.lang.Character.UnicodeBlock.LAO;
+import static java.lang.Character.UnicodeBlock.MYANMAR;
+import static java.lang.Character.UnicodeBlock.THAI;
+import static java.lang.Character.UnicodeBlock.TIBETAN;
import static app.revanced.integrations.shared.utils.StringRef.str;
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
@@ -7,10 +15,9 @@
import androidx.annotation.Nullable;
import java.nio.charset.StandardCharsets;
-import java.util.*;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import app.revanced.integrations.shared.patches.components.Filter;
import app.revanced.integrations.shared.patches.components.StringFilterGroup;
@@ -24,7 +31,7 @@
/**
*
- * Allows hiding home feed and search results based on keywords and/or channel names.
+ * Allows hiding home feed and search results based on video title keywords and/or channel names.
*
* Limitations:
* - Searching for a keyword phrase will give no search results.
@@ -39,13 +46,13 @@
* (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST").
* - Keywords present in the layout or video data cannot be used as filters, otherwise all videos
* will always be hidden. This patch checks for some words of these words.
+ * - When using whole word syntax, some keywords may need additional pluralized variations.
*/
@SuppressWarnings("unused")
public final class KeywordContentFilter extends Filter {
/**
- * Strings found in the buffer for every video.
- * Full strings should be specified, as they are compared using {@link String#contains(CharSequence)}.
+ * Strings found in the buffer for every videos. Full strings should be specified.
*
* This list does not include every common buffer string, and this can be added/changed as needed.
* Words must be entered with the exact casing as found in the buffer.
@@ -80,7 +87,7 @@ public final class KeywordContentFilter extends Filter {
"search_vwc_description_transition_key",
"g-high-recZ",
// Text and litho components found in the buffer that belong to path filters.
- "metadata.eml",
+ "expandable_metadata.eml",
"thumbnail.eml",
"avatar.eml",
"overflow_button.eml",
@@ -99,7 +106,8 @@ public final class KeywordContentFilter extends Filter {
"search_video_with_context.eml",
"video_with_context.eml", // Subscription tab videos.
"related_video_with_context.eml",
- "video_lockup_with_attachment.eml", // A/B tests.
+ // A/B test for subscribed video, and sometimes when tablet layout is enabled.
+ "video_lockup_with_attachment.eml",
"compact_video.eml",
"inline_shorts",
"shorts_video_cell",
@@ -131,6 +139,12 @@ public final class KeywordContentFilter extends Filter {
"overflow_button.eml"
);
+ /**
+ * Minimum keyword/phrase length to prevent excessively broad content filtering.
+ * Only applies when not using whole word syntax.
+ */
+ private static final int MINIMUM_KEYWORD_LENGTH = 3;
+
/**
* Threshold for {@link #filteredVideosPercentage}
* that indicates all or nearly all videos have been filtered.
@@ -142,6 +156,8 @@ public final class KeywordContentFilter extends Filter {
private static final long ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS = 60 * 1000; // 60 seconds
+ private static final int UTF8_MAX_BYTE_COUNT = 4;
+
/**
* Rolling average of how many videos were filtered by a keyword.
* Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER}
@@ -175,7 +191,6 @@ public final class KeywordContentFilter extends Filter {
private volatile String lastKeywordPhrasesParsed;
private volatile ByteTrieSearch bufferSearch;
- private volatile List regexPatterns;
private static void logNavigationState(String state) {
// Enable locally to debug filtering. Default off to reduce log spam.
@@ -194,7 +209,7 @@ private static String titleCaseFirstWordOnly(String sentence) {
return sentence;
}
final int firstCodePoint = sentence.codePointAt(0);
- // In some non-English languages title case is different from uppercase.
+ // In some non English languages title case is different than uppercase.
return new StringBuilder()
.appendCodePoint(Character.toTitleCase(firstCodePoint))
.append(sentence, Character.charCount(firstCodePoint), sentence.length())
@@ -226,19 +241,171 @@ private static String capitalizeAllFirstLetters(String sentence) {
}
/**
- * @return If the phrase will hide all videos. Not an exhaustive check.
+ * @return If the string contains any characters from languages that do not use spaces between words.
*/
- private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases) {
- for (String commonString : STRINGS_IN_EVERY_BUFFER) {
- if (Utils.containsAny(commonString, phrases)) {
+ private static boolean isLanguageWithNoSpaces(String text) {
+ for (int i = 0, length = text.length(); i < length;) {
+ final int codePoint = text.codePointAt(i);
+
+ Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
+ if (block == CJK_UNIFIED_IDEOGRAPHS // Chinese and Kanji
+ || block == HIRAGANA // Japanese Hiragana
+ || block == KATAKANA // Japanese Katakana
+ || block == THAI
+ || block == LAO
+ || block == MYANMAR
+ || block == KHMER
+ || block == TIBETAN) {
return true;
}
+
+ i += Character.charCount(codePoint);
+ }
+
+ return false;
+ }
+
+
+ /**
+ * @return If the phrase will hide all videos. Not an exhaustive check.
+ */
+ private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases, boolean matchWholeWords) {
+ for (String phrase : phrases) {
+ for (String commonString : STRINGS_IN_EVERY_BUFFER) {
+ if (matchWholeWords) {
+ byte[] commonStringBytes = commonString.getBytes(StandardCharsets.UTF_8);
+ int matchIndex = 0;
+ while (true) {
+ matchIndex = commonString.indexOf(phrase, matchIndex);
+ if (matchIndex < 0) break;
+
+ if (keywordMatchIsWholeWord(commonStringBytes, matchIndex, phrase.length())) {
+ return true;
+ }
+
+ matchIndex++;
+ }
+ } else if (Utils.containsAny(commonString, phrases)) {
+ return true;
+ }
+ }
}
+
return false;
}
- private synchronized void parseKeywords() { // Must be synchronized since Litho is multithreaded.
+ /**
+ * @return If the start and end indexes are not surrounded by other letters.
+ * If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
+ */
+ private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) {
+ final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex);
+ if (codePointBefore != null && Character.isLetter(codePointBefore)) {
+ return false;
+ }
+
+ final Integer codePointAfter = getUtf8CodePointAt(text, keywordStartIndex + keywordLength);
+ //noinspection RedundantIfStatement
+ if (codePointAfter != null && Character.isLetter(codePointAfter)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @return The UTF8 character point immediately before the index,
+ * or null if the bytes before the index is not a valid UTF8 character.
+ */
+ @Nullable
+ private static Integer getUtf8CodePointBefore(byte[] data, int index) {
+ int characterByteCount = 0;
+ while (--index >= 0 && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
+ if (isValidUtf8(data, index, characterByteCount)) {
+ return decodeUtf8ToCodePoint(data, index, characterByteCount);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The UTF8 character point at the index,
+ * or null if the index holds no valid UTF8 character.
+ */
+ @Nullable
+ private static Integer getUtf8CodePointAt(byte[] data, int index) {
+ int characterByteCount = 0;
+ final int dataLength = data.length;
+ while (index + characterByteCount < dataLength && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
+ if (isValidUtf8(data, index, characterByteCount)) {
+ return decodeUtf8ToCodePoint(data, index, characterByteCount);
+ }
+ }
+
+ return null;
+ }
+
+ public static boolean isValidUtf8(byte[] data, int startIndex, int numberOfBytes) {
+ switch (numberOfBytes) {
+ case 1 -> { // 0xxxxxxx (ASCII)
+ return (data[startIndex] & 0x80) == 0;
+ }
+ case 2 -> { // 110xxxxx, 10xxxxxx
+ return (data[startIndex] & 0xE0) == 0xC0
+ && (data[startIndex + 1] & 0xC0) == 0x80;
+ }
+ case 3 -> { // 1110xxxx, 10xxxxxx, 10xxxxxx
+ return (data[startIndex] & 0xF0) == 0xE0
+ && (data[startIndex + 1] & 0xC0) == 0x80
+ && (data[startIndex + 2] & 0xC0) == 0x80;
+ }
+ case 4 -> { // 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx
+ return (data[startIndex] & 0xF8) == 0xF0
+ && (data[startIndex + 1] & 0xC0) == 0x80
+ && (data[startIndex + 2] & 0xC0) == 0x80
+ && (data[startIndex + 3] & 0xC0) == 0x80;
+ }
+ }
+
+ throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
+ }
+
+ public static int decodeUtf8ToCodePoint(byte[] data, int startIndex, int numberOfBytes) {
+ switch (numberOfBytes) {
+ case 1 -> {
+ return data[startIndex];
+ }
+ case 2 -> {
+ return ((data[startIndex] & 0x1F) << 6) |
+ (data[startIndex + 1] & 0x3F);
+ }
+ case 3 -> {
+ return ((data[startIndex] & 0x0F) << 12) |
+ ((data[startIndex + 1] & 0x3F) << 6) |
+ (data[startIndex + 2] & 0x3F);
+ }
+ case 4 -> {
+ return ((data[startIndex] & 0x07) << 18) |
+ ((data[startIndex + 1] & 0x3F) << 12) |
+ ((data[startIndex + 2] & 0x3F) << 6) |
+ (data[startIndex + 3] & 0x3F);
+ }
+ }
+ throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
+ }
+
+ private static boolean phraseUsesWholeWordSyntax(String phrase) {
+ return phrase.startsWith("\"") && phrase.endsWith("\"");
+ }
+
+ private static String stripWholeWordSyntax(String phrase) {
+ return phrase.substring(1, phrase.length() - 1);
+ }
+
+ private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded.
String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get();
+
//noinspection StringEquality
if (rawKeywords == lastKeywordPhrasesParsed) {
Logger.printDebug(() -> "Using previously initialized search");
@@ -247,28 +414,44 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho
ByteTrieSearch search = new ByteTrieSearch();
String[] split = rawKeywords.split("\n");
-
- List patterns = new ArrayList<>();
-
if (split.length != 0) {
// Linked Set so log statement are more organized and easier to read.
- Set keywords = new LinkedHashSet<>(10 * split.length);
+ // Map is: Phrase -> isWholeWord
+ Map keywords = new LinkedHashMap<>(10 * split.length);
for (String phrase : split) {
- // Remove any trailing white space the user may have accidentally included.
+ // Remove any trailing spaces the user may have accidentally included.
phrase = phrase.stripTrailing();
if (phrase.isBlank()) continue;
- // Add common casing that might appear.
+ final boolean wholeWordMatching;
+ if (phraseUsesWholeWordSyntax(phrase)) {
+ if (phrase.length() == 2) {
+ continue; // Empty "" phrase
+ }
+ phrase = stripWholeWordSyntax(phrase);
+ wholeWordMatching = true;
+ } else if (phrase.length() < MINIMUM_KEYWORD_LENGTH && !isLanguageWithNoSpaces(phrase)) {
+ // Allow phrases of 1 and 2 characters if using a
+ // language that does not use spaces between words.
+
+ // Do not reset the setting. Keep the invalid keywords so the user can fix the mistake.
+ Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH));
+ continue;
+ } else {
+ wholeWordMatching = false;
+ }
+
+ // Common casing that might appear.
//
- // This could be simplified by adding case-insensitive search to the prefix search,
+ // This could be simplified by adding case insensitive search to the prefix search,
// which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII.
//
// But to support Unicode with ByteTrieSearch would require major changes because
// UTF-8 characters can be different byte lengths, which does
// not allow comparing two different byte arrays using simple plain array indexes.
//
- // Instead, add all common case variations of the words.
+ // Instead use all common case variations of the words.
String[] phraseVariations = {
phrase,
phrase.toLowerCase(),
@@ -276,22 +459,43 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho
capitalizeAllFirstLetters(phrase),
phrase.toUpperCase()
};
- if (phrasesWillHideAllVideos(phraseVariations)) {
- Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_common", phrase));
+ if (phrasesWillHideAllVideos(phraseVariations, wholeWordMatching)) {
+ String toastMessage;
+ // If whole word matching is off, but would pass with on, then show a different toast.
+ if (!wholeWordMatching && !phrasesWillHideAllVideos(phraseVariations, true)) {
+ toastMessage = "revanced_hide_keyword_toast_invalid_common_whole_word_required";
+ } else {
+ toastMessage = "revanced_hide_keyword_toast_invalid_common";
+ }
+
+ Utils.showToastLong(str(toastMessage, phrase));
continue;
}
- keywords.addAll(Arrays.asList(phraseVariations));
+ for (String variation : phraseVariations) {
+ // Check if the same phrase is declared both with and without quotes.
+ Boolean existing = keywords.get(variation);
+ if (existing == null) {
+ keywords.put(variation, wholeWordMatching);
+ } else if (existing != wholeWordMatching) {
+ Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_conflicting", phrase));
+ break;
+ }
+ }
}
- for (String keyword : keywords) {
- String regex = "\\b(" + Pattern.quote(keyword) + ")\\b";
- patterns.add(Pattern.compile(regex));
- // Use a callback to get the keyword that matched.
- // TrieSearch could have this built in, but that's slightly more complicated since
- // the strings are stored as a byte array and embedded in the search tree.
+ for (Map.Entry entry : keywords.entrySet()) {
+ String keyword = entry.getKey();
+ //noinspection ExtractMethodRecommender
+ final boolean isWholeWord = entry.getValue();
TrieSearch.TriePatternMatchedCallback callback =
- (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ (textSearched, startIndex, matchLength, callbackParameter) -> {
+ if (isWholeWord && !keywordMatchIsWholeWord(textSearched, startIndex, matchLength)) {
+ return false;
+ }
+
+ Logger.printDebug(() -> (isWholeWord ? "Matched whole keyword: '"
+ : "Matched keyword: '") + keyword + "'");
// noinspection unchecked
((MutableReference) callbackParameter).value = keyword;
return true;
@@ -300,11 +504,10 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho
search.addPattern(stringBytes, callback);
}
- Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords);
+ Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords.keySet());
}
bufferSearch = search;
- regexPatterns = patterns;
timeToResumeFiltering = 0;
filteredVideosPercentage = 0;
lastKeywordPhrasesParsed = rawKeywords; // Must set last.
@@ -402,7 +605,7 @@ public boolean isFiltered(String path, @Nullable String identifier, String allVa
// Field is intentionally compared using reference equality.
//noinspection StringEquality
if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) {
- // User changed the keywords.
+ // User changed the keywords or whole word setting.
parseKeywords();
}
@@ -414,21 +617,10 @@ public boolean isFiltered(String path, @Nullable String identifier, String allVa
return false; // Do not update statistics.
}
- if (Settings.HIDE_KEYWORD_CONTENT_FULL_WORD.get()) {
- String content = new String(protobufBufferArray, StandardCharsets.UTF_8);
- for (Pattern pattern : regexPatterns) {
- Matcher matcher = pattern.matcher(content);
- if (matcher.find()) {
- updateStats(true, matcher.group(1));
- return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- }
- } else {
- MutableReference matchRef = new MutableReference<>();
- if (bufferSearch.matches(protobufBufferArray, matchRef)) {
- updateStats(true, matchRef.value);
- return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
+ MutableReference matchRef = new MutableReference<>();
+ if (bufferSearch.matches(protobufBufferArray, matchRef)) {
+ updateStats(true, matchRef.value);
+ return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
updateStats(false, null);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
new file mode 100644
index 0000000000..c9cf9486a8
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
@@ -0,0 +1,31 @@
+package app.revanced.integrations.youtube.settings.preference;
+
+import static android.text.Html.FROM_HTML_MODE_COMPACT;
+
+import android.content.Context;
+import android.preference.Preference;
+import android.text.Html;
+import android.util.AttributeSet;
+
+/**
+ * Allows using basic html for the summary text.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+public class HtmlPreference extends Preference {
+ {
+ setSummary(Html.fromHtml(getSummary().toString(), FROM_HTML_MODE_COMPACT));
+ }
+
+ public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+ public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ public HtmlPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ public HtmlPreference(Context context) {
+ super(context);
+ }
+}
\ No newline at end of file
From 1fa19c4a38994df927ca7f29b79ec33ef5b77a34 Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 02:21:11 +0900
Subject: [PATCH 13/23] feat(YouTube - Hide player flyout menu): Remove `Hide
Ambient mode menu` setting
---
.../youtube/patches/components/PlayerFlyoutMenuFilter.java | 4 ----
.../app/revanced/integrations/youtube/settings/Settings.java | 1 -
.../revanced/integrations/youtube/utils/ExtendedUtils.java | 2 --
3 files changed, 7 deletions(-)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java
index c7f76d1615..260c887f0c 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java
@@ -61,10 +61,6 @@ public PlayerFlyoutMenuFilter() {
);
flyoutFilterGroupList.addAll(
- new ByteArrayFilterGroup(
- Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT,
- "yt_outline_screen_light"
- ),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK,
"yt_outline_person_radar"
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 855b131aec..aaa9527685 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -265,7 +265,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_hide_player_flyout_menu_report", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_menu_additional_settings", FALSE);
- public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AMBIENT = new BooleanSetting("revanced_hide_player_flyout_menu_ambient_mode", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_HELP = new BooleanSetting("revanced_hide_player_flyout_menu_help", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOOP = new BooleanSetting("revanced_hide_player_flyout_menu_loop_video", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PIP = new BooleanSetting("revanced_hide_player_flyout_menu_pip", TRUE, true);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java b/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java
index a8db2557c6..13a4ef52b8 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java
@@ -67,7 +67,6 @@ public static void setCommentPreviewSettings() {
}
private static final Setting>[] additionalSettings = {
- Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT,
Settings.HIDE_PLAYER_FLYOUT_MENU_HELP,
Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP,
Settings.HIDE_PLAYER_FLYOUT_MENU_PIP,
@@ -99,7 +98,6 @@ private static boolean isAdditionalSettingsEnabled() {
boolean additionalSettingsEnabled = true;
final BooleanSetting[] additionalSettings = {
- Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT,
Settings.HIDE_PLAYER_FLYOUT_MENU_HELP,
Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP,
Settings.HIDE_PLAYER_FLYOUT_MENU_PIP,
From 3469de08745c82f41472d64eeb9d8882c2e49b50 Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 02:23:18 +0900
Subject: [PATCH 14/23] feat(YouTube - Hide player flyout menu): Add `Hide
Sleep timer` setting
---
.../youtube/patches/components/PlayerFlyoutMenuFilter.java | 4 ++++
.../app/revanced/integrations/youtube/settings/Settings.java | 1 +
.../revanced/integrations/youtube/utils/ExtendedUtils.java | 2 ++
3 files changed, 7 insertions(+)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java
index 260c887f0c..6f2bcaad37 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuFilter.java
@@ -110,6 +110,10 @@ public PlayerFlyoutMenuFilter() {
Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME,
"volume_stable"
),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER,
+ "yt_outline_moon_z_"
+ ),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS,
"yt_outline_statistics_graph"
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index aaa9527685..948b5b7ed9 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -269,6 +269,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOOP = new BooleanSetting("revanced_hide_player_flyout_menu_loop_video", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PIP = new BooleanSetting("revanced_hide_player_flyout_menu_pip", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS = new BooleanSetting("revanced_hide_player_flyout_menu_premium_controls", TRUE);
+ public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER = new BooleanSetting("revanced_hide_player_flyout_menu_sleep_timer", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_menu_stable_volume", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS = new BooleanSetting("revanced_hide_player_flyout_menu_stats_for_nerds", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_menu_watch_in_vr", TRUE);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java b/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java
index 13a4ef52b8..477cc36ad9 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/utils/ExtendedUtils.java
@@ -71,6 +71,7 @@ public static void setCommentPreviewSettings() {
Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP,
Settings.HIDE_PLAYER_FLYOUT_MENU_PIP,
Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS,
+ Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER,
Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME,
Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS,
Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR,
@@ -102,6 +103,7 @@ private static boolean isAdditionalSettingsEnabled() {
Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP,
Settings.HIDE_PLAYER_FLYOUT_MENU_PIP,
Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS,
+ Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER,
Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME,
Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS,
Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR,
From 2a8cb5a1c520c4ff9d98fd44a322fec073ba448a Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 02:25:00 +0900
Subject: [PATCH 15/23] feat(YouTube - Shorts components): Add `Hide Use this
sound button` setting
---
.../youtube/patches/components/ShortsButtonFilter.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
index c1ca090fd4..e1f9f1e351 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
@@ -174,6 +174,10 @@ public ShortsButtonFilter() {
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
"yt_outline_dollar_sign_heart_"
+ ),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON,
+ "yt_outline_camera"
)
);
}
From df8edcc382d5d4871ce9f8768a5974c9f5aa1d4b Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 02:59:12 +0900
Subject: [PATCH 16/23] fix(YouTube Music - Return YouTube Dislike): Likes and
dislikes are switched in RTL layout
---
.../music/returnyoutubedislike/ReturnYouTubeDislike.java | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/app/src/main/java/app/revanced/integrations/music/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/music/returnyoutubedislike/ReturnYouTubeDislike.java
index a604cb5025..a0239b604f 100644
--- a/app/src/main/java/app/revanced/integrations/music/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/app/src/main/java/app/revanced/integrations/music/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -151,8 +151,6 @@ public class ReturnYouTubeDislike {
@NonNull
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable,
@NonNull RYDVoteData voteData) {
- // Note: Some locales use right to left layout (Arabic, Hebrew, etc).
- // If making changes to this code, change device settings to a RTL language and verify layout is correct.
String oldLikesString = oldSpannable.toString();
// YouTube creators can hide the like count on a video,
@@ -191,8 +189,8 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable,
// middle separator
String middleSeparatorString = compactLayout
- ? " " + MIDDLE_SEPARATOR_CHARACTER + " "
- : " \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character
+ ? "\u200E " + MIDDLE_SEPARATOR_CHARACTER + " "
+ : "\u200E \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
From 45c3c88af41b9a2fc7310bb9d7a74804224ddcdf Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 03:00:13 +0900
Subject: [PATCH 17/23] chore: fix build error
---
.../app/revanced/integrations/youtube/settings/Settings.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 948b5b7ed9..21cf3f60a0 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -377,6 +377,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_sound_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", TRUE);
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
+ public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE);
public static final BooleanSetting HIDE_SHORTS_LIVE_HEADER = new BooleanSetting("revanced_hide_shorts_live_header", FALSE);
public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE);
From 02b921ad6f6e8c891d00ec9a6f421ddd4e471bb6 Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 03:10:52 +0900
Subject: [PATCH 18/23] chore: lint code
---
.../components/ShareSheetMenuFilter.java | 4 ++--
.../components/KeywordContentFilter.java | 8 +++----
.../patches/misc/SpoofStreamingDataPatch.java | 2 +-
.../patches/misc/requests/PlayerRoutes.java | 6 +++--
.../youtube/settings/Settings.java | 2 +-
.../ExternalDownloaderPlaylistPreference.java | 2 +-
.../ExternalDownloaderVideoPreference.java | 2 +-
.../settings/preference/HtmlPreference.java | 3 +++
.../preference/ImportExportPreference.java | 4 +---
.../OpenDefaultAppSettingsPreference.java | 8 +------
.../ReVancedPreferenceFragment.java | 5 ++--
.../ReVancedSettingsPreference.java | 9 ++-----
.../WhitelistedChannelsPreference.java | 24 ++++++++-----------
13 files changed, 33 insertions(+), 46 deletions(-)
diff --git a/app/src/main/java/app/revanced/integrations/music/patches/components/ShareSheetMenuFilter.java b/app/src/main/java/app/revanced/integrations/music/patches/components/ShareSheetMenuFilter.java
index fe61f0bf6a..2a1bc5d9da 100644
--- a/app/src/main/java/app/revanced/integrations/music/patches/components/ShareSheetMenuFilter.java
+++ b/app/src/main/java/app/revanced/integrations/music/patches/components/ShareSheetMenuFilter.java
@@ -2,10 +2,10 @@
import androidx.annotation.Nullable;
-import app.revanced.integrations.shared.patches.components.Filter;
-import app.revanced.integrations.shared.patches.components.StringFilterGroup;
import app.revanced.integrations.music.patches.misc.ShareSheetPatch;
import app.revanced.integrations.music.settings.Settings;
+import app.revanced.integrations.shared.patches.components.Filter;
+import app.revanced.integrations.shared.patches.components.StringFilterGroup;
/**
* Abuse LithoFilter for {@link ShareSheetPatch}.
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java
index 2f313706d6..48dad65c09 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java
@@ -244,7 +244,7 @@ private static String capitalizeAllFirstLetters(String sentence) {
* @return If the string contains any characters from languages that do not use spaces between words.
*/
private static boolean isLanguageWithNoSpaces(String text) {
- for (int i = 0, length = text.length(); i < length;) {
+ for (int i = 0, length = text.length(); i < length; ) {
final int codePoint = text.codePointAt(i);
Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
@@ -296,7 +296,7 @@ private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases, boole
/**
* @return If the start and end indexes are not surrounded by other letters.
- * If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
+ * If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
*/
private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) {
final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex);
@@ -315,7 +315,7 @@ private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartInde
/**
* @return The UTF8 character point immediately before the index,
- * or null if the bytes before the index is not a valid UTF8 character.
+ * or null if the bytes before the index is not a valid UTF8 character.
*/
@Nullable
private static Integer getUtf8CodePointBefore(byte[] data, int index) {
@@ -331,7 +331,7 @@ private static Integer getUtf8CodePointBefore(byte[] data, int index) {
/**
* @return The UTF8 character point at the index,
- * or null if the index holds no valid UTF8 character.
+ * or null if the index holds no valid UTF8 character.
*/
@Nullable
private static Integer getUtf8CodePointAt(byte[] data, int index) {
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
index 3108f76ee1..d210231a0a 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
@@ -175,7 +175,7 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos
return null;
}
}
- } catch (Exception ex) {
+ } catch (Exception ex) {
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
}
}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
index a1a9181b3e..2addccfb87 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java
@@ -33,7 +33,7 @@ private PlayerRoutes() {
}
static String createInnertubeBody(ClientType clientType) {
- JSONObject innerTubeBody = new JSONObject();
+ JSONObject innerTubeBody = new JSONObject();
try {
JSONObject context = new JSONObject();
@@ -66,7 +66,9 @@ static String createInnertubeBody(ClientType clientType) {
return innerTubeBody.toString();
}
- /** @noinspection SameParameterValue*/
+ /**
+ * @noinspection SameParameterValue
+ */
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_URL, route);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 21cf3f60a0..2affd171fb 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -457,7 +457,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true);
public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE);
public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true);
- public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true,"revanced_spoof_streaming_data_user_dialog_message");
+ public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.ForceiOSAVCAvailability());
public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true, parent(SPOOF_STREAMING_DATA));
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java
index 295cb49f3b..e1f1ba3dcb 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java
@@ -25,7 +25,7 @@
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.utils.ExtendedUtils;
-@SuppressWarnings("unused")
+@SuppressWarnings({"unused", "deprecation"})
public class ExternalDownloaderPlaylistPreference extends Preference implements Preference.OnPreferenceClickListener {
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST;
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderVideoPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderVideoPreference.java
index 098d498a51..db59b317f7 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderVideoPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ExternalDownloaderVideoPreference.java
@@ -25,7 +25,7 @@
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.utils.ExtendedUtils;
-@SuppressWarnings("unused")
+@SuppressWarnings({"unused", "deprecation"})
public class ExternalDownloaderVideoPreference extends Preference implements Preference.OnPreferenceClickListener {
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO;
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
index c9cf9486a8..35b41c7094 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
@@ -19,12 +19,15 @@ public class HtmlPreference extends Preference {
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
+
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
+
public HtmlPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
+
public HtmlPreference(Context context) {
super(context);
}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ImportExportPreference.java
index 1a53dd1f8b..dbf4b1b6c4 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ImportExportPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ImportExportPreference.java
@@ -17,9 +17,7 @@
import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.shared.utils.Utils;
-/**
- * @noinspection deprecation, unused
- */
+@SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/OpenDefaultAppSettingsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/OpenDefaultAppSettingsPreference.java
index 0a8b608614..9870aac692 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/OpenDefaultAppSettingsPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/OpenDefaultAppSettingsPreference.java
@@ -10,9 +10,7 @@
import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.shared.utils.Utils;
-/**
- * @noinspection ALL
- */
+@SuppressWarnings({"unused", "deprecation"})
public class OpenDefaultAppSettingsPreference extends Preference {
{
setOnPreferenceClickListener(pref -> {
@@ -22,10 +20,6 @@ public class OpenDefaultAppSettingsPreference extends Preference {
final Intent intent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
: new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
- if (context == null) {
- context = Utils.getContext();
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
context.startActivity(intent);
} catch (Exception exception) {
Logger.printException(() -> "OpenDefaultAppSettings Failed");
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java
index 7f58dfc446..85a68b4227 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -116,8 +116,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
return;
}
- final Activity mActivity = getActivity();
- ReVancedSettingsPreference.initializeReVancedSettings(mActivity);
+ ReVancedSettingsPreference.initializeReVancedSettings();
if (settingImportInProgress) {
return;
@@ -293,7 +292,7 @@ public void onCreate(Bundle bundle) {
setPreferenceScreenToolbar();
// Initialize ReVanced settings
- ReVancedSettingsPreference.initializeReVancedSettings(getActivity());
+ ReVancedSettingsPreference.initializeReVancedSettings();
// Import/export
setBackupRestorePreference();
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java
index 984f97e609..6d8a09281d 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java
@@ -4,21 +4,16 @@
import static app.revanced.integrations.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.integrations.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
-import android.app.Activity;
import android.os.Build;
import android.preference.Preference;
-import androidx.annotation.NonNull;
-
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.patches.general.MiniplayerPatch;
import app.revanced.integrations.youtube.patches.utils.PatchStatus;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.utils.ExtendedUtils;
-/**
- * @noinspection ALL
- */
+@SuppressWarnings("deprecation")
public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
private static void enableDisablePreferences() {
@@ -42,7 +37,7 @@ private static void enableDisablePreferences(final boolean isAvailable, final Se
}
}
- public static void initializeReVancedSettings(@NonNull Activity activity) {
+ public static void initializeReVancedSettings() {
enableDisablePreferences();
AmbientModePreferenceLinks();
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java
index c7ea612c6c..a843727cca 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java
@@ -22,9 +22,7 @@
import app.revanced.integrations.youtube.whitelist.Whitelist;
import app.revanced.integrations.youtube.whitelist.Whitelist.WhitelistType;
-/**
- * @noinspection ALL
- */
+@SuppressWarnings({"unused", "deprecation"})
public class WhitelistedChannelsPreference extends Preference implements Preference.OnPreferenceClickListener {
private static final WhitelistType whitelistTypePlaybackSpeed = WhitelistType.PLAYBACK_SPEED;
@@ -38,7 +36,7 @@ public class WhitelistedChannelsPreference extends Preference implements Prefere
final int entrySize = BooleanUtils.toInteger(playbackSpeedIncluded)
+ BooleanUtils.toInteger(sponsorBlockIncluded);
- if (entrySize != 0 && mEntries == null && mEntryValues == null) {
+ if (entrySize != 0) {
mEntries = new String[entrySize];
mEntryValues = new WhitelistType[entrySize];
@@ -112,16 +110,14 @@ private static void showWhitelistedChannelDialog(Context context, WhitelistType
entriesContainer.setOrientation(LinearLayout.VERTICAL);
for (final VideoChannel entry : mEntries) {
String author = entry.getChannelName();
- View entryView = getEntryView(context, author, v -> {
- new AlertDialog.Builder(context)
- .setMessage(str("revanced_whitelist_remove_dialog_message", author, whitelistType.getFriendlyName()))
- .setPositiveButton(android.R.string.ok, (dialog, which) -> {
- Whitelist.removeFromWhitelist(whitelistType, entry.getChannelId());
- entriesContainer.removeView(entriesContainer.findViewWithTag(author));
- })
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- });
+ View entryView = getEntryView(context, author, v -> new AlertDialog.Builder(context)
+ .setMessage(str("revanced_whitelist_remove_dialog_message", author, whitelistType.getFriendlyName()))
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ Whitelist.removeFromWhitelist(whitelistType, entry.getChannelId());
+ entriesContainer.removeView(entriesContainer.findViewWithTag(author));
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show());
entryView.setTag(author);
entriesContainer.addView(entryView);
}
From a5c3015de34d5d5ad2406958d3d1676b02b0406a Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 21:58:31 +0900
Subject: [PATCH 19/23] fix(YouTube - Spoof streaming data) : App crashes when
loading ads in Shorts
---
.../patches/misc/SpoofStreamingDataPatch.java | 100 +++----
.../misc/client/DeviceHardwareSupport.java | 46 ++--
.../misc/requests/StreamingDataRequest.java | 243 ++++++++++++++++++
.../misc/requests/StreamingDataRequester.java | 150 -----------
.../java/org/chromium/net/UrlRequest.java | 4 -
5 files changed, 295 insertions(+), 248 deletions(-)
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java
delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
index d210231a0a..168558a84c 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java
@@ -3,24 +3,19 @@
import android.net.Uri;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import org.chromium.net.UrlRequest;
-
import java.nio.ByteBuffer;
import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.shared.utils.Utils;
import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
-import app.revanced.integrations.youtube.patches.misc.requests.StreamingDataRequester;
+import app.revanced.integrations.youtube.patches.misc.requests.StreamingDataRequest;
import app.revanced.integrations.youtube.settings.Settings;
+import app.revanced.integrations.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch {
@@ -30,37 +25,8 @@ public class SpoofStreamingDataPatch {
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
- private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
-
- private static volatile Future currentVideoStream;
-
- private static String url;
- private static Map playerHeaders;
-
- /**
- * Injection point.
- * Blocks /get_watch requests by returning an unreachable URI.
- *
- * @param playerRequestUri The URI of the player request.
- * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
- */
- public static Uri blockGetWatchRequest(Uri playerRequestUri) {
- if (SPOOF_STREAMING_DATA) {
- try {
- String path = playerRequestUri.getPath();
-
- if (path != null && path.contains("get_watch")) {
- Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
-
- return UNREACHABLE_HOST_URI;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "blockGetWatchRequest failure", ex);
- }
- }
- return playerRequestUri;
- }
+ private static volatile Map fetchHeaders;
/**
* Injection point.
@@ -96,37 +62,45 @@ public static boolean isSpoofingEnabled() {
/**
* Injection point.
*/
- public static void setHeader(String url, Map playerHeaders) {
+ public static void setFetchHeaders(String url, Map headers) {
if (SPOOF_STREAMING_DATA) {
- SpoofStreamingDataPatch.url = url;
- SpoofStreamingDataPatch.playerHeaders = playerHeaders;
+ try {
+ Uri uri = Uri.parse(url);
+ String path = uri.getPath();
+ if (path != null && path.contains("browse")) {
+ fetchHeaders = headers;
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "setFetchHeaders failure", ex);
+ }
}
}
/**
* Injection point.
*/
- public static UrlRequest buildRequest(UrlRequest.Builder builder) {
+ public static void fetchStreamingData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
if (SPOOF_STREAMING_DATA) {
try {
- Uri uri = Uri.parse(url);
- String path = uri.getPath();
- if (path != null && path.contains("player") && !path.contains("heartbeat")) {
- String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
- currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders);
+ final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
+ // Shorts shelf in home and subscription feed causes player response hook to be called,
+ // and the 'is opening/playing' parameter will be false.
+ // This hook will be called again when the Short is actually opened.
+ if (videoIdIsShort && !isShortAndOpeningOrPlaying) {
+ return;
}
+
+ StreamingDataRequest.fetchRequestIfNeeded(videoId, fetchHeaders);
} catch (Exception ex) {
- Logger.printException(() -> "buildRequest failure", ex);
+ Logger.printException(() -> "fetchStreamingData failure", ex);
}
}
-
- return builder.build();
}
/**
* Injection point.
* Fix playback by replace the streaming data.
- * Called after {@link #buildRequest(UrlRequest.Builder)}.
+ * Called after {@link #setFetchHeaders(String, Map)} .
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
@@ -134,23 +108,17 @@ public static ByteBuffer getStreamingData(String videoId) {
try {
Utils.verifyOffMainThread();
- var future = currentVideoStream;
- if (future != null) {
- final long maxSecondsToWait = 20;
- var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS);
+ StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
+ if (request != null) {
+ var stream = request.getStream();
if (stream != null) {
- Logger.printDebug(() -> "Overriding video stream");
+ Logger.printDebug(() -> "Overriding video stream: " + videoId);
return stream;
}
-
- Logger.printDebug(() -> "Not overriding streaming data (video stream is null)");
}
- } catch (TimeoutException ex) {
- Logger.printInfo(() -> "getStreamingData timed out", ex);
- } catch (InterruptedException ex) {
- Logger.printException(() -> "getStreamingData interrupted", ex);
- Thread.currentThread().interrupt(); // Restore interrupt status flag.
- } catch (ExecutionException ex) {
+
+ Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
+ } catch (Exception ex) {
Logger.printException(() -> "getStreamingData failure", ex);
}
}
@@ -175,7 +143,7 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos
return null;
}
}
- } catch (Exception ex) {
+ } catch (Exception ex) {
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
}
}
@@ -191,7 +159,7 @@ public static String appendSpoofedClient(String videoFormat) {
if (SPOOF_STREAMING_DATA && Settings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
&& !TextUtils.isEmpty(videoFormat)) {
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
- return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequester.getLastSpoofedClientName()); // u202D = left to right override
+ return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override
}
} catch (Exception ex) {
Logger.printException(() -> "appendSpoofedClient failure", ex);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java
index d17f9525e4..9677f82a3a 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java
@@ -8,49 +8,39 @@
import app.revanced.integrations.youtube.settings.Settings;
public class DeviceHardwareSupport {
- private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding();
- private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding();
+ private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
+ private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1;
- private static boolean deviceHasVP9HardwareDecoding() {
+ static {
+ boolean vp9found = false;
+ boolean av1found = false;
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
+ final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
- final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater
? codecInfo.isHardwareAccelerated()
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
for (String type : codecInfo.getSupportedTypes()) {
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
- Logger.printDebug(() -> "Device supports VP9 hardware decoding.");
- return true;
+ vp9found = true;
+ } else if (type.equalsIgnoreCase("video/av01")) {
+ av1found = true;
}
}
}
}
- Logger.printDebug(() -> "Device does not support VP9 hardware decoding.");
- return false;
- }
-
- private static boolean deviceHasAV1HardwareDecoding() {
- // It appears all devices with hardware AV1 are also Android 10 or newer.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
-
- for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
- if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) {
- for (String type : codecInfo.getSupportedTypes()) {
- if (type.equalsIgnoreCase("video/av01")) {
- Logger.printDebug(() -> "Device supports AV1 hardware decoding.");
- return true;
- }
- }
- }
- }
- }
+ DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found;
+ DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found;
- Logger.printDebug(() -> "Device does not support AV1 hardware decoding.");
- return false;
+ Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1
+ ? "Device supports AV1 hardware decoding\n"
+ : "Device does not support AV1 hardware decoding\n"
+ + (DEVICE_HAS_HARDWARE_DECODING_VP9
+ ? "Device supports VP9 hardware decoding"
+ : "Device does not support VP9 hardware decoding"));
}
public static boolean allowVP9() {
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java
new file mode 100644
index 0000000000..b659af7b44
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java
@@ -0,0 +1,243 @@
+package app.revanced.integrations.youtube.patches.misc.requests;
+
+import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import app.revanced.integrations.shared.utils.Logger;
+import app.revanced.integrations.shared.utils.Utils;
+import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
+import app.revanced.integrations.youtube.settings.Settings;
+
+public class StreamingDataRequest {
+ private static final ClientType[] allClientTypes = {
+ ClientType.IOS,
+ ClientType.ANDROID_VR,
+ ClientType.ANDROID_UNPLUGGED,
+ ClientType.ANDROID_TESTSUITE,
+ ClientType.ANDROID_EMBEDDED_PLAYER,
+ ClientType.WEB,
+ ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER
+ };
+
+ private static final ClientType[] clientTypesToUse;
+
+ static {
+ final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
+ ClientType[] clientTypeArray = new ClientType[allClientTypes.length + 1];
+ clientTypeArray[0] = clientType;
+ int i = 1;
+ for (ClientType c : allClientTypes) {
+ clientTypeArray[i] = c;
+ i++;
+ }
+ clientTypeArray = Arrays.stream(clientTypeArray)
+ .distinct()
+ .toArray(ClientType[]::new);
+ clientTypesToUse = Arrays.copyOfRange(clientTypeArray, 0, 3);
+ }
+
+ private static String lastSpoofedClientName = "Unknown";
+
+ public static String getLastSpoofedClientName() {
+ return lastSpoofedClientName;
+ }
+
+ /**
+ * How long to keep fetches until they are expired.
+ */
+ private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute
+
+ private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds
+
+ @GuardedBy("itself")
+ private static final Map cache = new HashMap<>();
+
+ @SuppressLint("ObsoleteSdkInt")
+ public static void fetchRequestIfNeeded(@Nullable String videoId, Map fetchHeaders) {
+ Objects.requireNonNull(videoId);
+ synchronized (cache) {
+ final long now = System.currentTimeMillis();
+
+ // Remove any expired entries.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ cache.values().removeIf(request -> {
+ final boolean expired = request.isExpired(now);
+ if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
+ return expired;
+ });
+ } else {
+ for (Iterator> it = cache.entrySet().iterator(); it.hasNext();) {
+ final Map.Entry entry = it.next();
+ final StreamingDataRequest request = entry.getValue();
+ final boolean expired = request.isExpired(now);
+ if (expired) {
+ Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
+ it.remove();
+ }
+ }
+ }
+
+ if (!cache.containsKey(videoId)) {
+ cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
+ }
+ }
+ }
+
+ @Nullable
+ public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) {
+ synchronized (cache) {
+ return cache.get(videoId);
+ }
+ }
+
+ private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
+ Logger.printInfo(() -> toastMessage, ex);
+ }
+
+ @Nullable
+ private static HttpURLConnection send(ClientType clientType, String videoId,
+ Map playerHeaders) {
+ Objects.requireNonNull(clientType);
+ Objects.requireNonNull(videoId);
+ Objects.requireNonNull(playerHeaders);
+
+ final long startTime = System.currentTimeMillis();
+ String clientTypeName = clientType.name();
+ Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
+
+ try {
+ HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
+
+ String authHeader = playerHeaders.get("Authorization");
+ String visitorId = playerHeaders.get("X-Goog-Visitor-Id");
+ connection.setRequestProperty("Authorization", authHeader);
+ connection.setRequestProperty("X-Goog-Visitor-Id", visitorId);
+
+ String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
+ byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
+ connection.setFixedLengthStreamingMode(requestBody.length);
+ connection.getOutputStream().write(requestBody);
+
+ final int responseCode = connection.getResponseCode();
+ if (responseCode == 200) return connection;
+
+ handleConnectionError(clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.getResponseMessage(),
+ null);
+ } catch (SocketTimeoutException ex) {
+ handleConnectionError("Connection timeout", ex);
+ } catch (IOException ex) {
+ handleConnectionError("Network error", ex);
+ } catch (Exception ex) {
+ Logger.printException(() -> "send failed", ex);
+ } finally {
+ Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
+ }
+
+ return null;
+ }
+
+ private static ByteBuffer fetch(@NonNull String videoId, Map playerHeaders) {
+ // Retry with different client if empty response body is received.
+ for (ClientType clientType : clientTypesToUse) {
+ HttpURLConnection connection = send(clientType, videoId, playerHeaders);
+ if (connection != null) {
+ try {
+ // gzip encoding doesn't response with content length (-1),
+ // but empty response body does.
+ if (connection.getContentLength() != 0) {
+ try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) >= 0) {
+ baos.write(buffer, 0, bytesRead);
+ }
+
+ lastSpoofedClientName = clientType.friendlyName;
+
+ return ByteBuffer.wrap(baos.toByteArray());
+ }
+ }
+ }
+ } catch (IOException ex) {
+ Logger.printException(() -> "Fetch failed while processing response data", ex);
+ }
+ }
+ }
+
+ handleConnectionError("Could not fetch any client streams", null);
+ return null;
+ }
+
+ /**
+ * Time this instance and the fetch future was created.
+ */
+ private final long timeFetched;
+ private final String videoId;
+ private final Future future;
+
+ private StreamingDataRequest(String videoId, Map playerHeaders) {
+ Objects.requireNonNull(playerHeaders);
+ this.timeFetched = System.currentTimeMillis();
+ this.videoId = videoId;
+ this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
+ }
+
+ public boolean isExpired(long now) {
+ final long timeSinceCreation = now - timeFetched;
+ if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
+ return true;
+ }
+
+ // Only expired if the fetch failed (API null response).
+ return (fetchCompleted() && getStream() == null);
+ }
+
+ /**
+ * @return if the RYD fetch call has completed.
+ */
+ public boolean fetchCompleted() {
+ return future.isDone();
+ }
+
+ @Nullable
+ public ByteBuffer getStream() {
+ try {
+ return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException ex) {
+ Logger.printInfo(() -> "getStream timed out", ex);
+ } catch (InterruptedException ex) {
+ Logger.printException(() -> "getStream interrupted", ex);
+ Thread.currentThread().interrupt(); // Restore interrupt status flag.
+ } catch (ExecutionException ex) {
+ Logger.printException(() -> "getStream failure", ex);
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java
deleted file mode 100644
index f9af4cde1c..0000000000
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequester.java
+++ /dev/null
@@ -1,150 +0,0 @@
-package app.revanced.integrations.youtube.patches.misc.requests;
-
-import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Future;
-
-import app.revanced.integrations.shared.settings.BaseSettings;
-import app.revanced.integrations.shared.utils.Logger;
-import app.revanced.integrations.shared.utils.Utils;
-import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType;
-import app.revanced.integrations.youtube.settings.Settings;
-
-public class StreamingDataRequester {
- private static final ClientType[] allClientTypes = {
- ClientType.IOS,
- ClientType.ANDROID_VR,
- ClientType.ANDROID_UNPLUGGED,
- ClientType.ANDROID_TESTSUITE,
- ClientType.ANDROID_EMBEDDED_PLAYER,
- ClientType.WEB,
- ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER
- };
-
- private static ClientType[] clientTypesToUse;
-
- static {
- final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
- clientTypesToUse = new ClientType[allClientTypes.length + 1];
- clientTypesToUse[0] = clientType;
- int i = 1;
- for (ClientType c : allClientTypes) {
- clientTypesToUse[i] = c;
- i++;
- }
- clientTypesToUse = Arrays.stream(clientTypesToUse)
- .distinct()
- .toArray(ClientType[]::new);
- }
-
- private static String lastSpoofedClientName = "Unknown";
-
- public static String getLastSpoofedClientName() {
- return lastSpoofedClientName;
- }
-
- private StreamingDataRequester() {
- }
-
- private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) {
- if (showToast) Utils.showToastShort(toastMessage);
- Logger.printInfo(() -> toastMessage, ex);
- }
-
- @Nullable
- private static HttpURLConnection send(ClientType clientType, String videoId,
- Map playerHeaders,
- boolean showErrorToasts) {
- final long startTime = System.currentTimeMillis();
- String clientTypeName = clientType.name();
- Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name());
-
- try {
- HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
-
- String authHeader = playerHeaders.get("Authorization");
- String visitorId = playerHeaders.get("X-Goog-Visitor-Id");
- connection.setRequestProperty("Authorization", authHeader);
- connection.setRequestProperty("X-Goog-Visitor-Id", visitorId);
-
- String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
- byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
- connection.setFixedLengthStreamingMode(requestBody.length);
- connection.getOutputStream().write(requestBody);
-
- final int responseCode = connection.getResponseCode();
- if (responseCode == 200) return connection;
-
- handleConnectionError(clientTypeName + " not available with response code: "
- + responseCode + " message: " + connection.getResponseMessage(),
- null, showErrorToasts);
- } catch (SocketTimeoutException ex) {
- handleConnectionError("Connection timeout", ex, showErrorToasts);
- } catch (IOException ex) {
- handleConnectionError("Network error", ex, showErrorToasts);
- } catch (Exception ex) {
- Logger.printException(() -> "send failed", ex);
- } finally {
- Logger.printDebug(() -> clientTypeName + " took: " + (System.currentTimeMillis() - startTime) + "ms");
- }
-
- return null;
- }
-
- public static Future fetch(@NonNull String videoId, Map playerHeaders) {
- Objects.requireNonNull(videoId);
-
- return Utils.submitOnBackgroundThread(() -> {
- final boolean debugEnabled = BaseSettings.ENABLE_DEBUG_LOGGING.get();
-
- // Retry with different client if empty response body is received.
- int i = 0;
- for (ClientType clientType : clientTypesToUse) {
- // Show an error if the last client type fails, or if the debug is enabled then show for all attempts.
- final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled;
-
- HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
- if (connection != null) {
- try {
- // gzip encoding doesn't response with content length (-1),
- // but empty response body does.
- if (connection.getContentLength() != 0) {
- try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- byte[] buffer = new byte[8192];
- int bytesRead;
- while ((bytesRead = inputStream.read(buffer)) >= 0) {
- baos.write(buffer, 0, bytesRead);
- }
-
- lastSpoofedClientName = clientType.friendlyName;
-
- return ByteBuffer.wrap(baos.toByteArray());
- }
- }
- }
- } catch (IOException ex) {
- Logger.printException(() -> "Fetch failed while processing response data", ex);
- }
- }
- }
-
- handleConnectionError("Could not fetch any client streams", null, debugEnabled);
- return null;
- });
- }
-}
diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java
index 4c02f1a400..565fc22274 100644
--- a/stub/src/main/java/org/chromium/net/UrlRequest.java
+++ b/stub/src/main/java/org/chromium/net/UrlRequest.java
@@ -1,8 +1,4 @@
package org.chromium.net;
public abstract class UrlRequest {
- public abstract class Builder {
- public abstract Builder addHeader(String name, String value);
- public abstract UrlRequest build();
- }
}
From 7ee5630e5edfb5dc3908626167c49b2c812a8244 Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 22:07:40 +0900
Subject: [PATCH 20/23] feat(YouTube - Shorts components): Clarify the
description of some settings
---
.../components/ShortsButtonFilter.java | 46 +++++++++++--------
.../youtube/settings/Settings.java | 5 +-
2 files changed, 31 insertions(+), 20 deletions(-)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
index e1f9f1e351..73913bf270 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
@@ -29,8 +29,8 @@ public final class ShortsButtonFilter extends Filter {
private final StringFilterGroup subscribeButton;
private final StringFilterGroup joinButton;
- private final StringFilterGroup paidPromotionButton;
private final StringFilterGroup pausedOverlayButtons;
+ private final ByteArrayFilterGroupList pausedOverlayButtonsGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroup shortsCommentDisabled;
@@ -40,9 +40,9 @@ public final class ShortsButtonFilter extends Filter {
private final StringFilterGroup actionBar;
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
- private final ByteArrayFilterGroup shopButton = new ByteArrayFilterGroup(
- Settings.HIDE_SHORTS_SHOP_BUTTON,
- "yt_outline_bag_"
+ private final ByteArrayFilterGroup useThisSoundButton = new ByteArrayFilterGroup(
+ Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON,
+ "yt_outline_camera"
);
public ShortsButtonFilter() {
@@ -81,6 +81,11 @@ public ShortsButtonFilter() {
"immersive_live_header"
);
+ StringFilterGroup paidPromotionButton = new StringFilterGroup(
+ Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL,
+ "reel_player_disclosure.eml"
+ );
+
joinButton = new StringFilterGroup(
Settings.HIDE_SHORTS_JOIN_BUTTON,
"sponsor_button"
@@ -91,11 +96,6 @@ public ShortsButtonFilter() {
"subscribe_button"
);
- paidPromotionButton = new StringFilterGroup(
- Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL,
- "reel_player_disclosure.eml"
- );
-
actionBar = new StringFilterGroup(
null,
"shorts_action_bar"
@@ -148,11 +148,20 @@ public ShortsButtonFilter() {
)
);
+ //
+ // Paused overlay buttons.
+ //
+ pausedOverlayButtonsGroupList.addAll(
+ new ByteArrayFilterGroup(
+ Settings.HIDE_SHORTS_SHOPPING_BUTTON,
+ "yt_outline_bag_"
+ )
+ );
+
//
// Suggested actions.
//
suggestedActionsGroupList.addAll(
- shopButton,
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_TAGGED_PRODUCTS,
// Product buttons show pictures of the products, and does not have any unique icons to identify.
@@ -160,7 +169,11 @@ public ShortsButtonFilter() {
"PAproduct_listZ"
),
new ByteArrayFilterGroup(
- Settings.HIDE_SHORTS_LOCATION_LABEL,
+ Settings.HIDE_SHORTS_SHOP_BUTTON,
+ "yt_outline_bag_"
+ ),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_SHORTS_LOCATION_BUTTON,
"yt_outline_location_point_"
),
new ByteArrayFilterGroup(
@@ -168,24 +181,21 @@ public ShortsButtonFilter() {
"yt_outline_list_add_"
),
new ByteArrayFilterGroup(
- Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS,
+ Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON,
"yt_outline_search_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
"yt_outline_dollar_sign_heart_"
),
- new ByteArrayFilterGroup(
- Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON,
- "yt_outline_camera"
- )
+ useThisSoundButton
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
- if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
+ if (matchedGroup == subscribeButton || matchedGroup == joinButton) {
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
if (StringUtils.startsWithAny(path, REEL_CHANNEL_BAR_PATH, REEL_LIVE_HEADER_PATH, REEL_METAPANEL_PATH)) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
@@ -220,7 +230,7 @@ public boolean isFiltered(String path, @Nullable String identifier, String allVa
if (Settings.HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS.get()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
} else if (StringUtils.contains(path, SHORTS_PAUSED_STATE_BUTTON_PATH)) {
- if (shopButton.check(protobufBufferArray).isFiltered()) {
+ if (pausedOverlayButtonsGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 2affd171fb..dbddbbf4d5 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -371,11 +371,12 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_PAUSED_HEADER = new BooleanSetting("revanced_hide_shorts_paused_header", FALSE, true);
public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE);
public static final BooleanSetting HIDE_SHORTS_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_shorts_paid_promotion_label", TRUE, true);
+ public static final BooleanSetting HIDE_SHORTS_SHOPPING_BUTTON = new BooleanSetting("revanced_hide_shorts_shopping_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
- public static final BooleanSetting HIDE_SHORTS_LOCATION_LABEL = new BooleanSetting("revanced_hide_shorts_location_label", TRUE);
+ public static final BooleanSetting HIDE_SHORTS_LOCATION_BUTTON = new BooleanSetting("revanced_hide_shorts_location_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_sound_button", TRUE);
- public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", TRUE);
+ public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON = new BooleanSetting("revanced_hide_shorts_search_suggestions_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE);
From e7285fd4e592c60758bd1d6e41224cf2b485ff28 Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 22:10:03 +0900
Subject: [PATCH 21/23] fix(YouTube - Shorts components): `Hide Use this sound
button` doesn't work
---
.../patches/components/ShortsButtonFilter.java | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
index 73913bf270..bbd92ba4aa 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
@@ -30,6 +30,7 @@ public final class ShortsButtonFilter extends Filter {
private final StringFilterGroup subscribeButton;
private final StringFilterGroup joinButton;
private final StringFilterGroup pausedOverlayButtons;
+ private final StringFilterGroup metaPanelButton;
private final ByteArrayFilterGroupList pausedOverlayButtonsGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroup shortsCommentDisabled;
@@ -86,6 +87,11 @@ public ShortsButtonFilter() {
"reel_player_disclosure.eml"
);
+ metaPanelButton = new StringFilterGroup(
+ null,
+ "|ContainerType|button.eml|"
+ );
+
joinButton = new StringFilterGroup(
Settings.HIDE_SHORTS_JOIN_BUTTON,
"sponsor_button"
@@ -107,7 +113,7 @@ public ShortsButtonFilter() {
);
addPathCallbacks(
- suggestedAction, actionBar, joinButton, subscribeButton,
+ suggestedAction, actionBar, joinButton, subscribeButton, metaPanelButton,
paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel,
videoTitle, reelSoundMetadata, infoPanel, liveHeader
);
@@ -203,6 +209,13 @@ public boolean isFiltered(String path, @Nullable String identifier, String allVa
return false;
}
+ if (matchedGroup == metaPanelButton) {
+ if (path.startsWith(REEL_METAPANEL_PATH) && useThisSoundButton.check(protobufBufferArray).isFiltered()) {
+ return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ }
+ return false;
+ }
+
// Video action buttons (like, dislike, comment, share, remix) have the same path.
if (matchedGroup == actionBar) {
// If the Comment button is hidden, there is no need to check {@code REEL_COMMENTS_DISABLED_PATTERN}.
From beb726fcb6ce668502f63c1cc918a7236a69213f Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 22:13:07 +0900
Subject: [PATCH 22/23] feat(YouTube - Shorts components): Add settings - `Hide
floating button`, `Hide Trends button`, `Hide Use template button`
---
.../patches/components/ShortsButtonFilter.java | 15 +++++++++++++++
.../integrations/youtube/settings/Settings.java | 3 +++
2 files changed, 18 insertions(+)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
index bbd92ba4aa..07ae96a699 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsButtonFilter.java
@@ -47,6 +47,13 @@ public final class ShortsButtonFilter extends Filter {
);
public ShortsButtonFilter() {
+ StringFilterGroup floatingButton = new StringFilterGroup(
+ Settings.HIDE_SHORTS_FLOATING_BUTTON,
+ "floating_action_button"
+ );
+
+ addIdentifierCallbacks(floatingButton);
+
pausedOverlayButtons = new StringFilterGroup(
null,
"shorts_paused_state"
@@ -158,6 +165,10 @@ public ShortsButtonFilter() {
// Paused overlay buttons.
//
pausedOverlayButtonsGroupList.addAll(
+ new ByteArrayFilterGroup(
+ Settings.HIDE_SHORTS_TRENDS_BUTTON,
+ "yt_outline_fire_"
+ ),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHOPPING_BUTTON,
"yt_outline_bag_"
@@ -194,6 +205,10 @@ public ShortsButtonFilter() {
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
"yt_outline_dollar_sign_heart_"
),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
+ "yt_outline_template_add"
+ ),
useThisSoundButton
);
}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index dbddbbf4d5..45c9de6aab 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -358,6 +358,7 @@ public class Settings extends BaseSettings {
// PreferenceScreen: Shorts
public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", TRUE);
+ public static final BooleanSetting HIDE_SHORTS_FLOATING_BUTTON = new BooleanSetting("revanced_hide_shorts_floating_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHELF = new BooleanSetting("revanced_hide_shorts_shelf", TRUE, true);
public static final BooleanSetting HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_shorts_shelf_home_related_videos", TRUE, true);
public static final BooleanSetting HIDE_SHORTS_SHELF_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_shorts_shelf_subscriptions", TRUE, true);
@@ -371,6 +372,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_PAUSED_HEADER = new BooleanSetting("revanced_hide_shorts_paused_header", FALSE, true);
public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE);
public static final BooleanSetting HIDE_SHORTS_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_shorts_paid_promotion_label", TRUE, true);
+ public static final BooleanSetting HIDE_SHORTS_TRENDS_BUTTON = new BooleanSetting("revanced_hide_shorts_trends_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHOPPING_BUTTON = new BooleanSetting("revanced_hide_shorts_shopping_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
@@ -379,6 +381,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON = new BooleanSetting("revanced_hide_shorts_search_suggestions_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE);
+ public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE);
public static final BooleanSetting HIDE_SHORTS_LIVE_HEADER = new BooleanSetting("revanced_hide_shorts_live_header", FALSE);
public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE);
From 48a8a87d9c85da2823fbd582dcfbcc6d6ab374ad Mon Sep 17 00:00:00 2001
From: inotia00 <108592928+inotia00@users.noreply.github.com>
Date: Sun, 1 Sep 2024 22:17:49 +0900
Subject: [PATCH 23/23] chore: bump dependencies
---
gradle/libs.versions.toml | 4 ++--
gradle/wrapper/gradle-wrapper.jar | Bin 43504 -> 43583 bytes
gradle/wrapper/gradle-wrapper.properties | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d24f0f51c2..d516bdb986 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,8 +1,8 @@
[versions]
# noinspection GradleDependency
agp = "8.0.2"
-annotation = "1.8.1"
-lang3 = "3.15.0"
+annotation = "1.8.2"
+lang3 = "3.17.0"
# noinspection GradleDependency
kotlin = "1.7.21"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 2c3521197d7c4586c843d1d3e9090525f1898cde..a4b76b9530d66f5e68d973ea569d8e19de379189 100644
GIT binary patch
delta 3990
zcmV;H4{7l5(*nQL0Kr1kzC=_KMxQY0|W5(lc#i
zH*M1^P4B}|{x<+fkObwl)u#`$GxKKV&3pg*-y6R6txw)0qU|Clf9Uds3x{_-**c=7
z&*)~RHPM>Rw#Hi1R({;bX|7?J@w}DMF>dQQU2}9yj%iLjJ*KD6IEB2^n#gK7M~}6R
zkH+)bc--JU^pV~7W=3{E*4|ZFpDpBa7;wh4_%;?XM-5ZgZNnVJ=vm!%a2CdQb?oTa
z70>8rTb~M$5Tp!Se+4_OKWOB1LF+7gv~$$fGC95ToUM(I>vrd$>9|@h=O?eARj0MH
zT4zo(M>`LWoYvE>pXvqG=d96D-4?VySz~=tPVNyD$XMshoTX(1ZLB5OU!I2OI{kb)
zS8$B8Qm>wLT6diNnyJZC?yp{Kn67S{TCOt-!OonOK7$K)e-13U9GlnQXPAb&SJ0#3
z+vs~+4Qovv(%i8g$I#FCpCG^C4DdyQw3phJ(f#y*pvNDQCRZ~MvW<}fUs~PL=4??j
zmhPyg<*I4RbTz|NHFE-DC7lf2=}-sGkE5e!RM%3ohM7_I^IF=?O{m*uUPH(V?gqyc(Rp?-Qu(3bBIL4Fz(v?=_Sh?LbK{nqZMD>#9D_hNhaV$0ef3@9V90|0u#|PUNTO>$F=qRhg1duaE
z0`v~X3G{8RVT@kOa-pU+z8{JWyP6GF*u2e8eKr7a2t1fuqQy)@d|Qn(%YLZ62TWtoX@$nL}9?atE#Yw`rd(>cr0gY;dT9~^oL;u)zgHUvxc2I*b&ZkGM-iq=&(?kyO(3}=P!
zRp=rErEyMT5UE9GjPHZ#T<`cnD)jyIL!8P{H@IU#`e8cAG5jMK
zVyKw7--dAC;?-qEu*rMr$5@y535qZ6p(R#+fLA_)G~!wnT~~)|s`}&fA(s6xXN`9j
zP#Fd3GBa#HeS{5&8p?%DKUyN^X9cYUc6vq}D_3xJ&d@=6j(6BZKPl?!k1?!`f3z&a
zR4ZF60Mx7oBxLSxGuzA*Dy5n-d2K=+)6VMZh_0KetK|{e;E{8NJJ!)=_E~1uu=A=r
zrn&gh)h*SFhsQJo!f+wKMIE;-EOaMSMB@aXRU(UcnJhZW^B^mgs|M9@5WF@s6B0p&
zm#CTz)yiQCgURE{%hjxHcJ6G&>G9i`7MyftL!QQd5
z@RflRs?7)99?X`kHNt>W3l7YqscBpi*R2+fsgABor>KVOu(i(`03aytf2UA!&SC9v
z!E}whj#^9~=XHMinFZ;6UOJjo=mmNaWkv~nC=qH9$s-8roGeyaW-E~SzZ3Gg>j
zZ8}<320rg4=$`M0nxN!w(PtHUjeeU?MvYgWKZ6kkzABK;vMN0|U;X9abJleJA(xy<}5h5P(5
z{RzAFPvMnX2m0yH0Jn2Uo-p`daE|(O`YQiC#jB8;6bVIUf?SY(k$#C0`d6qT`>Xe0+0}Oj0=F&*D;PVe=Z<=0AGI<6$gYLwa#r`
zm449x*fU;_+J>Mz!wa;T-wldoBB%&OEMJgtm#oaI60TSYCy7;+$5?q!zi5K`u66Wq
zvg)Fx$s`V3Em{=OEY{3lmh_7|08ykS&U9w!kp@Ctuzqe1JFOGz6%i5}Kmm9>^=gih
z?kRxqLA<3@e=}G4R_?phW{4DVr?`tPfyZSN@R=^;P;?!2bh~F1I|fB7P=V=9a6XU5
z<#0f>RS0O&rhc&nTRFOW7&QhevP0#>j0eq<1@D5yAlgMl5n&O9X|Vq}%RX}iNyRFF
z7sX&u#6?E~bm~N|z&YikXC=I0E*8Z$v7PtWfjy)$e_Ez25fnR1Q=q1`;U!~U>|&YS
zaOS8y!^ORmr2L4ik!IYR8@Dcx8MTC=(b4P6iE5CnrbI~7j7DmM8em$!da&D!6Xu)!vKPdLG
z9f#)se|6=5yOCe)N6xDhPI!m81*dNe7u985zi%IVfOfJh69+#ag4ELzGne?o`eA`42K4T)h3S+s)5IT97%O>du-
z0U54L8m4}rkRQ?QBfJ%DLssy^+a7Ajw;0&`NOTY4o;0-ivm9
zBz1C%nr_hQ)X)^QM6T1?=yeLkuG9Lf50(eH}`tFye;01&(p?8i+6h};VV-2B~qdxeC#=X
z(JLlzy&fHkyi9Ksbcs~&r^%lh^2COldLz^H@X!s~mr9Dr6z!j+4?zkD@Ls7F8(t(f
z9`U?P$Lmn*Y{K}aR4N&1N=?xtQ1%jqf1~pJyQ4SgBrEtR`j4lQuh7cqP49Em5cO=I
zB(He2`iPN5M=Y0}h(IU$37ANTGx&|b-u1BYA*#dE(L-lptoOpo&th~E)_)y-`6kSH
z3vvyVrcBwW^_XYReJ=JYd9OBQrzv;f2AQdZH#$Y{Y+Oa33M70XFI((fs;mB4e`<<{
ze4dv2B0V_?Ytsi>>g%qs*}oDGd5d(RNZ*6?7qNbdp7wP4T72=F&r?Ud#kZr8Ze5tB
z_oNb7{G+(o2ajL$!69FW@jjPQ2a5C)m!MKKRirC$_VYIuVQCpf9rIms0GRDf)8AH${I`q^~5rjot@#3$2#zT2f`(N^P7Z;6(@EK$q*Jgif00I6*^ZGV+XB5uw*1R-@23yTw&WKD{s1;HTL;dO)%5i#`dc6b7;5@^{KU%N|A-$zsYw4)7LA{3`Zp>1
z-?K9_IE&z)dayUM)wd8K^29m-l$lFhi$zj0l!u~4;VGR6Y!?MAfBC^?QD53hy6VdD
z@eUZIui}~L%#SmajaRq1J|#>
z4m=o$vZ*34=ZWK2!QMNEcp2Lbc5N1q!lEDq(bz0b;WI9;e>l=CG9^n#ro`w>_0F$Q
zfZ={2QyTkfByC&gy;x!r*NyXXbk=a%~~(#K?<
zTke0HuF5{Q+~?@!KDXR|g+43$+;ab`^flS%miup_0OUTm=nIc%d5nLP)i308PIjl_YMF6cpQ__6&$n6it8K-
z8PIjl_YMF6cpQ_!r)L8IivW`WdK8mBs6PXdjR2DYdK8nCs73=4j{uVadK8oNjwX|E
wpAeHLsTu^*Y>Trk?aBtSQ(D-o$(D8Px^?ZI-PUB?
z*1fv!{YdHme3Fc8%cR@*@zc5A_nq&2=R47Hp@$-JF4Fz*;SLw5}K^y>s-s;V!}b2i=5=M-
zComP?ju>8Fe@=H@rlwe1l`J*6BTTo`9b$zjQ@HxrAhp0D#u?M~TxGC_!?ccCHCjt|
zF*PgJf@kJB`|Ml}cmsyrAjO#Kjr^E5p29w+#>$C`Q|54BoDv$fQ9D?3n32P9LPMIzu?LjNqggOH=1@T{9bMn*u8(GI
z!;MLTtFPHal^S>VcJdiYqX0VU|Rn@A}C1xOlxCribxes0~+n2
z6qDaIA2$?e`opx3_KW!rAgbpzU)gFdjAKXh|5w``#F0R|c)Y)Du0_Ihhz^S?k^pk%
zP>9|pIDx)xHH^_~+aA=^$M!<8K~Hy(71nJGf6`HnjtS=4X4=Hk^O71oNia2V{HUCC
zoN3RSBS?mZCLw;l4W4a+D8qc)XJS`pUJ5X-f^1ytxwr`@si$lAE?{4G|o;
zO0l>`rr?;~c;{ZEFJ!!3=7=FdGJ?Q^xfNQh4A?i;IJ4}B+A?4olTK(fN++3CRBP97
ze~lG9h%oegkn)lpW-4F8o2`*WW0mZHwHez`ko@>U1_;EC_6ig|Drn@=DMV9YEUSCa
zIf$kHei3(u#zm9I!Jf(4t`Vm1lltJ&lVHy(eIXE8sy9sUpmz%I_gA#8x^Zv8%w?r2
z{GdkX1SkzRIr>prRK@rqn9j2wG|rUvf6PJbbin=yy-TAXrguvzN8jL$hUrIXzr^s5
zVM?H4;eM-QeRFr06@ifV(ocvk?_)~N@1c2ien56UjWXid6W%6ievIh)>dk|rIs##^kY67ib8Kw%#-oVFaXG7$ERyA9(NSJUvWiOA5H(!{uOpcW
zg&-?iqPhds%3%tFspHDqqr;A!e@B#iPQjHd=c>N1LoOEGRehVoPOdxJ>b6>yc#o#+
zl8s8!(|NMeqjsy@0x{8^j0d00SqRZjp{Kj)&4UHYGxG+z9b-)72I*&J70?+8e?p_@
z=>-(>l6z5vYlP~<2%DU02b!mA{7mS)NS_eLe=t)sm&+Pmk?asOEKlkPQ)EUvvfC=;4M&*|I!w}(@V_)eUKLA_t^%`o
z0PM9LV|UKTLnk|?M3u!|f2S0?UqZsEIH9*NJS-8lzu;A6-rr-ot=dg9SASoluZUkFH$7X;
zP=?kYX!K?JL-b~<#7wU;b;eS)O;@?h%sPPk{4xEBxb{!sm0AY|f9cNvx6>$3F!*0c
z75H=dy8JvTyO8}g1w{$9T$p~5en}AeSLoCF>_RT9YPMpChUjl310o*$QocjbH&
zbnwg#gssR#jDVN{uEi3n(PZ%PFZ|6J2
z5_rBf0-u>e4sFe0*Km49ATi7>Kn0f9!uc|rRMR1Dtt6m1LW8^>qFlo}h$@br=Rmpi
z;mI&>OF64Be{dVeHI8utrh)v^wsZ0jii%x8UgZ8TC%K~@I(4E};GFW&(;WVov}3%H
zH;IhRkfD^(vt^DjZz(MyHLZxv8}qzPc(%itBkBwf_fC~sDBgh<3XAv5cxxfF3<2U!
z03Xe&z`is!JDHbe;mNmfkH+_LFE*I2^mdL@7(@9DfAcP6O04V-ko;Rpgp<%Cj5r8Z
zd0`sXoIjV$j)--;jA6Zy^D5&5v$o^>e%>Q?9GLm{i~p^lAn!%ZtF$I~>39XVZxk0b
zROh^Bk9cE0AJBLozZIEmy7xG(yHWGztvfnr0(2ro1%>zsGMS^EMu+S$r=_;9
zWwZkgf7Q7`H9sLf2Go^Xy6&h~a&%s2_T@_Csf19MntF$aVFiFkvE3_hUg(B@&Xw@YJ
zpL$wNYf78=0c@!QU6_a$>CPiXT7QAGDM}7Z(0z#_ZA=fmLUj{2z7@Ypo71UDy8GHr
z-&TLKf6a5WCf@Adle3VglBt4>Z>;xF}}-S~B7<(%B;Y
z0QR55{z-buw>8ilNM3u6I+D$S%?)(p>=eBx-HpvZj{7c*_?K=d()*7q?93us}1dq%FAFYLsW8ZTQ_XZLh`P2*6(NgS}qGcfGXVWpwsp#Rs}IuKbk*`2}&)
zI^Vsk6S&Q4@oYS?dJ`NwMVBs6f57+RxdqVub#PvMu?$=^OJy5xEl0<5SLsSRy%%a0
zi}Y#1-F3m;Ieh#Y12UgW?-R)|eX>ZuF-2cc!1>~NS|XSF-6In>zBoZg+ml!6%fk7U
zw0LHcz8VQk(jOJ+Yu)|^|15ufl$KQd_1eUZZzj`aC%umU6F1&D5XVWce_wAe(qCSZ
zpX-QF4e{EmEVN9~6%bR5U*UT{eMHfcUo`jw*u?4r2s_$`}U{?NjvEm(u&<>B|%mq$Q3weshxk
z76<``8vh{+nX`@9CB6IE&z)I%IFjR^LH{s1p|eppv=x
za(g_jLU|xjWMAn-V7th$f({|LG8zzIE0g?cyW;%Dmtv%C+0@xVxPE^
zyZzi9P%JAD6ynwHptuzP`Kox7*9h7XSMonCalv;Md0i9Vb-c*!f0ubfk?&T&T}AHh
z4m8Bz{JllKcdNg?D^%a5MFQ;#1z|*}H^qHLzW)L}wp?2tY7RejtSh8<;Zw)QGJYUm
z|MbTxyj*McKlStlT9I5XlSWtQGN&-LTr2XyNU+`490rg?LYLMRnz-@oKqT1hpCGqP
zyRXt4=_Woj$%n5ee<3zhLF>5>`?m9a#xQH+Jk_+|RM8Vi;2*XbK-
zEL6sCpaGPzP>k8f4Kh|##_imt#zJMB;ir|JrMPGW`rityK1vHXMLy18%qmMQAm4WZ
zP)i30KR&5vs15)C+8dM66&$k~i|ZT;KR&5vs15)C+8dJ(sAmGPijyIz6_bsqKLSFH
zlOd=TljEpH0>h4zA*dCTK&emy#FCRCs1=i^sZ9bFmXjf<6_X39E(XY)00000#N437
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 68e8816d71..2b189974c2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME