feat(YouTube): separate the Bypass image region restrictions patch from the Alternative thumbnails patch (Reflecting changes in ReVanced)

This commit is contained in:
inotia00
2024-07-17 15:53:21 +09:00
parent 0309d371f7
commit d10869a129
21 changed files with 283 additions and 249 deletions

View File

@ -1,6 +0,0 @@
package app.revanced.patches.music.misc.alternativedomain
import app.revanced.patches.shared.alternativedomain.BaseAlternativeDomainPatch
import app.revanced.patches.music.utils.integrations.Constants.MISC_PATH
object AlternativeDomainBytecodePatch : BaseAlternativeDomainPatch("$MISC_PATH/AlternativeDomainPatch;")

View File

@ -1,35 +0,0 @@
package app.revanced.patches.music.misc.alternativedomain
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.SettingsPatch
import app.revanced.util.patch.BaseBytecodePatch
import java.io.Closeable
@Suppress("unused")
object AlternativeDomainPatch : BaseBytecodePatch(
name = "Alternative domain",
description = "Adds options to replace static images(avatars, playlist covers, etc.) domain.",
dependencies = setOf(
AlternativeDomainBytecodePatch::class,
SettingsPatch::class
),
compatiblePackages = COMPATIBLE_PACKAGE
), Closeable {
override fun execute(context: BytecodeContext) {
}
// Use Closeable for lexicographic arrangement of settings.
override fun close() {
SettingsPatch.addSwitchPreference(
CategoryType.MISC,
"revanced_use_alternative_domain",
"false"
)
SettingsPatch.addPreferenceWithIntent(
CategoryType.MISC,
"revanced_alternative_domain",
"revanced_use_alternative_domain"
)
}
}

View File

@ -0,0 +1,30 @@
package app.revanced.patches.music.misc.thumbnails
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.imageurlhook.CronetImageUrlHookPatch
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.SettingsPatch
import app.revanced.util.patch.BaseBytecodePatch
@Suppress("unused")
object BypassImageRegionRestrictionsPatch : BaseBytecodePatch(
name = "Bypass image region restrictions",
description = "Adds an option to use a different host for static images," +
"and can fix missing images that are blocked in some countries.",
dependencies = setOf(
CronetImageUrlHookPatch::class,
SettingsPatch::class
),
compatiblePackages = COMPATIBLE_PACKAGE
) {
override fun execute(context: BytecodeContext) {
CronetImageUrlHookPatch.addImageUrlHook()
SettingsPatch.addSwitchPreference(
CategoryType.MISC,
"revanced_bypass_image_region_restrictions",
"false"
)
}
}

View File

@ -0,0 +1,5 @@
package app.revanced.patches.music.utils.imageurlhook
import app.revanced.patches.shared.imageurlhook.BaseCronetImageUrlHookPatch
object CronetImageUrlHookPatch : BaseCronetImageUrlHookPatch(false)

View File

@ -1,30 +0,0 @@
package app.revanced.patches.shared.alternativedomain
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patches.shared.alternativedomain.fingerprints.MessageDigestImageUrlFingerprint
import app.revanced.patches.shared.alternativedomain.fingerprints.MessageDigestImageUrlParentFingerprint
import app.revanced.util.resultOrThrow
abstract class BaseAlternativeDomainPatch(
private val classDescriptor: String
) : BytecodePatch(
setOf(MessageDigestImageUrlParentFingerprint)
) {
override fun execute(context: BytecodeContext) {
MessageDigestImageUrlFingerprint.resolve(
context,
MessageDigestImageUrlParentFingerprint.resultOrThrow().classDef
)
MessageDigestImageUrlFingerprint.resultOrThrow().mutableMethod.addInstructions(
0, """
invoke-static { p1 }, $classDescriptor->overrideImageURL(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
"""
)
}
}

View File

@ -0,0 +1,132 @@
package app.revanced.patches.shared.imageurlhook
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.imageurlhook.fingerprints.MessageDigestImageUrlFingerprint
import app.revanced.patches.shared.imageurlhook.fingerprints.MessageDigestImageUrlParentFingerprint
import app.revanced.patches.shared.imageurlhook.fingerprints.cronet.RequestFingerprint
import app.revanced.patches.shared.imageurlhook.fingerprints.cronet.request.callback.OnFailureFingerprint
import app.revanced.patches.shared.imageurlhook.fingerprints.cronet.request.callback.OnResponseStartedFingerprint
import app.revanced.patches.shared.imageurlhook.fingerprints.cronet.request.callback.OnSucceededFingerprint
import app.revanced.patches.shared.integrations.Constants.PATCHES_PATH
import app.revanced.util.alsoResolve
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
abstract class BaseCronetImageUrlHookPatch(
private val resolveCronetRequest: Boolean
) : BytecodePatch(
setOf(
MessageDigestImageUrlParentFingerprint,
OnResponseStartedFingerprint,
RequestFingerprint
)
) {
companion object {
private const val INTEGRATION_SHARED_CLASS_DESCRIPTOR = "$PATCHES_PATH/BypassImageRegionRestrictionsPatch;"
private lateinit var loadImageUrlMethod: MutableMethod
private var loadImageUrlIndex = 0
private lateinit var loadImageSuccessCallbackMethod: MutableMethod
private var loadImageSuccessCallbackIndex = 0
private lateinit var loadImageErrorCallbackMethod: MutableMethod
private var loadImageErrorCallbackIndex = 0
}
/**
* @param highPriority If the hook should be called before all other hooks.
*/
internal fun addImageUrlHook(targetMethodClass: String = INTEGRATION_SHARED_CLASS_DESCRIPTOR, highPriority: Boolean = true) {
loadImageUrlMethod.addInstructions(
if (highPriority) 0 else loadImageUrlIndex,
"""
invoke-static { p1 }, $targetMethodClass->overrideImageURL(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
""",
)
loadImageUrlIndex += 2
}
/**
* If a connection completed, which includes normal 200 responses but also includes
* status 404 and other error like http responses.
*/
internal fun addImageUrlSuccessCallbackHook(targetMethodClass: String) {
loadImageSuccessCallbackMethod.addInstruction(
loadImageSuccessCallbackIndex++,
"invoke-static { p1, p2 }, $targetMethodClass->handleCronetSuccess(" +
"Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V",
)
}
/**
* If a connection outright failed to complete any connection.
*/
internal fun addImageUrlErrorCallbackHook(targetMethodClass: String) {
loadImageErrorCallbackMethod.addInstruction(
loadImageErrorCallbackIndex++,
"invoke-static { p1, p2, p3 }, $targetMethodClass->handleCronetFailure(" +
"Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Ljava/io/IOException;)V",
)
}
override fun execute(context: BytecodeContext) {
loadImageUrlMethod = MessageDigestImageUrlFingerprint
.alsoResolve(context, MessageDigestImageUrlParentFingerprint).mutableMethod
if (!resolveCronetRequest) return
loadImageSuccessCallbackMethod = OnSucceededFingerprint
.alsoResolve(context, OnResponseStartedFingerprint).mutableMethod
loadImageErrorCallbackMethod = OnFailureFingerprint
.alsoResolve(context, OnResponseStartedFingerprint).mutableMethod
// The URL is required for the failure callback hook, but the URL field is obfuscated.
// Add a helper get method that returns the URL field.
RequestFingerprint.resultOrThrow().apply {
// The url is the only string field that is set inside the constructor.
val urlFieldInstruction = mutableMethod.getInstructions().single {
if (it.opcode != Opcode.IPUT_OBJECT) return@single false
val reference = (it as ReferenceInstruction).reference as FieldReference
reference.type == "Ljava/lang/String;"
} as ReferenceInstruction
val urlFieldName = (urlFieldInstruction.reference as FieldReference).name
val definingClass = RequestFingerprint.IMPLEMENTATION_CLASS_NAME
val addedMethodName = "getHookedUrl"
mutableClass.methods.add(
ImmutableMethod(
definingClass,
addedMethodName,
emptyList(),
"Ljava/lang/String;",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
addInstructions(
"""
iget-object v0, p0, $definingClass->$urlFieldName:Ljava/lang/String;
return-object v0
""",
)
}
)
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.alternativedomain.fingerprints
package app.revanced.patches.shared.imageurlhook.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
@ -6,5 +6,5 @@ import com.android.tools.smali.dexlib2.AccessFlags
internal object MessageDigestImageUrlFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
parameters = listOf("Ljava/lang/String;", "L")
parameters = listOf("Ljava/lang/String;", "L")
)

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.alternativedomain.fingerprints
package app.revanced.patches.shared.imageurlhook.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
@ -6,7 +6,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
internal object MessageDigestImageUrlParentFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "Ljava/lang/String;",
parameters = emptyList(),
returnType = "Ljava/lang/String;",
parameters = listOf(),
strings = listOf("@#&=*+-_.,:!?()/~'%;\$"),
)

View File

@ -1,8 +1,8 @@
package app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet
package app.revanced.patches.shared.imageurlhook.fingerprints.cronet
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.RequestFingerprint.IMPLEMENTATION_CLASS_NAME
import app.revanced.patches.shared.imageurlhook.fingerprints.cronet.RequestFingerprint.IMPLEMENTATION_CLASS_NAME
import com.android.tools.smali.dexlib2.AccessFlags
internal object RequestFingerprint : MethodFingerprint(

View File

@ -1,4 +1,4 @@
package app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.request.callback
package app.revanced.patches.shared.imageurlhook.fingerprints.cronet.request.callback
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
@ -7,11 +7,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
internal object OnFailureFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf(
"Lorg/chromium/net/UrlRequest;",
"Lorg/chromium/net/UrlResponseInfo;",
"Lorg/chromium/net/CronetException;"
),
parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;", "Lorg/chromium/net/CronetException;"),
customFingerprint = { methodDef, _ ->
methodDef.name == "onFailed"
}

View File

@ -1,4 +1,4 @@
package app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.request.callback
package app.revanced.patches.shared.imageurlhook.fingerprints.cronet.request.callback
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
@ -8,7 +8,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
internal object OnResponseStartedFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;"),
parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;"),
strings = listOf(
"Content-Length",
"Content-Type",

View File

@ -1,4 +1,4 @@
package app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.request.callback
package app.revanced.patches.shared.imageurlhook.fingerprints.cronet.request.callback
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
@ -7,7 +7,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
internal object OnSucceededFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;"),
parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;"),
customFingerprint = { methodDef, _ ->
methodDef.name == "onSucceeded"
}

View File

@ -0,0 +1,41 @@
package app.revanced.patches.youtube.alternative.thumbnails
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.imageurlhook.CronetImageUrlHookPatch
import app.revanced.patches.youtube.utils.integrations.Constants.ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.navigation.NavigationBarHookPatch
import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch
import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.util.patch.BaseBytecodePatch
@Suppress("unused")
object AlternativeThumbnailsPatch : BaseBytecodePatch(
name = "Alternative thumbnails",
description = "Adds options to replace video thumbnails using the DeArrow API or image captures from the video.",
dependencies = setOf(
CronetImageUrlHookPatch::class,
NavigationBarHookPatch::class,
PlayerTypeHookPatch::class,
SettingsPatch::class,
),
compatiblePackages = COMPATIBLE_PACKAGE
) {
override fun execute(context: BytecodeContext) {
CronetImageUrlHookPatch.addImageUrlHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR)
CronetImageUrlHookPatch.addImageUrlSuccessCallbackHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR)
CronetImageUrlHookPatch.addImageUrlErrorCallbackHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR)
/**
* Add settings
*/
SettingsPatch.addPreference(
arrayOf(
"PREFERENCE_SCREEN: ALTERNATIVE_THUMBNAILS",
"SETTINGS: ALTERNATIVE_THUMBNAILS"
)
)
SettingsPatch.updatePatchStatus(this)
}
}

View File

@ -0,0 +1,35 @@
package app.revanced.patches.youtube.alternative.thumbnails
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.imageurlhook.CronetImageUrlHookPatch
import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.util.patch.BaseBytecodePatch
@Suppress("unused")
object BypassImageRegionRestrictionsPatch : BaseBytecodePatch(
name = "Bypass image region restrictions",
description = "Adds an option to use a different host for static images," +
"and can fix missing images that are blocked in some countries.",
dependencies = setOf(
CronetImageUrlHookPatch::class,
SettingsPatch::class,
),
compatiblePackages = COMPATIBLE_PACKAGE
) {
override fun execute(context: BytecodeContext) {
CronetImageUrlHookPatch.addImageUrlHook()
/**
* Add settings
*/
SettingsPatch.addPreference(
arrayOf(
"PREFERENCE_SCREEN: ALTERNATIVE_THUMBNAILS",
"SETTINGS: BYPASS_IMAGE_REGION_RESTRICTIONS"
)
)
SettingsPatch.updatePatchStatus(this)
}
}

View File

@ -1,6 +0,0 @@
package app.revanced.patches.youtube.alternativethumbnails.general
import app.revanced.patches.shared.alternativedomain.BaseAlternativeDomainPatch
import app.revanced.patches.youtube.utils.integrations.Constants.ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR
object AlternativeDomainBytecodePatch : BaseAlternativeDomainPatch(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR)

View File

@ -1,142 +0,0 @@
package app.revanced.patches.youtube.alternativethumbnails.general
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.RequestFingerprint
import app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.request.callback.OnFailureFingerprint
import app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.request.callback.OnResponseStartedFingerprint
import app.revanced.patches.youtube.alternativethumbnails.general.fingerprints.cronet.request.callback.OnSucceededFingerprint
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.integrations.Constants.ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.navigation.NavigationBarHookPatch
import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch
import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.util.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
@Suppress("unused")
object AlternativeThumbnailsPatch : BaseBytecodePatch(
name = "Alternative thumbnails",
description = "Adds options to replace video thumbnails using the DeArrow API or image captures from the video.",
dependencies = setOf(
AlternativeDomainBytecodePatch::class,
NavigationBarHookPatch::class,
PlayerTypeHookPatch::class,
SettingsPatch::class,
),
compatiblePackages = COMPATIBLE_PACKAGE,
fingerprints = setOf(
OnResponseStartedFingerprint,
RequestFingerprint,
)
) {
private lateinit var loadImageSuccessCallbackMethod: MutableMethod
private var loadImageSuccessCallbackIndex = 0
private lateinit var loadImageErrorCallbackMethod: MutableMethod
private var loadImageErrorCallbackIndex = 0
/**
* If a connection completed, which includes normal 200 responses but also includes
* status 404 and other error like http responses.
*/
@Suppress("SameParameterValue")
private fun addImageUrlSuccessCallbackHook(targetMethodClass: String) {
loadImageSuccessCallbackMethod.addInstruction(
loadImageSuccessCallbackIndex++,
"invoke-static { p1, p2 }, $targetMethodClass->handleCronetSuccess(" +
"Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V"
)
}
/**
* If a connection outright failed to complete any connection.
*/
@Suppress("SameParameterValue")
private fun addImageUrlErrorCallbackHook(targetMethodClass: String) {
loadImageErrorCallbackMethod.addInstruction(
loadImageErrorCallbackIndex++,
"invoke-static { p1, p2, p3 }, $targetMethodClass->handleCronetFailure(" +
"Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Ljava/io/IOException;)V"
)
}
override fun execute(context: BytecodeContext) {
fun MethodFingerprint.alsoResolve(fingerprint: MethodFingerprint) =
also { resolve(context, fingerprint.resultOrThrow().classDef) }.resultOrThrow()
fun MethodFingerprint.resolveAndLetMutableMethod(
fingerprint: MethodFingerprint,
block: (MutableMethod) -> Unit
) = alsoResolve(fingerprint).also { block(it.mutableMethod) }
OnSucceededFingerprint.resolveAndLetMutableMethod(OnResponseStartedFingerprint) {
loadImageSuccessCallbackMethod = it
addImageUrlSuccessCallbackHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR)
}
OnFailureFingerprint.resolveAndLetMutableMethod(OnResponseStartedFingerprint) {
loadImageErrorCallbackMethod = it
addImageUrlErrorCallbackHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR)
}
// The URL is required for the failure callback hook, but the URL field is obfuscated.
// Add a helper get method that returns the URL field.
RequestFingerprint.resultOrThrow().apply {
// The url is the only string field that is set inside the constructor.
val urlFieldInstruction = mutableMethod.getInstructions().first {
if (it.opcode != Opcode.IPUT_OBJECT)
return@first false
val reference = (it as ReferenceInstruction).reference as FieldReference
reference.type == "Ljava/lang/String;"
} as ReferenceInstruction
val urlFieldName = (urlFieldInstruction.reference as FieldReference).name
val definingClass = RequestFingerprint.IMPLEMENTATION_CLASS_NAME
val addedMethodName = "getHookedUrl"
mutableClass.methods.add(
ImmutableMethod(
definingClass,
addedMethodName,
emptyList(),
"Ljava/lang/String;",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(2)
).toMutable().apply {
addInstructions(
"""
iget-object v0, p0, $definingClass->${urlFieldName}:Ljava/lang/String;
return-object v0
"""
)
}
)
}
/**
* Add settings
*/
SettingsPatch.addPreference(
arrayOf(
"PREFERENCE_SCREEN: ALTERNATIVE_THUMBNAILS"
)
)
SettingsPatch.updatePatchStatus(this)
}
}

View File

@ -0,0 +1,5 @@
package app.revanced.patches.youtube.utils.imageurlhook
import app.revanced.patches.shared.imageurlhook.BaseCronetImageUrlHookPatch
object CronetImageUrlHookPatch : BaseCronetImageUrlHookPatch(true)