Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package app.revanced.patches.youtube.misc.downloadactions
Comment thread
Francesco146 marked this conversation as resolved.

import app.revanced.patcher.data.BytecodeContext
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.util.smali.ExternalLabel
import app.revanced.patches.youtube.misc.downloadactions.fingerprints.*
import app.revanced.patches.youtube.utils.compatibility.Constants
import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH
import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH
import app.revanced.patches.youtube.utils.mainactivity.MainActivityResolvePatch
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.Opcode
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.formats.Instruction35c
import com.android.tools.smali.dexlib2.iface.reference.MethodReference

@Suppress("unused")
object HookDownloadActionsPatch : BaseBytecodePatch(
name = "Hook download actions",
description = "Adds options to show the download playlist button and hook the download actions.",
dependencies = setOf(
MainActivityResolvePatch::class,
SharedResourceIdPatch::class,
SettingsPatch::class
),
compatiblePackages = Constants.COMPATIBLE_PACKAGE,
fingerprints = setOf(
OfflineVideoEndpointFingerprint,
PiPPlaybackFingerprint,
AccessibilityOfflineButtonSyncFingerprint,
DownloadPlaylistButtonOnClickFingerprint
)
) {
private const val INTEGRATIONS_DOWNLOAD_PLAYLIST_BUTTON_CLASS_DESCRIPTOR =
"$MISC_PATH/HookDownloadAction;"

private const val INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR =
"$INTEGRATIONS_PATH/utils/VideoUtils;"

override fun execute(context: BytecodeContext) {

// region Patch for hook download actions

OfflineVideoEndpointFingerprint.resultOrThrow().mutableMethod.apply {
addInstructionsWithLabels(
0, """
invoke-static/range {p3 .. p3}, $INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Ljava/lang/String;)Z
Comment thread
Francesco146 marked this conversation as resolved.
move-result v0
if-eqz v0, :show_native_downloader
return-void
""", ExternalLabel("show_native_downloader", getInstruction(0))
)
}

PiPPlaybackFingerprint.resultOrThrow().let {
Comment thread
Francesco146 marked this conversation as resolved.
it.mutableMethod.apply {
val insertIndex = it.scanResult.patternScanResult!!.endIndex
val insertRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA

addInstructions(
insertIndex, """
invoke-static {v$insertRegister}, $INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR->getExternalDownloaderLaunchedState(Z)Z
move-result v$insertRegister
"""
)
}
}

// endregion

// region Force show the playlist download button

AccessibilityOfflineButtonSyncFingerprint.resultOrThrow().let { parentResult ->
SetPlaylistDownloadButtonVisibilityFingerprint.also {
it.resolve(
context,
parentResult.classDef
)
}.resultOrThrow().let { setVisibilityMethod ->
setVisibilityMethod.mutableMethod.apply {
// Find the index of if-nez
val insertIndex = setVisibilityMethod.scanResult.patternScanResult!!.startIndex + 2
// Get register values used in if-nez
val insertRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA

// Add instructions just above the index of if-nez
addInstructions(
insertIndex,
"""
invoke-static {}, $INTEGRATIONS_DOWNLOAD_PLAYLIST_BUTTON_CLASS_DESCRIPTOR->isPlaylistDownloadButtonHooked()Z
move-result v$insertRegister
"""
)
}
}
}

// endregion

// region Hook Download Playlist Button OnClick method

DownloadPlaylistButtonOnClickFingerprint.resultOrThrow().let {
it.mutableMethod.apply {

// region Get the index of the instruction that initializes the onClickListener

val onClickListenerInitializeIndex = indexOfFirstInstructionOrThrow {
val reference = ((this as? ReferenceInstruction)?.reference as? MethodReference)

opcode == Opcode.INVOKE_VIRTUAL_RANGE
&& reference?.parameterTypes?.first() == "Ljava/lang/String;"
}

// endregion

// region Get the class that contains the onClick method

val onClickListenerInitializeReference =
getInstruction<ReferenceInstruction>(onClickListenerInitializeIndex).reference


val onClickClass = context.findClass(
(onClickListenerInitializeReference as MethodReference).returnType
)!!.mutableClass

// endregion

onClickClass.methods.find { method -> method.name == "onClick" }?.apply {

// region Get the index of playlist id

val insertIndex = implementation!!.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.INVOKE_STATIC
&& instruction.getReference<MethodReference>()?.name == "isEmpty"
}

val insertRegister = getInstruction<Instruction35c>(insertIndex).registerC

// endregion

addInstructions(
insertIndex,
"""
invoke-static {v$insertRegister}, $INTEGRATIONS_DOWNLOAD_PLAYLIST_BUTTON_CLASS_DESCRIPTOR->startPlaylistDownloadActivity(Ljava/lang/String;)Ljava/lang/String;
Comment thread
Francesco146 marked this conversation as resolved.
move-result-object v$insertRegister
""".trimIndent()
Comment thread
Francesco146 marked this conversation as resolved.
)
}
}
}

// endregion

/**
* Add settings
*/
SettingsPatch.addPreference(
arrayOf(
"PREFERENCE_SCREEN: GENERAL",
"SETTINGS: HOOK_DOWNLOAD_ACTIONS"
)
)

SettingsPatch.updatePatchStatus(this)
Comment thread
Francesco146 marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package app.revanced.patches.youtube.misc.downloadactions.fingerprints

import app.revanced.patcher.extensions.or
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.AccessibilityOfflineButtonSync
import app.revanced.util.fingerprint.LiteralValueFingerprint
import com.android.tools.smali.dexlib2.AccessFlags

object AccessibilityOfflineButtonSyncFingerprint : LiteralValueFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
literalSupplier = { AccessibilityOfflineButtonSync }
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package app.revanced.patches.youtube.misc.downloadactions.fingerprints

import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patches.youtube.misc.downloadactions.fingerprints.DownloadPlaylistButtonOnClickFingerprint.PLAYLIST_ON_CLICK_INITIALIZE_PAREMETER
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference


object DownloadPlaylistButtonOnClickFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
customFingerprint = { methodDef, _ ->
methodDef.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.parameterTypes == PLAYLIST_ON_CLICK_INITIALIZE_PAREMETER
} >= 0
}
) {
val PLAYLIST_ON_CLICK_INITIALIZE_PAREMETER = listOf(
"Ljava/lang/String;",
"Lcom/google/android/apps/youtube/app/offline/ui/OfflineArrowView;",
"I",
"Landroid/view/View${'$'}OnClickListener;"
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package app.revanced.patches.youtube.player.overlaybuttons.fingerprints
package app.revanced.patches.youtube.misc.downloadactions.fingerprints

import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package app.revanced.patches.youtube.player.overlaybuttons.fingerprints
package app.revanced.patches.youtube.misc.downloadactions.fingerprints

import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.revanced.patches.youtube.misc.downloadactions.fingerprints

import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode

object SetPlaylistDownloadButtonVisibilityFingerprint : MethodFingerprint(
returnType = "V",
opcodes = listOf(
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT,
Opcode.IF_NEZ,
Opcode.IGET,
Opcode.CONST_4
)
)
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
package app.revanced.patches.youtube.player.overlaybuttons

import app.revanced.patcher.data.BytecodeContext
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.patch.BytecodePatch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.youtube.player.overlaybuttons.fingerprints.OfflineVideoEndpointFingerprint
import app.revanced.patches.youtube.player.overlaybuttons.fingerprints.PiPPlaybackFingerprint
import app.revanced.patches.youtube.player.overlaybuttons.fingerprints.PlayerButtonConstructorFingerprint
import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH
import app.revanced.patches.youtube.utils.integrations.Constants.UTILS_PATH
import app.revanced.patches.youtube.utils.mainactivity.MainActivityResolvePatch
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch
import app.revanced.patches.youtube.video.information.VideoInformationPatch
import app.revanced.util.addFieldAndInstructions
import app.revanced.util.getReference
import app.revanced.util.getTargetIndexWithReferenceOrThrow
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.resultOrThrow
import app.revanced.util.*
import com.android.tools.smali.dexlib2.Opcode
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
Expand All @@ -38,48 +28,14 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference
)
object OverlayButtonsBytecodePatch : BytecodePatch(
setOf(
OfflineVideoEndpointFingerprint,
PiPPlaybackFingerprint,
PlayerButtonConstructorFingerprint
)
) {
private const val INTEGRATIONS_ALWAYS_REPEAT_CLASS_DESCRIPTOR =
"$UTILS_PATH/AlwaysRepeatPatch;"

private const val INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR =
"$INTEGRATIONS_PATH/utils/VideoUtils;"

override fun execute(context: BytecodeContext) {

// region patch for hook download button

OfflineVideoEndpointFingerprint.resultOrThrow().mutableMethod.apply {
addInstructionsWithLabels(
0, """
invoke-static/range {p3 .. p3}, $INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :show_native_downloader
return-void
""", ExternalLabel("show_native_downloader", getInstruction(0))
)
}

PiPPlaybackFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val insertIndex = it.scanResult.patternScanResult!!.endIndex
val insertRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA

addInstructions(
insertIndex, """
invoke-static {v$insertRegister}, $INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR->getExternalDownloaderLaunchedState(Z)Z
move-result v$insertRegister
"""
)
}
}

// endregion

// region patch for always repeat and pause

PlayerButtonConstructorFingerprint.resultOrThrow().mutableMethod.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import app.revanced.patches.shared.mapping.ResourceType.STYLE

@Patch(dependencies = [ResourceMappingPatch::class])
object SharedResourceIdPatch : ResourcePatch() {
var AccessibilityOfflineButtonSync = -1L
var AccountSwitcherAccessibility = -1L
var ActionBarRingo = -1L
var ActionBarRingoBackground = -1L
Expand Down Expand Up @@ -121,6 +122,7 @@ object SharedResourceIdPatch : ResourcePatch() {

override fun execute(context: ResourceContext) {

AccessibilityOfflineButtonSync = getId(STRING, "accessibility_offline_button_sync")
AccountSwitcherAccessibility = getId(STRING, "account_switcher_accessibility_label")
ActionBarRingo = getId(LAYOUT, "action_bar_ringo")
ActionBarRingoBackground = getId(LAYOUT, "action_bar_ringo_background")
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/youtube/settings/host/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,9 @@ You tab → View channel → Menu → Settings"</string>
Tap and hold to open YouTube settings."</string>
<string name="revanced_replace_toolbar_create_button_type_summary_off">"Tap to open YouTube settings.
Tap and hold to open RVX settings."</string>
<string name="revanced_hook_playlist_download_button_title">Override playlist download action</string>
<string name="revanced_hook_playlist_download_button_summary_off">Playlist download will behave like the original, and button might not appear.</string>
<string name="revanced_hook_playlist_download_button_summary_on">Playlist download button will be present, and the download will be handled by YTDLnis.</string>


<!-- PreferenceScreen: Player -->
Expand Down Expand Up @@ -900,6 +903,9 @@ Tap and hold to undo."</string>
<string name="revanced_external_downloader_package_name_title">External downloader package name</string>
<string name="revanced_external_downloader_package_name_summary">Package name of your installed external downloader app, such as NewPipe or YTDLnis.</string>
<string name="revanced_external_downloader_dialog_title">External downloader</string>
<string name="revanced_playlist_external_downloader_package_name_title">Playlist external downloader package name</string>
<string name="revanced_playlist_external_downloader_package_name_summary">Package name of your installed external downloader app used to download from playlists, currently only YTDLnis works.</string>
<string name="revanced_playlist_external_downloader_dialog_title">Playlist external downloader</string>
<string name="revanced_external_downloader_not_installed_dialog_title">Warning</string>
<string name="revanced_external_downloader_not_installed_dialog_message">"%1$s is not installed.
Please download %2$s from the website."</string>
Expand Down
Loading