feat(YouTube Music): Add Disable music video in album patch https://github.com/inotia00/ReVanced_Extended/issues/2568

This commit is contained in:
inotia00 2025-01-03 20:04:50 +09:00
parent 1da2664513
commit 8ab67bc6ef
12 changed files with 549 additions and 0 deletions

View File

@ -0,0 +1,109 @@
package app.revanced.extension.music.patches.misc;
import androidx.annotation.NonNull;
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.utils.VideoUtils;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class AlbumMusicVideoPatch {
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
private static final boolean DISABLE_MUSIC_VIDEO_IN_ALBUM =
Settings.DISABLE_MUSIC_VIDEO_IN_ALBUM.get();
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
@NonNull
private static volatile String playerResponseVideoId = "";
@NonNull
private static volatile String currentVideoId = "";
/**
* Injection point.
*/
public static void newPlayerResponse(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
return;
}
if (!playlistId.startsWith(YOUTUBE_MUSIC_ALBUM_PREFIX)) {
return;
}
if (playlistIndex < 0) {
return;
}
if (playerResponseVideoId.equals(videoId)) {
return;
}
playerResponseVideoId = videoId;
// Fetch from piped instances.
PipedRequester.fetchRequestIfNeeded(videoId, playlistId, playlistIndex);
}
/**
* Injection point.
*/
public static void newVideoLoaded(@NonNull String videoId) {
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
return;
}
if (currentVideoId.equals(videoId)) {
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);
}
private static void openOfficialMusicIfNeeded(@NonNull String videoId) {
try {
PipedRequester request = PipedRequester.getRequestForVideoId(videoId);
if (request == null) {
return;
}
String songId = request.getStream();
if (songId == null) {
return;
}
// It is handled by YouTube Music's internal code.
// There is a slight delay before the dismiss request is reflected.
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.
// To prevent fetch requests from being sent, set the video id to the newly opened video
VideoUtils.runOnMainThreadDelayed(() -> {
playerResponseVideoId = songId;
currentVideoId = songId;
VideoUtils.openInYouTubeMusic(songId);
}, 750);
// 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);
}
}
/**
* Injection point.
*/
public static boolean hideSnackBar() {
if (!DISABLE_MUSIC_VIDEO_IN_ALBUM) {
return false;
}
return isVideoLaunched.get();
}
}

View File

@ -0,0 +1,177 @@
package app.revanced.extension.music.patches.misc.requests;
import android.annotation.SuppressLint;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
public class PipedRequester {
/**
* How long to keep fetches until they are expired.
*/
private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute
private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds
@GuardedBy("itself")
private static final Map<String, PipedRequester> cache = new HashMap<>();
@SuppressLint("ObsoleteSdkInt")
public static void fetchRequestIfNeeded(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
synchronized (cache) {
final long now = System.currentTimeMillis();
cache.values().removeIf(request -> {
final boolean expired = request.isExpired(now);
if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
return expired;
});
if (!cache.containsKey(videoId)) {
PipedRequester pipedRequester = new PipedRequester(videoId, playlistId, playlistIndex);
cache.put(videoId, pipedRequester);
}
}
}
@Nullable
public static PipedRequester getRequestForVideoId(@Nullable String videoId) {
synchronized (cache) {
return cache.get(videoId);
}
}
/**
* TCP timeout
*/
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 2 * 1000; // 2 seconds
/**
* HTTP response timeout
*/
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 4 * 1000; // 4 seconds
@Nullable
private static JSONObject send(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
final long startTime = System.currentTimeMillis();
Logger.printDebug(() -> "Fetching piped instances (videoId: '" + videoId +
"', playlistId: '" + playlistId + "', playlistIndex: '" + playlistIndex + "'");
try {
HttpURLConnection connection = PipedRoutes.getPlaylistConnectionFromRoute(playlistId);
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) return Requester.parseJSONObject(connection);
handleConnectionError("API not available: " + responseCode);
} catch (SocketTimeoutException ex) {
handleConnectionError("Connection timeout", ex);
} catch (IOException ex) {
handleConnectionError("Network error", ex);
} catch (Exception ex) {
Logger.printException(() -> "send failed", ex);
} finally {
Logger.printDebug(() -> "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
}
return null;
}
@Nullable
private static String fetch(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
final JSONObject playlistJson = send(videoId, playlistId, playlistIndex);
if (playlistJson != null) {
try {
final String songId = playlistJson.getJSONArray("relatedStreams")
.getJSONObject(playlistIndex)
.getString("url")
.replaceAll("/.+=", "");
if (songId.isEmpty()) {
handleConnectionError("Url is empty!");
} else if (!songId.equals(videoId)) {
return songId;
}
} catch (JSONException e) {
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
}
}
return null;
}
private static void handleConnectionError(@NonNull String errorMessage) {
handleConnectionError(errorMessage, null);
}
private static void handleConnectionError(@NonNull String errorMessage, @Nullable Exception ex) {
if (ex != null) {
Logger.printInfo(() -> errorMessage, ex);
}
}
/**
* Time this instance and the fetch future was created.
*/
private final long timeFetched;
private final String videoId;
private final Future<String> future;
private PipedRequester(@NonNull String videoId, @NonNull String playlistId, final int playlistIndex) {
this.timeFetched = System.currentTimeMillis();
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playlistId, playlistIndex));
}
public boolean isExpired(long now) {
final long timeSinceCreation = now - timeFetched;
if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
return true;
}
// Only expired if the fetch failed (API null response).
return (fetchCompleted() && getStream() == null);
}
/**
* @return if the fetch call has completed.
*/
public boolean fetchCompleted() {
return future.isDone();
}
public String getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printInfo(() -> "getStream timed out", ex);
} catch (InterruptedException ex) {
Logger.printException(() -> "getStream interrupted", ex);
Thread.currentThread().interrupt(); // Restore interrupt status flag.
} catch (ExecutionException ex) {
Logger.printException(() -> "getStream failure", ex);
}
return null;
}
}

View File

@ -0,0 +1,22 @@
package app.revanced.extension.music.patches.misc.requests;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import java.io.IOException;
import java.net.HttpURLConnection;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
class PipedRoutes {
private static final String PIPED_URL = "https://pipedapi.kavin.rocks/";
private static final Route GET_PLAYLIST = new Route(GET, "playlists/{playlist_id}");
private PipedRoutes() {
}
static HttpURLConnection getPlaylistConnectionFromRoute(String... params) throws IOException {
return Requester.getConnectionFromRoute(PIPED_URL, GET_PLAYLIST, params);
}
}

View File

@ -178,6 +178,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 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 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_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 BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, 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 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); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);

View File

@ -71,6 +71,13 @@ public class VideoUtils extends IntentUtils {
launchView(url, context.getPackageName()); launchView(url, context.getPackageName());
} }
/**
* Rest of the implementation added by patch.
*/
public static void dismissQueue() {
Log.d("Extended: VideoUtils", "Queue dismissed");
}
/** /**
* Rest of the implementation added by patch. * Rest of the implementation added by patch.
*/ */

View File

@ -0,0 +1,85 @@
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.patch.bytecodePatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.dismiss.dismissQueueHookPatch
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.playservice.is_7_03_or_greater
import app.revanced.patches.music.utils.playservice.versionCheckPatch
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.music.video.information.videoIdHook
import app.revanced.patches.music.video.information.videoInformationPatch
import app.revanced.util.fingerprint.methodOrThrow
private const val EXTENSION_CLASS_DESCRIPTOR =
"$MISC_PATH/AlbumMusicVideoPatch;"
@Suppress("unused")
val albumMusicVideoPatch = bytecodePatch(
DISABLE_MUSIC_VIDEO_IN_ALBUM.title,
DISABLE_MUSIC_VIDEO_IN_ALBUM.summary,
false,
) {
compatibleWith(COMPATIBLE_PACKAGE)
dependsOn(
settingsPatch,
dismissQueueHookPatch,
videoInformationPatch,
versionCheckPatch,
)
execute {
// region hook player response
val fingerprint = if (is_7_03_or_greater) {
playerParameterBuilderFingerprint
} else {
playerParameterBuilderLegacyFingerprint
}
fingerprint.methodOrThrow().addInstruction(
0,
"invoke-static {p1, p4, p5}, $EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V"
)
// endregion
// region hook video id
videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V")
// endregion
// region patch for hide snack bar
snackBarParentFingerprint.methodOrThrow().addInstructionsWithLabels(
0, """
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideSnackBar()Z
move-result v0
if-eqz v0, :hide
return-void
:hide
nop
"""
)
// endregion
addSwitchPreference(
CategoryType.MISC,
"revanced_disable_music_video_in_album",
"false"
)
updatePatchStatus(DISABLE_MUSIC_VIDEO_IN_ALBUM)
}
}

View File

@ -0,0 +1,69 @@
package app.revanced.patches.music.misc.album
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
/**
* For targets 7.03 and later.
*/
internal val playerParameterBuilderFingerprint = legacyFingerprint(
name = "playerParameterBuilderFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "L",
parameters = listOf(
"Ljava/lang/String;", // VideoId.
"[B",
"Ljava/lang/String;", // Player parameters proto buffer.
"Ljava/lang/String;", // PlaylistId.
"I", // PlaylistIndex.
"I",
"L",
"Ljava/util/Set;",
"Ljava/lang/String;",
"Ljava/lang/String;",
"L",
"Z",
"Z",
"Z", // Appears to indicate if the video id is being opened or is currently playing.
),
strings = listOf("psps")
)
/**
* For targets 7.02 and earlier.
*/
internal val playerParameterBuilderLegacyFingerprint = legacyFingerprint(
name = "playerParameterBuilderLegacyFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "L",
parameters = listOf(
"Ljava/lang/String;", // VideoId.
"[B",
"Ljava/lang/String;", // Player parameters proto buffer.
"Ljava/lang/String;", // PlaylistId.
"I", // PlaylistIndex.
"I",
"Ljava/util/Set;",
"Ljava/lang/String;",
"Ljava/lang/String;",
"L",
"Z",
"Z", // Appears to indicate if the video id is being opened or is currently playing.
),
opcodes = listOf(
Opcode.INVOKE_INTERFACE,
Opcode.MOVE_RESULT_OBJECT,
Opcode.CHECK_CAST,
Opcode.INVOKE_INTERFACE
)
)
internal val snackBarParentFingerprint = legacyFingerprint(
name = "snackBarParentFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
parameters = listOf("L"),
strings = listOf("No suitable parent found from the given view. Please provide a valid view.")
)

View File

@ -0,0 +1,42 @@
package app.revanced.patches.music.utils.dismiss
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.music.utils.extension.Constants.EXTENSION_PATH
import app.revanced.util.addStaticFieldToExtension
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getWalkerMethod
private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR =
"$EXTENSION_PATH/utils/VideoUtils;"
@Suppress("unused")
val dismissQueueHookPatch = bytecodePatch(
description = "dismissQueueHookPatch"
) {
execute {
dismissQueueFingerprint.methodOrThrow().apply {
val dismissQueueIndex = indexOfDismissQueueInstruction(this)
getWalkerMethod(dismissQueueIndex).apply {
val smaliInstructions =
"""
if-eqz v0, :ignore
invoke-virtual {v0}, $definingClass->$name()V
:ignore
return-void
"""
addStaticFieldToExtension(
EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR,
"dismissQueue",
"dismissQueueClass",
definingClass,
smaliInstructions
)
}
}
}
}

View File

@ -0,0 +1,24 @@
package app.revanced.patches.music.utils.dismiss
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
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 dismissQueueFingerprint = legacyFingerprint(
name = "dismissQueueFingerprint",
returnType = "V",
parameters = listOf("L"),
customFingerprint = { method, _ ->
method.name == "handleDismissWatchEvent" &&
indexOfDismissQueueInstruction(method) >= 0
}
)
internal fun indexOfDismissQueueInstruction(method: Method) =
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.definingClass?.endsWith("/MppWatchWhileLayout;") == true
}

View File

@ -57,6 +57,10 @@ internal enum class PatchList(
"Disable dislike redirection", "Disable dislike redirection",
"Adds an option to disable redirection to the next track when clicking the Dislike button." "Adds an option to disable redirection to the next track when clicking the Dislike button."
), ),
DISABLE_MUSIC_VIDEO_IN_ALBUM(
"Disable music video in album",
"Adds option to redirect music videos from albums."
),
ENABLE_OPUS_CODEC( ENABLE_OPUS_CODEC(
"Enable OPUS codec", "Enable OPUS codec",
"Adds an options to enable the OPUS audio codec if the player response includes." "Adds an options to enable the OPUS audio codec if the player response includes."

View File

@ -11,6 +11,8 @@ var is_6_36_or_greater = false
private set private set
var is_6_42_or_greater = false var is_6_42_or_greater = false
private set private set
var is_7_03_or_greater = false
private set
var is_7_06_or_greater = false var is_7_06_or_greater = false
private set private set
var is_7_13_or_greater = false var is_7_13_or_greater = false
@ -43,6 +45,7 @@ val versionCheckPatch = resourcePatch(
is_6_27_or_greater = 234412000 <= playStoreServicesVersion is_6_27_or_greater = 234412000 <= playStoreServicesVersion
is_6_36_or_greater = 240399000 <= playStoreServicesVersion is_6_36_or_greater = 240399000 <= playStoreServicesVersion
is_6_42_or_greater = 240999000 <= playStoreServicesVersion is_6_42_or_greater = 240999000 <= playStoreServicesVersion
is_7_03_or_greater = 242199000 <= playStoreServicesVersion
is_7_06_or_greater = 242499000 <= playStoreServicesVersion is_7_06_or_greater = 242499000 <= playStoreServicesVersion
is_7_13_or_greater = 243199000 <= playStoreServicesVersion is_7_13_or_greater = 243199000 <= playStoreServicesVersion
is_7_17_or_greater = 243530000 <= playStoreServicesVersion is_7_17_or_greater = 243530000 <= playStoreServicesVersion

View File

@ -420,6 +420,12 @@ Click to see how to issue a API key."</string>
<string name="revanced_disable_cairo_splash_animation_summary">Disables Cairo splash animation when the app starts up.</string> <string name="revanced_disable_cairo_splash_animation_summary">Disables Cairo splash animation when the app starts up.</string>
<string name="revanced_disable_drc_audio_title">Disable DRC audio</string> <string name="revanced_disable_drc_audio_title">Disable DRC audio</string>
<string name="revanced_disable_drc_audio_summary">Disables DRC (Dynamic Range Compression) applied to audio.</string> <string name="revanced_disable_drc_audio_summary">Disables DRC (Dynamic Range Compression) applied to audio.</string>
<string name="revanced_disable_music_video_in_album_title">Disable music video in album</string>
<string name="revanced_disable_music_video_in_album_summary">"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.
A piped instance is used, but the API may not be available in some regions."</string>
<string name="revanced_enable_debug_logging_title">Enable debug logging</string> <string name="revanced_enable_debug_logging_title">Enable debug logging</string>
<string name="revanced_enable_debug_logging_summary">Prints the debug log.</string> <string name="revanced_enable_debug_logging_summary">Prints the debug log.</string>
<string name="revanced_enable_debug_buffer_logging_title">Enable debug buffer logging</string> <string name="revanced_enable_debug_buffer_logging_title">Enable debug buffer logging</string>