fix(twitch/block-embedded-ads): rewrite Kotlin classes to Java (#270)

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
oSumAtrIX 2023-01-02 08:55:19 +01:00 committed by GitHub
parent bc635a79c5
commit 843dd9de9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 276 additions and 224 deletions

View File

@ -0,0 +1,26 @@
package app.revanced.twitch.adblock;
import okhttp3.Request;
public interface IAdblockService {
String friendlyName();
Integer maxAttempts();
Boolean isAvailable();
Request rewriteHlsRequest(Request originalRequest);
static boolean isVod(Request request){
return request.url().pathSegments().contains("vod");
}
static String channelName(Request request) {
for (String pathSegment : request.url().pathSegments()) {
if (pathSegment.endsWith(".m3u8")) {
return pathSegment.replace(".m3u8", "");
}
}
return null;
}
}

View File

@ -1,18 +0,0 @@
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", "") }
}
}

View File

@ -0,0 +1,83 @@
package app.revanced.twitch.adblock;
import java.util.HashMap;
import java.util.Map;
import app.revanced.twitch.api.RetrofitClient;
import app.revanced.twitch.utils.LogHelper;
import app.revanced.twitch.utils.ReVancedUtils;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.ResponseBody;
public class PurpleAdblockService implements IAdblockService {
private final Map<String, Boolean> tunnels = new HashMap<>() {{
put("https://eu1.jupter.ga", false);
put("https://eu2.jupter.ga", false);
}};
@Override
public String friendlyName() {
return ReVancedUtils.getString("revanced_proxy_purpleadblock");
}
@Override
public Integer maxAttempts() {
return 3;
}
@Override
public Boolean isAvailable() {
for (String tunnel : tunnels.keySet()) {
var success = true;
try {
var response = RetrofitClient.getInstance().getPurpleAdblockApi().ping(tunnel).execute();
if (!response.isSuccessful()) {
LogHelper.error("PurpleAdBlock tunnel $tunnel returned an error: HTTP code %d", response.code());
LogHelper.debug(response.message());
if (response.errorBody() != null) {
LogHelper.debug(((ResponseBody) response.errorBody()).string());
}
success = false;
}
} catch (Exception ex) {
LogHelper.printException("PurpleAdBlock tunnel $tunnel is unavailable", ex);
success = false;
}
// Cache availability data
tunnels.put(tunnel, success);
if (success)
return true;
}
return false;
}
@Override
public Request rewriteHlsRequest(Request originalRequest) {
for (Map.Entry<String, Boolean> entry : tunnels.entrySet()) {
if (!entry.getValue()) continue;
var server = entry.getKey();
// Compose new URL
var url = HttpUrl.parse(server + "/channel/" + IAdblockService.channelName(originalRequest));
if (url == null) {
LogHelper.error("Failed to parse rewritten URL");
return null;
}
// Overwrite old request
return new Request.Builder()
.get()
.url(url)
.build();
}
LogHelper.error("No tunnels are available");
return null;
}
}

View File

@ -1,68 +0,0 @@
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()
}
}

View File

@ -0,0 +1,70 @@
package app.revanced.twitch.adblock;
import java.util.ArrayList;
import java.util.Random;
import app.revanced.twitch.utils.LogHelper;
import app.revanced.twitch.utils.ReVancedUtils;
import okhttp3.HttpUrl;
import okhttp3.Request;
public class TTVLolService implements IAdblockService {
@Override
public String friendlyName() {
return ReVancedUtils.getString("revanced_proxy_ttv_lol");
}
// TTV.lol is sometimes unstable
@Override
public Integer maxAttempts() {
return 4;
}
@Override
public Boolean isAvailable() {
return true;
}
@Override
public Request rewriteHlsRequest(Request originalRequest) {
var type = "vod";
if (!IAdblockService.isVod(originalRequest))
type = "playlist";
var url = HttpUrl.parse("https://api.ttv.lol/" +
type + "/" +
IAdblockService.channelName(originalRequest) +
".m3u8" + nextQuery()
);
if (url == null) {
LogHelper.error("Failed to parse rewritten URL");
return null;
}
// Overwrite old request
return new Request.Builder()
.get()
.url(url)
.addHeader("X-Donate-To", "https://ttv.lol/donate")
.build();
}
private String nextQuery() {
return SAMPLE_QUERY.replace("<SESSION>", generateSessionId());
}
private String generateSessionId() {
final var chars = "abcdef0123456789".toCharArray();
var sessionId = new ArrayList<Character>();
for (int i = 0; i < 32; i++)
sessionId.add(chars[randomSource.nextInt(16)]);
return sessionId.toString();
}
private final Random randomSource = new Random();
private final String 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";
}

View File

@ -1,52 +0,0 @@
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"
}
}

View File

@ -0,0 +1,97 @@
package app.revanced.twitch.api;
import static app.revanced.twitch.adblock.IAdblockService.channelName;
import static app.revanced.twitch.adblock.IAdblockService.isVod;
import androidx.annotation.NonNull;
import java.io.IOException;
import app.revanced.twitch.adblock.IAdblockService;
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.Interceptor;
import okhttp3.Response;
public class RequestInterceptor implements Interceptor {
private IAdblockService activeService = null;
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
var 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);
}
var isVod = "no";
if (isVod(originalRequest)) isVod = "yes";
LogHelper.debug("Found HLS manifest request. Is VOD? %s; Channel: %s", isVod, channelName(originalRequest));
// None of the services support VODs currently
if (isVod(originalRequest)) return chain.proceed(originalRequest);
updateActiveService();
if (activeService != null) {
var available = activeService.isAvailable();
var rewritten = activeService.rewriteHlsRequest(originalRequest);
if (!available || rewritten == null) {
ReVancedUtils.toast(String.format(ReVancedUtils.getString("revanced_embedded_ads_service_unavailable"), activeService.friendlyName()), true);
return chain.proceed(originalRequest);
}
LogHelper.debug("Rewritten HLS stream URL: %s", rewritten.url().toString());
var maxAttempts = activeService.maxAttempts();
for (var i = 1; i <= maxAttempts; i++) {
// Execute rewritten request and close body to allow multiple proceed() calls
var response = chain.proceed(rewritten);
response.close();
if (!response.isSuccessful()) {
LogHelper.error("Request failed (attempt %d/%d): HTTP error %d (%s)", i, maxAttempts, response.code(), response.message());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
LogHelper.printException("Failed to sleep" ,e);
}
} 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"), activeService.friendlyName()), true);
}
// Adblock disabled
return chain.proceed(originalRequest);
}
private void updateActiveService() {
var current = SettingsEnum.BLOCK_EMBEDDED_ADS.getString();
if (current.equals(ReVancedUtils.getString("key_revanced_proxy_ttv_lol")) && !(activeService instanceof TTVLolService))
activeService = new TTVLolService();
else if (current.equals(ReVancedUtils.getString("key_revanced_proxy_purpleadblock")) && !(activeService instanceof PurpleAdblockService))
activeService = new PurpleAdblockService();
else if (current.equals(ReVancedUtils.getString("key_revanced_proxy_disabled")))
activeService = null;
}
}

View File

@ -1,86 +0,0 @@
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)
}
}