diff --git a/extensions/spotify/build.gradle.kts b/extensions/spotify/build.gradle.kts
new file mode 100644
index 000000000..07cce9e23
--- /dev/null
+++ b/extensions/spotify/build.gradle.kts
@@ -0,0 +1,3 @@
+dependencies {
+ compileOnly(project(":extensions:spotify:stub"))
+}
diff --git a/extensions/spotify/src/main/AndroidManifest.xml b/extensions/spotify/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/spotify/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java
new file mode 100644
index 000000000..35be583b7
--- /dev/null
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java
@@ -0,0 +1,37 @@
+package app.revanced.extension.spotify.misc;
+
+import com.spotify.remoteconfig.internal.AccountAttribute;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @noinspection unused
+ */
+public final class UnlockPremiumPatch {
+ private static final Map OVERRIDES = Map.of(
+ // Disables player and app ads.
+ "ads", false,
+ // Works along on-demand, allows playing any song without restriction.
+ "player-license", "premium",
+ // Disables shuffle being initially enabled when first playing a playlist.
+ "shuffle", false,
+ // Allows playing any song on-demand, without a shuffled order.
+ "on-demand", true,
+ // Allows adding songs to queue and removes the smart shuffle mode restriction,
+ // allowing to pick any of the other modes.
+ "pick-and-shuffle", false,
+ // Disables shuffle-mode streaming-rule, which forces songs to be played shuffled
+ // and breaks the player when other patches are applied.
+ "streaming-rules", "",
+ // Enables premium UI in settings and removes the premium button in the nav-bar.
+ "nft-disabled", "1"
+ );
+
+ public static void overrideAttribute(Map attributes) {
+ for (var entry : OVERRIDES.entrySet()) {
+ var attribute = Objects.requireNonNull(attributes.get(entry.getKey()));
+ attribute.value_ = entry.getValue();
+ }
+ }
+}
diff --git a/extensions/spotify/stub/build.gradle.kts b/extensions/spotify/stub/build.gradle.kts
new file mode 100644
index 000000000..a8da923ed
--- /dev/null
+++ b/extensions/spotify/stub/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ id(libs.plugins.android.library.get().pluginId)
+}
+
+android {
+ namespace = "app.revanced.extension"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
diff --git a/extensions/spotify/stub/src/main/AndroidManifest.xml b/extensions/spotify/stub/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/spotify/stub/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/spotify/stub/src/main/java/com/spotify/remoteconfig/internal/AccountAttribute.java b/extensions/spotify/stub/src/main/java/com/spotify/remoteconfig/internal/AccountAttribute.java
new file mode 100644
index 000000000..5b6d0fa18
--- /dev/null
+++ b/extensions/spotify/stub/src/main/java/com/spotify/remoteconfig/internal/AccountAttribute.java
@@ -0,0 +1,5 @@
+package com.spotify.remoteconfig.internal;
+
+public final class AccountAttribute {
+ public Object value_;
+}
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 92106329f..f84f9113e 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -816,6 +816,10 @@ public final class app/revanced/patches/spotify/lite/ondemand/OnDemandPatchKt {
public static final fun getOnDemandPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/spotify/misc/UnlockPremiumPatchKt {
+ public static final fun getUnlockPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt {
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
new file mode 100644
index 000000000..1fe64b8cf
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
@@ -0,0 +1,18 @@
+package app.revanced.patches.spotify.misc
+
+import app.revanced.patcher.fingerprint
+
+internal val accountAttributeFingerprint = fingerprint {
+ custom { _, c -> c.endsWith("internal/AccountAttribute;") }
+}
+
+internal val productStateProtoFingerprint = fingerprint {
+ returns("Ljava/util/Map;")
+ custom { _, classDef ->
+ classDef.endsWith("ProductStateProto;")
+ }
+}
+
+internal val buildQueryParametersFingerprint = fingerprint {
+ strings("trackRows", "device_type:tablet")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
new file mode 100644
index 000000000..dff32638e
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
@@ -0,0 +1,36 @@
+package app.revanced.patches.spotify.misc
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import com.android.tools.smali.dexlib2.AccessFlags
+
+@Suppress("unused")
+val unlockPremiumPatch = bytecodePatch(
+ name = "Unlock Spotify Premium",
+ description = "Unlock Spotify Premium features. Server-sided features like downloading songs are still locked.",
+) {
+ compatibleWith("com.spotify.music")
+
+ extendWith("extensions/spotify.rve")
+
+ execute {
+ // Make _value accessible so that it can be overridden in the extension.
+ accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply {
+ accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
+ }
+
+ // Override the attributes map in the getter method.
+ val attributesMapRegister = 0
+ val instantiateUnmodifiableMapIndex = 1
+ productStateProtoFingerprint.method.addInstruction(
+ instantiateUnmodifiableMapIndex,
+ "invoke-static { v$attributesMapRegister }," +
+ "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;->overrideAttribute(Ljava/util/Map;)V",
+ )
+
+ // Add the query parameter trackRows to show popular tracks in the artist page.
+ val addQueryParameterIndex = buildQueryParametersFingerprint.stringMatches!!.first().index - 1
+ buildQueryParametersFingerprint.method.replaceInstruction(addQueryParameterIndex, "nop")
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt
deleted file mode 100644
index 761e32206..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package app.revanced.patches.spotify.navbar
-
-import app.revanced.patcher.fingerprint
-import app.revanced.util.literal
-import com.android.tools.smali.dexlib2.AccessFlags
-
-internal val addNavBarItemFingerprint = fingerprint {
- accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
- returns("V")
- literal { showBottomNavigationItemsTextId }
-}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt
index c191c676b..ddc92bcc4 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt
@@ -1,50 +1,12 @@
package app.revanced.patches.spotify.navbar
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patcher.patch.resourcePatch
-import app.revanced.patches.shared.misc.mapping.get
-import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
-import app.revanced.patches.shared.misc.mapping.resourceMappings
-
-internal var showBottomNavigationItemsTextId = -1L
- private set
-internal var premiumTabId = -1L
- private set
-
-private val premiumNavbarTabResourcePatch = resourcePatch {
- dependsOn(resourceMappingPatch)
-
- execute {
- premiumTabId = resourceMappings["id", "premium_tab"]
-
- showBottomNavigationItemsTextId = resourceMappings[
- "bool",
- "show_bottom_navigation_items_text",
- ]
- }
-}
+import app.revanced.patches.spotify.misc.unlockPremiumPatch
+@Deprecated("Superseded by unlockPremiumPatch", ReplaceWith("unlockPremiumPatch"))
@Suppress("unused")
val premiumNavbarTabPatch = bytecodePatch(
- name = "Premium navbar tab",
description = "Hides the premium tab from the navigation bar.",
) {
- dependsOn(premiumNavbarTabResourcePatch)
-
- compatibleWith("com.spotify.music")
-
- // If the navigation bar item is the premium tab, do not add it.
- execute {
- addNavBarItemFingerprint.method.addInstructions(
- 0,
- """
- const v1, $premiumTabId
- if-ne p5, v1, :continue
- return-void
- :continue
- nop
- """,
- )
- }
+ dependsOn(unlockPremiumPatch)
}