From 88435902ff5daa876fccb176fa3f7c827928fc58 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:18:10 +0900 Subject: [PATCH] feat(YouTube Music - Disable music video in album): Add redirection option --- .../patches/misc/AlbumMusicVideoPatch.java | 134 +++++++++++++++--- .../extension/music/settings/Settings.java | 3 + .../ReVancedPreferenceFragment.java | 7 +- .../music/misc/album/AlbumMusicVideoPatch.kt | 48 +++++++ .../patches/music/misc/album/Fingerprints.kt | 20 +++ .../music/settings/host/values/arrays.xml | 16 +++ .../music/settings/host/values/strings.xml | 12 +- 7 files changed, 217 insertions(+), 23 deletions(-) diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java index d7ccccbad..08fca8a31 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java @@ -1,20 +1,58 @@ package app.revanced.extension.music.patches.misc; +import android.view.View; + +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import app.revanced.extension.music.patches.misc.requests.PipedRequester; import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; import app.revanced.extension.music.utils.VideoUtils; import app.revanced.extension.shared.utils.Logger; @SuppressWarnings("unused") public class AlbumMusicVideoPatch { - private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK"; + + public enum RedirectType { + REDIRECT_DISMISS(true), + REDIRECT(false), + ON_CLICK_DISMISS(true), + ON_CLICK(false), + ON_LONG_CLICK_DISMISS(true), + ON_LONG_CLICK(false); + + public final boolean dismissQueue; + + RedirectType(boolean dismissQueue) { + this.dismissQueue = dismissQueue; + } + } + + private static final RedirectType REDIRECT_TYPE = + Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE.get(); + private static final boolean DISABLE_MUSIC_VIDEO_IN_ALBUM = Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM.get(); + private static final boolean DISMISS_QUEUE = + DISABLE_MUSIC_VIDEO_IN_ALBUM && REDIRECT_TYPE.dismissQueue; + + private static final boolean REDIRECT = + REDIRECT_TYPE == RedirectType.REDIRECT || REDIRECT_TYPE == RedirectType.REDIRECT_DISMISS; + + private static final boolean ON_CLICK = + REDIRECT_TYPE == RedirectType.ON_CLICK || REDIRECT_TYPE == RedirectType.ON_CLICK_DISMISS; + + private static final boolean ON_LONG_CLICK = + REDIRECT_TYPE == RedirectType.ON_LONG_CLICK || REDIRECT_TYPE == RedirectType.ON_LONG_CLICK_DISMISS; + + private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK"; + private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false); @NonNull @@ -23,6 +61,16 @@ public class AlbumMusicVideoPatch { @NonNull private static volatile String currentVideoId = ""; + @GuardedBy("itself") + private static final Map lastVideoIds = new LinkedHashMap<>() { + private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; + } + }; + /** * Injection point. */ @@ -41,7 +89,7 @@ public class AlbumMusicVideoPatch { } playerResponseVideoId = videoId; - // Fetch from piped instances. + // Fetch Piped instance. PipedRequester.fetchRequestIfNeeded(videoId, playlistId, playlistIndex); } @@ -56,13 +104,10 @@ public class AlbumMusicVideoPatch { return; } currentVideoId = videoId; - - // If the user is using a not fast enough internet connection, there will be a slight delay. - // Otherwise, the video may open repeatedly. - VideoUtils.runOnMainThreadDelayed(() -> openOfficialMusicIfNeeded(videoId), 750); + checkVideo(videoId); } - private static void openOfficialMusicIfNeeded(@NonNull String videoId) { + private static void checkVideo(@NonNull String videoId) { try { PipedRequester request = PipedRequester.getRequestForVideoId(videoId); if (request == null) { @@ -72,13 +117,46 @@ public class AlbumMusicVideoPatch { if (songId == null) { return; } + synchronized (lastVideoIds) { + if (lastVideoIds.put(videoId, songId) == null) { + Logger.printDebug(() -> "Official song found, videoId: " + videoId + ", songId: " + songId); + if (REDIRECT) { + openMusic(songId); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "check failure", ex); + } + } - // It is handled by YouTube Music's internal code. - // There is a slight delay before the dismiss request is reflected. - VideoUtils.dismissQueue(); + /** + * Injection point. + */ + public static boolean openMusic() { + if (DISABLE_MUSIC_VIDEO_IN_ALBUM && ON_CLICK) { + try { + String videoId = VideoInformation.getVideoId(); + synchronized (lastVideoIds) { + String songId = lastVideoIds.get(videoId); + if (songId != null) { + openMusic(songId); + return true; + } + } + } catch (Exception ex) { + Logger.printException(() -> "openMusic failure", ex); + } + } + return false; + } + + private static void openMusic(@NonNull String songId) { + try { + if (DISMISS_QUEUE) { + VideoUtils.dismissQueue(); + } - // Every time a new video is opened, a snack bar appears indicating that the account has been switched. - // To prevent this, hide the snack bar while a new video is opening. isVideoLaunched.compareAndSet(false, true); // The newly opened video is not a music video. @@ -87,12 +165,33 @@ public class AlbumMusicVideoPatch { playerResponseVideoId = songId; currentVideoId = songId; VideoUtils.openInYouTubeMusic(songId); - }, 750); + }, 500); - // If a new video is opened, the snack bar will be shown. VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 1500); } catch (Exception ex) { - Logger.printException(() -> "openOfficialMusicIfNeeded failure", ex); + Logger.printException(() -> "openMusic failure", ex); + } + } + + /** + * Injection point. + */ + public static void setAudioVideoSwitchToggleOnLongClickListener(View view) { + if (DISABLE_MUSIC_VIDEO_IN_ALBUM && ON_LONG_CLICK) { + view.setOnLongClickListener(v -> { + try { + String videoId = VideoInformation.getVideoId(); + synchronized (lastVideoIds) { + String songId = lastVideoIds.get(videoId); + if (songId != null) { + openMusic(songId); + } + } + } catch (Exception ex) { + Logger.printException(() -> "onLongClickListener failure", ex); + } + return true; + }); } } @@ -100,10 +199,7 @@ public class AlbumMusicVideoPatch { * Injection point. */ public static boolean hideSnackBar() { - if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) { - return false; - } - return isVideoLaunched.get(); + return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched.get(); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java index 4ff5dc57e..f2638e371 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -6,6 +6,7 @@ import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviou import androidx.annotation.NonNull; +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; @@ -179,6 +180,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true); public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true); public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true); + public static final EnumSetting DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT_DISMISS, 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); @@ -248,6 +250,7 @@ public class Settings extends BaseSettings { CHANGE_START_PAGE.key, CUSTOM_FILTER_STRINGS.key, CUSTOM_PLAYBACK_SPEEDS.key, + DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE.key, EXTERNAL_DOWNLOADER_PACKAGE_NAME.key, HIDE_ACCOUNT_MENU_FILTER_STRINGS.key, SB_API_URL.key, diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java index 313588ef9..1245e8dce 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java @@ -4,6 +4,7 @@ import static app.revanced.extension.music.settings.Settings.BYPASS_IMAGE_REGION import static app.revanced.extension.music.settings.Settings.CHANGE_START_PAGE; import static app.revanced.extension.music.settings.Settings.CUSTOM_FILTER_STRINGS; import static app.revanced.extension.music.settings.Settings.CUSTOM_PLAYBACK_SPEEDS; +import static app.revanced.extension.music.settings.Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE; import static app.revanced.extension.music.settings.Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME; import static app.revanced.extension.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS; import static app.revanced.extension.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS; @@ -160,9 +161,11 @@ public class ReVancedPreferenceFragment extends PreferenceFragment { Logger.printDebug(() -> "Failed to find the right value: " + dataString); } } else if (settings instanceof EnumSetting enumSetting) { - if (settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT) + if (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)) { + || settings.equals(SPOOF_STREAMING_DATA_TYPE) + ) { ResettableListPreference.showDialog(mActivity, enumSetting, 0); } } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt index 51bf2d4c1..1479cc65f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt @@ -2,6 +2,7 @@ package app.revanced.patches.music.misc.album import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.music.utils.dismiss.dismissQueueHookPatch @@ -9,13 +10,21 @@ import app.revanced.patches.music.utils.extension.Constants.MISC_PATH import app.revanced.patches.music.utils.patch.PatchList.DISABLE_MUSIC_VIDEO_IN_ALBUM import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.video.information.videoIdHook import app.revanced.patches.music.video.information.videoInformationPatch import app.revanced.patches.music.video.playerresponse.hookPlayerResponse import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch +import app.revanced.util.findMethodOrThrow import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference private const val EXTENSION_CLASS_DESCRIPTOR = "$MISC_PATH/AlbumMusicVideoPatch;" @@ -64,11 +73,50 @@ val albumMusicVideoPatch = bytecodePatch( // endregion + // region patch for setOnClick / setOnLongClick listener + + audioVideoSwitchToggleConstructorFingerprint.methodOrThrow().apply { + val onClickListenerIndex = indexOfAudioVideoSwitchSetOnClickListenerInstruction(this) + val viewRegister = getInstruction(onClickListenerIndex).registerC + + addInstruction( + onClickListenerIndex + 1, + "invoke-static { v$viewRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->setAudioVideoSwitchToggleOnLongClickListener(Landroid/view/View;)V" + ) + + val onClickListenerSyntheticIndex = indexOfFirstInstructionReversedOrThrow(onClickListenerIndex) { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.name == "" + } + val onClickListenerSyntheticClass = (getInstruction(onClickListenerSyntheticIndex).reference as MethodReference).definingClass + + findMethodOrThrow(onClickListenerSyntheticClass) { + name == "onClick" + }.addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->openMusic()Z + move-result v0 + if-eqz v0, :ignore + return-void + :ignore + nop + """ + ) + } + + // endregion + addSwitchPreference( CategoryType.MISC, "revanced_disable_music_video_in_album", "false" ) + addPreferenceWithIntent( + CategoryType.MISC, + "revanced_disable_music_video_in_album_redirect_type", + "revanced_disable_music_video_in_album" + ) updatePatchStatus(DISABLE_MUSIC_VIDEO_IN_ALBUM) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt index e91201b8c..52f0bf163 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/Fingerprints.kt @@ -1,9 +1,29 @@ package app.revanced.patches.music.misc.album 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 audioVideoSwitchToggleConstructorFingerprint = legacyFingerprint( + name = "audioVideoSwitchToggleConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + opcodes = listOf(Opcode.INVOKE_DIRECT), + customFingerprint = { method, _ -> + indexOfAudioVideoSwitchSetOnClickListenerInstruction(method) >= 0 + } +) + +internal fun indexOfAudioVideoSwitchSetOnClickListenerInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Lcom/google/android/apps/youtube/music/player/AudioVideoSwitcherToggleView;->setOnClickListener(Landroid/view/View${'$'}OnClickListener;)V" + } internal val snackBarParentFingerprint = legacyFingerprint( name = "snackBarParentFingerprint", diff --git a/patches/src/main/resources/music/settings/host/values/arrays.xml b/patches/src/main/resources/music/settings/host/values/arrays.xml index e61708942..97897dac9 100644 --- a/patches/src/main/resources/music/settings/host/values/arrays.xml +++ b/patches/src/main/resources/music/settings/host/values/arrays.xml @@ -14,6 +14,22 @@ FEmusic_library_landing FEmusic_library_corpus_artists + + @string/revanced_disable_music_video_in_album_redirect_type_entry_redirect_dismiss + @string/revanced_disable_music_video_in_album_redirect_type_entry_redirect + @string/revanced_disable_music_video_in_album_redirect_type_entry_on_click_dismiss + @string/revanced_disable_music_video_in_album_redirect_type_entry_on_click + @string/revanced_disable_music_video_in_album_redirect_type_entry_on_long_click_dismiss + @string/revanced_disable_music_video_in_album_redirect_type_entry_on_long_click + + + REDIRECT_DISMISS + REDIRECT + ON_CLICK_DISMISS + ON_CLICK + ON_LONG_CLICK_DISMISS + ON_LONG_CLICK + @string/revanced_extended_settings_export_as_file @string/revanced_extended_settings_import_as_file diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml index ef3c97f92..e93faecae 100644 --- a/patches/src/main/resources/music/settings/host/values/strings.xml +++ b/patches/src/main/resources/music/settings/host/values/strings.xml @@ -423,9 +423,17 @@ Click to see how to issue a API key." Disable music video in album "When a non-premium user plays a song included in an album, the music video is sometimes played instead of the official song. -If such a music video is detected playing, it is redirected to the official song. +Find the official song if a music video is detected playing from an album. -A piped instance is used, but the API may not be available in some regions." +• Powered by Piped Instance API." + Redirection type + Specifies how to redirect to official song. + Redirect (Dismiss queue) + Redirect + Tap Audio / Video toggle (Dismiss queue) + Tap Audio / Video toggle + Tap and hold Audio / Video toggle (Dismiss queue) + Tap and hold Audio / Video toggle Enable debug logging Prints the debug log. Enable debug buffer logging