From 8736b6a80b48cb1f4562c9f9919804006ddb18bd Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:25:58 +0200 Subject: [PATCH] feat(Spotify): Add `Change lyrics provider` patch (#4937) --- .../spotify/shared/ComponentFilters.java | 8 +- patches/api/patches.api | 4 + .../createbutton/HideCreateButtonPatch.kt | 4 +- .../misc/fix/login/FixFacebookLoginPatch.kt | 2 +- .../misc/lyrics/ChangeLyricsProviderPatch.kt | 123 ++++++++++++++++++ .../spotify/misc/lyrics/Fingerprints.kt | 21 +++ 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java index 0349c713b..21f1dd3e3 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java @@ -1,11 +1,14 @@ package app.revanced.extension.spotify.shared; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; public final class ComponentFilters { public interface ComponentFilter { + @NonNull String getFilterValue(); String getFilterRepresentation(); default boolean filterUnavailable() { @@ -20,7 +23,8 @@ public final class ComponentFilters { // Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded. // 0 is returned when a resource has not been found. private int resourceId = -1; - private String stringfiedResourceId = null; + @Nullable + private String stringfiedResourceId; public ResourceIdComponentFilter(String resourceName, String resourceType) { this.resourceName = resourceName; @@ -34,6 +38,7 @@ public final class ComponentFilters { return resourceId; } + @NonNull @Override public String getFilterValue() { if (stringfiedResourceId == null) { @@ -66,6 +71,7 @@ public final class ComponentFilters { this.string = string; } + @NonNull @Override public String getFilterValue() { return string; diff --git a/patches/api/patches.api b/patches/api/patches.api index ba28dbbfa..5a6c50d0f 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -921,6 +921,10 @@ public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginP public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatchKt { + public static final fun getChangeLyricsProviderPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt { public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt index 9bdd69be8..38581c8c9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt @@ -94,10 +94,10 @@ val hideCreateButtonPatch = bytecodePatch( """ invoke-static { v$oldNavigationBarItemTitleResIdRegister }, $isOldCreateButtonDescriptor move-result v0 - + # If this navigation bar item is not the Create button, jump to the normal method logic. if-eqz v0, :normal-method-logic - + # Return null early because this method return value is a BottomNavigationItemView. const/4 v0, 0 return-object v0 diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt index 8c415c5b5..f8be93ac5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt @@ -19,7 +19,7 @@ val fixFacebookLoginPatch = bytecodePatch( // signature checks. val katanaProxyLoginMethodHandlerClass = katanaProxyLoginMethodHandlerClassFingerprint.originalClassDef - // Always return 0 (no Intent was launched) as the result of trying to authorize with the Facebook app to + // Always return 0 (no Intent was launched) as the result of trying to authorize with the Facebook app to // make the login fallback to a web browser window. katanaProxyLoginMethodTryAuthorizeFingerprint .match(katanaProxyLoginMethodHandlerClass) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt new file mode 100644 index 000000000..99ec038c2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt @@ -0,0 +1,123 @@ +package app.revanced.patches.spotify.misc.lyrics + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import java.net.InetAddress +import java.net.URI +import java.net.URISyntaxException +import java.net.UnknownHostException +import java.util.logging.Logger + +@Suppress("unused") +val changeLyricsProviderPatch = bytecodePatch( + name = "Change lyrics provider", + description = "Changes the lyrics provider to a custom one.", + use = false, +) { + compatibleWith("com.spotify.music") + + val lyricsProviderHost by stringOption( + key = "lyricsProviderHost", + default = "lyrics.natanchiodi.fr", + title = "Lyrics provider host", + description = "The domain name or IP address of a custom lyrics provider.", + required = false, + ) { + // Fix bad data if the user enters a URL (https://whatever.com/path). + val host = try { + URI(it!!).host ?: it + } catch (e: URISyntaxException) { + return@stringOption false + } + + // Do a courtesy check if the host can be resolved. + // If it does not resolve, then print a warning but use the host anyway. + // Unresolvable hosts should not be rejected, since the patching environment + // may not allow network connections or the network may be down. + try { + InetAddress.getByName(host) + } catch (e: UnknownHostException) { + Logger.getLogger(this::class.java.name).warning( + "Host \"$host\" did not resolve to any domain." + ) + } + true + } + + execute { + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + Logger.getLogger(this::class.java.name).severe( + "Change lyrics provider patch is not supported for this target version." + ) + return@execute + } + + val httpClientBuilderMethod = httpClientBuilderFingerprint.originalMethod + + // region Create a modified copy of the HTTP client builder method with the custom lyrics provider host. + val patchedHttpClientBuilderMethod = with(httpClientBuilderMethod) { + val invokeBuildUrlIndex = indexOfFirstInstructionOrThrow { + getReference()?.returnType == "Lokhttp3/HttpUrl;" + } + val setUrlBuilderHostIndex = indexOfFirstInstructionReversedOrThrow(invokeBuildUrlIndex) { + val reference = getReference() + reference?.definingClass == "Lokhttp3/HttpUrl${"$"}Builder;" && + reference.parameterTypes.firstOrNull() == "Ljava/lang/String;" + } + val hostRegister = getInstruction(setUrlBuilderHostIndex).registerD + + MutableMethod(this).apply { + name = "rv_getCustomLyricsProviderHttpClient" + addInstruction( + setUrlBuilderHostIndex, + "const-string v$hostRegister, \"$lyricsProviderHost\"" + ) + + // Add the patched method to the class. + httpClientBuilderFingerprint.classDef.methods.add(this) + } + } + //endregion + + // region Replace the call to the HTTP client builder method used exclusively for lyrics by the modified one. + getLyricsHttpClientFingerprint(httpClientBuilderMethod).method.apply { + val getLyricsHttpClientIndex = indexOfFirstInstructionOrThrow { + getReference() == httpClientBuilderMethod + } + val getLyricsHttpClientInstruction = getInstruction(getLyricsHttpClientIndex) + + // Replace the original method call with a call to our patched method. + replaceInstruction( + getLyricsHttpClientIndex, + BuilderInstruction35c( + getLyricsHttpClientInstruction.opcode, + getLyricsHttpClientInstruction.registerCount, + getLyricsHttpClientInstruction.registerC, + getLyricsHttpClientInstruction.registerD, + getLyricsHttpClientInstruction.registerE, + getLyricsHttpClientInstruction.registerF, + getLyricsHttpClientInstruction.registerG, + ImmutableMethodReference( + patchedHttpClientBuilderMethod.definingClass, + patchedHttpClientBuilderMethod.name, // Only difference from the original method. + patchedHttpClientBuilderMethod.parameters, + patchedHttpClientBuilderMethod.returnType + ) + ) + ) + } + //endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt new file mode 100644 index 000000000..f55fc349f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.spotify.misc.lyrics + +import app.revanced.patcher.fingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val httpClientBuilderFingerprint = fingerprint { + strings("client == null", "scheduler == null") +} + +internal fun getLyricsHttpClientFingerprint(httpClientBuilderMethodReference: MethodReference) = + fingerprint { + returns(httpClientBuilderMethodReference.returnType) + parameters() + custom { method, _ -> + method.indexOfFirstInstruction { + getReference() == httpClientBuilderMethodReference + } >= 0 + } + }