mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-21 16:57:18 +02:00
feat(YouTube Music): add Spoof client
patch
This commit is contained in:
parent
96f2db9d3b
commit
481b1537e0
@ -0,0 +1,84 @@
|
||||
package app.revanced.extension.music.patches.misc;
|
||||
|
||||
import app.revanced.extension.music.patches.misc.client.AppClient.ClientType;
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofClientPatch {
|
||||
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
|
||||
private static final ClientType clientType = ClientType.IOS_MUSIC;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getClientTypeId(int originalClientTypeId) {
|
||||
if (SPOOF_CLIENT_ENABLED) {
|
||||
return clientType.id;
|
||||
}
|
||||
|
||||
return originalClientTypeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String getClientVersion(String originalClientVersion) {
|
||||
if (SPOOF_CLIENT_ENABLED) {
|
||||
return clientType.clientVersion;
|
||||
}
|
||||
|
||||
return originalClientVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String getClientModel(String originalClientModel) {
|
||||
if (SPOOF_CLIENT_ENABLED) {
|
||||
return clientType.deviceModel;
|
||||
}
|
||||
|
||||
return originalClientModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String getOsVersion(String originalOsVersion) {
|
||||
if (SPOOF_CLIENT_ENABLED) {
|
||||
return clientType.osVersion;
|
||||
}
|
||||
|
||||
return originalOsVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String getUserAgent(String originalUserAgent) {
|
||||
if (SPOOF_CLIENT_ENABLED) {
|
||||
return clientType.userAgent;
|
||||
}
|
||||
|
||||
return originalUserAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isClientSpoofingEnabled() {
|
||||
return SPOOF_CLIENT_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* When spoofing the client to iOS, the playback speed menu is missing from the player response.
|
||||
* Return true to force create the playback speed menu.
|
||||
*/
|
||||
public static boolean forceCreatePlaybackSpeedMenu(boolean original) {
|
||||
if (SPOOF_CLIENT_ENABLED) {
|
||||
return true;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package app.revanced.extension.music.patches.misc.client;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class AppClient {
|
||||
|
||||
/**
|
||||
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
||||
*
|
||||
* <p>
|
||||
* It can be extracted by getting the latest release version of the app on
|
||||
* <a href="https://apps.apple.com/us/app/music-watch-listen-stream/id544007664/">the App
|
||||
* Store page of the YouTube app</a>, in the {@code What’s New} section.
|
||||
* </p>
|
||||
*/
|
||||
private static final String CLIENT_VERSION_IOS = "6.21";
|
||||
private static final String DEVICE_MAKE_IOS = "Apple";
|
||||
/**
|
||||
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
|
||||
* information.
|
||||
* </p>
|
||||
*/
|
||||
private static final String DEVICE_MODEL_IOS = "iPhone16,2";
|
||||
private static final String OS_NAME_IOS = "iOS";
|
||||
private static final String OS_VERSION_IOS = "17.7.2.21H221";
|
||||
private static final String USER_AGENT_VERSION_IOS = "17_7_2";
|
||||
private static final String USER_AGENT_IOS = "com.google.ios.youtubemusic/" +
|
||||
CLIENT_VERSION_IOS +
|
||||
"(" +
|
||||
DEVICE_MODEL_IOS +
|
||||
"; U; CPU iOS " +
|
||||
USER_AGENT_VERSION_IOS +
|
||||
" like Mac OS X)";
|
||||
|
||||
private AppClient() {
|
||||
}
|
||||
|
||||
public enum ClientType {
|
||||
IOS_MUSIC(26,
|
||||
DEVICE_MAKE_IOS,
|
||||
DEVICE_MODEL_IOS,
|
||||
CLIENT_VERSION_IOS,
|
||||
OS_NAME_IOS,
|
||||
OS_VERSION_IOS,
|
||||
null,
|
||||
USER_AGENT_IOS,
|
||||
true
|
||||
);
|
||||
|
||||
/**
|
||||
* YouTube
|
||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||
*/
|
||||
public final int id;
|
||||
|
||||
/**
|
||||
* Device manufacturer.
|
||||
*/
|
||||
@Nullable
|
||||
public final String deviceMake;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
||||
*/
|
||||
public final String deviceModel;
|
||||
|
||||
/**
|
||||
* Device OS name.
|
||||
*/
|
||||
@Nullable
|
||||
public final String osName;
|
||||
|
||||
/**
|
||||
* Device OS version.
|
||||
*/
|
||||
public final String osVersion;
|
||||
|
||||
/**
|
||||
* Player user-agent.
|
||||
*/
|
||||
public final String userAgent;
|
||||
|
||||
/**
|
||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
public final Integer androidSdkVersion;
|
||||
|
||||
/**
|
||||
* App version.
|
||||
*/
|
||||
public final String clientVersion;
|
||||
|
||||
/**
|
||||
* If the client can access the API logged in.
|
||||
*/
|
||||
public final boolean canLogin;
|
||||
|
||||
ClientType(int id,
|
||||
@Nullable String deviceMake,
|
||||
String deviceModel,
|
||||
String clientVersion,
|
||||
@Nullable String osName,
|
||||
String osVersion,
|
||||
Integer androidSdkVersion,
|
||||
String userAgent,
|
||||
boolean canLogin
|
||||
) {
|
||||
this.id = id;
|
||||
this.deviceMake = deviceMake;
|
||||
this.deviceModel = deviceModel;
|
||||
this.clientVersion = clientVersion;
|
||||
this.osName = osName;
|
||||
this.osVersion = osVersion;
|
||||
this.androidSdkVersion = androidSdkVersion;
|
||||
this.userAgent = userAgent;
|
||||
this.canLogin = canLogin;
|
||||
}
|
||||
}
|
||||
}
|
@ -172,6 +172,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true);
|
||||
public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
|
||||
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
|
||||
public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false);
|
||||
|
||||
|
||||
|
@ -157,7 +157,7 @@ public class AppClient {
|
||||
* Device manufacturer.
|
||||
*/
|
||||
@Nullable
|
||||
public final String make;
|
||||
public final String deviceMake;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
||||
@ -197,7 +197,7 @@ public class AppClient {
|
||||
public final boolean canLogin;
|
||||
|
||||
ClientType(int id,
|
||||
@Nullable String make,
|
||||
@Nullable String deviceMake,
|
||||
String deviceModel,
|
||||
String clientVersion,
|
||||
@Nullable String osName,
|
||||
@ -208,7 +208,7 @@ public class AppClient {
|
||||
) {
|
||||
this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
|
||||
this.id = id;
|
||||
this.make = make;
|
||||
this.deviceMake = deviceMake;
|
||||
this.deviceModel = deviceModel;
|
||||
this.clientVersion = clientVersion;
|
||||
this.osName = osName;
|
||||
|
@ -56,8 +56,8 @@ public final class PlayerRoutes {
|
||||
client.put("clientVersion", clientType.clientVersion);
|
||||
client.put("deviceModel", clientType.deviceModel);
|
||||
client.put("osVersion", clientType.osVersion);
|
||||
if (clientType.make != null) {
|
||||
client.put("deviceMake", clientType.make);
|
||||
if (clientType.deviceMake != null) {
|
||||
client.put("deviceMake", clientType.deviceMake);
|
||||
}
|
||||
if (clientType.osName != null) {
|
||||
client.put("osName", clientType.osName);
|
||||
|
@ -14,6 +14,14 @@ internal val pendingIntentReceiverFingerprint = legacyFingerprint(
|
||||
}
|
||||
)
|
||||
|
||||
internal val playbackSpeedBottomSheetFingerprint = legacyFingerprint(
|
||||
name = "playbackSpeedBottomSheetFingerprint",
|
||||
returnType = "V",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||
parameters = listOf("L"),
|
||||
strings = listOf("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT")
|
||||
)
|
||||
|
||||
internal val playbackSpeedFingerprint = legacyFingerprint(
|
||||
name = "playbackSpeedFingerprint",
|
||||
returnType = "V",
|
||||
|
@ -0,0 +1,66 @@
|
||||
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",
|
||||
returnType = "V",
|
||||
parameters = listOf("L"),
|
||||
opcodes = listOf(
|
||||
Opcode.CHECK_CAST,
|
||||
Opcode.IGET,
|
||||
Opcode.AND_INT_LIT16,
|
||||
),
|
||||
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<MethodReference>()
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
reference?.name == "build" &&
|
||||
reference.parameterTypes.isEmpty() &&
|
||||
reference.returnType.startsWith("L")
|
||||
}
|
||||
|
||||
internal val setPlayerRequestClientTypeFingerprint = legacyFingerprint(
|
||||
name = "setPlayerRequestClientTypeFingerprint",
|
||||
opcodes = listOf(
|
||||
Opcode.IGET,
|
||||
Opcode.IPUT, // Sets ClientInfo.clientId.
|
||||
),
|
||||
strings = listOf("10.29"),
|
||||
)
|
||||
|
||||
/**
|
||||
* This is the fingerprint used in the 'client-spoof' patch around 2022.
|
||||
* (Integrated into [baseSpoofUserAgentPatch] now.)
|
||||
*
|
||||
* This method is modified by [baseSpoofUserAgentPatch], so the fingerprint does not check the [Opcode].
|
||||
*/
|
||||
internal val userAgentHeaderBuilderFingerprint = legacyFingerprint(
|
||||
name = "userAgentHeaderBuilderFingerprint",
|
||||
returnType = "Ljava/lang/String;",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
|
||||
parameters = listOf("Landroid/content/Context;"),
|
||||
strings = listOf("(Linux; U; Android "),
|
||||
)
|
@ -0,0 +1,255 @@
|
||||
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.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.music.utils.extension.Constants.MISC_PATH
|
||||
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_CLIENT
|
||||
import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint
|
||||
import app.revanced.patches.music.utils.settings.CategoryType
|
||||
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
||||
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
|
||||
import app.revanced.patches.shared.indexOfModelInstruction
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
import app.revanced.util.fingerprint.methodOrThrow
|
||||
import app.revanced.util.fingerprint.mutableClassOrThrow
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.getWalkerMethod
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||
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.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.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;"
|
||||
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,
|
||||
) {
|
||||
dependsOn(settingsPatch)
|
||||
|
||||
compatibleWith(COMPATIBLE_PACKAGE)
|
||||
|
||||
execute {
|
||||
|
||||
// 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<FieldReference>()?.type == CLIENT_INFO_CLASS_DESCRIPTOR
|
||||
}?.getReference<FieldReference>()
|
||||
?: throw PatchException("Could not find clientInfoField")
|
||||
|
||||
// Client info object's client type field.
|
||||
val clientInfoClientTypeField =
|
||||
getInstruction(result.patternMatch!!.endIndex)
|
||||
.getReference<FieldReference>()
|
||||
?: throw PatchException("Could not find clientInfoClientTypeField")
|
||||
|
||||
val clientInfoVersionIndex = result.stringMatches!!.first().index
|
||||
val clientInfoVersionRegister =
|
||||
getInstruction<OneRegisterInstruction>(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<FieldReference>()
|
||||
?: 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<FieldReference>()
|
||||
opcode == Opcode.IPUT_OBJECT &&
|
||||
reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR &&
|
||||
reference.type == "Ljava/lang/String;"
|
||||
}
|
||||
getInstruction<ReferenceInstruction>(clientInfoClientModelIndex).reference
|
||||
}
|
||||
|
||||
val clientInfoOsVersionField = with (createPlayerRequestBodyWithVersionReleaseFingerprint.methodOrThrow()) {
|
||||
val buildIndex = indexOfBuildInstruction(this)
|
||||
val clientInfoOsVersionIndex = indexOfFirstInstructionOrThrow(buildIndex - 5) {
|
||||
val reference = getReference<FieldReference>()
|
||||
opcode == Opcode.IPUT_OBJECT &&
|
||||
reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR &&
|
||||
reference.type == "Ljava/lang/String;"
|
||||
}
|
||||
getInstruction<ReferenceInstruction>(clientInfoOsVersionIndex).reference
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Spoof client type for /player requests.
|
||||
|
||||
createPlayerRequestBodyFingerprint.matchOrThrow().let {
|
||||
it.method.apply {
|
||||
val setClientInfoMethodName = "setClientInfo"
|
||||
val checkCastIndex = it.patternMatch!!.startIndex
|
||||
|
||||
val checkCastInstruction = getInstruction<OneRegisterInstruction>(checkCastIndex)
|
||||
val requestMessageInstanceRegister = checkCastInstruction.registerA
|
||||
val clientInfoContainerClassName =
|
||||
checkCastInstruction.getReference<TypeReference>()!!.type
|
||||
|
||||
addInstruction(
|
||||
checkCastIndex + 1,
|
||||
"invoke-static { v$requestMessageInstanceRegister }," +
|
||||
" $definingClass->$setClientInfoMethodName($clientInfoContainerClassName)V",
|
||||
)
|
||||
|
||||
// Change client info to use the spoofed values.
|
||||
// Do this in a helper method, to remove the need of picking out multiple free registers from the hooked code.
|
||||
it.classDef.methods.add(
|
||||
ImmutableMethod(
|
||||
definingClass,
|
||||
setClientInfoMethodName,
|
||||
listOf(
|
||||
ImmutableMethodParameter(
|
||||
clientInfoContainerClassName,
|
||||
annotations,
|
||||
"clientInfoContainer"
|
||||
)
|
||||
),
|
||||
"V",
|
||||
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
||||
annotations,
|
||||
null,
|
||||
MutableMethodImplementation(3),
|
||||
).toMutable().apply {
|
||||
addInstructions(
|
||||
"""
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isClientSpoofingEnabled()Z
|
||||
move-result v0
|
||||
if-eqz v0, :disabled
|
||||
|
||||
iget-object v0, p0, $clientInfoField
|
||||
|
||||
# Set client type to the spoofed value.
|
||||
iget v1, v0, $clientInfoClientTypeField
|
||||
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientTypeId(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
|
||||
|
||||
# Set client version to the spoofed value.
|
||||
iget-object v1, v0, $clientInfoClientVersionField
|
||||
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v1
|
||||
iput-object v1, v0, $clientInfoClientVersionField
|
||||
|
||||
# Set client os version to the spoofed value.
|
||||
iget-object v1, v0, $clientInfoOsVersionField
|
||||
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v1
|
||||
iput-object v1, v0, $clientInfoOsVersionField
|
||||
|
||||
:disabled
|
||||
return-void
|
||||
""",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Spoof user-agent
|
||||
|
||||
userAgentHeaderBuilderFingerprint.methodOrThrow().apply {
|
||||
val insertIndex = implementation!!.instructions.lastIndex
|
||||
val insertRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA
|
||||
|
||||
addInstructions(
|
||||
insertIndex, """
|
||||
invoke-static { v$insertRegister }, $EXTENSION_CLASS_DESCRIPTOR->getUserAgent(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$insertRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
playbackSpeedBottomSheetFingerprint.mutableClassOrThrow().let {
|
||||
val onItemClickMethod =
|
||||
it.methods.find { method -> method.name == "onItemClick" }
|
||||
?: throw PatchException("Failed to find onItemClick method")
|
||||
|
||||
onItemClickMethod.apply {
|
||||
val createPlaybackSpeedMenuItemIndex = indexOfFirstInstructionReversedOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
reference?.returnType == "V" &&
|
||||
reference.parameterTypes.firstOrNull()?.startsWith("[L") == true
|
||||
}
|
||||
val createPlaybackSpeedMenuItemMethod = getWalkerMethod(createPlaybackSpeedMenuItemIndex)
|
||||
createPlaybackSpeedMenuItemMethod.apply {
|
||||
val shouldCreateMenuIndex = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
reference?.returnType == "Z" &&
|
||||
reference.parameterTypes.isEmpty()
|
||||
} + 2
|
||||
val shouldCreateMenuRegister = getInstruction<OneRegisterInstruction>(shouldCreateMenuIndex - 1).registerA
|
||||
|
||||
addInstructions(
|
||||
shouldCreateMenuIndex,
|
||||
"""
|
||||
invoke-static { v$shouldCreateMenuRegister }, $EXTENSION_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenu(Z)Z
|
||||
move-result v$shouldCreateMenuRegister
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSwitchPreference(
|
||||
CategoryType.MISC,
|
||||
"revanced_spoof_client",
|
||||
"false"
|
||||
)
|
||||
|
||||
updatePatchStatus(SPOOF_CLIENT)
|
||||
|
||||
}
|
||||
}
|
@ -141,6 +141,10 @@ internal enum class PatchList(
|
||||
"Spoof app version",
|
||||
"Adds options to spoof the YouTube Music client version. This can remove the radio mode restriction in Canadian regions or disable real-time lyrics."
|
||||
),
|
||||
SPOOF_CLIENT(
|
||||
"Spoof client",
|
||||
"Adds options to spoof the client to allow track playback."
|
||||
),
|
||||
TRANSLATIONS_FOR_YOUTUBE_MUSIC(
|
||||
"Translations for YouTube Music",
|
||||
"Add translations or remove string resources."
|
||||
|
@ -1,18 +1,8 @@
|
||||
package app.revanced.patches.music.video.playback
|
||||
|
||||
import app.revanced.util.fingerprint.legacyFingerprint
|
||||
import app.revanced.util.or
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val playbackSpeedBottomSheetFingerprint = legacyFingerprint(
|
||||
name = "playbackSpeedBottomSheetFingerprint",
|
||||
returnType = "V",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||
parameters = listOf("L"),
|
||||
strings = listOf("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT")
|
||||
)
|
||||
|
||||
internal val userQualityChangeFingerprint = legacyFingerprint(
|
||||
name = "userQualityChangeFingerprint",
|
||||
returnType = "V",
|
||||
|
@ -8,6 +8,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.music.utils.extension.Constants.VIDEO_PATH
|
||||
import app.revanced.patches.music.utils.patch.PatchList.VIDEO_PLAYBACK
|
||||
import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint
|
||||
import app.revanced.patches.music.utils.playbackSpeedFingerprint
|
||||
import app.revanced.patches.music.utils.playbackSpeedParentFingerprint
|
||||
import app.revanced.patches.music.utils.settings.CategoryType
|
||||
@ -54,8 +55,9 @@ val videoPlaybackPatch = bytecodePatch(
|
||||
playbackSpeedBottomSheetFingerprint.mutableClassOrThrow().let {
|
||||
val onItemClickMethod =
|
||||
it.methods.find { method -> method.name == "onItemClick" }
|
||||
?: throw PatchException("Failed to find onItemClick method")
|
||||
|
||||
onItemClickMethod?.apply {
|
||||
onItemClickMethod.apply {
|
||||
val targetIndex = indexOfFirstInstructionOrThrow(Opcode.IGET)
|
||||
val targetRegister =
|
||||
getInstruction<TwoRegisterInstruction>(targetIndex).registerA
|
||||
@ -64,7 +66,7 @@ val videoPlaybackPatch = bytecodePatch(
|
||||
targetIndex + 1,
|
||||
"invoke-static {v$targetRegister}, $EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->userSelectedPlaybackSpeed(F)V"
|
||||
)
|
||||
} ?: throw PatchException("Failed to find onItemClick method")
|
||||
}
|
||||
}
|
||||
|
||||
playbackSpeedFingerprint.matchOrThrow(playbackSpeedParentFingerprint).let {
|
||||
|
@ -442,6 +442,14 @@ This is required for the app to work."</string>
|
||||
Tap on the continue button and disable battery optimizations."</string>
|
||||
<string name="gms_core_dialog_continue_text">Continue</string>
|
||||
|
||||
<string name="revanced_spoof_client_title">Spoof client</string>
|
||||
<string name="revanced_spoof_client_summary">"Spoof the client to prevent playback issues.
|
||||
|
||||
Limitations:
|
||||
• OPUS audio codec may not be supported.
|
||||
• Seekbar thumbnail may not be present.
|
||||
• Watch history does not work with a brand account.</string>
|
||||
|
||||
<string name="revanced_sanitize_sharing_links_title">Sanitize sharing links</string>
|
||||
<string name="revanced_sanitize_sharing_links_summary">Removes tracking query parameters from URLs when sharing links.</string>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user