Skip to content

Commit

Permalink
feat(twitch): block-embedded-ads patch (#231)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Schneeberger <tim.schneeberger@outlook.de>
  • Loading branch information
Ushie and timschneeb committed Dec 5, 2022
1 parent 8805851 commit a098594
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 1 deletion.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Expand Up @@ -46,4 +46,6 @@ dependencies {
compileOnly(project(mapOf("path" to ":dummy")))
compileOnly("androidx.annotation:annotation:1.5.0")
compileOnly("androidx.appcompat:appcompat:1.5.1")
compileOnly("com.squareup.okhttp3:okhttp:4.10.0")
compileOnly("com.squareup.retrofit2:retrofit:2.9.0")
}
18 changes: 18 additions & 0 deletions app/src/main/java/app/revanced/twitch/adblock/IAdblockService.kt
@@ -0,0 +1,18 @@
package app.revanced.twitch.adblock

import okhttp3.Request

interface IAdblockService {
fun friendlyName(): String
fun maxAttempts(): Int
fun isAvailable(): Boolean
fun rewriteHlsRequest(originalRequest: Request): Request?

companion object {
fun Request.isVod() = url.pathSegments.contains("vod")
fun Request.channelName() =
url.pathSegments
.firstOrNull { it.endsWith(".m3u8") }
.run { this?.replace(".m3u8", "") }
}
}
@@ -0,0 +1,68 @@
package app.revanced.twitch.adblock

import app.revanced.twitch.adblock.IAdblockService.Companion.channelName
import app.revanced.twitch.api.RetrofitClient
import app.revanced.twitch.utils.LogHelper
import app.revanced.twitch.utils.ReVancedUtils
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.ResponseBody

class PurpleAdblockService : IAdblockService {
private val tunnels = mutableMapOf(
/* tunnel url */ /* alive */
"https://eu1.jupter.ga" to false,
"https://eu2.jupter.ga" to false
)

override fun friendlyName(): String = ReVancedUtils.getString("revanced_proxy_purpleadblock")

override fun maxAttempts(): Int = 3

override fun isAvailable(): Boolean {
for(tunnel in tunnels.keys) {
var success = true
try {
val response = RetrofitClient.getInstance().purpleAdblockApi.ping(tunnel).execute()
if (!response.isSuccessful) {
LogHelper.error("PurpleAdBlock tunnel $tunnel returned an error: HTTP code %d", response.code())
LogHelper.debug(response.message())
LogHelper.debug((response.errorBody() as ResponseBody).string())
success = false
}
} catch (ex: Exception) {
LogHelper.printException("PurpleAdBlock tunnel $tunnel is unavailable", ex)
success = false
}

// Cache availability data
tunnels[tunnel] = success

if(success)
return true
}

return false
}

override fun rewriteHlsRequest(originalRequest: Request): Request? {
val server = tunnels.filter { it.value }.map { it.key }.firstOrNull()
server ?: run {
LogHelper.error("No tunnels are available")
return null
}

// Compose new URL
val url = "$server/channel/${originalRequest.channelName()}".toHttpUrlOrNull()
if (url == null) {
LogHelper.error("Failed to parse rewritten URL")
return null
}

// Overwrite old request
return Request.Builder()
.get()
.url(url)
.build()
}
}
52 changes: 52 additions & 0 deletions app/src/main/java/app/revanced/twitch/adblock/TTVLolService.kt
@@ -0,0 +1,52 @@
package app.revanced.twitch.adblock

import app.revanced.twitch.adblock.IAdblockService.Companion.channelName
import app.revanced.twitch.adblock.IAdblockService.Companion.isVod
import app.revanced.twitch.utils.LogHelper
import app.revanced.twitch.utils.ReVancedUtils
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import java.util.Random

class TTVLolService : IAdblockService {

override fun friendlyName(): String = ReVancedUtils.getString("revanced_proxy_ttv_lol")

// TTV.lol is sometimes unstable
override fun maxAttempts(): Int = 4

override fun isAvailable(): Boolean = true

override fun rewriteHlsRequest(originalRequest: Request): Request? {
// Compose new URL
val url = "https://api.ttv.lol/${if (originalRequest.isVod()) "vod" else "playlist"}/${originalRequest.channelName()}.m3u8${nextQuery()}".toHttpUrlOrNull()
if (url == null) {
LogHelper.error("Failed to parse rewritten URL")
return null
}

// Overwrite old request
return Request.Builder()
.get()
.url(url)
.addHeader("X-Donate-To", "https://ttv.lol/donate")
.build()
}

private fun nextQuery(): String {
return SAMPLE_QUERY.replace("<SESSION>", generateSessionId())
}

private fun generateSessionId() =
(1..32)
.map { "abcdef0123456789"[randomSource.nextInt(16)] }
.joinToString("")

private val randomSource = Random()

companion object {

private const val SAMPLE_QUERY =
"%3Fallow_source%3Dtrue%26fast_bread%3Dtrue%26allow_audio_only%3Dtrue%26p%3D0%26play_session_id%3D<SESSION>%26player_backend%3Dmediaplayer%26warp%3Dfalse%26force_preroll%3Dfalse%26mobile_cellular%3Dfalse"
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/app/revanced/twitch/api/PurpleAdblockApi.java
@@ -0,0 +1,12 @@
package app.revanced.twitch.api;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Url;

/* only used for service pings */
public interface PurpleAdblockApi {
@GET /* root */
Call<ResponseBody> ping(@Url String baseUrl);
}
86 changes: 86 additions & 0 deletions app/src/main/java/app/revanced/twitch/api/RequestInterceptor.kt
@@ -0,0 +1,86 @@
package app.revanced.twitch.api

import app.revanced.twitch.adblock.IAdblockService
import app.revanced.twitch.adblock.IAdblockService.Companion.channelName
import app.revanced.twitch.adblock.IAdblockService.Companion.isVod
import app.revanced.twitch.adblock.PurpleAdblockService
import app.revanced.twitch.adblock.TTVLolService
import app.revanced.twitch.settings.SettingsEnum
import app.revanced.twitch.utils.LogHelper
import app.revanced.twitch.utils.ReVancedUtils
import okhttp3.*

class RequestInterceptor : Interceptor {
private var activeService: IAdblockService? = null

private fun updateActiveService() {
val current = SettingsEnum.BLOCK_EMBEDDED_ADS.string
activeService = if(current == ReVancedUtils.getString("key_revanced_proxy_ttv_lol") && activeService !is TTVLolService)
TTVLolService()
else if(current == ReVancedUtils.getString("key_revanced_proxy_purpleadblock") && activeService !is PurpleAdblockService)
PurpleAdblockService()
else if(current == ReVancedUtils.getString("key_revanced_proxy_disabled"))
null
else
activeService
}

override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
LogHelper.debug("Intercepted request to URL: %s", originalRequest.url.toString())

// Skip if not HLS manifest request
if (!originalRequest.url.host.contains("usher.ttvnw.net")) {
return chain.proceed(originalRequest)
}

LogHelper.debug("Found HLS manifest request. Is VOD? %s; Channel: %s",
if (originalRequest.isVod()) "yes" else "no", originalRequest.channelName())

// None of the services support VODs currently
if(originalRequest.isVod())
return chain.proceed(originalRequest)

updateActiveService()

activeService?.let {
val available = it.isAvailable()
val rewritten = it.rewriteHlsRequest(originalRequest)

if (!available || rewritten == null) {
ReVancedUtils.toast(
String.format(ReVancedUtils.getString("revanced_embedded_ads_service_unavailable"), it.friendlyName()),
true
)
return chain.proceed(originalRequest)
}

LogHelper.debug("Rewritten HLS stream URL: %s", rewritten.url.toString())

val maxAttempts = it.maxAttempts()
for(i in 1..maxAttempts) {
// Execute rewritten request and close body to allow multiple proceed() calls
val response = chain.proceed(rewritten).apply { close() }
if(!response.isSuccessful) {
LogHelper.error("Request failed (attempt %d/%d): HTTP error %d (%s)",
i, maxAttempts, response.code, response.message)
Thread.sleep(50)
}
else {
// Accept response from ad blocker
LogHelper.debug("Ad-blocker used")
return chain.proceed(rewritten)
}
}

// maxAttempts exceeded; giving up on using the ad blocker
ReVancedUtils.toast(
String.format(ReVancedUtils.getString("revanced_embedded_ads_service_failed"), it.friendlyName()),
true
)
}

// Adblock disabled
return chain.proceed(originalRequest)
}
}
25 changes: 25 additions & 0 deletions app/src/main/java/app/revanced/twitch/api/RetrofitClient.java
@@ -0,0 +1,25 @@
package app.revanced.twitch.api;

import retrofit2.Retrofit;

public class RetrofitClient {

private static RetrofitClient instance = null;
private final PurpleAdblockApi purpleAdblockApi;

private RetrofitClient() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build();
purpleAdblockApi = retrofit.create(PurpleAdblockApi.class);
}

public static synchronized RetrofitClient getInstance() {
if (instance == null) {
instance = new RetrofitClient();
}
return instance;
}

public PurpleAdblockApi getPurpleAdblockApi() {
return purpleAdblockApi;
}
}
@@ -0,0 +1,9 @@
package app.revanced.twitch.patches;

import app.revanced.twitch.api.RequestInterceptor;

public class EmbeddedAdsPatch {
public static RequestInterceptor createRequestInterceptor() {
return new RequestInterceptor();
}
}
Expand Up @@ -10,6 +10,7 @@ public enum SettingsEnum {
/* Ads */
BLOCK_VIDEO_ADS("revanced_block_video_ads", true, ReturnType.BOOLEAN),
BLOCK_AUDIO_ADS("revanced_block_audio_ads", true, ReturnType.BOOLEAN),
BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", "ttv-lol", ReturnType.STRING),

/* Chat */
SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", "cross-out", ReturnType.STRING),
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/app/revanced/twitch/utils/LogHelper.java
Expand Up @@ -43,7 +43,7 @@ public static void printException(String message, Throwable ex) {

private static void showDebugToast(String msg) {
if(SettingsEnum.DEBUG_MODE.getBoolean()) {
ReVancedUtils.ifContextAttached((c) -> Toast.makeText(c, msg, Toast.LENGTH_SHORT).show());
ReVancedUtils.toast(msg, false);
}
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/app/revanced/twitch/utils/ReVancedUtils.java
Expand Up @@ -2,6 +2,9 @@

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;

public class ReVancedUtils {
@SuppressLint("StaticFieldLeak")
Expand Down Expand Up @@ -53,6 +56,10 @@ public interface SafeContextAccessLambda {
void run(Context ctx);
}

public static void runOnMainThread(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}

/**
* Get resource id safely
* @return May return 0 if resource not found or context not attached
Expand Down Expand Up @@ -84,4 +91,13 @@ public static int getDrawableId(String name) {
public static String getString(String name) {
return ifContextAttached((c) -> c.getString(getStringId(name)), "");
}

public static void toast(String message) {
toast(message, true);
}
public static void toast(String message, boolean longLength) {
ifContextAttached((c) -> {
runOnMainThread(() -> Toast.makeText(c, message, longLength ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show());
});
}
}

0 comments on commit a098594

Please sign in to comment.