Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(twitch):
block-embedded-ads
patch (#231)
Co-authored-by: Tim Schneeberger <tim.schneeberger@outlook.de>
- Loading branch information
1 parent
8805851
commit a098594
Showing
11 changed files
with
290 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
app/src/main/java/app/revanced/twitch/adblock/IAdblockService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", "") } | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
app/src/main/java/app/revanced/twitch/adblock/PurpleAdblockService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
52
app/src/main/java/app/revanced/twitch/adblock/TTVLolService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
12
app/src/main/java/app/revanced/twitch/api/PurpleAdblockApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
86
app/src/main/java/app/revanced/twitch/api/RequestInterceptor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
25
app/src/main/java/app/revanced/twitch/api/RetrofitClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
app/src/main/java/app/revanced/twitch/patches/EmbeddedAdsPatch.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters