fix(pt/betteranime): Fix http 405 on search + add automatic login (#2839)

This commit is contained in:
Claudemirovsky 2024-01-29 09:51:38 -03:00 committed by GitHub
parent 44a284a85b
commit 388b2213fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 329 additions and 173 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Better Anime' extName = 'Better Anime'
extClass = '.BetterAnime' extClass = '.BetterAnime'
extVersionCode = 8 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -55,12 +55,10 @@ object BAFilters {
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams() if (filters.isEmpty()) return FilterSearchParams()
val genres = listOf(" ") + filters.parseCheckbox<GenresFilter>(BAFiltersData.GENRES)
return FilterSearchParams( return FilterSearchParams(
filters.asQueryPart<LanguageFilter>(), filters.asQueryPart<LanguageFilter>(),
filters.asQueryPart<YearFilter>(), filters.asQueryPart<YearFilter>(),
genres, filters.parseCheckbox<GenresFilter>(BAFiltersData.GENRES),
) )
} }
@ -73,7 +71,7 @@ object BAFilters {
Pair("Dublado", "dublado"), Pair("Dublado", "dublado"),
) )
val YEARS = arrayOf(EVERY) + (2023 downTo 1976).map { val YEARS = arrayOf(EVERY) + (2024 downTo 1976).map {
Pair(it.toString(), it.toString()) Pair(it.toString(), it.toString())
}.toTypedArray() }.toTypedArray()

View File

@ -1,12 +1,10 @@
package eu.kanade.tachiyomi.animeextension.pt.betteranime package eu.kanade.tachiyomi.animeextension.pt.betteranime
import android.app.Application import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.LivewireResponseDto import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.ComponentsDto
import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.PayloadData import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.PayloadData
import eu.kanade.tachiyomi.animeextension.pt.betteranime.dto.PayloadItem
import eu.kanade.tachiyomi.animeextension.pt.betteranime.extractors.BetterAnimeExtractor import eu.kanade.tachiyomi.animeextension.pt.betteranime.extractors.BetterAnimeExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -19,10 +17,14 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.encodeToString import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import okhttp3.Interceptor import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -33,7 +35,6 @@ import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException
class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -46,12 +47,12 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client = network.client.newBuilder() override val client = network.client.newBuilder()
.addInterceptor(::loginInterceptor) .addInterceptor(LoginInterceptor(network.client, baseUrl, headers))
.build() .build()
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
@ -64,58 +65,34 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// so we use the latest added anime page instead. // so we use the latest added anime page instead.
override fun popularAnimeParse(response: Response) = latestUpdatesParse(response) override fun popularAnimeParse(response: Response) = latestUpdatesParse(response)
override fun popularAnimeRequest(page: Int): Request = override fun popularAnimeRequest(page: Int) = GET("$baseUrl/ultimosAdicionados?page=$page", headers)
GET("$baseUrl/ultimosAdicionados?page=$page")
override fun popularAnimeSelector() = TODO() override fun popularAnimeSelector() = TODO()
override fun popularAnimeFromElement(element: Element) = TODO() override fun popularAnimeFromElement(element: Element) = TODO()
override fun popularAnimeNextPageSelector() = TODO() override fun popularAnimeNextPageSelector() = TODO()
// ============================== Episodes ============================== // =============================== Latest ===============================
override fun episodeListSelector(): String = "ul#episodesList > li.list-group-item-action > a" override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ultimosLancamentos?page=$page", headers)
override fun episodeListParse(response: Response) = override fun latestUpdatesSelector() = "div.list-animes article"
super.episodeListParse(response).reversed()
override fun episodeFromElement(element: Element) = SEpisode.create().apply { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
val episodeName = element.text() val img = element.selectFirst("img")!!
setUrlWithoutDomain(element.attr("href")) val url = element.selectFirst("a")?.attr("href")!!
name = episodeName setUrlWithoutDomain(url)
episode_number = episodeName.substringAfterLast(" ").toFloatOrNull() ?: 0F title = element.selectFirst("h3")?.text()!!
thumbnail_url = "https:" + img.attr("src")
} }
// ============================ Video Links ============================= override fun latestUpdatesNextPageSelector() = "ul.pagination li.page-item:contains():not(.disabled)"
override fun videoListParse(response: Response): List<Video> {
val html = response.body.string()
val extractor = BetterAnimeExtractor(client, baseUrl, json)
return extractor.videoListFromHtml(html)
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element) override fun getFilterList() = BAFilters.FILTER_LIST
override fun searchAnimeSelector() = latestUpdatesSelector()
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchAnimeParse(response: Response): AnimesPage {
val body = response.body.string()
val data = json.decodeFromString<LivewireResponseDto>(body)
val html = data.effects.html?.unescape().orEmpty()
val document = Jsoup.parse(html)
val animes = document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
val hasNext = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNext)
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH_PATH)) { return if (query.startsWith(PREFIX_SEARCH_PATH)) {
val path = query.removePrefix(PREFIX_SEARCH_PATH) val path = query.removePrefix(PREFIX_SEARCH_PATH)
client.newCall(GET("$baseUrl/$path")) client.newCall(GET("$baseUrl/$path", headers))
.awaitSuccess() .awaitSuccess()
.use(::searchAnimeByPathParse) .use(::searchAnimeByPathParse)
} else { } else {
@ -129,10 +106,12 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val searchParams = buildList { val calls = buildJsonArray {
add(PayloadItem(PayloadData(method = "search"), "callMethod")) val payloadSerializer = PayloadData.serializer()
add(json.encodeToJsonElement(payloadSerializer, PayloadData(method = "search")))
add( add(
PayloadItem( json.encodeToJsonElement(
payloadSerializer,
PayloadData( PayloadData(
method = "gotoPage", method = "gotoPage",
params = listOf( params = listOf(
@ -140,27 +119,75 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
JsonPrimitive("page"), JsonPrimitive("page"),
), ),
), ),
"callMethod",
), ),
) )
val params = BAFilters.getSearchParameters(filters)
val data = buildList {
if (params.genres.size > 1) {
add(PayloadData(name = "byGenres", value = params.genres))
}
listOf(
params.year to "byYear",
params.language to "byLanguage",
query to "searchTerm",
).forEach { it.first.toPayloadData(it.second)?.let(::add) }
}
addAll(data.map { PayloadItem(it, "syncInput") })
} }
return wireRequest("anime-search", searchParams)
val params = BAFilters.getSearchParameters(filters)
val updates = buildJsonObject {
if (params.genres.isNotEmpty()) {
putJsonArray("byGenres") {
params.genres.forEach { add(JsonPrimitive(it)) }
}
}
listOf(
params.year to "byYear",
params.language to "byLanguage",
query to "searchTerm",
).forEach { if (it.first.isNotEmpty()) put(it.second, it.first) }
}
if (wireToken.isBlank()) {
updateSnapshot(GET("$baseUrl/pesquisa", headers))
}
val data = buildJsonObject {
put("_token", wireToken)
putJsonArray("components") {
add(
buildJsonObject {
put("calls", calls)
put("snapshot", snapshot)
put("updates", updates)
},
)
}
}
val reqBody = json.encodeToString(JsonObject.serializer(), data).toRequestBody("application/json".toMediaType())
val headers = headersBuilder()
.add("x-livewire", "true")
.add("x-csrf-token", wireToken)
.build()
return POST("$baseUrl/livewire/update", headers, reqBody)
} }
private var snapshot = ""
private var wireToken = ""
private fun updateSnapshot(request: Request) {
val document = client.newCall(request).execute().asJsoup()
val wireElement = document.selectFirst("[wire:snapshot]")!!
snapshot = wireElement.attr("wire:snapshot")
wireToken = document.selectFirst("script[data-csrf]")!!.attr("data-csrf")
}
override fun searchAnimeParse(response: Response): AnimesPage {
val body = response.body.string()
val data = json.decodeFromString<ComponentsDto>(body)
val html = data.components.firstOrNull()?.effects?.html?.unescape().orEmpty()
val document = Jsoup.parse(html)
val animes = document.select(searchAnimeSelector()).map(::searchAnimeFromElement)
val hasNext = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNext)
}
override fun searchAnimeSelector() = latestUpdatesSelector()
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document) val doc = getRealDoc(document)
@ -182,24 +209,34 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
// =============================== Latest =============================== // ============================== Episodes ==============================
override fun latestUpdatesNextPageSelector() = "ul.pagination li.page-item:contains():not(.disabled)" override fun episodeListSelector() = "ul#episodesList > li.list-group-item-action > a"
override fun latestUpdatesSelector() = "div.list-animes article"
override fun latestUpdatesRequest(page: Int): Request = override fun episodeListParse(response: Response) =
GET("$baseUrl/ultimosLancamentos?page=$page") super.episodeListParse(response).reversed()
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val img = element.selectFirst("img")!! val episodeName = element.text()
val url = element.selectFirst("a")?.attr("href")!! setUrlWithoutDomain(element.attr("href"))
setUrlWithoutDomain(url) name = episodeName
title = element.selectFirst("h3")?.text()!! episode_number = episodeName.substringAfterLast(" ").toFloatOrNull() ?: 0F
thumbnail_url = "https:" + img.attr("src")
} }
// ============================ Video Links =============================
private val extractor by lazy { BetterAnimeExtractor(client, baseUrl, json) }
override fun videoListParse(response: Response): List<Video> {
val html = response.body.string()
return extractor.videoListFromHtml(html)
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES entries = PREF_QUALITY_ENTRIES
@ -212,26 +249,13 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(videoQualityPref)
} }
override fun getFilterList(): AnimeFilterList = BAFilters.FILTER_LIST
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun loginInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if ("/dmca" in response.request.url.toString()) {
response.close()
throw IOException(ERROR_LOGIN_MISSING)
}
return response
}
private fun getRealDoc(document: Document): Document { private fun getRealDoc(document: Document): Document {
return document.selectFirst("div.anime-title a")?.let { link -> return document.selectFirst("div.anime-title a")?.let { link ->
client.newCall(GET(link.attr("href"))) client.newCall(GET(link.attr("href"), headers))
.execute() .execute()
.asJsoup() .asJsoup()
} ?: document } ?: document
@ -244,49 +268,12 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
private var initialData: String = ""
private var wireToken: String = ""
private fun updateInitialData(request: Request) {
val document = client.newCall(request).execute().asJsoup()
val wireElement = document.selectFirst("[wire:id]")
wireToken = document.html()
.substringAfter("livewire_token")
.substringAfter("'")
.substringBefore("'")
initialData = wireElement!!.attr("wire:initial-data").dropLast(1)
}
private fun wireRequest(path: String, updates: List<PayloadItem>): Request {
if (wireToken.isBlank()) {
updateInitialData(GET("$baseUrl/pesquisa"))
}
val url = "$baseUrl/livewire/message/$path"
val items = updates.joinToString(",") { json.encodeToString(it) }
val data = "$initialData, \"updates\": [$items]}"
val reqBody = data.toRequestBody("application/json".toMediaType())
val headers = headersBuilder()
.add("x-livewire", "true")
.add("x-csrf-token", wireToken)
.build()
return POST(url, headers, reqBody)
}
private fun Element.getInfo(key: String): String? { private fun Element.getInfo(key: String): String? {
return selectFirst("p:containsOwn($key) > span") return selectFirst("p:containsOwn($key) > span")
?.text() ?.text()
?.trim() ?.trim()
} }
private fun String.toPayloadData(name: String): PayloadData? {
return when {
isNotBlank() -> PayloadData(name = name, value = listOf(this))
else -> null
}
}
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith( return sortedWith(
@ -298,9 +285,6 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
const val PREFIX_SEARCH_PATH = "path:" const val PREFIX_SEARCH_PATH = "path:"
private const val ERROR_LOGIN_MISSING = "Login necessário. " +
"Abra a WebView, insira os dados de sua conta e realize o login."
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida" private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720p" private const val PREF_QUALITY_DEFAULT = "720p"

View File

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.animeextension.pt.betteranime
import android.util.Base64
import eu.kanade.tachiyomi.network.POST
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
internal class LoginInterceptor(
private val client: OkHttpClient,
private val baseUrl: String,
private val headers: Headers,
) : Interceptor {
private val recapBypasser by lazy { RecaptchaV3Bypasser(client, headers) }
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val originalResponse = chain.proceed(originalRequest)
if (!originalResponse.request.url.encodedPath.contains("/dmca")) {
return originalResponse
}
originalResponse.close()
val (token, recaptchaToken) = recapBypasser.getRecaptchaToken("$baseUrl/login")
if (recaptchaToken.isBlank()) throw IOException(FAILED_AUTOLOGIN_MESSAGE)
val formBody = FormBody.Builder()
.add("_token", token)
.add("g-recaptcha-response", recaptchaToken)
.add("email", String(Base64.decode("aGVmaWczNTY0NUBuYW1ld29rLmNvbQ==", Base64.DEFAULT)))
.add("password", String(Base64.decode("SE1HNFdoVEI0QnRJWTlIdg==", Base64.DEFAULT)))
.build()
val loginRes = chain.proceed(POST("$baseUrl/login", headers, formBody))
loginRes.close()
if (!loginRes.isSuccessful) throw IOException(FAILED_AUTOLOGIN_MESSAGE)
return chain.proceed(originalRequest)
}
companion object {
private const val FAILED_AUTOLOGIN_MESSAGE = "Falha na tentativa de logar automaticamente! " +
"Tente manualmente na WebView."
}
}

View File

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.animeextension.pt.betteranime
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
internal class RecaptchaV3Bypasser(private val client: OkHttpClient, private val headers: Headers) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class AndroidJSI(private val latch: CountDownLatch) {
var token = ""
@JavascriptInterface
fun sendResponse(response: String) {
token = response.substringAfter("uvresp\",\"").substringBefore('"')
latch.countDown()
}
}
@SuppressLint("SetJavaScriptEnabled")
fun getRecaptchaToken(targetUrl: String): Pair<String, String> {
val latch = CountDownLatch(1)
var webView: WebView? = null
var token = ""
val androidjsi = AndroidJSI(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
}
webview.addJavascriptInterface(androidjsi, "androidjsi")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.evaluateJavascript("document.querySelector('input[name=_token]').value") {
token = it.trim('"')
}
}
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
val url = request?.url.toString()
val reqHeaders = request?.requestHeaders.orEmpty()
// Our beloved token
if (url.contains("/recaptcha/api2/anchor")) {
// Injects the script to click on the captcha box
return injectScripts(url, reqHeaders, CLICK_BOX_SCRIPT, INTERCEPTOR_SCRIPT)
} else if (reqHeaders.get("Accept").orEmpty().contains("text/html")) {
// Injects the XMLHttpRequest hack
return injectScripts(url, reqHeaders, INTERCEPTOR_SCRIPT)
}
return super.shouldInterceptRequest(view, request)
}
}
webview.loadUrl(targetUrl)
}
latch.await(20, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return Pair(token, androidjsi.token)
}
private fun Headers.toWebViewHeaders() = toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
.toMutableMap()
.apply {
remove("cross-origin-embedder-policy")
remove("content-security-policy")
remove("report-to")
}
private fun injectScripts(
url: String,
reqHeaders: Map<String, String>,
vararg scripts: String,
): WebResourceResponse {
val headers = Headers.Builder().apply {
reqHeaders.entries.forEach { (key, value) -> add(key, value) }
}.build()
val res = client.newCall(GET(url, headers)).execute()
val newHeaders = res.headers.toWebViewHeaders()
val body = res.body.string()
val newBody = if (res.isSuccessful) {
body.substringBeforeLast("</body>") + scripts.joinToString("\n") + "</body></html>"
} else {
body
}
return WebResourceResponse(
"text/html", // mimeType
"utf-8", // encoding
res.code, // status code
res.message.ifEmpty { "ok" }, // reason phrase
newHeaders, // response headers
ByteArrayInputStream(newBody.toByteArray()), // data
)
}
}
private const val INTERCEPTOR_SCRIPT = """
<script type="text/javascript">
const originalOpen = window.XMLHttpRequest.prototype.open
window.XMLHttpRequest.prototype.open = function(_unused_method, url, _unused_arg) {
if (url.includes('/api2/userverify')) {
originalOpen.apply(this, arguments) // call the original open method
this.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
const responseBody = this.responseText
window.androidjsi.sendResponse(responseBody)
}
}
} else {
originalOpen.apply(this, arguments)
}
}
</script>
"""
private const val CLICK_BOX_SCRIPT = """
<script type="text/javascript">
setInterval(async () => {
const items = document.querySelectorAll("#recaptcha-anchor, .recaptcha-checkbox, #rc-anchor-container span[role=checkbox]")
items.forEach(x => {try { x.click() } catch (e) {} })
}, 500)
</script>"""

View File

@ -1,55 +1,24 @@
package eu.kanade.tachiyomi.animeextension.pt.betteranime.dto package eu.kanade.tachiyomi.animeextension.pt.betteranime.dto
import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable @Serializable
data class ChangePlayerDto( data class ChangePlayerDto(val frameLink: String? = null)
val frameLink: String? = null,
)
@Serializable @Serializable
data class LivewireResponseDto( data class ComponentsDto(val components: List<LivewireResponseDto>)
val effects: LivewireEffects,
)
@Serializable @Serializable
data class LivewireEffects( data class LivewireResponseDto(val effects: LivewireEffects)
val html: String? = null,
)
@ExperimentalSerializationApi
@Serializable @Serializable
data class PayloadItem( data class LivewireEffects(val html: String? = null)
val payload: PayloadData,
val type: String,
)
@ExperimentalSerializationApi
@Serializable @Serializable
data class PayloadData( data class PayloadData(
val name: String = "",
val method: String = "", val method: String = "",
@Serializable(with = ValueSerializer::class) @EncodeDefault val params: List<JsonElement> = emptyList(),
val value: List<String> = emptyList<String>(), @EncodeDefault val path: String = "",
@EncodeDefault val params: List<JsonElement> = emptyList<JsonElement>(),
@EncodeDefault val id: String = "",
) )
object ValueSerializer : JsonTransformingSerializer<List<String>>(
ListSerializer(String.serializer()),
) {
override fun transformSerialize(element: JsonElement): JsonElement {
require(element is JsonArray)
if (element.size > 1) {
return JsonArray(element.drop(1))
}
return element.first()
}
}