From 0d2d45a597dbfa610f03eac1fb9103ceea29e2de Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Wed, 29 Jan 2025 19:41:46 +0900 Subject: [PATCH] feat(YouTube Music - Spoof client): Add support for latest versions, Remove 'Spoof streaming data' patch - 'Spoof client' patch now supports YouTube Music 7.25.53+ - 'Spoof client' patch now spoofs as many innerTube contexts as possible, just like the 'Spoof streaming data' patch - 'Spoof streaming data' patch is no longer a shared patch - Nevertheless, 'BlockRequestPatch' is still a shared patch, so each setting is located in a shared path (BaseSettings) - In YouTube Music 7.16.53 or earlier, 'iOS YouTube Music 6.21' is the only client that does not require 'BlockRequestPatch' - 'iOS YouTube Music 7.04' supports opus codec when 'Enable OPUS codec' is turned on --- .../music/patches/misc/client/AppClient.java | 118 ------ .../extension/music/settings/Settings.java | 5 - .../ReVancedPreferenceFragment.java | 2 - .../extension/shared/patches/PatchStatus.java | 9 +- .../shared/patches/client/MusicAppClient.java | 189 +++++++++ .../{AppClient.kt => YouTubeAppClient.kt} | 62 +-- .../{WebClient.kt => YouTubeWebClient.kt} | 2 +- .../patches/spoof/BlockRequestPatch.java | 110 ++++++ .../patches/spoof}/SpoofClientPatch.java | 60 ++- .../spoof/SpoofStreamingDataPatch.java | 141 +------ .../patches/spoof/requests/PlayerRoutes.kt | 12 +- .../spoof/requests/StreamingDataRequest.kt | 17 +- .../shared/settings/BaseSettings.java | 36 +- .../general/requests/VideoDetailsRequest.kt | 4 +- .../patches/video/requests/MusicRequest.kt | 8 +- .../music/utils/fix/client/Fingerprints.kt | 24 -- .../utils/fix/client/SpoofClientPatch.kt | 234 ++++++----- .../streamingdata/SpoofStreamingDataPatch.kt | 67 ---- .../patches/music/utils/patch/PatchList.kt | 4 - .../revanced/patches/shared/Fingerprints.kt | 10 +- .../spoof/blockrequest/BlockRequestPatch.kt | 57 +++ .../shared/spoof/blockrequest/Fingerprints.kt | 40 ++ .../BaseSpoofStreamingDataPatch.kt | 370 ------------------ .../utils/fix}/streamingdata/Fingerprints.kt | 33 +- .../streamingdata/SpoofStreamingDataPatch.kt | 348 +++++++++++++++- .../music/settings/host/values/arrays.xml | 10 +- .../music/settings/host/values/strings.xml | 12 +- 27 files changed, 999 insertions(+), 985 deletions(-) delete mode 100644 extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java create mode 100644 extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java rename extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/{AppClient.kt => YouTubeAppClient.kt} (84%) rename extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/{WebClient.kt => YouTubeWebClient.kt} (97%) create mode 100644 extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/BlockRequestPatch.java rename extensions/shared/src/main/java/app/revanced/extension/{music/patches/misc => shared/patches/spoof}/SpoofClientPatch.java (53%) delete mode 100644 patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/BlockRequestPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/Fingerprints.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt rename patches/src/main/kotlin/app/revanced/patches/{shared/spoof => youtube/utils/fix}/streamingdata/Fingerprints.kt (86%) diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java deleted file mode 100644 index 487ef08c4..000000000 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java +++ /dev/null @@ -1,118 +0,0 @@ -package app.revanced.extension.music.patches.misc.client; - -import android.os.Build; - -public class AppClient { - - // Audio codec is MP4A. - private static final String CLIENT_VERSION_ANDROID_MUSIC_4_27 = "4.27.53"; - - // Audio codec is OPUS. - private static final String CLIENT_VERSION_ANDROID_MUSIC_5_29 = "5.29.53"; - - private static final String PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music"; - private static final String DEVICE_MODEL_ANDROID_MUSIC = Build.MODEL; - private static final String OS_VERSION_ANDROID_MUSIC = Build.VERSION.RELEASE; - - // Audio codec is MP4A. - private static final String CLIENT_VERSION_IOS_MUSIC_6_21 = "6.21"; - - // Audio codec is OPUS. - private static final String CLIENT_VERSION_IOS_MUSIC_7_04 = "7.04"; - - private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic"; - private static final String DEVICE_MODEL_IOS_MUSIC = "iPhone14,3"; - private static final String OS_VERSION_IOS_MUSIC = "15.7.1.19H117"; - private static final String USER_AGENT_VERSION_IOS_MUSIC = "15_7_1"; - - private AppClient() { - } - - private static String androidUserAgent(String clientVersion) { - return PACKAGE_NAME_ANDROID_MUSIC + - "/" + - clientVersion + - " (Linux; U; Android " + - OS_VERSION_ANDROID_MUSIC + - "; GB) gzip"; - } - - private static String iOSUserAgent(String clientVersion) { - return PACKAGE_NAME_IOS_MUSIC + - "/" + - clientVersion + - "(" + - DEVICE_MODEL_IOS_MUSIC + - "; U; CPU iOS " + - USER_AGENT_VERSION_IOS_MUSIC + - " like Mac OS X)"; - } - - public enum ClientType { - ANDROID_MUSIC_4_27(21, - DEVICE_MODEL_ANDROID_MUSIC, - OS_VERSION_ANDROID_MUSIC, - androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_4_27), - CLIENT_VERSION_ANDROID_MUSIC_4_27 - ), - ANDROID_MUSIC_5_29(21, - DEVICE_MODEL_ANDROID_MUSIC, - OS_VERSION_ANDROID_MUSIC, - androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_5_29), - CLIENT_VERSION_ANDROID_MUSIC_5_29 - ), - IOS_MUSIC_6_21( - 26, - DEVICE_MODEL_IOS_MUSIC, - OS_VERSION_IOS_MUSIC, - iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_6_21), - CLIENT_VERSION_IOS_MUSIC_6_21 - ), - IOS_MUSIC_7_04( - 26, - DEVICE_MODEL_IOS_MUSIC, - OS_VERSION_IOS_MUSIC, - iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_7_04), - CLIENT_VERSION_IOS_MUSIC_7_04 - ); - - /** - * YouTube - * client type - */ - public final int id; - - /** - * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) - */ - public final String deviceModel; - - /** - * Device OS version. - */ - public final String osVersion; - - /** - * Player user-agent. - */ - public final String userAgent; - - /** - * App version. - */ - public final String clientVersion; - - ClientType(int id, - String deviceModel, - String osVersion, - String userAgent, - String clientVersion - ) { - this.id = id; - this.deviceModel = deviceModel; - this.clientVersion = clientVersion; - this.osVersion = osVersion; - this.userAgent = userAgent; - } - } -} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java index 541234ec6..41db5e573 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -8,7 +8,6 @@ import androidx.annotation.NonNull; import app.revanced.extension.music.patches.general.ChangeStartPagePatch.StartPage; import app.revanced.extension.music.patches.misc.AlbumMusicVideoPatch.RedirectType; -import app.revanced.extension.music.patches.misc.client.AppClient.ClientType; import app.revanced.extension.music.patches.utils.PatchStatus; import app.revanced.extension.music.sponsorblock.SponsorBlockSettings; import app.revanced.extension.shared.settings.BaseSettings; @@ -186,9 +185,6 @@ public class Settings extends BaseSettings { public static final EnumSetting DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true); public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true); public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); - public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true); - public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS_MUSIC_6_21, true); - // PreferenceScreen: Return YouTube Dislike public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE); @@ -271,7 +267,6 @@ public class Settings extends BaseSettings { SETTINGS_IMPORT_EXPORT.key, SPOOF_APP_VERSION_TARGET.key, SPOOF_CLIENT_TYPE.key, - SPOOF_STREAMING_DATA_TYPE.key, RETURN_YOUTUBE_USERNAME_ABOUT.key, RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.key, RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key, diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java index ba0316daa..6175d3d1f 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java @@ -20,7 +20,6 @@ import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams; import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog; import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT; import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY; -import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE; import static app.revanced.extension.shared.settings.Setting.getSettingFromPath; import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray; import static app.revanced.extension.shared.utils.StringRef.str; @@ -165,7 +164,6 @@ public class ReVancedPreferenceFragment extends PreferenceFragment { || settings.equals(DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE) || settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT) || settings.equals(SPOOF_CLIENT_TYPE) - || settings.equals(SPOOF_STREAMING_DATA_TYPE) ) { ResettableListPreference.showDialog(mActivity, enumSetting, 0); } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java index 20bf9d5e0..88d0d0251 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java @@ -6,8 +6,13 @@ public class PatchStatus { return false; } - public static boolean SpoofStreamingDataMusic() { - // Replace this with true If the Spoof streaming data patch succeeds in YouTube Music + public static boolean SpoofClient() { + // Replace this with true If the Spoof streaming data patch succeeds in YouTube Music. + return false; + } + + public static boolean SpoofStreamingData() { + // Replace this with true If the Spoof streaming data patch succeeds in YouTube. return false; } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java new file mode 100644 index 000000000..59fd1ce87 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java @@ -0,0 +1,189 @@ +package app.revanced.extension.shared.patches.client; + +import android.os.Build; + +import java.util.Locale; + +public class MusicAppClient { + + // Audio codec is MP4A. + private static final String CLIENT_VERSION_ANDROID_MUSIC_4_27 = "4.27.53"; + + // Audio codec is OPUS. + private static final String CLIENT_VERSION_ANDROID_MUSIC_5_29 = "5.29.53"; + + private static final String PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music"; + private static final String DEVICE_BRAND_ANDROID_MUSIC = Build.BRAND; + private static final String DEVICE_MAKE_ANDROID_MUSIC = Build.MANUFACTURER; + private static final String DEVICE_MODEL_ANDROID_MUSIC = Build.MODEL; + private static final String BUILD_ID_ANDROID_MUSIC = Build.ID; + private static final String OS_NAME_ANDROID_MUSIC = "Android Automotive"; + private static final String OS_VERSION_ANDROID_MUSIC = Build.VERSION.RELEASE; + + // Audio codec is MP4A. + private static final String CLIENT_VERSION_IOS_MUSIC_6_21 = "6.21"; + private static final String DEVICE_MODEL_IOS_MUSIC_6_21 = "iPhone14,3"; + private static final String OS_VERSION_IOS_MUSIC_6_21 = "15.7.1.19H117"; + private static final String USER_AGENT_VERSION_IOS_MUSIC_6_21 = "15_7_1"; + + // Audio codec is OPUS. + private static final String CLIENT_VERSION_IOS_MUSIC_7_04 = "7.04"; + private static final String DEVICE_MODEL_IOS_MUSIC_7_04 = "iPhone17,2"; + private static final String OS_VERSION_IOS_MUSIC_7_04 = "18.2.1.22C161"; + private static final String USER_AGENT_VERSION_IOS_MUSIC_7_04 = "18_2_1"; + + private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic"; + private static final String DEVICE_BRAND_IOS_MUSIC = "Apple"; + private static final String DEVICE_MAKE_IOS_MUSIC = "Apple"; + private static final String OS_NAME_IOS_MUSIC = "iOS"; + + private MusicAppClient() { + } + + private static String androidUserAgent(String clientVersion) { + return PACKAGE_NAME_ANDROID_MUSIC + + "/" + + clientVersion + + "(Linux; U; Android " + + OS_VERSION_ANDROID_MUSIC + + "; " + + Locale.getDefault() + + "; " + + DEVICE_MODEL_ANDROID_MUSIC + + " Build/" + + BUILD_ID_ANDROID_MUSIC + + ") gzip"; + } + + private static String iOSUserAgent(String clientVersion, String deviceModel, String osVersion) { + return PACKAGE_NAME_IOS_MUSIC + + "/" + + clientVersion + + " (" + + deviceModel + + "; U; CPU iOS " + + osVersion + + " like Mac OS X; " + + Locale.getDefault() + + ")"; + } + + public enum ActionButtonType { + NONE, // No action button (~ 6.14) + YOUTUBE_BUTTON, // Type of action button is YouTubeButton (6.15 ~ 7.16) + LITHO // Type of action button is ComponentHost (7.17 ~) + } + + public enum ClientType { + ANDROID_MUSIC_4_27(21, + DEVICE_BRAND_ANDROID_MUSIC, + DEVICE_MAKE_ANDROID_MUSIC, + DEVICE_MODEL_ANDROID_MUSIC, + OS_NAME_ANDROID_MUSIC, + OS_VERSION_ANDROID_MUSIC, + androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_4_27), + CLIENT_VERSION_ANDROID_MUSIC_4_27, + ActionButtonType.NONE + ), + ANDROID_MUSIC_5_29(21, + DEVICE_BRAND_ANDROID_MUSIC, + DEVICE_MAKE_ANDROID_MUSIC, + DEVICE_MODEL_ANDROID_MUSIC, + OS_NAME_ANDROID_MUSIC, + OS_VERSION_ANDROID_MUSIC, + androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_5_29), + CLIENT_VERSION_ANDROID_MUSIC_5_29, + ActionButtonType.NONE + ), + IOS_MUSIC_6_21( + 26, + DEVICE_BRAND_IOS_MUSIC, + DEVICE_MAKE_IOS_MUSIC, + DEVICE_MODEL_IOS_MUSIC_6_21, + OS_NAME_IOS_MUSIC, + OS_VERSION_IOS_MUSIC_6_21, + iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_6_21, DEVICE_MODEL_IOS_MUSIC_6_21, USER_AGENT_VERSION_IOS_MUSIC_6_21), + CLIENT_VERSION_IOS_MUSIC_6_21, + ActionButtonType.YOUTUBE_BUTTON + ), + IOS_MUSIC_7_04( + 26, + DEVICE_BRAND_IOS_MUSIC, + DEVICE_MAKE_IOS_MUSIC, + DEVICE_MODEL_IOS_MUSIC_7_04, + OS_NAME_IOS_MUSIC, + OS_VERSION_IOS_MUSIC_7_04, + iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_7_04, DEVICE_MODEL_IOS_MUSIC_7_04, USER_AGENT_VERSION_IOS_MUSIC_7_04), + CLIENT_VERSION_IOS_MUSIC_7_04, + ActionButtonType.LITHO + ); + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device brand, equivalent to {@link Build#BRAND} (System property: ro.product.vendor.brand) + */ + public final String deviceBrand; + + /** + * Device make, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer) + */ + public final String deviceMake; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String deviceModel; + + /** + * Device OS name. + */ + public final String osName; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * App version. + */ + public final String clientVersion; + + private final ActionButtonType actionButtonType; + + ClientType(int id, + String deviceBrand, + String deviceMake, + String deviceModel, + String osName, + String osVersion, + String userAgent, + String clientVersion, + ActionButtonType actionButtonType + ) { + this.id = id; + this.deviceBrand = deviceBrand; + this.deviceMake = deviceMake; + this.deviceModel = deviceModel; + this.clientVersion = clientVersion; + this.osName = osName; + this.osVersion = osVersion; + this.userAgent = userAgent; + this.actionButtonType = actionButtonType; + } + + public boolean isYouTubeButton() { + return actionButtonType == ActionButtonType.YOUTUBE_BUTTON; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt similarity index 84% rename from extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.kt rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt index 9ebc13426..f2459659d 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt @@ -1,7 +1,6 @@ package app.revanced.extension.shared.patches.client import android.os.Build -import app.revanced.extension.shared.patches.PatchStatus import app.revanced.extension.shared.settings.BaseSettings import org.apache.commons.lang3.ArrayUtils import java.util.Locale @@ -9,7 +8,7 @@ import java.util.Locale /** * Used to fetch streaming data. */ -object AppClient { +object YouTubeAppClient { // IOS /** * Video not playable: Paid / Movie / Private / Age-restricted @@ -108,7 +107,6 @@ object AppClient { */ private const val ANDROID_SDK_VERSION_ANDROID_VR = "32" private const val BUILD_ID_ANDROID_VR = "SQ3A.220605.009.A1" - private const val CHIPSET_ANDROID_VR = "Qualcomm;SXR2230P" private val USER_AGENT_ANDROID_VR = androidUserAgent( packageName = PACKAGE_NAME_ANDROID_VR, @@ -137,7 +135,6 @@ object AppClient { private const val ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34" private const val BUILD_ID_ANDROID_UNPLUGGED = "UTT3.240625.001.K5" private const val GMS_CORE_VERSION_CODE_ANDROID_UNPLUGGED = "244336107" - private const val CHIPSET_ANDROID_UNPLUGGED = "Mediatek;MT8696" private val USER_AGENT_ANDROID_UNPLUGGED = androidUserAgent( packageName = PACKAGE_NAME_ANDROID_UNPLUGGED, @@ -166,7 +163,6 @@ object AppClient { private const val ANDROID_SDK_VERSION_ANDROID_CREATOR = "35" private const val BUILD_ID_ANDROID_CREATOR = "AP3A.241005.015.A2" private const val GMS_CORE_VERSION_CODE_ANDROID_CREATOR = "244738035" - private const val CHIPSET_ANDROID_CREATOR = "Google;Tensor G4" private val USER_AGENT_ANDROID_CREATOR = androidUserAgent( packageName = PACKAGE_NAME_ANDROID_CREATOR, @@ -177,39 +173,6 @@ object AppClient { ) - // ANDROID MUSIC - /** - * Video not playable: All videos that can't be played on YouTube Music - */ - private const val PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music" - - /** - * Older client versions don't seem to require poToken. - * It is not the default client yet, as it requires sufficient testing. - */ - private const val CLIENT_VERSION_ANDROID_MUSIC = "4.27.53" - - /** - * The device machine id for the Google Pixel 4. - * See [this GitLab](https://dumps.tadiphone.dev/dumps/google/flame) for more information. - */ - private const val DEVICE_MODEL_ANDROID_MUSIC = "Pixel 4" - private const val DEVICE_MAKE_ANDROID_MUSIC = "Google" - private const val OS_VERSION_ANDROID_MUSIC = "11" - private const val ANDROID_SDK_VERSION_ANDROID_MUSIC = "30" - private const val BUILD_ID_ANDROID_MUSIC = "SPP2.210219.008" - private const val GMS_CORE_VERSION_CODE_ANDROID_MUSIC = "244738022" - private const val CHIPSET_ANDROID_MUSIC = "Qualcomm;SM8150" - - private val USER_AGENT_ANDROID_MUSIC = androidUserAgent( - packageName = PACKAGE_NAME_ANDROID_MUSIC, - clientVersion = CLIENT_VERSION_ANDROID_MUSIC, - osVersion = OS_VERSION_ANDROID_MUSIC, - deviceModel = DEVICE_MODEL_ANDROID_MUSIC, - buildId = BUILD_ID_ANDROID_MUSIC - ) - - /** * Same format as Android YouTube User-Agent. * Example: 'com.google.android.youtube/19.46.40(Linux; U; Android 13; in_ID; 21061110AG Build/TP1A.220624.014) gzip' @@ -240,10 +203,7 @@ object AppClient { } fun availableClientTypes(preferredClient: ClientType): Array { - val availableClientTypes = if (PatchStatus.SpoofStreamingDataMusic()) - ClientType.CLIENT_ORDER_TO_USE_YOUTUBE_MUSIC - else - ClientType.CLIENT_ORDER_TO_USE_YOUTUBE + val availableClientTypes = ClientType.CLIENT_ORDER_TO_USE_YOUTUBE if (ArrayUtils.contains(availableClientTypes, preferredClient)) { val clientToUse: Array = arrayOfNulls(availableClientTypes.size) @@ -400,19 +360,6 @@ object AppClient { "iOS Force AVC" else "iOS" - ), - ANDROID_MUSIC( - id = 21, - deviceMake = DEVICE_MAKE_ANDROID_MUSIC, - deviceModel = DEVICE_MODEL_ANDROID_MUSIC, - osVersion = OS_VERSION_ANDROID_MUSIC, - userAgent = USER_AGENT_ANDROID_MUSIC, - androidSdkVersion = ANDROID_SDK_VERSION_ANDROID_MUSIC, - clientVersion = CLIENT_VERSION_ANDROID_MUSIC, - gmscoreVersionCode = GMS_CORE_VERSION_CODE_ANDROID_MUSIC, - requireAuth = true, - clientName = "ANDROID_MUSIC", - friendlyName = "Android Music" ); companion object { @@ -424,11 +371,6 @@ object AppClient { IOS, ANDROID_VR_NO_AUTH, ) - - internal val CLIENT_ORDER_TO_USE_YOUTUBE_MUSIC: Array = arrayOf( - ANDROID_VR, - ANDROID_MUSIC, - ) } } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/WebClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeWebClient.kt similarity index 97% rename from extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/WebClient.kt rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeWebClient.kt index f39a56c60..331d1d9a4 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/WebClient.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeWebClient.kt @@ -4,7 +4,7 @@ package app.revanced.extension.shared.patches.client * Used to fetch video information. */ @Suppress("unused") -object WebClient { +object YouTubeWebClient { /** * This user agent does not require a PoToken in [ClientType.MWEB] * https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259 diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/BlockRequestPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/BlockRequestPatch.java new file mode 100644 index 000000000..c0728522d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/BlockRequestPatch.java @@ -0,0 +1,110 @@ +package app.revanced.extension.shared.patches.spoof; + +import android.net.Uri; + +import app.revanced.extension.shared.patches.PatchStatus; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.PackageUtils; + +@SuppressWarnings("unused") +public class BlockRequestPatch { + /** + * Used in YouTube. + */ + public static final boolean SPOOF_STREAMING_DATA = + BaseSettings.SPOOF_STREAMING_DATA.get() && PatchStatus.SpoofStreamingData(); + + /** + * Used in YouTube Music. + */ + public static final boolean SPOOF_CLIENT = + BaseSettings.SPOOF_CLIENT.get() && PatchStatus.SpoofClient(); + + /** + * In order to load the action bar normally, + * Some versions must block the initplayback request. + */ + private static final boolean IS_7_17_OR_GREATER = + PackageUtils.getAppVersionName().compareTo("7.17.00") >= 0; + + private static final boolean IS_YOUTUBE_BUTTON = + BaseSettings.SPOOF_CLIENT_TYPE.get().isYouTubeButton(); + + private static final boolean BLOCK_REQUEST; + + static { + if (SPOOF_STREAMING_DATA) { + BLOCK_REQUEST = true; + } else { + if (!SPOOF_CLIENT) { + BLOCK_REQUEST = false; + } else { + if (!IS_7_17_OR_GREATER && !IS_YOUTUBE_BUTTON) { + // If the current version is lower than 7.16 and the client action button type is not YouTubeButton, + // the initplayback request must be blocked. + BLOCK_REQUEST = true; + } else { + // If the current version is higher than 7.17, + // the initplayback request must always be blocked. + BLOCK_REQUEST = IS_7_17_OR_GREATER; + } + } + } + } + + /** + * 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); + + /** + * 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 (BLOCK_REQUEST) { + 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 (BLOCK_REQUEST) { + try { + var originalUri = Uri.parse(originalUrlString); + String path = originalUri.getPath(); + + if (path != null && path.contains("initplayback")) { + Logger.printDebug(() -> "Blocking 'initplayback' by clearing query"); + + return originalUri.buildUpon().clearQuery().build().toString(); + } + } catch (Exception ex) { + Logger.printException(() -> "blockInitPlaybackRequest failure", ex); + } + } + + return originalUrlString; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java similarity index 53% rename from extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java index e3e651a36..675068ffd 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java @@ -1,66 +1,98 @@ -package app.revanced.extension.music.patches.misc; +package app.revanced.extension.shared.patches.spoof; -import app.revanced.extension.music.patches.misc.client.AppClient.ClientType; +import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType; import app.revanced.extension.music.settings.Settings; @SuppressWarnings("unused") -public class SpoofClientPatch { +public class SpoofClientPatch extends BlockRequestPatch { private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); - public static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); /** * Injection point. */ - public static int getClientTypeId(int originalClientTypeId) { + public static int getClientId(int original) { if (SPOOF_CLIENT) { return CLIENT_TYPE.id; } - return originalClientTypeId; + return original; } /** * Injection point. */ - public static String getClientVersion(String originalClientVersion) { + public static String getClientVersion(String original) { if (SPOOF_CLIENT) { return CLIENT_TYPE.clientVersion; } - return originalClientVersion; + return original; } /** * Injection point. */ - public static String getClientModel(String originalClientModel) { + public static String getDeviceBrand(String original) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.deviceBrand; + } + + return original; + } + + /** + * Injection point. + */ + public static String getDeviceMake(String original) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.deviceMake; + } + + return original; + } + + /** + * Injection point. + */ + public static String getDeviceModel(String original) { if (SPOOF_CLIENT) { return CLIENT_TYPE.deviceModel; } - return originalClientModel; + return original; } /** * Injection point. */ - public static String getOsVersion(String originalOsVersion) { + public static String getOsName(String original) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.osName; + } + + return original; + } + + /** + * Injection point. + */ + public static String getOsVersion(String original) { if (SPOOF_CLIENT) { return CLIENT_TYPE.osVersion; } - return originalOsVersion; + return original; } /** * Injection point. */ - public static String getUserAgent(String originalUserAgent) { + public static String getUserAgent(String original) { if (SPOOF_CLIENT) { return CLIENT_TYPE.userAgent; } - return originalUserAgent; + return original; } /** diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java index 124790b88..d6db51b90 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -1,11 +1,8 @@ package app.revanced.extension.shared.patches.spoof; -import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataMusic; - import android.net.Uri; import android.text.TextUtils; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.nio.ByteBuffer; @@ -13,7 +10,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; -import app.revanced.extension.shared.patches.client.AppClient.ClientType; +import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType; import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.Setting; @@ -21,10 +18,7 @@ import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Utils; @SuppressWarnings("unused") -public class SpoofStreamingDataPatch { - private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_STREAMING_DATA.get(); - private static final boolean SPOOF_STREAMING_DATA_YOUTUBE = SPOOF_STREAMING_DATA && !SpoofStreamingDataMusic(); - private static final boolean SPOOF_STREAMING_DATA_MUSIC = SPOOF_STREAMING_DATA && SpoofStreamingDataMusic(); +public class SpoofStreamingDataPatch extends BlockRequestPatch { private static final String PO_TOKEN = BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get(); private static final String VISITOR_DATA = @@ -50,58 +44,6 @@ public class SpoofStreamingDataPatch { } }); - /** - * 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) { - // An exception may be thrown when the /get_watch request is blocked when connected to Wi-Fi in YouTube Music. - if (SPOOF_STREAMING_DATA_YOUTUBE || Utils.getNetworkType() == Utils.NetworkType.MOBILE) { - 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 clearing query"); - - return originalUri.buildUpon().clearQuery().build().toString(); - } - } catch (Exception ex) { - Logger.printException(() -> "blockInitPlaybackRequest failure", ex); - } - } - - return originalUrlString; - } - /** * Injection point. */ @@ -120,73 +62,20 @@ public class SpoofStreamingDataPatch { return false; } - private static volatile String auth = ""; - private static volatile Map requestHeader; - - private static final String AUTHORIZATION_HEADER = "Authorization"; - - private static final String[] REQUEST_HEADER_KEYS = { - AUTHORIZATION_HEADER, - "X-GOOG-API-FORMAT-VERSION", - "X-Goog-Visitor-Id" + /** + * Parameters causing playback issues. + */ + private static final String[] PATH_NO_VIDEO_ID = { + "ad_break", // This request fetches a list of times when ads can be displayed. + "get_drm_license", // Waiting for a paid video to start. + "heartbeat", // This request determines whether to pause playback when the user is AFK. + "refresh", // Waiting for a livestream to start. }; - /** - * If the /get_watch request is not blocked, - * fetchRequest will not be invoked at the point where the video starts. - *

- * An additional method is used to invoke fetchRequest in YouTube Music: - * 1. Save the requestHeader in a field. - * 2. Invoke fetchRequest with the videoId used in PlaybackStartDescriptor. - *

- * - * @param requestHeaders Save the request Headers used for login to a field. - * Only used in YouTube Music where login is required. - */ - private static void setRequestHeaders(Map requestHeaders) { - if (SPOOF_STREAMING_DATA_MUSIC) { - try { - // Save requestHeaders whenever an account is switched. - String authorization = requestHeaders.get(AUTHORIZATION_HEADER); - if (authorization == null || auth.equals(authorization)) { - return; - } - for (String key : REQUEST_HEADER_KEYS) { - if (requestHeaders.get(key) == null) { - return; - } - } - auth = authorization; - requestHeader = requestHeaders; - } catch (Exception ex) { - Logger.printException(() -> "setRequestHeaders failure", ex); - } - } - } - - /** - * Injection point. - */ - public static void fetchStreams(@NonNull String videoId) { - if (SPOOF_STREAMING_DATA_MUSIC) { - try { - if (requestHeader != null) { - StreamingDataRequest.fetchRequest(videoId, requestHeader, VISITOR_DATA, PO_TOKEN); - } else { - Logger.printDebug(() -> "Ignoring request with no header."); - } - } catch (Exception ex) { - Logger.printException(() -> "fetchStreams failure", ex); - } - } - } - /** * Injection point. */ public static void fetchStreams(String url, Map requestHeaders) { - setRequestHeaders(requestHeaders); - if (SPOOF_STREAMING_DATA) { try { Uri uri = Uri.parse(url); @@ -195,11 +84,7 @@ public class SpoofStreamingDataPatch { return; } - // 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start. - // 'heartbeat' has no video id and appears to be only after playback has started. - // 'refresh' has no video id and appears to happen when waiting for a livestream to start. - // 'ad_break' has no video id. - if (path.contains("get_drm_license") || path.contains("heartbeat") || path.contains("refresh") || path.contains("ad_break")) { + if (Utils.containsAny(path, PATH_NO_VIDEO_ID)) { Logger.printDebug(() -> "Ignoring path: " + path); return; } @@ -262,7 +147,7 @@ public class SpoofStreamingDataPatch { * Called after {@link #getStreamingData(String)}. */ public static void setApproxDurationMs(String videoId, long approxDurationMs) { - if (SPOOF_STREAMING_DATA_YOUTUBE && approxDurationMs != Long.MAX_VALUE) { + if (SPOOF_STREAMING_DATA && approxDurationMs != Long.MAX_VALUE) { approxDurationMsMap.put(videoId, approxDurationMs); Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs); } @@ -284,7 +169,7 @@ public class SpoofStreamingDataPatch { * Called after {@link #getStreamingData(String)}. */ public static long getApproxDurationMs(String videoId) { - if (SPOOF_STREAMING_DATA_YOUTUBE && videoId != null) { + if (SPOOF_STREAMING_DATA && videoId != null) { final Long approxDurationMs = approxDurationMsMap.get(videoId); if (approxDurationMs != null) { Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId); diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt index 5bd228d84..158354778 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt @@ -1,7 +1,7 @@ package app.revanced.extension.shared.patches.spoof.requests -import app.revanced.extension.shared.patches.client.AppClient -import app.revanced.extension.shared.patches.client.WebClient +import app.revanced.extension.shared.patches.client.YouTubeAppClient +import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Route import app.revanced.extension.shared.requests.Route.CompiledRoute @@ -69,7 +69,7 @@ object PlayerRoutes { @JvmStatic fun createApplicationRequestBody( - clientType: AppClient.ClientType, + clientType: YouTubeAppClient.ClientType, videoId: String, playlistId: String? = null, botGuardPoToken: String = "", @@ -130,7 +130,7 @@ object PlayerRoutes { @JvmStatic fun createWebInnertubeBody( - clientType: WebClient.ClientType, + clientType: YouTubeWebClient.ClientType, videoId: String ): ByteArray { val innerTubeBody = JSONObject() @@ -161,7 +161,7 @@ object PlayerRoutes { @JvmStatic fun getPlayerResponseConnectionFromRoute( route: CompiledRoute, - clientType: AppClient.ClientType + clientType: YouTubeAppClient.ClientType ): HttpURLConnection { return getPlayerResponseConnectionFromRoute( route, @@ -174,7 +174,7 @@ object PlayerRoutes { @JvmStatic fun getPlayerResponseConnectionFromRoute( route: CompiledRoute, - clientType: WebClient.ClientType + clientType: YouTubeWebClient.ClientType ): HttpURLConnection { return getPlayerResponseConnectionFromRoute( route, diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt index bd82b3fee..88fc71da2 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt @@ -1,8 +1,7 @@ package app.revanced.extension.shared.patches.spoof.requests import androidx.annotation.GuardedBy -import app.revanced.extension.shared.patches.client.AppClient -import app.revanced.extension.shared.patches.client.AppClient.availableClientTypes +import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute @@ -93,16 +92,16 @@ class StreamingDataRequest private constructor( "X-GOOG-API-FORMAT-VERSION", VISITOR_ID_HEADER ) - private val SPOOF_STREAMING_DATA_TYPE: AppClient.ClientType = + private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType = BaseSettings.SPOOF_STREAMING_DATA_TYPE.get() - private val CLIENT_ORDER_TO_USE: Array = - availableClientTypes(SPOOF_STREAMING_DATA_TYPE) + private val CLIENT_ORDER_TO_USE: Array = + YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE) private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean = - SPOOF_STREAMING_DATA_TYPE == AppClient.ClientType.ANDROID_VR_NO_AUTH + SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH - private var lastSpoofedClientType: AppClient.ClientType? = null + private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null /** @@ -163,7 +162,7 @@ class StreamingDataRequest private constructor( } private fun send( - clientType: AppClient.ClientType, + clientType: YouTubeAppClient.ClientType, videoId: String, playerHeaders: Map, visitorId: String, @@ -279,7 +278,7 @@ class StreamingDataRequest private constructor( } else { BufferedInputStream(connection.inputStream).use { inputStream -> ByteArrayOutputStream().use { stream -> - val buffer = ByteArray(4096) + val buffer = ByteArray(2048) var bytesRead: Int while ((inputStream.read(buffer) .also { bytesRead = it }) >= 0 diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index 63c3c5b05..a8c3e5fbd 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -5,7 +5,8 @@ import static java.lang.Boolean.TRUE; import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean; import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat; -import app.revanced.extension.shared.patches.client.AppClient.ClientType; +import app.revanced.extension.shared.patches.client.MusicAppClient; +import app.revanced.extension.shared.patches.client.YouTubeAppClient; import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability; /** @@ -25,6 +26,28 @@ public class BaseSettings { public static final EnumSetting REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true); + /** + * These settings are used by YouTube Music. + * Some patches are in a shared path, so they are declared here. + */ + public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true); + public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true); + + /** + * These settings are used by YouTube. + * Some patches are in a shared path, so they are declared here. + */ + 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 EnumSetting SPOOF_STREAMING_DATA_LANGUAGE = new EnumSetting<>("revanced_spoof_streaming_data_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability()); + 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"); + public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE); + // Client type must be last spoof setting due to cyclic references. + public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true); + + public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true); + public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true); + /** * These settings are used by YouTube and YouTube Music. */ @@ -39,17 +62,6 @@ public class BaseSettings { public static final EnumSetting RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = new EnumSetting<>("revanced_return_youtube_username_display_format", DisplayFormat.USERNAME_ONLY, true); public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false); - 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 EnumSetting SPOOF_STREAMING_DATA_LANGUAGE = new EnumSetting<>("revanced_spoof_streaming_data_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability()); - 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"); - public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE); - // Client type must be last spoof setting due to cyclic references. - public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true); - - public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true); - public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true); - /** * @noinspection DeprecatedIsStillUsed */ diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt index e7eddf002..1fc0f0476 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt @@ -2,7 +2,7 @@ package app.revanced.extension.youtube.patches.general.requests import android.annotation.SuppressLint import androidx.annotation.GuardedBy -import app.revanced.extension.shared.patches.client.WebClient +import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.utils.Logger @@ -81,7 +81,7 @@ class VideoDetailsRequest private constructor( private fun sendRequest(videoId: String): JSONObject? { val startTime = System.currentTimeMillis() - val clientType = WebClient.ClientType.MWEB + val clientType = YouTubeWebClient.ClientType.MWEB val clientTypeName = clientType.name Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt index 5ceb2615f..d48ca8b51 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt @@ -2,8 +2,8 @@ package app.revanced.extension.youtube.patches.video.requests import android.annotation.SuppressLint import androidx.annotation.GuardedBy -import app.revanced.extension.shared.patches.client.AppClient -import app.revanced.extension.shared.patches.client.WebClient +import app.revanced.extension.shared.patches.client.YouTubeAppClient +import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.utils.Logger @@ -119,7 +119,7 @@ class MusicRequest private constructor( Objects.requireNonNull(videoId) val startTime = System.currentTimeMillis() - val clientType = AppClient.ClientType.ANDROID_VR + val clientType = YouTubeAppClient.ClientType.ANDROID_VR val clientTypeName = clientType.name Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } @@ -163,7 +163,7 @@ class MusicRequest private constructor( Objects.requireNonNull(videoId) val startTime = System.currentTimeMillis() - val clientType = WebClient.ClientType.MWEB + val clientType = YouTubeWebClient.ClientType.MWEB val clientTypeName = clientType.name Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt index fcbbdf9ac..2e75b52cd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt @@ -2,13 +2,9 @@ package app.revanced.patches.music.utils.fix.client import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch import app.revanced.util.fingerprint.legacyFingerprint -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction import app.revanced.util.or import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val createPlayerRequestBodyFingerprint = legacyFingerprint( name = "createPlayerRequestBodyFingerprint", @@ -22,26 +18,6 @@ internal val createPlayerRequestBodyFingerprint = legacyFingerprint( strings = listOf("ms"), ) -internal val createPlayerRequestBodyWithVersionReleaseFingerprint = legacyFingerprint( - name = "createPlayerRequestBodyWithVersionReleaseFingerprint", - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("L"), - strings = listOf("Google Inc."), - customFingerprint = { method, _ -> - indexOfBuildInstruction(method) >= 0 - }, -) - -fun indexOfBuildInstruction(method: Method) = - method.indexOfFirstInstruction { - val reference = getReference() - opcode == Opcode.INVOKE_VIRTUAL && - reference?.name == "build" && - reference.parameterTypes.isEmpty() && - reference.returnType.startsWith("L") - } - internal val setPlayerRequestClientTypeFingerprint = legacyFingerprint( name = "setPlayerRequestClientTypeFingerprint", opcodes = listOf( diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt index af3dc5463..f689c5dc5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt @@ -3,24 +3,28 @@ package app.revanced.patches.music.utils.fix.client import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.revanced.patches.music.utils.compatibility.Constants -import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.music.utils.patch.PatchList.SPOOF_CLIENT import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint -import app.revanced.patches.music.utils.playservice.is_7_25_or_greater -import app.revanced.patches.music.utils.playservice.versionCheckPatch import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.extension.Constants.SPOOF_PATH +import app.revanced.patches.shared.indexOfBrandInstruction +import app.revanced.patches.shared.indexOfManufacturerInstruction import app.revanced.patches.shared.indexOfModelInstruction -import app.revanced.util.Utils.printWarn +import app.revanced.patches.shared.indexOfReleaseInstruction +import app.revanced.util.findMethodOrThrow import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.mutableClassOrThrow @@ -32,109 +36,118 @@ import app.revanced.util.or import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.iface.reference.TypeReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter private const val EXTENSION_CLASS_DESCRIPTOR = - "$MISC_PATH/SpoofClientPatch;" + "$SPOOF_PATH/SpoofClientPatch;" private const val CLIENT_INFO_CLASS_DESCRIPTOR = "Lcom/google/protos/youtube/api/innertube/InnertubeContext\$ClientInfo;" @Suppress("unused") val spoofClientPatch = bytecodePatch( SPOOF_CLIENT.title, - SPOOF_CLIENT.summary, - false, + SPOOF_CLIENT.summary ) { - compatibleWith( - Constants.YOUTUBE_MUSIC_PACKAGE_NAME( - "6.20.51", - "6.29.59", - "6.42.55", - "6.51.53", - "7.16.53", - ), - ) + compatibleWith(COMPATIBLE_PACKAGE) dependsOn( settingsPatch, - versionCheckPatch, + blockRequestPatch, ) execute { - if (is_7_25_or_greater) { - printWarn("\"${SPOOF_CLIENT.title}\" is not supported in this version. Use YouTube Music 7.24.51 or earlier.") - return@execute - } + lateinit var clientInfoReference: Reference + lateinit var clientIdReference: Reference + lateinit var clientVersionReference: Reference + lateinit var deviceBrandReference: Reference + lateinit var deviceMakeReference: Reference + lateinit var deviceModelReference: Reference + lateinit var osNameReference: Reference + lateinit var osVersionReference: Reference + + fun MutableMethod.getFieldReference(index: Int) = + getInstruction(index).reference // region Get field references to be used below. - val (clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField) = - setPlayerRequestClientTypeFingerprint.matchOrThrow().let { result -> - with(result.method) { - // Field in the player request object that holds the client info object. - val clientInfoField = instructions.find { instruction -> - // requestMessage.clientInfo = clientInfoBuilder.build(); - instruction.opcode == Opcode.IPUT_OBJECT && - instruction.getReference()?.type == CLIENT_INFO_CLASS_DESCRIPTOR - }?.getReference() - ?: throw PatchException("Could not find clientInfoField") - - // Client info object's client type field. - val clientInfoClientTypeField = - getInstruction(result.patternMatch!!.endIndex) - .getReference() - ?: throw PatchException("Could not find clientInfoClientTypeField") - - val clientInfoVersionIndex = result.stringMatches!!.first().index - val clientInfoVersionRegister = - getInstruction(clientInfoVersionIndex).registerA - val clientInfoClientVersionFieldIndex = - indexOfFirstInstructionOrThrow(clientInfoVersionIndex) { - opcode == Opcode.IPUT_OBJECT && - (this as TwoRegisterInstruction).registerA == clientInfoVersionRegister - } - - // Client info object's client version field. - val clientInfoClientVersionField = - getInstruction(clientInfoClientVersionFieldIndex) - .getReference() - ?: throw PatchException("Could not find clientInfoClientVersionField") - - Triple(clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField) - } - } - - val clientInfoClientModelField = - with(createPlayerRequestBodyWithModelFingerprint.methodOrThrow()) { - // The next IPUT_OBJECT instruction after getting the client model is setting the client model field. - val clientInfoClientModelIndex = - indexOfFirstInstructionOrThrow(indexOfModelInstruction(this)) { - val reference = getReference() - opcode == Opcode.IPUT_OBJECT && - reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && - reference.type == "Ljava/lang/String;" - } - getInstruction(clientInfoClientModelIndex).reference - } - - val clientInfoOsVersionField = - with(createPlayerRequestBodyWithVersionReleaseFingerprint.methodOrThrow()) { - val buildIndex = indexOfBuildInstruction(this) - val clientInfoOsVersionIndex = indexOfFirstInstructionOrThrow(buildIndex - 5) { - val reference = getReference() + setPlayerRequestClientTypeFingerprint.matchOrThrow().let { + it.method.apply { + val clientInfoIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.IPUT_OBJECT && - reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && - reference.type == "Ljava/lang/String;" + getReference()?.type == CLIENT_INFO_CLASS_DESCRIPTOR } - getInstruction(clientInfoOsVersionIndex).reference + val clientIdIndex = it.patternMatch!!.endIndex + val dummyClientVersionIndex = it.stringMatches!!.first().index + val dummyClientVersionRegister = + getInstruction(dummyClientVersionIndex).registerA + val clientVersionIndex = + indexOfFirstInstructionOrThrow(dummyClientVersionIndex) { + opcode == Opcode.IPUT_OBJECT && + (this as TwoRegisterInstruction).registerA == dummyClientVersionRegister + } + + clientInfoReference = + getFieldReference(clientInfoIndex) + clientIdReference = + getFieldReference(clientIdIndex) + clientVersionReference = + getFieldReference(clientVersionIndex) } + } + + fun MutableMethod.getClientInfoIndex( + startIndex: Int, + reversed: Boolean = false + ): Int { + val filter: Instruction.() -> Boolean = { + val reference = getReference() + opcode == Opcode.IPUT_OBJECT && + reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && + reference.type == "Ljava/lang/String;" + } + return if (reversed) { + indexOfFirstInstructionReversedOrThrow(startIndex, filter) + } else { + indexOfFirstInstructionOrThrow(startIndex, filter) + } + } + + createPlayerRequestBodyWithModelFingerprint.methodOrThrow().apply { + val buildManufacturerIndex = + indexOfManufacturerInstruction(this) + val deviceBrandIndex = + getClientInfoIndex(indexOfBrandInstruction(this)) + val deviceMakeIndex = + getClientInfoIndex(buildManufacturerIndex) + val deviceModelIndex = + getClientInfoIndex(indexOfModelInstruction(this)) + val chipSetIndex = + getClientInfoIndex(buildManufacturerIndex, true) + val osNameIndex = + getClientInfoIndex(chipSetIndex - 1, true) + val osVersionIndex = + getClientInfoIndex(indexOfReleaseInstruction(this)) + + deviceBrandReference = + getFieldReference(deviceBrandIndex) + deviceMakeReference = + getFieldReference(deviceMakeIndex) + deviceModelReference = + getFieldReference(deviceModelIndex) + osNameReference = + getFieldReference(osNameIndex) + osVersionReference = + getFieldReference(osVersionIndex) + } // endregion @@ -181,31 +194,49 @@ val spoofClientPatch = bytecodePatch( move-result v0 if-eqz v0, :disabled - iget-object v0, p0, $clientInfoField + iget-object v0, p0, $clientInfoReference - # Set client type to the spoofed value. - iget v1, v0, $clientInfoClientTypeField - invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientTypeId(I)I + # Set client id. + iget v1, v0, $clientIdReference + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientId(I)I move-result v1 - iput v1, v0, $clientInfoClientTypeField - - # Set client model to the spoofed value. - iget-object v1, v0, $clientInfoClientModelField - invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientModel(Ljava/lang/String;)Ljava/lang/String; - move-result-object v1 - iput-object v1, v0, $clientInfoClientModelField + iput v1, v0, $clientIdReference - # Set client version to the spoofed value. - iget-object v1, v0, $clientInfoClientVersionField + # Set client version. + iget-object v1, v0, $clientVersionReference invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String; move-result-object v1 - iput-object v1, v0, $clientInfoClientVersionField + iput-object v1, v0, $clientVersionReference - # Set client os version to the spoofed value. - iget-object v1, v0, $clientInfoOsVersionField + # Set device brand. + iget-object v1, v0, $deviceBrandReference + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getDeviceBrand(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $deviceBrandReference + + # Set device make. + iget-object v1, v0, $deviceMakeReference + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getDeviceMake(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $deviceMakeReference + + # Set device model. + iget-object v1, v0, $deviceModelReference + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getDeviceModel(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $deviceModelReference + + # Set os name. + iget-object v1, v0, $osNameReference + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsName(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $osNameReference + + # Set os version. + iget-object v1, v0, $osVersionReference invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion(Ljava/lang/String;)Ljava/lang/String; move-result-object v1 - iput-object v1, v0, $clientInfoOsVersionField + iput-object v1, v0, $osVersionReference :disabled return-void @@ -273,10 +304,17 @@ val spoofClientPatch = bytecodePatch( // endregion + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { + name == "SpoofClient" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + addSwitchPreference( CategoryType.MISC, "revanced_spoof_client", - "false" + "true" ) addPreferenceWithIntent( CategoryType.MISC, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt deleted file mode 100644 index a30c435c2..000000000 --- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt +++ /dev/null @@ -1,67 +0,0 @@ -package app.revanced.patches.music.utils.fix.streamingdata - -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME -import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA -import app.revanced.patches.music.utils.settings.CategoryType -import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus -import app.revanced.patches.music.utils.settings.addPreferenceWithIntent -import app.revanced.patches.music.utils.settings.addSwitchPreference -import app.revanced.patches.music.utils.settings.settingsPatch -import app.revanced.patches.music.video.playerresponse.hookPlayerResponse -import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch -import app.revanced.patches.shared.extension.Constants.PATCHES_PATH -import app.revanced.patches.shared.extension.Constants.SPOOF_PATH -import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch -import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch -import app.revanced.util.findMethodOrThrow - -const val EXTENSION_CLASS_DESCRIPTOR = - "$SPOOF_PATH/SpoofStreamingDataPatch;" - -@Suppress("unused") -val spoofStreamingDataPatch = baseSpoofStreamingDataPatch( - { - compatibleWith(COMPATIBLE_PACKAGE) - - dependsOn( - baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME), - settingsPatch, - playerResponseMethodHookPatch, - ) - }, - { - findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { - name == "SpoofStreamingDataMusic" - }.replaceInstruction( - 0, - "const/4 v0, 0x1" - ) - - hookPlayerResponse( - "$EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;)V", - true - ) - - addSwitchPreference( - CategoryType.MISC, - "revanced_spoof_streaming_data", - "true" - ) - addPreferenceWithIntent( - CategoryType.MISC, - "revanced_spoof_streaming_data_type", - "revanced_spoof_streaming_data" - ) - addSwitchPreference( - CategoryType.MISC, - "revanced_spoof_streaming_data_stats_for_nerds", - "true", - "revanced_spoof_streaming_data" - ) - - updatePatchStatus(SPOOF_STREAMING_DATA) - - } -) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt index bb89a82b8..b4c7ed91c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt @@ -153,10 +153,6 @@ internal enum class PatchList( "Spoof client", "Adds options to spoof the client to allow playback." ), - SPOOF_STREAMING_DATA( - "Spoof streaming data", - "Adds options to spoof the streaming data to allow playback." - ), TRANSLATIONS_FOR_YOUTUBE_MUSIC( "Translations for YouTube Music", "Add translations or remove string resources." diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt index 699e4fb0e..1bd5d1bab 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt @@ -18,11 +18,19 @@ internal val createPlayerRequestBodyWithModelFingerprint = legacyFingerprint( parameters = emptyList(), opcodes = listOf(Opcode.OR_INT_LIT16), customFingerprint = { method, _ -> - indexOfModelInstruction(method) >= 0 && + indexOfBrandInstruction(method) >= 0 && + indexOfManufacturerInstruction(method) >= 0 && + indexOfModelInstruction(method) >= 0 && indexOfReleaseInstruction(method) >= 0 } ) +fun indexOfBrandInstruction(method: Method) = + method.indexOfFieldReference("Landroid/os/Build;->BRAND:Ljava/lang/String;") + +fun indexOfManufacturerInstruction(method: Method) = + method.indexOfFieldReference("Landroid/os/Build;->MANUFACTURER:Ljava/lang/String;") + fun indexOfModelInstruction(method: Method) = method.indexOfFieldReference("Landroid/os/Build;->MODEL:Ljava/lang/String;") diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/BlockRequestPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/BlockRequestPatch.kt new file mode 100644 index 000000000..1063044ad --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/BlockRequestPatch.kt @@ -0,0 +1,57 @@ +package app.revanced.patches.shared.spoof.blockrequest + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.extension.Constants.SPOOF_PATH +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +const val EXTENSION_CLASS_DESCRIPTOR = + "$SPOOF_PATH/BlockRequestPatch;" + +val blockRequestPatch = bytecodePatch( + description = "blockRequestPatch" +) { + execute { + // region Block /initplayback requests to fall back to /get_watch requests. + + buildInitPlaybackRequestFingerprint.matchOrThrow().let { + it.method.apply { + val moveUriStringIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(moveUriStringIndex).registerA + + addInstructions( + moveUriStringIndex + 1, + """ + invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """, + ) + } + } + + // endregion + + // region Block /get_watch requests to fall back to /player requests. + + buildPlayerRequestURIFingerprint.methodOrThrow().apply { + val invokeToStringIndex = indexOfToStringInstruction(this) + val uriRegister = + getInstruction(invokeToStringIndex).registerC + + addInstructions( + invokeToStringIndex, + """ + invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; + move-result-object v$uriRegister + """, + ) + } + + // endregion + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/Fingerprints.kt new file mode 100644 index 000000000..13797f4de --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/blockrequest/Fingerprints.kt @@ -0,0 +1,40 @@ +package app.revanced.patches.shared.spoof.blockrequest + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val buildInitPlaybackRequestFingerprint = legacyFingerprint( + name = "buildInitPlaybackRequestFingerprint", + returnType = "Lorg/chromium/net/UrlRequest\$Builder;", + opcodes = listOf( + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with. + ), + strings = listOf( + "Content-Type", + "Range", + ), +) + +internal val buildPlayerRequestURIFingerprint = legacyFingerprint( + name = "buildPlayerRequestURIFingerprint", + returnType = "Ljava/lang/String;", + strings = listOf( + "key", + "asig", + ), + customFingerprint = { method, _ -> + indexOfToStringInstruction(method) >= 0 + }, +) + +internal fun indexOfToStringInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;" + } + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt deleted file mode 100644 index 451a17c12..000000000 --- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt +++ /dev/null @@ -1,370 +0,0 @@ -package app.revanced.patches.shared.spoof.streamingdata - -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.instructions -import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction -import app.revanced.patcher.patch.BytecodePatchBuilder -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.revanced.patches.shared.extension.Constants.SPOOF_PATH -import app.revanced.patches.shared.formatStreamModelConstructorFingerprint -import app.revanced.util.findInstructionIndicesReversedOrThrow -import app.revanced.util.fingerprint.definingClassOrThrow -import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall -import app.revanced.util.fingerprint.matchOrThrow -import app.revanced.util.fingerprint.methodOrThrow -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstructionOrThrow -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter - -const val EXTENSION_CLASS_DESCRIPTOR = - "$SPOOF_PATH/SpoofStreamingDataPatch;" - -fun baseSpoofStreamingDataPatch( - block: BytecodePatchBuilder.() -> Unit = {}, - executeBlock: BytecodePatchContext.() -> Unit = {}, -) = bytecodePatch( - name = "Spoof streaming data", - description = "Adds options to spoof the streaming data to allow playback." -) { - block() - - execute { - // region Block /initplayback requests to fall back to /get_watch requests. - - buildInitPlaybackRequestFingerprint.matchOrThrow().let { - it.method.apply { - val moveUriStringIndex = it.patternMatch!!.startIndex - val targetRegister = - getInstruction(moveUriStringIndex).registerA - - addInstructions( - moveUriStringIndex + 1, - """ - invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String; - move-result-object v$targetRegister - """, - ) - } - } - - // endregion - - // region Block /get_watch requests to fall back to /player requests. - - buildPlayerRequestURIFingerprint.methodOrThrow().apply { - val invokeToStringIndex = indexOfToStringInstruction(this) - val uriRegister = - getInstruction(invokeToStringIndex).registerC - - addInstructions( - invokeToStringIndex, - """ - invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; - move-result-object v$uriRegister - """, - ) - } - - // endregion - - // region Get replacement streams at player requests. - - buildRequestFingerprint.methodOrThrow().apply { - val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this) - val urlRegister = - getInstruction(newRequestBuilderIndex).registerD - - val entrySetIndex = indexOfEntrySetInstruction(this) - val mapRegister = if (entrySetIndex < 0) - urlRegister + 1 - else - getInstruction(entrySetIndex).registerC - - var smaliInstructions = - "invoke-static { v$urlRegister, v$mapRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->" + - "fetchStreams(Ljava/lang/String;Ljava/util/Map;)V" - - if (entrySetIndex < 0) smaliInstructions = """ - move-object/from16 v$mapRegister, p1 - - """ + smaliInstructions - - // Copy request headers for streaming data fetch. - addInstructions(newRequestBuilderIndex + 2, smaliInstructions) - } - - // endregion - - // region Replace the streaming data. - - val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let { - with(it.method) { - getInstruction(it.patternMatch!!.startIndex).reference - } - } - - val streamingDataFormatsReference = with( - videoStreamingDataConstructorFingerprint.methodOrThrow( - videoStreamingDataToStringFingerprint - ) - ) { - val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this) - val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex) - val longMaxValueRegister = - getInstruction(longMaxValueIndex).registerA - val videoIdIndex = - indexOfFirstInstructionOrThrow(longMaxValueIndex) { - val reference = getReference() - opcode == Opcode.IGET_OBJECT && - reference?.type == "Ljava/lang/String;" && - reference.definingClass == definingClass - } - - val definingClassRegister = - getInstruction(videoIdIndex).registerB - val videoIdReference = - getInstruction(videoIdIndex).reference - - addInstructions( - longMaxValueIndex + 1, """ - # Get video id. - iget-object v$longMaxValueRegister, v$definingClassRegister, $videoIdReference - - # Override approxDurationMs. - invoke-static { v$longMaxValueRegister }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMs(Ljava/lang/String;)J - move-result-wide v$longMaxValueRegister - """ - ) - removeInstruction(longMaxValueIndex) - - getInstruction(getFormatsFieldIndex).reference - } - - createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint) - .let { result -> - result.method.apply { - val setStreamDataMethodName = "patch_setStreamingData" - val calcApproxDurationMsMethodName = "patch_calcApproxDurationMs" - val resultClassDef = result.classDef - val resultMethodType = resultClassDef.type - val setStreamingDataIndex = result.patternMatch!!.startIndex - val setStreamingDataField = - getInstruction(setStreamingDataIndex).getReference() - .toString() - - val playerProtoClass = - getInstruction(setStreamingDataIndex + 1).getReference()!!.definingClass - val protobufClass = - protobufClassParseByteBufferFingerprint.definingClassOrThrow() - - val getStreamingDataField = instructions.find { instruction -> - instruction.opcode == Opcode.IGET_OBJECT && - instruction.getReference()?.definingClass == playerProtoClass - }?.getReference() - ?: throw PatchException("Could not find getStreamingDataField") - - val videoDetailsIndex = result.patternMatch!!.endIndex - val videoDetailsRegister = - getInstruction(videoDetailsIndex).registerA - val videoDetailsClass = - getInstruction(videoDetailsIndex).getReference()!!.type - - addInstruction( - videoDetailsIndex + 1, - "invoke-direct { p0, v$videoDetailsRegister }, " + - "$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V", - ) - - result.classDef.methods.add( - ImmutableMethod( - resultMethodType, - setStreamDataMethodName, - listOf( - ImmutableMethodParameter( - videoDetailsClass, - annotations, - "videoDetails" - ) - ), - "V", - AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, - annotations, - null, - MutableMethodImplementation(9), - ).toMutable().apply { - addInstructionsWithLabels( - 0, - """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z - move-result v0 - if-eqz v0, :disabled - - # Get video id. - iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String; - if-eqz v2, :disabled - - # Get streaming data. - invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; - move-result-object v3 - - if-eqz v3, :disabled - - # Parse streaming data. - sget-object v4, $playerProtoClass->a:$playerProtoClass - invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass - move-result-object v5 - check-cast v5, $playerProtoClass - - iget-object v6, v5, $getStreamingDataField - if-eqz v6, :disabled - - # Caculate approxDurationMs. - invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V - - # Set spoofed streaming data. - iput-object v6, p0, $setStreamingDataField - - :disabled - return-void - """, - ) - }, - ) - - resultClassDef.methods.add( - ImmutableMethod( - resultMethodType, - calcApproxDurationMsMethodName, - listOf( - ImmutableMethodParameter( - "Ljava/lang/String;", - annotations, - "videoId" - ) - ), - "V", - AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, - annotations, - null, - MutableMethodImplementation(12), - ).toMutable().apply { - addInstructionsWithLabels( - 0, - """ - # Get video format list. - iget-object v0, p0, $setStreamingDataField - iget-object v0, v0, $streamingDataFormatsReference - invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator; - move-result-object v0 - - # Initialize approxDurationMs field. - const-wide v1, 0x7fffffffffffffffL - - :loop - # Loop over all video formats to get the approxDurationMs - invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z - move-result v3 - const-wide/16 v4, 0x0 - - if-eqz v3, :exit - invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object; - move-result-object v3 - check-cast v3, ${(approxDurationMsReference as FieldReference).definingClass} - - # Get approxDurationMs from format - iget-wide v6, v3, $approxDurationMsReference - - # Compare with zero to make sure approxDurationMs is not negative - cmp-long v8, v6, v4 - if-lez v8, :loop - - # Only use the min value of approxDurationMs - invoke-static {v1, v2, v6, v7}, Ljava/lang/Math;->min(JJ)J - move-result-wide v1 - goto :loop - - :exit - # Save approxDurationMs to integrations - invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;J)V - - return-void - """, - ) - }, - ) - } - } - - // endregion - - // region Remove /videoplayback request body to fix playback. - // This is needed when using iOS client as streaming data source. - - buildMediaDataSourceFingerprint.methodOrThrow().apply { - val targetIndex = instructions.lastIndex - - addInstructions( - targetIndex, - """ - # Field a: Stream uri. - # Field c: Http method. - # Field d: Post data. - move-object/from16 v0, p0 - iget-object v1, v0, $definingClass->a:Landroid/net/Uri; - iget v2, v0, $definingClass->c:I - iget-object v3, v0, $definingClass->d:[B - invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B - move-result-object v1 - iput-object v1, v0, $definingClass->d:[B - """, - ) - } - - // endregion - - // region Append spoof info. - - nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply { - findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> - val register = getInstruction(index).registerA - - addInstructions( - index, """ - invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String; - move-result-object v$register - """ - ) - } - } - - // endregion - - // region Fix iOS livestream current time. - - hlsCurrentTimeFingerprint.injectLiteralInstructionBooleanCall( - HLS_CURRENT_TIME_FEATURE_FLAG, - "$EXTENSION_CLASS_DESCRIPTOR->fixHLSCurrentTime(Z)Z" - ) - - // endregion - - executeBlock() - - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt similarity index 86% rename from patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt index 79e334099..d7bf166aa 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.shared.spoof.streamingdata +package app.revanced.patches.youtube.utils.fix.streamingdata import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.getReference @@ -15,37 +15,6 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference const val STREAMING_DATA_INTERFACE = "Lcom/google/protos/youtube/api/innertube/StreamingDataOuterClass${'$'}StreamingData;" -internal val buildInitPlaybackRequestFingerprint = legacyFingerprint( - name = "buildInitPlaybackRequestFingerprint", - returnType = "Lorg/chromium/net/UrlRequest\$Builder;", - opcodes = listOf( - Opcode.MOVE_RESULT_OBJECT, - Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with. - ), - strings = listOf( - "Content-Type", - "Range", - ), -) - -internal val buildPlayerRequestURIFingerprint = legacyFingerprint( - name = "buildPlayerRequestURIFingerprint", - returnType = "Ljava/lang/String;", - strings = listOf( - "key", - "asig", - ), - customFingerprint = { method, _ -> - indexOfToStringInstruction(method) >= 0 - }, -) - -internal fun indexOfToStringInstruction(method: Method) = - method.indexOfFirstInstruction { - opcode == Opcode.INVOKE_VIRTUAL && - getReference().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;" - } - internal val buildMediaDataSourceFingerprint = legacyFingerprint( name = "buildMediaDataSourceFingerprint", accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt index a2ba66890..fbb9151f0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt @@ -1,29 +1,355 @@ package app.revanced.patches.youtube.utils.fix.streamingdata -import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.extension.Constants.SPOOF_PATH +import app.revanced.patches.shared.formatStreamModelConstructorFingerprint +import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.definingClassOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter -val spoofStreamingDataPatch = baseSpoofStreamingDataPatch( - { - compatibleWith(COMPATIBLE_PACKAGE) +const val EXTENSION_CLASS_DESCRIPTOR = + "$SPOOF_PATH/SpoofStreamingDataPatch;" - dependsOn( - baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME), - settingsPatch +val spoofStreamingDataPatch = bytecodePatch( + SPOOF_STREAMING_DATA.title, + SPOOF_STREAMING_DATA.summary +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME), + blockRequestPatch, + ) + + execute { + + // region Get replacement streams at player requests. + + buildRequestFingerprint.methodOrThrow().apply { + val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this) + val urlRegister = + getInstruction(newRequestBuilderIndex).registerD + + val entrySetIndex = indexOfEntrySetInstruction(this) + val mapRegister = if (entrySetIndex < 0) + urlRegister + 1 + else + getInstruction(entrySetIndex).registerC + + var smaliInstructions = + "invoke-static { v$urlRegister, v$mapRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->" + + "fetchStreams(Ljava/lang/String;Ljava/util/Map;)V" + + if (entrySetIndex < 0) smaliInstructions = """ + move-object/from16 v$mapRegister, p1 + + """ + smaliInstructions + + // Copy request headers for streaming data fetch. + addInstructions(newRequestBuilderIndex + 2, smaliInstructions) + } + + // endregion + + // region Replace the streaming data. + + val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let { + with(it.method) { + getInstruction(it.patternMatch!!.startIndex).reference + } + } + + val streamingDataFormatsReference = with( + videoStreamingDataConstructorFingerprint.methodOrThrow( + videoStreamingDataToStringFingerprint + ) + ) { + val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this) + val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex) + val longMaxValueRegister = + getInstruction(longMaxValueIndex).registerA + val videoIdIndex = + indexOfFirstInstructionOrThrow(longMaxValueIndex) { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.type == "Ljava/lang/String;" && + reference.definingClass == definingClass + } + + val definingClassRegister = + getInstruction(videoIdIndex).registerB + val videoIdReference = + getInstruction(videoIdIndex).reference + + addInstructions( + longMaxValueIndex + 1, """ + # Get video id. + iget-object v$longMaxValueRegister, v$definingClassRegister, $videoIdReference + + # Override approxDurationMs. + invoke-static { v$longMaxValueRegister }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMs(Ljava/lang/String;)J + move-result-wide v$longMaxValueRegister + """ + ) + removeInstruction(longMaxValueIndex) + + getInstruction(getFormatsFieldIndex).reference + } + + createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint) + .let { result -> + result.method.apply { + val setStreamDataMethodName = "patch_setStreamingData" + val calcApproxDurationMsMethodName = "patch_calcApproxDurationMs" + val resultClassDef = result.classDef + val resultMethodType = resultClassDef.type + val setStreamingDataIndex = result.patternMatch!!.startIndex + val setStreamingDataField = + getInstruction(setStreamingDataIndex).getReference() + .toString() + + val playerProtoClass = + getInstruction(setStreamingDataIndex + 1).getReference()!!.definingClass + val protobufClass = + protobufClassParseByteBufferFingerprint.definingClassOrThrow() + + val getStreamingDataField = instructions.find { instruction -> + instruction.opcode == Opcode.IGET_OBJECT && + instruction.getReference()?.definingClass == playerProtoClass + }?.getReference() + ?: throw PatchException("Could not find getStreamingDataField") + + val videoDetailsIndex = result.patternMatch!!.endIndex + val videoDetailsRegister = + getInstruction(videoDetailsIndex).registerA + val videoDetailsClass = + getInstruction(videoDetailsIndex).getReference()!!.type + + addInstruction( + videoDetailsIndex + 1, + "invoke-direct { p0, v$videoDetailsRegister }, " + + "$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V", + ) + + result.classDef.methods.add( + ImmutableMethod( + resultMethodType, + setStreamDataMethodName, + listOf( + ImmutableMethodParameter( + videoDetailsClass, + annotations, + "videoDetails" + ) + ), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + annotations, + null, + MutableMethodImplementation(9), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z + move-result v0 + if-eqz v0, :disabled + + # Get video id. + iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String; + if-eqz v2, :disabled + + # Get streaming data. + invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; + move-result-object v3 + + if-eqz v3, :disabled + + # Parse streaming data. + sget-object v4, $playerProtoClass->a:$playerProtoClass + invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass + move-result-object v5 + check-cast v5, $playerProtoClass + + iget-object v6, v5, $getStreamingDataField + if-eqz v6, :disabled + + # Caculate approxDurationMs. + invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V + + # Set spoofed streaming data. + iput-object v6, p0, $setStreamingDataField + + :disabled + return-void + """, + ) + }, + ) + + resultClassDef.methods.add( + ImmutableMethod( + resultMethodType, + calcApproxDurationMsMethodName, + listOf( + ImmutableMethodParameter( + "Ljava/lang/String;", + annotations, + "videoId" + ) + ), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + annotations, + null, + MutableMethodImplementation(12), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + # Get video format list. + iget-object v0, p0, $setStreamingDataField + iget-object v0, v0, $streamingDataFormatsReference + invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator; + move-result-object v0 + + # Initialize approxDurationMs field. + const-wide v1, 0x7fffffffffffffffL + + :loop + # Loop over all video formats to get the approxDurationMs + invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z + move-result v3 + const-wide/16 v4, 0x0 + + if-eqz v3, :exit + invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object; + move-result-object v3 + check-cast v3, ${(approxDurationMsReference as FieldReference).definingClass} + + # Get approxDurationMs from format + iget-wide v6, v3, $approxDurationMsReference + + # Compare with zero to make sure approxDurationMs is not negative + cmp-long v8, v6, v4 + if-lez v8, :loop + + # Only use the min value of approxDurationMs + invoke-static {v1, v2, v6, v7}, Ljava/lang/Math;->min(JJ)J + move-result-wide v1 + goto :loop + + :exit + # Save approxDurationMs to integrations + invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;J)V + + return-void + """, + ) + }, + ) + } + } + + // endregion + + // region Remove /videoplayback request body to fix playback. + // This is needed when using iOS client as streaming data source. + + buildMediaDataSourceFingerprint.methodOrThrow().apply { + val targetIndex = instructions.lastIndex + + addInstructions( + targetIndex, + """ + # Field a: Stream uri. + # Field c: Http method. + # Field d: Post data. + move-object/from16 v0, p0 + iget-object v1, v0, $definingClass->a:Landroid/net/Uri; + iget v2, v0, $definingClass->c:I + iget-object v3, v0, $definingClass->d:[B + invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B + move-result-object v1 + iput-object v1, v0, $definingClass->d:[B + """, + ) + } + + // endregion + + // region Append spoof info. + + nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> + val register = getInstruction(index).registerA + + addInstructions( + index, """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """ + ) + } + } + + // endregion + + // region Fix iOS livestream current time. + + hlsCurrentTimeFingerprint.injectLiteralInstructionBooleanCall( + HLS_CURRENT_TIME_FEATURE_FLAG, + "$EXTENSION_CLASS_DESCRIPTOR->fixHLSCurrentTime(Z)Z" ) - }, - { + + // endregion + + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { + name == "SpoofStreamingData" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + addPreference( arrayOf( "SETTINGS: SPOOF_STREAMING_DATA" ), SPOOF_STREAMING_DATA ) - } -) +} diff --git a/patches/src/main/resources/music/settings/host/values/arrays.xml b/patches/src/main/resources/music/settings/host/values/arrays.xml index 4d7f253d5..68fa7ba45 100644 --- a/patches/src/main/resources/music/settings/host/values/arrays.xml +++ b/patches/src/main/resources/music/settings/host/values/arrays.xml @@ -76,14 +76,16 @@ 6.42.55 - @string/revanced_spoof_client_type_entry_ios_music_6_21 - @string/revanced_spoof_client_type_entry_android_music_5_29 @string/revanced_spoof_client_type_entry_android_music_4_27 + @string/revanced_spoof_client_type_entry_android_music_5_29 + @string/revanced_spoof_client_type_entry_ios_music_6_21 + @string/revanced_spoof_client_type_entry_ios_music_7_04 - IOS_MUSIC_6_21 - ANDROID_MUSIC_5_29 ANDROID_MUSIC_4_27 + ANDROID_MUSIC_5_29 + IOS_MUSIC_6_21 + IOS_MUSIC_7_04 @string/revanced_spoof_streaming_data_type_entry_android_vr diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml index 2c400f9e3..fbc3a29db 100644 --- a/patches/src/main/resources/music/settings/host/values/strings.xml +++ b/patches/src/main/resources/music/settings/host/values/strings.xml @@ -479,17 +479,7 @@ Info: Android Music 4.27.53 Android Music 5.29.53 iOS Music 6.21 - - Spoof streaming data - "Spoof the streaming data to prevent playback issues. - -• When used with 'Spoof client', playback issues may occur." - Default client - Defines a default client that fetches streaming data. - Show in Stats for nerds - Shows the client used to fetch streaming data in Stats for nerds. - Android VR - Android Music + iOS Music 7.04 Open default app settings To open YouTube Music links in RVX Music, enable Open supported links and enable all the Supported web addresses.