From d647061d949e8780b7ba66a21e865e2ae4ff3c83 Mon Sep 17 00:00:00 2001 From: Francesco Marastoni <49027005+Francesco146@users.noreply.github.com> Date: Tue, 6 Aug 2024 02:48:40 +0000 Subject: [PATCH] feat(YouTube): add `Hook download actions` patch (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(YouTube/Download Playlist Button): add playlist download button Co-authored-by: Hoàng Gia Bảo <70064328+YT-Advanced@users.noreply.github.com> Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com> * feat(YouTube/Download Playlist Button): add playlist external downloader field * refactor(YouTube/Download Playlist Button): remove duplicate resources --------- Co-authored-by: Hoàng Gia Bảo <70064328+YT-Advanced@users.noreply.github.com> Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com> --- .../HookDownloadActionsPatch.kt | 174 ++++++++++++++++++ ...cessibilityOfflineButtonSyncFingerprint.kt | 12 ++ ...ownloadPlaylistButtonOnClickFingerprint.kt | 29 +++ .../OfflineVideoEndpointFingerprint.kt | 2 +- .../fingerprints/PiPPlaybackFingerprint.kt | 2 +- ...listDownloadButtonVisibilityFingerprint.kt | 15 ++ .../OverlayButtonsBytecodePatch.kt | 46 +---- .../utils/resourceid/SharedResourceIdPatch.kt | 2 + .../youtube/settings/host/values/strings.xml | 6 + .../youtube/settings/xml/revanced_prefs.xml | 8 +- 10 files changed, 247 insertions(+), 49 deletions(-) create mode 100644 src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/HookDownloadActionsPatch.kt create mode 100644 src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/AccessibilityOfflineButtonSyncFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/DownloadPlaylistButtonOnClickFingerprint.kt rename src/main/kotlin/app/revanced/patches/youtube/{player/overlaybuttons => misc/downloadactions}/fingerprints/OfflineVideoEndpointFingerprint.kt (87%) rename src/main/kotlin/app/revanced/patches/youtube/{player/overlaybuttons => misc/downloadactions}/fingerprints/PiPPlaybackFingerprint.kt (86%) create mode 100644 src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/SetPlaylistDownloadButtonVisibilityFingerprint.kt diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/HookDownloadActionsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/HookDownloadActionsPatch.kt new file mode 100644 index 000000000..1ccc4ba52 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/HookDownloadActionsPatch.kt @@ -0,0 +1,174 @@ +package app.revanced.patches.youtube.misc.downloadactions + +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 + 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(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(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(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()?.name == "isEmpty" + } + + val insertRegister = getInstruction(insertIndex).registerC + + // endregion + + addInstructions( + insertIndex, + """ + invoke-static {v$insertRegister}, $INTEGRATIONS_DOWNLOAD_PLAYLIST_BUTTON_CLASS_DESCRIPTOR->startPlaylistDownloadActivity(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$insertRegister + """.trimIndent() + ) + } + } + } + + // endregion + + /** + * Add settings + */ + SettingsPatch.addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: HOOK_DOWNLOAD_ACTIONS" + ) + ) + + SettingsPatch.updatePatchStatus(this) + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/AccessibilityOfflineButtonSyncFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/AccessibilityOfflineButtonSyncFingerprint.kt new file mode 100644 index 000000000..cc7e9433f --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/AccessibilityOfflineButtonSyncFingerprint.kt @@ -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 } +) diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/DownloadPlaylistButtonOnClickFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/DownloadPlaylistButtonOnClickFingerprint.kt new file mode 100644 index 000000000..91135c871 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/DownloadPlaylistButtonOnClickFingerprint.kt @@ -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()?.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;" + ) +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/fingerprints/OfflineVideoEndpointFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/OfflineVideoEndpointFingerprint.kt similarity index 87% rename from src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/fingerprints/OfflineVideoEndpointFingerprint.kt rename to src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/OfflineVideoEndpointFingerprint.kt index 7d5a03766..280db49a1 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/fingerprints/OfflineVideoEndpointFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/OfflineVideoEndpointFingerprint.kt @@ -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 diff --git a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/fingerprints/PiPPlaybackFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/PiPPlaybackFingerprint.kt similarity index 86% rename from src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/fingerprints/PiPPlaybackFingerprint.kt rename to src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/PiPPlaybackFingerprint.kt index 04ef6cd29..ae1bad0fb 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/fingerprints/PiPPlaybackFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/PiPPlaybackFingerprint.kt @@ -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 diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/SetPlaylistDownloadButtonVisibilityFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/SetPlaylistDownloadButtonVisibilityFingerprint.kt new file mode 100644 index 000000000..3b5c41eda --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/downloadactions/fingerprints/SetPlaylistDownloadButtonVisibilityFingerprint.kt @@ -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 + ) +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsBytecodePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsBytecodePatch.kt index 0ee132f45..4c4d634ab 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsBytecodePatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsBytecodePatch.kt @@ -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 @@ -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(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 { diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt index 3a5bca2e5..f066d69c4 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt @@ -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 @@ -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") diff --git a/src/main/resources/youtube/settings/host/values/strings.xml b/src/main/resources/youtube/settings/host/values/strings.xml index f31507442..f4804a209 100644 --- a/src/main/resources/youtube/settings/host/values/strings.xml +++ b/src/main/resources/youtube/settings/host/values/strings.xml @@ -505,6 +505,9 @@ You tab → View channel → Menu → Settings" Tap and hold to open YouTube settings." "Tap to open YouTube settings. Tap and hold to open RVX settings." + Override playlist download action + Playlist download will behave like the original, and button might not appear. + Playlist download button will be present, and the download will be handled by YTDLnis. @@ -900,6 +903,9 @@ Tap and hold to undo." External downloader package name Package name of your installed external downloader app, such as NewPipe or YTDLnis. External downloader + Playlist external downloader package name + Package name of your installed external downloader app used to download from playlists, currently only YTDLnis works. + Playlist external downloader Warning "%1$s is not installed. Please download %2$s from the website." diff --git a/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 46b6d5e92..f0ed5ed08 100644 --- a/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -230,6 +230,11 @@ SETTINGS: LAYOUT_SWITCH --> + + + SETTINGS: OVERLAY_BUTTONS -->