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
This commit is contained in:
inotia00
2025-01-29 19:41:46 +09:00
parent 1151a9a5be
commit 0d2d45a597
27 changed files with 999 additions and 985 deletions

View File

@ -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
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
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;
}
}
}

View File

@ -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<RedirectType> 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<ClientType> 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,

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
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;
}
}
}

View File

@ -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<ClientType> {
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<ClientType?> = 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<ClientType> = arrayOf(
ANDROID_VR,
ANDROID_MUSIC,
)
}
}
}

View File

@ -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

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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;
}
/**

View File

@ -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.
* <p>
* 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<String, String> 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.
* <p>
* 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.
* <p>
*
* @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<String, String> 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<String, String> 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);

View File

@ -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,

View File

@ -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<AppClient.ClientType> =
availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
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<String, String>,
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

View File

@ -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<AppLanguage> 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<MusicAppClient.ClientType> 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<AppLanguage> 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<YouTubeAppClient.ClientType> 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<DisplayFormat> 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<AppLanguage> 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<ClientType> 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
*/

View File

@ -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" }

View File

@ -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" }