mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 05:07:41 +02:00
feat(YouTube Music): add Spoof client
patch
This commit is contained in:
@ -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 {
|
||||
|
Reference in New Issue
Block a user