You've already forked revanced-integrations
mirror of
https://github.com/revanced/revanced-integrations
synced 2025-11-19 03:23:27 +01:00
Compare commits
148 Commits
v0.116.0-d
...
v0.121.0-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21bccf6a99 | ||
|
|
40cfa1e9af | ||
|
|
0fbf7a3434 | ||
|
|
6bd5aae977 | ||
|
|
bf5071107b | ||
|
|
8cbb50b8ed | ||
|
|
98c91af130 | ||
|
|
959ae4f3be | ||
|
|
04608d32e8 | ||
|
|
3e08847dce | ||
|
|
debd0a2e11 | ||
|
|
e68646d4f7 | ||
|
|
c1c7e3b596 | ||
|
|
e68f558e9c | ||
|
|
f19f122fee | ||
|
|
6d5a5c8281 | ||
|
|
650a5e06ee | ||
|
|
bb9b35554f | ||
|
|
2bcd3b4ae5 | ||
|
|
59687f1a39 | ||
|
|
cd6ba256a5 | ||
|
|
16f1163a34 | ||
|
|
c47fcde242 | ||
|
|
f5add51fa7 | ||
|
|
bd307e475f | ||
|
|
d3eba27c90 | ||
|
|
12e663c548 | ||
|
|
7d02774ea1 | ||
|
|
cc3c9e78b2 | ||
|
|
fd09e46d01 | ||
|
|
d97b390866 | ||
|
|
6f79746d78 | ||
|
|
4f50ac6c49 | ||
|
|
9670bd305b | ||
|
|
f7e99832fd | ||
|
|
f4b6d33245 | ||
|
|
b25ab1a0d8 | ||
|
|
9b2add7553 | ||
|
|
dceaf7a1ec | ||
|
|
49d9bf80f2 | ||
|
|
8a40ca616e | ||
|
|
1479d6bc26 | ||
|
|
771dd608dc | ||
|
|
92523fe1f7 | ||
|
|
a1477c097c | ||
|
|
486c894257 | ||
|
|
78b5fe2128 | ||
|
|
245c3b3537 | ||
|
|
d4b859d6fb | ||
|
|
4498f39b8c | ||
|
|
176196b52c | ||
|
|
30788ba1a3 | ||
|
|
1d13cf2d5d | ||
|
|
6eb301776e | ||
|
|
ed6571734e | ||
|
|
3ac869fa6a | ||
|
|
d47fd19d0b | ||
|
|
6730aaf2b8 | ||
|
|
1efd5c8315 | ||
|
|
63ddb0b31d | ||
|
|
1a702bce60 | ||
|
|
bedb02e4f6 | ||
|
|
6075c9ed17 | ||
|
|
4215be4250 | ||
|
|
a198ef83dd | ||
|
|
b280de3195 | ||
|
|
3d0fc1d610 | ||
|
|
25f73eb3a9 | ||
|
|
f451d67dcb | ||
|
|
5185673780 | ||
|
|
93a30453d9 | ||
|
|
d156951460 | ||
|
|
9685070eda | ||
|
|
1689bf4125 | ||
|
|
c992289d59 | ||
|
|
e2254681cd | ||
|
|
b87d806659 | ||
|
|
b472aeeed7 | ||
|
|
7dfa0e4653 | ||
|
|
165b061fa9 | ||
|
|
78bea48e40 | ||
|
|
27f49dfd1e | ||
|
|
da923a38a0 | ||
|
|
4b256f501b | ||
|
|
e7e02e1e30 | ||
|
|
cc416bc4f6 | ||
|
|
91096532ee | ||
|
|
444b80026d | ||
|
|
5dadb0d523 | ||
|
|
1e6fabceea | ||
|
|
00c4c4025b | ||
|
|
a01db14c1e | ||
|
|
67ada816d4 | ||
|
|
b0b6ff6a82 | ||
|
|
ab9989d41f | ||
|
|
09e8b3e345 | ||
|
|
01019b09c1 | ||
|
|
62f92c38c1 | ||
|
|
e72b65b599 | ||
|
|
245265587a | ||
|
|
83987747e6 | ||
|
|
a591c62543 | ||
|
|
e6903bff95 | ||
|
|
89993619fd | ||
|
|
f71c1a0c15 | ||
|
|
b76794b08c | ||
|
|
4b0925e337 | ||
|
|
aa0282f300 | ||
|
|
ba5e7d870e | ||
|
|
d20b768bc2 | ||
|
|
a113905e5c | ||
|
|
2cd1738d24 | ||
|
|
978f630c02 | ||
|
|
bfae6b56ab | ||
|
|
ef1cca02c1 | ||
|
|
b193b3dbc1 | ||
|
|
9272df52f3 | ||
|
|
2352fa5426 | ||
|
|
aa10de41b6 | ||
|
|
2c3418041c | ||
|
|
4a242c7a91 | ||
|
|
7b524efdbd | ||
|
|
526d66f6a9 | ||
|
|
f0bfcef0d7 | ||
|
|
e3b8e8be41 | ||
|
|
acf7e2d1dd | ||
|
|
17ed396739 | ||
|
|
830cfb561f | ||
|
|
d86851baf1 | ||
|
|
c034d474ff | ||
|
|
690fec6b5a | ||
|
|
20e2ecf199 | ||
|
|
61d997e1db | ||
|
|
04c81b39e9 | ||
|
|
b7a176ff3e | ||
|
|
1afd520c7b | ||
|
|
dc955d1bc2 | ||
|
|
f3fc0d1f7d | ||
|
|
ca4927e2a9 | ||
|
|
6e8d13bfbb | ||
|
|
90a7e604d3 | ||
|
|
6801ed8d28 | ||
|
|
ba8f4f9b5f | ||
|
|
a178a223c2 | ||
|
|
fb3b7b4c80 | ||
|
|
297d76f176 | ||
|
|
acae4b1bc8 | ||
|
|
d4570de0a7 |
6
.github/workflows/pull_request.yml
vendored
6
.github/workflows/pull_request.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: PR to main
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
|
||||
538
CHANGELOG.md
538
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class BrandingWaterMarkPatch {
|
||||
|
||||
// Used by: app.revanced.patches.youtube.layout.watermark.patch.HideWatermarkPatch
|
||||
public static boolean isBrandingWatermarkShown() {
|
||||
return SettingsEnum.HIDE_VIDEO_WATERMARK.getBoolean() == false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.net.Uri;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class BypassURLRedirectsPatch {
|
||||
private static final String YOUTUBE_REDIRECT_PATH = "/redirect";
|
||||
|
||||
/**
|
||||
* Convert the YouTube redirect URI string to the redirect query URI.
|
||||
*
|
||||
* @param uri The YouTube redirect URI string.
|
||||
* @return The redirect query URI.
|
||||
*/
|
||||
public static Uri parseRedirectUri(String uri) {
|
||||
final var parsed = Uri.parse(uri);
|
||||
|
||||
if (SettingsEnum.BYPASS_URL_REDIRECTS.getBoolean() && parsed.getPath().equals(YOUTUBE_REDIRECT_PATH)) {
|
||||
var query = Uri.parse(Uri.decode(parsed.getQueryParameter("q")));
|
||||
|
||||
LogHelper.printDebug(() -> "Bypassing YouTube redirect URI: " + query);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public final class DisableFineScrubbingGesturePatch {
|
||||
/**
|
||||
* Disables the fine scrubbing gesture.
|
||||
* @param tracker The velocity tracker that is used to determine the gesture.
|
||||
* @param event The motion event that is used to determine the gesture.
|
||||
*/
|
||||
public static void disableGesture(VelocityTracker tracker, MotionEvent event) {
|
||||
if (SettingsEnum.DISABLE_FINE_SCRUBBING_GESTURE.getBoolean()) return;
|
||||
|
||||
tracker.addMovement(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public final class DisableFullscreenAmbientModePatch {
|
||||
public static boolean enableFullScreenAmbientMode() {
|
||||
return !SettingsEnum.DISABLE_FULLSCREEN_AMBIENT_MODE.getBoolean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.widget.ImageView;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public final class DisableSuggestedVideoEndScreenPatch {
|
||||
public static void closeEndScreen(ImageView imageView) {
|
||||
if (!SettingsEnum.DISABLE_SUGGESTED_VIDEO_END_SCREEN.getBoolean()) return;
|
||||
|
||||
imageView.performClick();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public final class EnableTabletLayoutPatch {
|
||||
public static boolean enableTabletLayout() {
|
||||
return SettingsEnum.TABLET_LAYOUT.getBoolean();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import app.revanced.integrations.shared.PlayerType;
|
||||
public class MinimizedPlaybackPatch {
|
||||
|
||||
public static boolean isPlaybackNotShort() {
|
||||
return !PlayerType.getCurrent().isNoneOrHidden();
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
|
||||
}
|
||||
|
||||
public static boolean overrideMinimizedPlaybackAvailable() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class SpoofSignatureVerificationPatch {
|
||||
/**
|
||||
* Enable/disable all workarounds that are required due to signature spoofing.
|
||||
*/
|
||||
private static final boolean WORKAROUND = true;
|
||||
|
||||
/**
|
||||
* Protobuf parameters used for autoplay in scrim.
|
||||
* Prepend this parameter to mute video playback (for autoplay in feed)
|
||||
*/
|
||||
private static final String PROTOBUF_PARAMETER_SCRIM = "SAFgAXgB";
|
||||
|
||||
/**
|
||||
* Protobuf parameter also used by
|
||||
* <a href="https://github.com/yt-dlp/yt-dlp/blob/81ca451480051d7ce1a31c017e005358345a9149/yt_dlp/extractor/youtube.py#L3602">yt-dlp</a>
|
||||
* <br>
|
||||
* Known issue: captions are positioned on upper area in the player.
|
||||
*/
|
||||
private static final String PROTOBUF_PLAYER_PARAMS = "CgIQBg==";
|
||||
|
||||
/**
|
||||
* Target Protobuf parameters.
|
||||
*/
|
||||
private static final String[] PROTOBUF_PARAMETER_TARGETS = {
|
||||
"YAHI", // Autoplay in feed
|
||||
"SAFg" // Autoplay in scrim
|
||||
};
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param originalValue originalValue protobuf parameter
|
||||
*/
|
||||
public static String overrideProtobufParameter(String originalValue) {
|
||||
try {
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + originalValue);
|
||||
|
||||
if (!WORKAROUND) return PROTOBUF_PLAYER_PARAMS;
|
||||
|
||||
var isPlayingVideo = originalValue.contains(PROTOBUF_PLAYER_PARAMS);
|
||||
if (isPlayingVideo) return originalValue;
|
||||
|
||||
boolean isPlayingFeed = containsAny(originalValue, PROTOBUF_PARAMETER_TARGETS) && PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL;
|
||||
if (isPlayingFeed) {
|
||||
// Videos in feed won't autoplay with sound.
|
||||
return PROTOBUF_PARAMETER_SCRIM + PROTOBUF_PLAYER_PARAMS;
|
||||
} else {
|
||||
// Spoof the parameter to prevent playback issues.
|
||||
return PROTOBUF_PLAYER_PARAMS;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "overrideProtobufParameter failure", ex);
|
||||
}
|
||||
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getSeekbarThumbnailOverrideValue() {
|
||||
return SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
|
||||
*/
|
||||
public static void seekbarImageViewCreated(ImageView view) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) {
|
||||
view.setVisibility(View.GONE);
|
||||
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
|
||||
ViewGroup parentLayout = (ViewGroup) view.getParent();
|
||||
parentLayout.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
|
||||
import app.revanced.integrations.shared.VideoState;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Hooking class for the current playing video.
|
||||
*/
|
||||
@@ -25,6 +24,10 @@ public final class VideoInformation {
|
||||
private static String videoId = "";
|
||||
private static long videoLength = 0;
|
||||
private static long videoTime = -1;
|
||||
|
||||
@NonNull
|
||||
private static volatile String playerResponseVideoId = "";
|
||||
|
||||
/**
|
||||
* The current playback speed
|
||||
*/
|
||||
@@ -61,6 +64,18 @@ public final class VideoInformation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
*
|
||||
* @param videoId The id of the last video loaded.
|
||||
*/
|
||||
public static void setPlayerResponseVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
if (!playerResponseVideoId.equals(videoId)) {
|
||||
LogHelper.printDebug(() -> "New player response video id: " + videoId);
|
||||
playerResponseVideoId = videoId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when user selects a playback speed.
|
||||
@@ -141,6 +156,22 @@ public final class VideoInformation {
|
||||
return videoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Differs from {@link #videoId} as this is the video id for the
|
||||
* last player response received, which may not be the current video playing.
|
||||
*
|
||||
* If Shorts are loading the background, this commonly will be
|
||||
* different from the Short that is currently on screen.
|
||||
*
|
||||
* For most use cases, you should instead use {@link #getVideoId()}.
|
||||
*
|
||||
* @return The id of the last video loaded. Empty string if not set yet.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getPlayerResponseVideoId() {
|
||||
return playerResponseVideoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current playback speed.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package app.revanced.integrations.patches.announcements;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes;
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.UUID;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT;
|
||||
|
||||
public final class AnnouncementsPatch {
|
||||
private final static String CONSUMER = getOrSetConsumer();
|
||||
|
||||
private AnnouncementsPatch() {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void showAnnouncement(final Activity context) {
|
||||
if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return;
|
||||
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER);
|
||||
|
||||
LogHelper.printDebug(() -> "Get latest announcement route connection url: " + connection.getURL().toString());
|
||||
|
||||
try {
|
||||
// Do not show the announcement if the request failed.
|
||||
if (connection.getResponseCode() != 200) {
|
||||
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return;
|
||||
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
|
||||
ReVancedUtils.showToastLong("Failed to get announcement");
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
final var message = "Failed connecting to announcements provider";
|
||||
|
||||
LogHelper.printException(() -> message, ex);
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false);
|
||||
|
||||
// Do not show the announcement if it is older or the same as the last one.
|
||||
final byte[] hashBytes = MessageDigest.getInstance("SHA-256").digest(jsonString.getBytes(StandardCharsets.UTF_8));
|
||||
final var hash = java.util.Base64.getEncoder().encodeToString(hashBytes);
|
||||
if (hash.equals(SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString())) return;
|
||||
|
||||
// Parse the announcement. Fall-back to raw string if it fails.
|
||||
String title;
|
||||
String message;
|
||||
Level level = Level.INFO;
|
||||
try {
|
||||
final var announcement = new JSONObject(jsonString);
|
||||
|
||||
title = announcement.getString("title");
|
||||
message = announcement.getJSONObject("content").getString("message");
|
||||
|
||||
if (!announcement.isNull("level")) level = Level.fromInt(announcement.getInt("level"));
|
||||
} catch (Throwable ex) {
|
||||
LogHelper.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
|
||||
|
||||
title = "Announcement";
|
||||
message = jsonString;
|
||||
}
|
||||
|
||||
final var finalTitle = title;
|
||||
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
|
||||
final Level finalLevel = level;
|
||||
|
||||
ReVancedUtils.runOnMainThread(() -> {
|
||||
// Show the announcement.
|
||||
var alertDialog = new android.app.AlertDialog.Builder(context)
|
||||
.setTitle(finalTitle)
|
||||
.setMessage(finalMessage)
|
||||
.setIcon(finalLevel.icon)
|
||||
.setPositiveButton("Ok", (dialog, which) -> {
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(hash);
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton("Dismiss", (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
|
||||
// Make links clickable.
|
||||
((TextView)alertDialog.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
final var message = "Failed to get announcement";
|
||||
|
||||
LogHelper.printException(() -> message, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the last announcement hash if it is not empty.
|
||||
*
|
||||
* @return true if the last announcement hash was empty.
|
||||
*/
|
||||
private static boolean emptyLastAnnouncementHash() {
|
||||
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true;
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getOrSetConsumer() {
|
||||
final var consumer = SettingsEnum.ANNOUNCEMENT_CONSUMER.getString();
|
||||
if (!consumer.isEmpty()) return consumer;
|
||||
|
||||
final var uuid = UUID.randomUUID().toString();
|
||||
SettingsEnum.ANNOUNCEMENT_CONSUMER.saveValue(uuid);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// TODO: Use better icons.
|
||||
private enum Level {
|
||||
INFO(android.R.drawable.ic_dialog_info),
|
||||
WARNING(android.R.drawable.ic_dialog_alert),
|
||||
SEVERE(android.R.drawable.ic_dialog_alert);
|
||||
|
||||
public final int icon;
|
||||
|
||||
Level(int icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public static Level fromInt(int value) {
|
||||
return values()[Math.min(value, values().length - 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.integrations.patches.announcements.requests;
|
||||
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.requests.Route;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import static app.revanced.integrations.requests.Route.Method.GET;
|
||||
|
||||
public class AnnouncementsRoutes {
|
||||
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v2";
|
||||
|
||||
|
||||
public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?consumer={consumer}");
|
||||
|
||||
private AnnouncementsRoutes() {
|
||||
}
|
||||
|
||||
public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException {
|
||||
return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public final class HideInfoCardsFilterPatch extends Filter {
|
||||
|
||||
public HideInfoCardsFilterPatch() {
|
||||
identifierFilterGroupList.addAll(
|
||||
new StringFilterGroup(
|
||||
SettingsEnum.HIDE_INFO_CARDS,
|
||||
"info_card_teaser_overlay.eml"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.StringTrieSearch;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@@ -18,14 +19,18 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
SettingsEnum.HIDE_MIX_PLAYLISTS,
|
||||
"&list="
|
||||
);
|
||||
private final StringFilterGroup searchResultShelfHeader;
|
||||
private final StringFilterGroup inFeedSurvey;
|
||||
private final StringFilterGroup notifyMe;
|
||||
private final StringFilterGroup expandableMetadata;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
"home_video_with_context",
|
||||
"related_video_with_context",
|
||||
"comment_thread", // skip filtering anything in the comments
|
||||
"|comment.", // skip filtering anything in the comments replies
|
||||
"comment_thread", // Whitelist comments
|
||||
"|comment.", // Whitelist comment replies
|
||||
"library_recent_shelf"
|
||||
);
|
||||
|
||||
@@ -60,7 +65,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"compact_banner"
|
||||
);
|
||||
|
||||
final var inFeedSurvey = new StringFilterGroup(
|
||||
inFeedSurvey = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_FEED_SURVEY,
|
||||
"in_feed_survey",
|
||||
"slimline_survey"
|
||||
@@ -92,8 +97,16 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"channel_guidelines_entry_banner"
|
||||
);
|
||||
|
||||
// The player audio track button does the exact same function as the audio track flyout menu option.
|
||||
// But if the copy url button is shown, these button clashes and the the audio button does not work.
|
||||
// Previously this was a setting to show/hide the player button.
|
||||
// But it was decided it's simpler to always hide this button because:
|
||||
// - it doesn't work with copy video url feature
|
||||
// - the button is rare
|
||||
// - always hiding makes the ReVanced settings simpler and easier to understand
|
||||
// - nobody is going to notice the redundant button is always hidden
|
||||
final var audioTrackButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_AUDIO_TRACK_BUTTON,
|
||||
null,
|
||||
"multi_feed_icon_button"
|
||||
);
|
||||
|
||||
@@ -102,11 +115,16 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"official_card"
|
||||
);
|
||||
|
||||
final var expandableMetadata = new StringFilterGroup(
|
||||
expandableMetadata = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_EXPANDABLE_CHIP,
|
||||
"inline_expander"
|
||||
);
|
||||
|
||||
final var videoQualityMenuFooter = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_VIDEO_QUALITY_MENU_FOOTER,
|
||||
"quality_sheet_footer"
|
||||
);
|
||||
|
||||
final var chapters = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_CHAPTERS,
|
||||
"macro_markers_carousel"
|
||||
@@ -137,56 +155,104 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"cell_divider" // layout residue (gray line above the buttoned ad),
|
||||
);
|
||||
|
||||
final var timedReactions = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_TIMED_REACTIONS,
|
||||
"emoji_control_panel",
|
||||
"timed_reaction"
|
||||
);
|
||||
|
||||
searchResultShelfHeader = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SEARCH_RESULT_SHELF_HEADER,
|
||||
"shelf_header.eml"
|
||||
);
|
||||
|
||||
notifyMe = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_NOTIFY_ME_BUTTON,
|
||||
"set_reminder_button"
|
||||
);
|
||||
|
||||
final var joinMembership = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_JOIN_MEMBERSHIP_BUTTON,
|
||||
"compact_sponsor_button"
|
||||
);
|
||||
|
||||
final var chipsShelf = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_CHIPS_SHELF,
|
||||
"chips_shelf"
|
||||
);
|
||||
|
||||
final var channelWatermark = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK,
|
||||
"featured_channel_watermark_overlay"
|
||||
);
|
||||
|
||||
this.pathFilterGroupList.addAll(
|
||||
channelBar,
|
||||
communityPosts,
|
||||
paidContent,
|
||||
latestPosts,
|
||||
chapters,
|
||||
channelWatermark,
|
||||
communityGuidelines,
|
||||
quickActions,
|
||||
expandableMetadata,
|
||||
relatedVideos,
|
||||
compactBanner,
|
||||
inFeedSurvey,
|
||||
joinMembership,
|
||||
medicalPanel,
|
||||
notifyMe,
|
||||
videoQualityMenuFooter,
|
||||
infoPanel,
|
||||
subscribersCommunityGuidelines,
|
||||
channelGuidelines,
|
||||
audioTrackButton,
|
||||
artistCard,
|
||||
timedReactions,
|
||||
imageShelf,
|
||||
subscribersCommunityGuidelines,
|
||||
channelMemberShelf,
|
||||
custom
|
||||
);
|
||||
|
||||
this.identifierFilterGroupList.addAll(
|
||||
graySeparator,
|
||||
chipsShelf
|
||||
chipsShelf,
|
||||
chapters
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
|
||||
// The groups are excluded from the filter due to the exceptions list below.
|
||||
// Filter them separately here.
|
||||
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
|
||||
if (matchedGroup != custom && exceptions.matches(path))
|
||||
return false; // Exceptions are not filtered.
|
||||
|
||||
// TODO: This also hides the feed Shorts shelf header
|
||||
if (matchedGroup == searchResultShelfHeader && matchedIndex != 0) return false;
|
||||
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(final byte[] bytes) {
|
||||
return mixPlaylists.check(bytes).isFiltered();
|
||||
final boolean isMixPlaylistFiltered = mixPlaylists.check(bytes).isFiltered();
|
||||
|
||||
if (isMixPlaylistFiltered)
|
||||
LogHelper.printDebug(() -> "Filtered mix playlist");
|
||||
|
||||
return isMixPlaylistFiltered;
|
||||
}
|
||||
|
||||
public static boolean showWatermark() {
|
||||
return !SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK.getBoolean();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.*;
|
||||
|
||||
@@ -13,12 +14,24 @@ import java.util.function.Consumer;
|
||||
|
||||
abstract class FilterGroup<T> {
|
||||
final static class FilterGroupResult {
|
||||
SettingsEnum setting;
|
||||
boolean filtered;
|
||||
private SettingsEnum setting;
|
||||
private int matchedIndex;
|
||||
private int matchedLength;
|
||||
// In the future it might be useful to include which pattern matched,
|
||||
// but for now that is not needed.
|
||||
|
||||
FilterGroupResult(SettingsEnum setting, boolean filtered) {
|
||||
FilterGroupResult() {
|
||||
this(null, -1, 0);
|
||||
}
|
||||
|
||||
FilterGroupResult(SettingsEnum setting, int matchedIndex, int matchedLength) {
|
||||
setValues(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
|
||||
public void setValues(SettingsEnum setting, int matchedIndex, int matchedLength) {
|
||||
this.setting = setting;
|
||||
this.filtered = filtered;
|
||||
this.matchedIndex = matchedIndex;
|
||||
this.matchedLength = matchedLength;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +43,21 @@ abstract class FilterGroup<T> {
|
||||
}
|
||||
|
||||
public boolean isFiltered() {
|
||||
return filtered;
|
||||
return matchedIndex >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matched index of first pattern that matched, or -1 if nothing matched.
|
||||
*/
|
||||
public int getMatchedIndex() {
|
||||
return matchedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the matched filter pattern.
|
||||
*/
|
||||
public int getMatchedLength() {
|
||||
return matchedLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,14 +108,40 @@ class StringFilterGroup extends FilterGroup<String> {
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final String string) {
|
||||
return new FilterGroupResult(setting, isEnabled() && ReVancedUtils.containsAny(string, filters));
|
||||
int matchedIndex = -1;
|
||||
int matchedLength = 0;
|
||||
if (isEnabled()) {
|
||||
for (String pattern : filters) {
|
||||
if (!string.isEmpty()) {
|
||||
final int indexOf = pattern.indexOf(string);
|
||||
if (indexOf >= 0) {
|
||||
matchedIndex = indexOf;
|
||||
matchedLength = pattern.length();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
||||
final class CustomFilterGroup extends StringFilterGroup {
|
||||
|
||||
public CustomFilterGroup(final SettingsEnum setting, final SettingsEnum filter) {
|
||||
super(setting, filter.getString().split(","));
|
||||
private static String[] getFilterPatterns(SettingsEnum setting) {
|
||||
String[] patterns = setting.getString().split("\\s+");
|
||||
for (String pattern : patterns) {
|
||||
if (!StringTrieSearch.isValidPattern(pattern)) {
|
||||
ReVancedUtils.showToastLong("Invalid custom filter, resetting to default");
|
||||
setting.saveValue(setting.defaultValue);
|
||||
return getFilterPatterns(setting);
|
||||
}
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
public CustomFilterGroup(SettingsEnum setting, SettingsEnum filter) {
|
||||
super(setting, getFilterPatterns(filter));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,19 +208,22 @@ class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final byte[] bytes) {
|
||||
var matched = false;
|
||||
int matchedLength = 0;
|
||||
int matchedIndex = -1;
|
||||
if (isEnabled()) {
|
||||
if (failurePatterns == null) {
|
||||
buildFailurePatterns(); // Lazy load.
|
||||
}
|
||||
for (int i = 0, length = filters.length; i < length; i++) {
|
||||
if (indexOf(bytes, filters[i], failurePatterns[i]) >= 0) {
|
||||
matched = true;
|
||||
byte[] filter = filters[i];
|
||||
matchedIndex = indexOf(bytes, filter, failurePatterns[i]);
|
||||
if (matchedIndex >= 0) {
|
||||
matchedLength = filter.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matched);
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +260,10 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
|
||||
continue;
|
||||
}
|
||||
for (V pattern : group.filters) {
|
||||
search.addPattern(pattern, (textSearched, matchedStartIndex, callbackParameter) -> {
|
||||
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
if (group.isEnabled()) {
|
||||
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
|
||||
result.setting = group.setting;
|
||||
result.filtered = true;
|
||||
result.setValues(group.setting, matchedStartIndex, matchedLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -241,9 +296,10 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
|
||||
if (search == null) {
|
||||
buildSearch(); // Lazy load.
|
||||
}
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(null, false);
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
||||
search.matches(stack, result);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
protected abstract TrieSearch<V> createSearchGraph();
|
||||
@@ -292,10 +348,10 @@ abstract class Filter {
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (SettingsEnum.DEBUG.getBoolean()) {
|
||||
if (pathFilterGroupList == matchedList) {
|
||||
LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered path: " + path);
|
||||
} else if (identifierFilterGroupList == matchedList) {
|
||||
if (matchedList == identifierFilterGroupList) {
|
||||
LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered identifier: " + identifier);
|
||||
} else {
|
||||
LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered path: " + path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -381,15 +437,15 @@ public final class LithoFilterPatch {
|
||||
|
||||
static {
|
||||
for (Filter filter : filters) {
|
||||
filterGroupLists(pathSearchTree, filter, filter.pathFilterGroupList);
|
||||
filterGroupLists(identifierSearchTree, filter, filter.identifierFilterGroupList);
|
||||
filterGroupLists(pathSearchTree, filter, filter.pathFilterGroupList);
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Using: "
|
||||
+ pathSearchTree.numberOfPatterns() + " path filters"
|
||||
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB), "
|
||||
+ identifierSearchTree.numberOfPatterns() + " identifier filters"
|
||||
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB)");
|
||||
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
|
||||
+ pathSearchTree.numberOfPatterns() + " path filters"
|
||||
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
|
||||
}
|
||||
|
||||
private static <T> void filterGroupLists(TrieSearch<T> pathSearchTree,
|
||||
@@ -399,7 +455,7 @@ public final class LithoFilterPatch {
|
||||
continue;
|
||||
}
|
||||
for (T pattern : group.filters) {
|
||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, callbackParameter) -> {
|
||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
if (!group.isEnabled()) return false;
|
||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||
return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.TrieSearch;
|
||||
|
||||
/**
|
||||
* Searches for video id's in the proto buffer of Shorts dislike.
|
||||
*
|
||||
* Because multiple litho dislike spans are created in the background
|
||||
* (and also anytime litho refreshes the components, which is somewhat arbitrary),
|
||||
* that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()}
|
||||
* unreliable to determine which video id a Shorts litho span belongs to.
|
||||
*
|
||||
* But the correct video id does appear in the protobuffer just before a Shorts litho span is created.
|
||||
*
|
||||
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
|
||||
/**
|
||||
* Last unique video id's loaded. Value is ignored and Map is treated as a Set.
|
||||
* Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry().
|
||||
*/
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, Boolean> lastVideoIds = new LinkedHashMap<>() {
|
||||
/**
|
||||
* Number of video id's to keep track of for searching thru the buffer.
|
||||
* A minimum value of 3 should be sufficient, but check a few more just in case.
|
||||
*/
|
||||
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
try {
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
synchronized (lastVideoIds) {
|
||||
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
|
||||
LogHelper.printDebug(() -> "New video id: " + videoId);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "newPlayerResponseVideoId failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
|
||||
|
||||
public ReturnYouTubeDislikeFilterPatch() {
|
||||
pathFilterGroupList.addAll(
|
||||
new StringFilterGroup(SettingsEnum.RYD_SHORTS, "|shorts_dislike_button.eml|")
|
||||
);
|
||||
// After the dislikes icon name is some binary data and then the video id for that specific short.
|
||||
videoIdFilterGroup.addAll(
|
||||
// Video was previously disliked before video was opened.
|
||||
new ByteArrayAsStringFilterGroup(null, "ic_right_dislike_on_shadowed"),
|
||||
// Video was not already disliked.
|
||||
new ByteArrayAsStringFilterGroup(null, "ic_right_dislike_off_shadowed")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
|
||||
if (result.isFiltered()) {
|
||||
String matchedVideoId = findVideoId(protobufBufferArray);
|
||||
// Matched video will be null if in incognito mode.
|
||||
// Must pass a null id to correctly clear out the current video data.
|
||||
// Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened,
|
||||
// the new incognito Short will show the old prior data.
|
||||
ReturnYouTubeDislikePatch.newVideoLoaded(matchedVideoId, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String findVideoId(byte[] protobufBufferArray) {
|
||||
synchronized (lastVideoIds) {
|
||||
for (String videoId : lastVideoIds.keySet()) {
|
||||
if (byteArrayContainsString(protobufBufferArray, videoId)) {
|
||||
return videoId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This could use {@link TrieSearch}, but since the video ids are constantly changing
|
||||
* the overhead of updating the Trie might negate the search performance gain.
|
||||
*/
|
||||
private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) {
|
||||
for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) {
|
||||
boolean found = true;
|
||||
for (int j = 0, textLength = text.length(); j < textLength; j++) {
|
||||
if (array[i + j] != (byte) text.charAt(j)) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,32 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
/** @noinspection unused*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public final class ShortsFilter extends Filter {
|
||||
private static final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
|
||||
public static PivotBar pivotBar; // Set by patch.
|
||||
private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
|
||||
|
||||
private final StringFilterGroup channelBar;
|
||||
private final StringFilterGroup subscribeButton;
|
||||
private final StringFilterGroup subscribeButtonPaused;
|
||||
private final StringFilterGroup soundButton;
|
||||
private final StringFilterGroup infoPanel;
|
||||
private final StringFilterGroup shortsShelfHeader;
|
||||
private final StringFilterGroup shelfHeader;
|
||||
|
||||
private final StringFilterGroup videoActionButton;
|
||||
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
public ShortsFilter() {
|
||||
// Home / subscription feed components.
|
||||
var thanksButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_THANKS_BUTTON,
|
||||
"suggested_action"
|
||||
);
|
||||
var shorts = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS,
|
||||
"shorts_shelf",
|
||||
@@ -33,37 +34,80 @@ public final class ShortsFilter extends Filter {
|
||||
"shorts_grid",
|
||||
"shorts_video_cell",
|
||||
"shorts_pivot_item"
|
||||
|
||||
);
|
||||
// Feed Shorts shelf header.
|
||||
// Use a different filter group for this pattern, as it requires an additional check after matching.
|
||||
shortsShelfHeader = new StringFilterGroup(
|
||||
shelfHeader = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS,
|
||||
"shelf_header.eml"
|
||||
);
|
||||
identifierFilterGroupList.addAll(shorts, shortsShelfHeader, thanksButton);
|
||||
|
||||
// Home / subscription feed components.
|
||||
var thanksButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_THANKS_BUTTON,
|
||||
"suggested_action"
|
||||
);
|
||||
|
||||
identifierFilterGroupList.addAll(shorts, shelfHeader, thanksButton);
|
||||
|
||||
// Shorts player components.
|
||||
var joinButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_JOIN_BUTTON,
|
||||
"sponsor_button"
|
||||
);
|
||||
var subscribeButton = new StringFilterGroup(
|
||||
|
||||
subscribeButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON,
|
||||
"subscribe_button"
|
||||
);
|
||||
|
||||
subscribeButtonPaused = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED,
|
||||
"shorts_paused_state"
|
||||
);
|
||||
|
||||
channelBar = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_CHANNEL_BAR,
|
||||
REEL_CHANNEL_BAR_PATH
|
||||
);
|
||||
|
||||
soundButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_SOUND_BUTTON,
|
||||
"reel_pivot_button"
|
||||
);
|
||||
|
||||
infoPanel = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_INFO_PANEL,
|
||||
"shorts_info_panel_overview"
|
||||
);
|
||||
pathFilterGroupList.addAll(joinButton, subscribeButton, channelBar, soundButton, infoPanel);
|
||||
|
||||
videoActionButton = new StringFilterGroup(
|
||||
null,
|
||||
"ContainerType|shorts_video_action_button"
|
||||
);
|
||||
|
||||
pathFilterGroupList.addAll(
|
||||
joinButton, subscribeButton, subscribeButtonPaused,
|
||||
channelBar, soundButton, infoPanel, videoActionButton
|
||||
);
|
||||
|
||||
var shortsCommentButton = new ByteArrayAsStringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"reel_comment_button"
|
||||
);
|
||||
|
||||
var shortsShareButton = new ByteArrayAsStringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"reel_share_button"
|
||||
);
|
||||
|
||||
var shortsRemixButton = new ByteArrayAsStringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"reel_remix_button"
|
||||
);
|
||||
|
||||
videoActionButtonGroupList.addAll(shortsCommentButton, shortsShareButton, shortsRemixButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,28 +115,43 @@ public final class ShortsFilter extends Filter {
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (matchedList == pathFilterGroupList) {
|
||||
// Always filter if matched.
|
||||
if (matchedGroup == soundButton || matchedGroup == infoPanel || matchedGroup == channelBar)
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
if (matchedGroup == soundButton ||
|
||||
matchedGroup == infoPanel ||
|
||||
matchedGroup == channelBar ||
|
||||
matchedGroup == subscribeButtonPaused
|
||||
) return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
|
||||
// Video action buttons (comment, share, remix) have the same path.
|
||||
if (matchedGroup == videoActionButton) {
|
||||
if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter other path groups from pathFilterGroupList, only when reelChannelBar is visible
|
||||
// to avoid false positives.
|
||||
if (!path.startsWith(REEL_CHANNEL_BAR_PATH))
|
||||
return false;
|
||||
} else if (matchedGroup == shortsShelfHeader) {
|
||||
if (path.startsWith(REEL_CHANNEL_BAR_PATH))
|
||||
if (matchedGroup == subscribeButton) return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex
|
||||
);
|
||||
|
||||
return false;
|
||||
} else if (matchedGroup == shelfHeader) {
|
||||
// Because the header is used in watch history and possibly other places, check for the index,
|
||||
// which is 0 when the shelf header is used for Shorts.
|
||||
if (matchedIndex != 0) return false;
|
||||
}
|
||||
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
|
||||
public static void hideShortsShelf(final View shortsShelfView) {
|
||||
hideViewBy1dpUnderCondition(SettingsEnum.HIDE_SHORTS, shortsShelfView);
|
||||
}
|
||||
|
||||
// Additional components that have to be hidden by setting their visibility
|
||||
// region Hide the buttons in older versions of YouTube. New versions use Litho.
|
||||
|
||||
public static void hideShortsCommentsButton(final View commentsButtonView) {
|
||||
hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON, commentsButtonView);
|
||||
@@ -106,6 +165,8 @@ public final class ShortsFilter extends Filter {
|
||||
hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_SHARE_BUTTON, shareButtonView);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
public static void hideNavigationBar() {
|
||||
if (!SettingsEnum.HIDE_SHORTS_NAVIGATION_BAR.getBoolean()) return;
|
||||
if (pivotBar == null) return;
|
||||
|
||||
@@ -3,71 +3,47 @@ package app.revanced.integrations.patches.playback.quality;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.integrations.patches.components.VideoQualityMenuFilterPatch;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import com.facebook.litho.ComponentHost;
|
||||
import kotlin.Deprecated;
|
||||
|
||||
// This patch contains the logic to show the old video quality menu.
|
||||
// Two methods are required, because the quality menu is a RecyclerView in the new YouTube version
|
||||
// and a ListView in the old one.
|
||||
/**
|
||||
* This patch contains the logic to show the old video quality menu.
|
||||
* Two methods are required, because the quality menu is a RecyclerView in the new YouTube version
|
||||
* and a ListView in the old one.
|
||||
*/
|
||||
public final class OldVideoQualityMenuPatch {
|
||||
|
||||
public static void onFlyoutMenuCreate(final LinearLayout linearLayout) {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
|
||||
if (!SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU.getBoolean()) return;
|
||||
|
||||
// The quality menu is a RecyclerView with 3 children. The third child is the "Advanced" quality menu.
|
||||
addRecyclerListener(linearLayout, 3, 2, recyclerView -> {
|
||||
// Check if the current view is the quality menu.
|
||||
if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) {
|
||||
VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false;
|
||||
linearLayout.setVisibility(View.GONE);
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
// Check if the current view is the quality menu.
|
||||
if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) {
|
||||
VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false;
|
||||
|
||||
// Click the "Advanced" quality menu to show the "old" quality menu.
|
||||
((ComponentHost) recyclerView.getChildAt(0)).getChildAt(3).performClick();
|
||||
LogHelper.printDebug(() -> "Advanced quality menu in new type of quality menu clicked");
|
||||
((ViewGroup) recyclerView.getParent().getParent().getParent()).setVisibility(View.GONE);
|
||||
View advancedQualityView = ((ViewGroup) recyclerView.getChildAt(0)).getChildAt(3);
|
||||
if (advancedQualityView != null) {
|
||||
// Click the "Advanced" quality menu to show the "old" quality menu.
|
||||
advancedQualityView.performClick();
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void addRecyclerListener(@NonNull LinearLayout linearLayout,
|
||||
int expectedLayoutChildCount, int recyclerViewIndex,
|
||||
@NonNull RecyclerViewGlobalLayoutListener listener) {
|
||||
if (linearLayout.getChildCount() != expectedLayoutChildCount) return;
|
||||
|
||||
var layoutChild = linearLayout.getChildAt(recyclerViewIndex);
|
||||
if (!(layoutChild instanceof RecyclerView)) return;
|
||||
final var recyclerView = (RecyclerView) layoutChild;
|
||||
|
||||
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
try {
|
||||
listener.recyclerOnGlobalLayout(recyclerView);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "addRecyclerListener failure", ex);
|
||||
} finally {
|
||||
// Remove the listener because it will be added again.
|
||||
recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public interface RecyclerViewGlobalLayoutListener {
|
||||
void recyclerOnGlobalLayout(@NonNull RecyclerView recyclerView);
|
||||
}
|
||||
|
||||
@Deprecated(message = "This patch is deprecated because the quality menu is not a ListView anymore")
|
||||
/**
|
||||
* Injection point. Only used if spoofing to an old app version.
|
||||
*/
|
||||
public static void showOldVideoQualityMenu(final ListView listView) {
|
||||
if (!SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU.getBoolean()) return;
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
import android.preference.ListPreference;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.integrations.patches.components.PlaybackSpeedMenuFilterPatch;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import com.facebook.litho.ComponentHost;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static app.revanced.integrations.patches.playback.quality.OldVideoQualityMenuPatch.addRecyclerListener;
|
||||
|
||||
public class CustomPlaybackSpeedPatch {
|
||||
/**
|
||||
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
||||
* Limit is required otherwise double digit speeds show up out of order in the UI selector.
|
||||
*/
|
||||
public static final float MAXIMUM_PLAYBACK_SPEED = 10;
|
||||
|
||||
@@ -26,16 +26,6 @@ public class CustomPlaybackSpeedPatch {
|
||||
*/
|
||||
public static float[] customPlaybackSpeeds;
|
||||
|
||||
/**
|
||||
* Minimum value of {@link #customPlaybackSpeeds}
|
||||
*/
|
||||
public static float minPlaybackSpeed;
|
||||
|
||||
/**
|
||||
* Maxium value of {@link #customPlaybackSpeeds}
|
||||
*/
|
||||
public static float maxPlaybackSpeed;
|
||||
|
||||
/**
|
||||
* PreferenceList entries and values, of all available playback speeds.
|
||||
*/
|
||||
@@ -69,8 +59,6 @@ public class CustomPlaybackSpeedPatch {
|
||||
loadCustomSpeeds();
|
||||
return;
|
||||
}
|
||||
minPlaybackSpeed = Math.min(minPlaybackSpeed, speed);
|
||||
maxPlaybackSpeed = Math.max(maxPlaybackSpeed, speed);
|
||||
customPlaybackSpeeds[i] = speed;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@@ -106,25 +94,37 @@ public class CustomPlaybackSpeedPatch {
|
||||
preference.setEntryValues(preferenceListEntryValues);
|
||||
}
|
||||
|
||||
/*
|
||||
* To reduce copy and paste between two similar code paths.
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onFlyoutMenuCreate(final LinearLayout linearLayout) {
|
||||
// The playback rate menu is a RecyclerView with 2 children. The third child is the "Advanced" quality menu.
|
||||
addRecyclerListener(linearLayout, 2, 1, recyclerView -> {
|
||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible) {
|
||||
PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false;
|
||||
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
// For some reason, the custom playback speed flyout panel is activated when the user opens the share panel. (A/B tests)
|
||||
// Check the child count of playback speed flyout panel to prevent this issue.
|
||||
// Child count of playback speed flyout panel is always 8.
|
||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible
|
||||
&& ((ViewGroup) recyclerView.getChildAt(0)).getChildCount() == 8) {
|
||||
PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false;
|
||||
ViewGroup parentView3rd = (ViewGroup) recyclerView.getParent().getParent().getParent();
|
||||
ViewGroup parentView4th = (ViewGroup) parentView3rd.getParent();
|
||||
|
||||
if (recyclerView.getChildCount() == 1 && recyclerView.getChildAt(0) instanceof ComponentHost) {
|
||||
linearLayout.setVisibility(View.GONE);
|
||||
// Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
|
||||
// This only shows in phone layout.
|
||||
parentView4th.getChildAt(0).performClick();
|
||||
|
||||
// Close the new Playback speed menu and instead show the old one.
|
||||
// In tablet layout there is no Dismiss View, instead we just hide all two parent views.
|
||||
parentView3rd.setVisibility(View.GONE);
|
||||
parentView4th.setVisibility(View.GONE);
|
||||
|
||||
// This works without issues for both tablet and phone layouts,
|
||||
// So no code is needed to check whether the current device is a tablet or phone.
|
||||
|
||||
// Close the new Playback speed menu and show the old one.
|
||||
showOldPlaybackSpeedMenu();
|
||||
|
||||
// DismissView [R.id.touch_outside] is the 1st ChildView of the 3rd ParentView.
|
||||
((ViewGroup) linearLayout.getParent().getParent().getParent())
|
||||
.getChildAt(0).performClick();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.integrations.patches;
|
||||
package app.revanced.integrations.patches.spoof;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.revanced.integrations.patches.spoof;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class SpoofDeviceDimensionsPatch {
|
||||
private static final boolean SPOOF = SettingsEnum.SPOOF_DEVICE_DIMENSIONS.getBoolean();
|
||||
public static int getMinHeightOrWidth(int minHeightOrWidth) {
|
||||
return SPOOF ? 64 : minHeightOrWidth;
|
||||
}
|
||||
|
||||
public static int getMaxHeightOrWidth(int maxHeightOrWidth) {
|
||||
return SPOOF ? 4096 : maxHeightOrWidth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package app.revanced.integrations.patches.spoof;
|
||||
|
||||
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
||||
|
||||
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.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class SpoofSignaturePatch {
|
||||
/**
|
||||
* Parameter (also used by
|
||||
* <a href="https://github.com/yt-dlp/yt-dlp/blob/81ca451480051d7ce1a31c017e005358345a9149/yt_dlp/extractor/youtube.py#L3602">yt-dlp</a>)
|
||||
* to fix playback issues.
|
||||
*/
|
||||
private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Parameters used in YouTube Shorts.
|
||||
*/
|
||||
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
|
||||
|
||||
/**
|
||||
* Last video id loaded. Used to prevent reloading the same spec multiple times.
|
||||
*/
|
||||
private static volatile String lastPlayerResponseVideoId;
|
||||
|
||||
private static volatile Future<StoryboardRenderer> rendererFuture;
|
||||
|
||||
@Nullable
|
||||
private static StoryboardRenderer getRenderer() {
|
||||
if (rendererFuture != null) {
|
||||
try {
|
||||
return rendererFuture.get(5000, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
// Should never happen.
|
||||
LogHelper.printException(() -> "Could not get renderer", ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called off the main thread, and called multiple times for each video.
|
||||
*
|
||||
* @param parameters Original protobuf parameter value.
|
||||
*/
|
||||
public static String spoofParameter(String parameters) {
|
||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters;
|
||||
|
||||
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
|
||||
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
||||
// Clips are 60 seconds or less in length, so no spoofing.
|
||||
var isClip = parameters.length() > 150;
|
||||
if (isClip) return parameters;
|
||||
|
||||
// Shorts do not need to be spoofed.
|
||||
if (parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) return parameters;
|
||||
|
||||
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL && containsAny(parameters, AUTOPLAY_PARAMETERS);
|
||||
if (isPlayingFeed) return SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean() ?
|
||||
// Prepend the scrim parameter to mute videos in feed.
|
||||
SCRIM_PARAMETER + INCOGNITO_PARAMETERS :
|
||||
// In order to prevent videos that are auto-played in feed to be added to history,
|
||||
// only spoof the parameter if the video is not playing in the feed.
|
||||
// This will cause playback issues in the feed, but it's better than manipulating the history.
|
||||
parameters;
|
||||
|
||||
fetchStoryboardRenderer();
|
||||
|
||||
return INCOGNITO_PARAMETERS;
|
||||
}
|
||||
|
||||
private static void fetchStoryboardRenderer() {
|
||||
String videoId = VideoInformation.getPlayerResponseVideoId();
|
||||
if (!videoId.equals(lastPlayerResponseVideoId)) {
|
||||
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
|
||||
lastPlayerResponseVideoId = videoId;
|
||||
}
|
||||
// Block until the fetch is completed. Without this, occasionally when a new video is opened
|
||||
// the video will be frozen a few seconds while the audio plays.
|
||||
// This is because the main thread is calling to get the storyboard but the fetch is not completed.
|
||||
// To prevent this, call get() here and block until the fetch is completed.
|
||||
// So later when the main thread calls to get the renderer it will never block as the future is done.
|
||||
getRenderer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getSeekbarThumbnailOverrideValue() {
|
||||
return SettingsEnum.SPOOF_SIGNATURE.getBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called from background threads and from the main thread.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
|
||||
StoryboardRenderer renderer = getRenderer();
|
||||
if (renderer != null) return renderer.getSpec();
|
||||
}
|
||||
|
||||
return originalStoryboardRendererSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getRecommendedLevel(int originalLevel) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
|
||||
StoryboardRenderer renderer = getRenderer();
|
||||
if (renderer != null) {
|
||||
Integer recommendedLevel = renderer.getRecommendedLevel();
|
||||
if (recommendedLevel != null) return recommendedLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return originalLevel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package app.revanced.integrations.patches.spoof;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class StoryboardRenderer {
|
||||
private final String spec;
|
||||
@Nullable
|
||||
private final Integer recommendedLevel;
|
||||
|
||||
public StoryboardRenderer(String spec, @Nullable Integer recommendedLevel) {
|
||||
this.spec = spec;
|
||||
this.recommendedLevel = recommendedLevel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getSpec() {
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Recommended image quality level, or NULL if no recommendation exists.
|
||||
*/
|
||||
@Nullable
|
||||
public Integer getRecommendedLevel() {
|
||||
return recommendedLevel;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StoryboardRenderer{" +
|
||||
"spec='" + spec + '\'' +
|
||||
", recommendedLevel=" + recommendedLevel +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package app.revanced.integrations.patches.spoof.requests;
|
||||
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.requests.Route;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
final class PlayerRoutes {
|
||||
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
|
||||
static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
|
||||
Route.Method.POST,
|
||||
"player" +
|
||||
"?fields=storyboards.playerStoryboardSpecRenderer," +
|
||||
"storyboards.playerLiveStoryboardSpecRenderer," +
|
||||
"playabilityStatus.status"
|
||||
).compile();
|
||||
|
||||
static final String ANDROID_INNER_TUBE_BODY;
|
||||
static final String TV_EMBED_INNER_TUBE_BODY;
|
||||
|
||||
static {
|
||||
JSONObject innerTubeBody = new JSONObject();
|
||||
|
||||
try {
|
||||
JSONObject context = new JSONObject();
|
||||
|
||||
JSONObject client = new JSONObject();
|
||||
client.put("clientName", "ANDROID");
|
||||
client.put("clientVersion", "18.37.36");
|
||||
client.put("androidSdkVersion", 34);
|
||||
|
||||
context.put("client", client);
|
||||
|
||||
innerTubeBody.put("context", context);
|
||||
innerTubeBody.put("videoId", "%s");
|
||||
} catch (JSONException e) {
|
||||
LogHelper.printException(() -> "Failed to create innerTubeBody", e);
|
||||
}
|
||||
|
||||
ANDROID_INNER_TUBE_BODY = innerTubeBody.toString();
|
||||
|
||||
JSONObject tvEmbedInnerTubeBody = new JSONObject();
|
||||
|
||||
try {
|
||||
JSONObject context = new JSONObject();
|
||||
|
||||
JSONObject client = new JSONObject();
|
||||
client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER");
|
||||
client.put("clientVersion", "2.0");
|
||||
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) {
|
||||
LogHelper.printException(() -> "Failed to create tvEmbedInnerTubeBody", e);
|
||||
}
|
||||
|
||||
TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString();
|
||||
}
|
||||
|
||||
private PlayerRoutes() {
|
||||
}
|
||||
|
||||
/** @noinspection SameParameterValue*/
|
||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException {
|
||||
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
||||
|
||||
connection.setRequestProperty(
|
||||
"User-Agent", "com.google.android.youtube/" +
|
||||
ReVancedUtils.getVersionName() +
|
||||
" (Linux; U; Android 12; GB) gzip"
|
||||
);
|
||||
connection.setRequestProperty("X-Goog-Api-Format-Version", "2");
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
|
||||
connection.setUseCaches(false);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package app.revanced.integrations.patches.spoof.requests;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.*;
|
||||
|
||||
public class StoryboardRendererRequester {
|
||||
private StoryboardRendererRequester() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject fetchPlayerResponse(@NonNull String requestBody) {
|
||||
try {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
Objects.requireNonNull(requestBody);
|
||||
|
||||
final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER);
|
||||
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
||||
|
||||
LogHelper.printException(() -> "API not available: " + responseCode);
|
||||
connection.disconnect();
|
||||
} catch (SocketTimeoutException ex) {
|
||||
LogHelper.printException(() -> "API timed out", ex);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) {
|
||||
try {
|
||||
return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK");
|
||||
} catch (JSONException e) {
|
||||
LogHelper.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 innerTubeBody) {
|
||||
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody);
|
||||
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
|
||||
return getStoryboardRendererUsingResponse(playerResponse);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) {
|
||||
try {
|
||||
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
|
||||
final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer")
|
||||
? "playerLiveStoryboardSpecRenderer"
|
||||
: "playerStoryboardSpecRenderer";
|
||||
|
||||
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
|
||||
StoryboardRenderer renderer = new StoryboardRenderer(
|
||||
rendererElement.getString("spec"),
|
||||
rendererElement.has("recommendedLevel")
|
||||
? rendererElement.getInt("recommendedLevel")
|
||||
: null
|
||||
);
|
||||
|
||||
LogHelper.printDebug(() -> "Fetched: " + renderer);
|
||||
|
||||
return renderer;
|
||||
} catch (JSONException e) {
|
||||
LogHelper.printException(() -> "Failed to get storyboardRenderer", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
||||
try {
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
var renderer = getStoryboardRendererUsingBody(String.format(ANDROID_INNER_TUBE_BODY, videoId));
|
||||
if (renderer == null) {
|
||||
LogHelper.printDebug(() -> videoId + " not available using Android client");
|
||||
renderer = getStoryboardRendererUsingBody(String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId));
|
||||
if (renderer == null) {
|
||||
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
|
||||
}
|
||||
}
|
||||
|
||||
return renderer;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.revanced.integrations.patches.theme;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.ThemeHelper;
|
||||
|
||||
public class ThemeLithoComponentsPatch {
|
||||
public class ThemePatch {
|
||||
// color constants used in relation with litho components
|
||||
private static final int[] WHITE_VALUES = {
|
||||
-1, // comments chip background
|
||||
@@ -40,6 +41,10 @@ public class ThemeLithoComponentsPatch {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
public static boolean gradientLoadingScreenEnabled() {
|
||||
return SettingsEnum.GRADIENT_LOADING_SCREEN.getBoolean();
|
||||
}
|
||||
|
||||
private static int getBlackColor() {
|
||||
if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1");
|
||||
return blackColor;
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.integrations.requests;
|
||||
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -16,10 +17,14 @@ public class Requester {
|
||||
}
|
||||
|
||||
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
|
||||
String url = apiUrl + route.compile(params).getCompiledRoute();
|
||||
return getConnectionFromCompiledRoute(apiUrl, route.compile(params));
|
||||
}
|
||||
|
||||
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
||||
String url = apiUrl + route.getCompiledRoute();
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod(route.getMethod().name());
|
||||
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced");
|
||||
connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + "; ReVanced/" + ReVancedUtils.getVersionName());
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,6 @@ import androidx.annotation.NonNull;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
/**
|
||||
* ReturnYouTubeDislike API estimated like/dislike/view counts.
|
||||
*
|
||||
@@ -81,17 +79,21 @@ public final class RYDVoteData {
|
||||
}
|
||||
|
||||
public void updateUsingVote(Vote vote) {
|
||||
if (vote == Vote.LIKE) {
|
||||
likeCount = fetchedLikeCount + 1;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
} else if (vote == Vote.DISLIKE) {
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount + 1;
|
||||
} else if (vote == Vote.LIKE_REMOVE) {
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
switch (vote) {
|
||||
case LIKE:
|
||||
likeCount = fetchedLikeCount + 1;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
break;
|
||||
case DISLIKE:
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount + 1;
|
||||
break;
|
||||
case LIKE_REMOVE:
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
updatePercentages();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user