Skip to content
This repository was archived by the owner on Oct 26, 2024. It is now read-only.

Commit 00ea006

Browse files
authored
feat(twitch): block-embedded-ads patch support (#227)
1 parent 617a4eb commit 00ea006

11 files changed

Lines changed: 290 additions & 1 deletion

File tree

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ dependencies {
4646
compileOnly(project(mapOf("path" to ":dummy")))
4747
compileOnly("androidx.annotation:annotation:1.5.0")
4848
compileOnly("androidx.appcompat:appcompat:1.5.1")
49+
compileOnly("com.squareup.okhttp3:okhttp:4.10.0")
50+
compileOnly("com.squareup.retrofit2:retrofit:2.9.0")
4951
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package app.revanced.twitch.adblock
2+
3+
import okhttp3.Request
4+
5+
interface IAdblockService {
6+
fun friendlyName(): String
7+
fun maxAttempts(): Int
8+
fun isAvailable(): Boolean
9+
fun rewriteHlsRequest(originalRequest: Request): Request?
10+
11+
companion object {
12+
fun Request.isVod() = url.pathSegments.contains("vod")
13+
fun Request.channelName() =
14+
url.pathSegments
15+
.firstOrNull { it.endsWith(".m3u8") }
16+
.run { this?.replace(".m3u8", "") }
17+
}
18+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package app.revanced.twitch.adblock
2+
3+
import app.revanced.twitch.adblock.IAdblockService.Companion.channelName
4+
import app.revanced.twitch.api.RetrofitClient
5+
import app.revanced.twitch.utils.LogHelper
6+
import app.revanced.twitch.utils.ReVancedUtils
7+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
8+
import okhttp3.Request
9+
import okhttp3.ResponseBody
10+
11+
class PurpleAdblockService : IAdblockService {
12+
private val tunnels = mutableMapOf(
13+
/* tunnel url */ /* alive */
14+
"https://eu1.jupter.ga" to false,
15+
"https://eu2.jupter.ga" to false
16+
)
17+
18+
override fun friendlyName(): String = ReVancedUtils.getString("revanced_proxy_purpleadblock")
19+
20+
override fun maxAttempts(): Int = 3
21+
22+
override fun isAvailable(): Boolean {
23+
for(tunnel in tunnels.keys) {
24+
var success = true
25+
try {
26+
val response = RetrofitClient.getInstance().purpleAdblockApi.ping(tunnel).execute()
27+
if (!response.isSuccessful) {
28+
LogHelper.error("PurpleAdBlock tunnel $tunnel returned an error: HTTP code %d", response.code())
29+
LogHelper.debug(response.message())
30+
LogHelper.debug((response.errorBody() as ResponseBody).string())
31+
success = false
32+
}
33+
} catch (ex: Exception) {
34+
LogHelper.printException("PurpleAdBlock tunnel $tunnel is unavailable", ex)
35+
success = false
36+
}
37+
38+
// Cache availability data
39+
tunnels[tunnel] = success
40+
41+
if(success)
42+
return true
43+
}
44+
45+
return false
46+
}
47+
48+
override fun rewriteHlsRequest(originalRequest: Request): Request? {
49+
val server = tunnels.filter { it.value }.map { it.key }.firstOrNull()
50+
server ?: run {
51+
LogHelper.error("No tunnels are available")
52+
return null
53+
}
54+
55+
// Compose new URL
56+
val url = "$server/channel/${originalRequest.channelName()}".toHttpUrlOrNull()
57+
if (url == null) {
58+
LogHelper.error("Failed to parse rewritten URL")
59+
return null
60+
}
61+
62+
// Overwrite old request
63+
return Request.Builder()
64+
.get()
65+
.url(url)
66+
.build()
67+
}
68+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package app.revanced.twitch.adblock
2+
3+
import app.revanced.twitch.adblock.IAdblockService.Companion.channelName
4+
import app.revanced.twitch.adblock.IAdblockService.Companion.isVod
5+
import app.revanced.twitch.utils.LogHelper
6+
import app.revanced.twitch.utils.ReVancedUtils
7+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
8+
import okhttp3.Request
9+
import java.util.Random
10+
11+
class TTVLolService : IAdblockService {
12+
13+
override fun friendlyName(): String = ReVancedUtils.getString("revanced_proxy_ttv_lol")
14+
15+
// TTV.lol is sometimes unstable
16+
override fun maxAttempts(): Int = 4
17+
18+
override fun isAvailable(): Boolean = true
19+
20+
override fun rewriteHlsRequest(originalRequest: Request): Request? {
21+
// Compose new URL
22+
val url = "https://api.ttv.lol/${if (originalRequest.isVod()) "vod" else "playlist"}/${originalRequest.channelName()}.m3u8${nextQuery()}".toHttpUrlOrNull()
23+
if (url == null) {
24+
LogHelper.error("Failed to parse rewritten URL")
25+
return null
26+
}
27+
28+
// Overwrite old request
29+
return Request.Builder()
30+
.get()
31+
.url(url)
32+
.addHeader("X-Donate-To", "https://ttv.lol/donate")
33+
.build()
34+
}
35+
36+
private fun nextQuery(): String {
37+
return SAMPLE_QUERY.replace("<SESSION>", generateSessionId())
38+
}
39+
40+
private fun generateSessionId() =
41+
(1..32)
42+
.map { "abcdef0123456789"[randomSource.nextInt(16)] }
43+
.joinToString("")
44+
45+
private val randomSource = Random()
46+
47+
companion object {
48+
49+
private const val SAMPLE_QUERY =
50+
"%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"
51+
}
52+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package app.revanced.twitch.api;
2+
3+
import okhttp3.ResponseBody;
4+
import retrofit2.Call;
5+
import retrofit2.http.GET;
6+
import retrofit2.http.Url;
7+
8+
/* only used for service pings */
9+
public interface PurpleAdblockApi {
10+
@GET /* root */
11+
Call<ResponseBody> ping(@Url String baseUrl);
12+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package app.revanced.twitch.api
2+
3+
import app.revanced.twitch.adblock.IAdblockService
4+
import app.revanced.twitch.adblock.IAdblockService.Companion.channelName
5+
import app.revanced.twitch.adblock.IAdblockService.Companion.isVod
6+
import app.revanced.twitch.adblock.PurpleAdblockService
7+
import app.revanced.twitch.adblock.TTVLolService
8+
import app.revanced.twitch.settings.SettingsEnum
9+
import app.revanced.twitch.utils.LogHelper
10+
import app.revanced.twitch.utils.ReVancedUtils
11+
import okhttp3.*
12+
13+
class RequestInterceptor : Interceptor {
14+
private var activeService: IAdblockService? = null
15+
16+
private fun updateActiveService() {
17+
val current = SettingsEnum.BLOCK_EMBEDDED_ADS.string
18+
activeService = if(current == ReVancedUtils.getString("key_revanced_proxy_ttv_lol") && activeService !is TTVLolService)
19+
TTVLolService()
20+
else if(current == ReVancedUtils.getString("key_revanced_proxy_purpleadblock") && activeService !is PurpleAdblockService)
21+
PurpleAdblockService()
22+
else if(current == ReVancedUtils.getString("key_revanced_proxy_disabled"))
23+
null
24+
else
25+
activeService
26+
}
27+
28+
override fun intercept(chain: Interceptor.Chain): Response {
29+
val originalRequest = chain.request()
30+
LogHelper.debug("Intercepted request to URL: %s", originalRequest.url.toString())
31+
32+
// Skip if not HLS manifest request
33+
if (!originalRequest.url.host.contains("usher.ttvnw.net")) {
34+
return chain.proceed(originalRequest)
35+
}
36+
37+
LogHelper.debug("Found HLS manifest request. Is VOD? %s; Channel: %s",
38+
if (originalRequest.isVod()) "yes" else "no", originalRequest.channelName())
39+
40+
// None of the services support VODs currently
41+
if(originalRequest.isVod())
42+
return chain.proceed(originalRequest)
43+
44+
updateActiveService()
45+
46+
activeService?.let {
47+
val available = it.isAvailable()
48+
val rewritten = it.rewriteHlsRequest(originalRequest)
49+
50+
if (!available || rewritten == null) {
51+
ReVancedUtils.toast(
52+
String.format(ReVancedUtils.getString("revanced_embedded_ads_service_unavailable"), it.friendlyName()),
53+
true
54+
)
55+
return chain.proceed(originalRequest)
56+
}
57+
58+
LogHelper.debug("Rewritten HLS stream URL: %s", rewritten.url.toString())
59+
60+
val maxAttempts = it.maxAttempts()
61+
for(i in 1..maxAttempts) {
62+
// Execute rewritten request and close body to allow multiple proceed() calls
63+
val response = chain.proceed(rewritten).apply { close() }
64+
if(!response.isSuccessful) {
65+
LogHelper.error("Request failed (attempt %d/%d): HTTP error %d (%s)",
66+
i, maxAttempts, response.code, response.message)
67+
Thread.sleep(50)
68+
}
69+
else {
70+
// Accept response from ad blocker
71+
LogHelper.debug("Ad-blocker used")
72+
return chain.proceed(rewritten)
73+
}
74+
}
75+
76+
// maxAttempts exceeded; giving up on using the ad blocker
77+
ReVancedUtils.toast(
78+
String.format(ReVancedUtils.getString("revanced_embedded_ads_service_failed"), it.friendlyName()),
79+
true
80+
)
81+
}
82+
83+
// Adblock disabled
84+
return chain.proceed(originalRequest)
85+
}
86+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package app.revanced.twitch.api;
2+
3+
import retrofit2.Retrofit;
4+
5+
public class RetrofitClient {
6+
7+
private static RetrofitClient instance = null;
8+
private final PurpleAdblockApi purpleAdblockApi;
9+
10+
private RetrofitClient() {
11+
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build();
12+
purpleAdblockApi = retrofit.create(PurpleAdblockApi.class);
13+
}
14+
15+
public static synchronized RetrofitClient getInstance() {
16+
if (instance == null) {
17+
instance = new RetrofitClient();
18+
}
19+
return instance;
20+
}
21+
22+
public PurpleAdblockApi getPurpleAdblockApi() {
23+
return purpleAdblockApi;
24+
}
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package app.revanced.twitch.patches;
2+
3+
import app.revanced.twitch.api.RequestInterceptor;
4+
5+
public class EmbeddedAdsPatch {
6+
public static RequestInterceptor createRequestInterceptor() {
7+
return new RequestInterceptor();
8+
}
9+
}

app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum SettingsEnum {
1010
/* Ads */
1111
BLOCK_VIDEO_ADS("revanced_block_video_ads", true, ReturnType.BOOLEAN),
1212
BLOCK_AUDIO_ADS("revanced_block_audio_ads", true, ReturnType.BOOLEAN),
13+
BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", "ttv-lol", ReturnType.STRING),
1314

1415
/* Chat */
1516
SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", "cross-out", ReturnType.STRING),

app/src/main/java/app/revanced/twitch/utils/LogHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static void printException(String message, Throwable ex) {
4343

4444
private static void showDebugToast(String msg) {
4545
if(SettingsEnum.DEBUG_MODE.getBoolean()) {
46-
ReVancedUtils.ifContextAttached((c) -> Toast.makeText(c, msg, Toast.LENGTH_SHORT).show());
46+
ReVancedUtils.toast(msg, false);
4747
}
4848
}
4949
}

0 commit comments

Comments
 (0)