diff --git a/src/pt/anitube/build.gradle b/src/pt/anitube/build.gradle index 39b6693e77..a70e106166 100644 --- a/src/pt/anitube/build.gradle +++ b/src/pt/anitube/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Anitube' extClass = '.Anitube' - extVersionCode = 17 + extVersionCode = 18 } apply from: "$rootDir/common.gradle" diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt index 953b5f7e0e..4746bcf797 100644 --- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt +++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt @@ -1,26 +1,31 @@ package eu.kanade.tachiyomi.animeextension.pt.anitube import android.app.Application -import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.AnimeFilterList -import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale @@ -38,8 +43,10 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { Injekt.get().getSharedPreferences("source_$id", 0x0000) } + private val json: Json by injectLazy() + override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") + .add("Referer", baseUrl) .add("Accept-Language", ACCEPT_LANGUAGE) // ============================== Popular =============================== @@ -79,30 +86,6 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector() // =============================== Search =============================== - override suspend fun getSearchAnime( - page: Int, - query: String, - filters: AnimeFilterList, - ): AnimesPage { - return if (query.startsWith(PREFIX_SEARCH)) { - val path = query.removePrefix(PREFIX_SEARCH) - client.newCall(GET("$baseUrl/$path")) - .awaitSuccess() - .use(::searchAnimeByIdParse) - } else { - super.getSearchAnime(page, query, filters) - } - } - - private fun searchAnimeByIdParse(response: Response): AnimesPage { - val details = animeDetailsParse(response).apply { - setUrlWithoutDomain(response.request.url.toString()) - initialized = true - } - - return AnimesPage(listOf(details), false) - } - override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { @@ -114,14 +97,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { val char = params.initialChar when { season.isNotBlank() -> "$baseUrl/temporada/$season/$year" - genre.isNotBlank() -> - "$baseUrl/genero/$genre/page/$page/${ - char.replace( - "todos", - "", - ) - }" - + genre.isNotBlank() -> "$baseUrl/genero/$genre/page/$page/${char.replace("todos", "")}" else -> "$baseUrl/anime/page/$page/letra/$char" } } else { @@ -143,7 +119,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { val infos = content.selectFirst("div.anime_infos")!! title = doc.selectFirst("div.anime_container_titulo")!!.text() - thumbnail_url = content.selectFirst("img")?.attr("src") + thumbnail_url = content.selectFirst("img")?.imgAttr() genre = infos.getInfo("Gêneros") author = infos.getInfo("Autor") artist = infos.getInfo("Estúdio") @@ -159,6 +135,14 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { } } + fun Element.imgAttr(): String = when { + hasAttr("data-cfsrc") -> absUrl("data-cfsrc") + hasAttr("data-lazy-src") -> absUrl("data-lazy-src") + hasAttr("data-src") -> absUrl("data-src").substringBefore(" ") + hasAttr("srcset") -> absUrl("srcset").substringBefore(" ") + else -> absUrl("src") + } + // ============================== Episodes ============================== override fun episodeListSelector() = "div.animepag_episodios_item > a" @@ -179,19 +163,81 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override fun episodeFromElement(element: Element) = SEpisode.create().apply { setUrlWithoutDomain(element.attr("href")) episode_number = element.selectFirst("div.animepag_episodios_item_views")!! - .text() + .text().trim() .substringAfter(" ") .toFloatOrNull() ?: 0F - name = element.selectFirst("div.animepag_episodios_item_nome")!!.text() + name = element.selectFirst("div[class*='animepag_episodios_item_views']")!!.text() date_upload = element.selectFirst("div.animepag_episodios_item_date")!! .text() .toDate() } // ============================ Video Links ============================= - private val extractor by lazy { AnitubeExtractor(headers, client, preferences) } + override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response, headers).let { + val auth = getToken() + + it.map { video -> + Video( + url = video.url, + quality = video.quality, + videoUrl = "${video.videoUrl}${auth.value}", + headers = video.headers, + subtitleTracks = video.subtitleTracks, + audioTracks = video.audioTracks, + ) + } + } + + private fun getToken(): AnitubeToken { + val headers = Headers.Builder() + .set("Accept", "*/*") + .set("Accept-Encoding", "br, zstd") + .set("Accept-Language", "pt-BR,en-US;q=0.7,en;q=0.3") + .set("Cache-Control", "no-cache") + .set("Connection", "keep-alive") + .set("Pragma", "no-cache") + .set("Referer", "$baseUrl/") + .set("Sec-Fetch-Dest", "empty") + .set("Sec-Fetch-Mode", "cors") + .set("Sec-Fetch-Site", "same-site") + .apply { + headers["User-Agent"]?.let { + set("User-Agent", it) + } + } + .build() + + val client = OkHttpClient() + + val ads = client.newCall(GET("https://widgets.outbrain.com/outbrain.js", headers)) + .execute() + + val form = FormBody.Builder() + .add("category", "client") + .add("type", "premium") + .add("ad", ads.body.string()) + .build() + + val response = client + .newCall(POST(url = "https://ads.anitube.vip", headers = headers, body = form)) + .execute() + + val token = response.parseAs>().first() + + val tokenUrl = "https://ads.anitube.vip".toHttpUrl().newBuilder() + .addQueryParameter("token", token.value) + .build() + + return client.newCall(GET(tokenUrl, headers)) + .execute() + .parseAs>() + .first() + } + + private inline fun Response.parseAs(): T = use { + json.decodeFromStream(it.body.byteStream()) + } - override fun videoListParse(response: Response) = extractor.getVideoList(response) override fun videoListSelector() = throw UnsupportedOperationException() override fun videoFromElement(element: Element) = throw UnsupportedOperationException() override fun videoUrlParse(document: Document) = throw UnsupportedOperationException() @@ -211,22 +257,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { val entry = entryValues[index] as String preferences.edit().putString(key, entry).commit() } - }.also(screen::addPreference) - - // Auth Code - EditTextPreference(screen.context).apply { - key = PREF_AUTHCODE_KEY - title = "Auth Code" - setDefaultValue(PREF_AUTHCODE_DEFAULT) - summary = PREF_AUTHCODE_SUMMARY - - setOnPreferenceChangeListener { _, newValue -> - runCatching { - val value = (newValue as String).trim().ifBlank { PREF_AUTHCODE_DEFAULT } - preferences.edit().putString(key, value).commit() - }.getOrDefault(false) - } - }.also(screen::addPreference) + }.let(screen::addPreference) } // ============================= Utilities ============================== @@ -275,14 +306,10 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { } companion object { - const val PREFIX_SEARCH = "id:" private val DATE_FORMATTER by lazy { SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH) } private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" - private const val PREF_AUTHCODE_KEY = "authcode" - private const val PREF_AUTHCODE_SUMMARY = "Código de Autenticação" - private const val PREF_AUTHCODE_DEFAULT = "" private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_TITLE = "Qualidade preferida" private const val PREF_QUALITY_DEFAULT = "HD" diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/AnitubeToken.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/AnitubeToken.kt new file mode 100644 index 0000000000..5926bffae8 --- /dev/null +++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/AnitubeToken.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.animeextension.pt.anitube + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class AnitubeToken( + @SerialName("publicidade") + val value: String, +) diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/AnitubeUrlActivity.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/AnitubeUrlActivity.kt deleted file mode 100644 index 267dcde425..0000000000 --- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/AnitubeUrlActivity.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.animeextension.pt.anitube - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -/** - * Springboard that accepts https://anitube.vip// intents - * and redirects them to the main Aniyomi process. - */ -class AnitubeUrlActivity : Activity() { - - private val tag = javaClass.simpleName - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size > 1) { - val searchQuery = "${pathSegments[0]}/${pathSegments[1]}" - - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.ANIMESEARCH" - putExtra("query", "${Anitube.PREFIX_SEARCH}$searchQuery") - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e(tag, e.toString()) - } - } else { - Log.e(tag, "could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -} diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt index 3d8a833926..457e28432a 100644 --- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt +++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt @@ -1,163 +1,18 @@ package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors -import android.content.SharedPreferences -import android.util.Log import eu.kanade.tachiyomi.animesource.model.Video -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.FormBody import okhttp3.Headers -import okhttp3.OkHttpClient import okhttp3.Response -class AnitubeExtractor( - private val headers: Headers, - private val client: OkHttpClient, - private val preferences: SharedPreferences, -) { - - private fun getAdsUrl( - serverUrl: String, - thumbUrl: String, - link: String, - linkHeaders: Headers, - ): String { - val videoName = serverUrl.split('/').last() - - val docLink = client.newCall(GET(link, headers = linkHeaders)).execute().asJsoup() - - val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content") - - if (!refresh.isNullOrBlank()) { - val newLink = refresh.substringAfter("=") - val newHeaders = linkHeaders.newBuilder().set("Referer", link).build() - Log.d("AnitubeExtractor", "Following link redirection to $newLink") - - return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders) - } - - Log.d("AnitubeExtractor", "Fetching ADS URL") - - val newHeaders = linkHeaders.newBuilder().set("Referer", link).build() - - try { - val adsUrl = - client.newCall( - GET( - "$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&url=$serverUrl", - headers = newHeaders, - ), - ) - .execute() - .body.string() - .substringAfter("ADS_URL") - .substringAfter('"') - .substringBefore('"') - - if (adsUrl.startsWith("http")) { - Log.d("AnitubeExtractor", "ADS URL: $adsUrl") - return adsUrl - } - } catch (_: Exception) { - } - - // Try default url - Log.e("AnitubeExtractor", "Failed to get the ADS URL, trying the default") - return "https://www.popads.net/js/adblock.js" - } - - private fun getAuthCode(serverUrl: String, thumbUrl: String, link: String): String { - var authCode = preferences.getString(PREF_AUTHCODE_KEY, "")!! - - if (authCode.isNotBlank()) { - Log.d("AnitubeExtractor", "AuthCode found in preferences") - - val isSuccessful = client.newCall(GET("${serverUrl}$authCode", headers = headers)) - .execute().isSuccessful - - if (isSuccessful) { - Log.d("AnitubeExtractor", "AuthCode is OK") - return authCode - } - Log.d("AnitubeExtractor", "AuthCode is invalid") - } - - Log.d("AnitubeExtractor", "Fetching new authCode") - - val adsUrl = getAdsUrl(serverUrl, thumbUrl, link, headers) - - val adsContent = client.newCall(GET(adsUrl)).execute().body.string() - - val body = FormBody.Builder() - .add("category", "client") - .add("type", "premium") - .add("ad", adsContent) - .build() - - val newHeaders = headers.newBuilder() - .set("Referer", SITE_URL) - .add("Accept", "*/*") - .add("Cache-Control", "no-cache") - .add("Pragma", "no-cache") - .add("Connection", "keep-alive") - .add("Sec-Fetch-Dest", "empty") - .add("Sec-Fetch-Mode", "cors") - .add("Sec-Fetch-Site", "same-site") - .build() - - val publicidade = - client.newCall(POST("$ADS_URL/", headers = newHeaders, body = body)) - .execute() - .body.string() - .substringAfter("\"publicidade\"") - .substringAfter('"') - .substringBefore('"') - - if (publicidade.isBlank()) { - Log.e( - "AnitubeExtractor", - "Failed to fetch \"publicidade\" code, the current response: $publicidade", - ) - - throw Exception("Por favor, abra o vídeo uma vez no navegador para liberar o IP") - } - - authCode = - client.newCall( - GET( - "$ADS_URL/?token=$publicidade", - headers = newHeaders, - ), - ) - .execute() - .body.string() - .substringAfter("\"publicidade\"") - .substringAfter('"') - .substringBefore('"') - - if (authCode.startsWith("?")) { - Log.d("AnitubeExtractor", "Auth code fetched successfully") - preferences.edit().putString(PREF_AUTHCODE_KEY, authCode).apply() - } else { - Log.e( - "AnitubeExtractor", - "Failed to fetch auth code, the current response: $authCode", - ) - } - - return authCode - } - - fun getVideoList(response: Response): List