Skip to content

feat(Spotify): Remove ads section from browse #5193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import static java.lang.Boolean.TRUE;

import app.revanced.extension.spotify.shared.ComponentFilters.*;
import com.spotify.home.evopage.homeapi.proto.Section;

import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -99,8 +98,16 @@ private static class OverrideAttribute {
* response which delivers home sections.
*/
private static final List<Integer> REMOVED_HOME_SECTIONS = List.of(
Section.VIDEO_BRAND_AD_FIELD_NUMBER,
Section.IMAGE_BRAND_AD_FIELD_NUMBER
com.spotify.home.evopage.homeapi.proto.Section.VIDEO_BRAND_AD_FIELD_NUMBER,
com.spotify.home.evopage.homeapi.proto.Section.IMAGE_BRAND_AD_FIELD_NUMBER
);

/**
* A list of browse sections feature types ids which should be removed. These ids match the ones from the protobuf
* response which delivers browse sections.
*/
private static final List<Integer> REMOVED_BROWSE_SECTIONS = List.of(
com.spotify.browsita.v1.resolved.Section.BRAND_ADS_FIELD_NUMBER
);

/**
Expand Down Expand Up @@ -174,26 +181,58 @@ public static String removeStationString(String spotifyUriOrUrl) {
}
}

/**
* Injection point. Remove ads sections from home.
* Depends on patching abstract protobuf list ensureIsMutable method.
*/
public static void removeHomeSections(List<Section> sections) {

private interface FeatureTypeIdProvider<T> {
int getFeatureTypeId(T section);
}

private static <T> void removeSections(
List<T> sections,
FeatureTypeIdProvider<T> featureTypeExtractor,
List<Integer> idsToRemove
) {
try {
Iterator<Section> iterator = sections.iterator();
Iterator<T> iterator = sections.iterator();

while (iterator.hasNext()) {
Section section = iterator.next();
if (REMOVED_HOME_SECTIONS.contains(section.featureTypeCase_)) {
Logger.printInfo(() -> "Removing home section with feature type id " + section.featureTypeCase_);
T section = iterator.next();
int featureTypeId = featureTypeExtractor.getFeatureTypeId(section);
if (idsToRemove.contains(featureTypeId)) {
Logger.printInfo(() -> "Removing section with feature type id " + featureTypeId);
iterator.remove();
}
}
} catch (Exception ex) {
Logger.printException(() -> "removeHomeSections failure", ex);
Logger.printException(() -> "removeSections failure", ex);
}
}

/**
* Injection point. Remove ads sections from home.
* Depends on patching abstract protobuf list ensureIsMutable method.
*/
public static void removeHomeSections(List<com.spotify.home.evopage.homeapi.proto.Section> sections) {
Logger.printInfo(() -> "Removing ads section from home");
removeSections(
sections,
section -> section.featureTypeCase_,
REMOVED_HOME_SECTIONS
);
}

/**
* Injection point. Remove ads sections from browse.
* Depends on patching abstract protobuf list ensureIsMutable method.
*/
public static void removeBrowseSections(List<com.spotify.browsita.v1.resolved.Section> sections) {
Logger.printInfo(() -> "Removing ads section from browse");
removeSections(
sections,
section -> section.sectionTypeCase_,
REMOVED_BROWSE_SECTIONS
);
}

/**
* Injection point. Returns whether the context menu item is a Premium ad.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.spotify.browsita.v1.resolved;

public final class Section {
public static final int BRAND_ADS_FIELD_NUMBER = 6;
public int sectionTypeCase_;
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,28 @@ internal val abstractProtobufListEnsureIsMutableFingerprint = fingerprint {
}
}

internal val homeSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
}

internal val homeStructureGetSectionsFingerprint = fingerprint {
private fun structureGetSectionsFingerprint(className: String) = fingerprint {
custom { method, classDef ->
classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
classDef.endsWith(className) && method.indexOfFirstInstruction {
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
} >= 0
}
}

internal val homeSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
}

internal val homeStructureGetSectionsFingerprint =
structureGetSectionsFingerprint("homeapi/proto/HomeStructure;")

internal val browseSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("browsita/v1/resolved/Section;") }
}

internal val browseStructureGetSectionsFingerprint =
structureGetSectionsFingerprint("browsita/v1/resolved/BrowseStructure;")

internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
returns("Ljava/lang/Object;")
parameters("Ljava/lang/Object;")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.revanced.patches.spotify.misc

import app.revanced.patcher.Fingerprint
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
Expand Down Expand Up @@ -178,21 +179,40 @@ val unlockPremiumPatch = bytecodePatch(
abstractProtobufListEnsureIsMutableFingerprint.match(abstractProtobufListClassDef)
.method.returnEarly()

// Make featureTypeCase_ accessible so we can check the home section type in the extension.
homeSectionFingerprint.classDef.publicizeField("featureTypeCase_")

// Remove ads sections from home.
homeStructureGetSectionsFingerprint.method.apply {
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA

addInstruction(
getSectionsIndex + 1,
"invoke-static { v$sectionsRegister }, " +
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
)
fun injectRemoveSectionCall(
sectionFingerprint: Fingerprint,
structureFingerprint: Fingerprint,
fieldName: String,
methodName: String
) {
// Make field accessible so we can check the home/browse section type in the extension.
sectionFingerprint.classDef.publicizeField(fieldName)

structureFingerprint.method.apply {
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA

addInstruction(
getSectionsIndex + 1,
"invoke-static { v$sectionsRegister }, " +
"$EXTENSION_CLASS_DESCRIPTOR->$methodName(Ljava/util/List;)V"
)
}
}

injectRemoveSectionCall(
homeSectionFingerprint,
homeStructureGetSectionsFingerprint,
"featureTypeCase_",
"removeHomeSections"
)

injectRemoveSectionCall(
browseSectionFingerprint,
browseStructureGetSectionsFingerprint,
"sectionTypeCase_",
"removeBrowseSections"
)

// Replace a fetch request that returns and maps Singles with their static onErrorReturn value.
fun MutableMethod.replaceFetchRequestSingleWithError(requestClassName: String) {
Expand Down
Loading