fix(en/gogoanime): Update baseUrl (#2550)

This commit is contained in:
Claudemirovsky 2023-11-25 15:13:20 -03:00 committed by GitHub
parent 135faf95f0
commit 15fbd2149f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 215 deletions

View File

@ -7,7 +7,7 @@ ext {
extName = 'Gogoanime'
pkgNameSuffix = 'en.gogoanime'
extClass = '.GogoAnime'
extVersionCode = 76
extVersionCode = 77
libVersion = '13'
}
@ -15,8 +15,7 @@ dependencies {
implementation(project(':lib-streamwish-extractor'))
implementation(project(':lib-mp4upload-extractor'))
implementation(project(':lib-dood-extractor'))
implementation(project(':lib-playlist-utils'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
implementation(project(':lib-gogostream-extractor'))
}
apply from: "$rootDir/common.gradle"

View File

@ -1,14 +1,12 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.BuildConfig
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.GogoCdnExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
@ -16,6 +14,7 @@ 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.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
@ -24,19 +23,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
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.lang.Exception
@ExperimentalSerializationApi
class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Gogoanime"
@ -47,16 +41,17 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
override val client = network.cloudflareClient
private val json: Json by injectLazy()
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
private val preferences: SharedPreferences by lazy {
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/popular.html?page=$page", headers)
override fun popularAnimeSelector(): String = "div.img a"
@ -70,44 +65,28 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeNextPageSelector(): String = "ul.pagination-list li:last-child:not(.selected)"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/?page=$page", headers)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home.html?page=$page", headers)
override fun latestUpdatesSelector(): String = "div.img a"
override fun latestUpdatesFromElement(element: Element): SAnime {
val imgUrl = element.selectFirst("img")!!.attr("src")
val newUrl = imgUrl.replaceFirst("https://", "").substringAfter("/").replaceFirst("cover", "/category").substringBeforeLast('.')
val finalUrl = newUrl.let { url ->
url.lastIndexOf('-').let { lastIndex ->
val suffix = url.substring(lastIndex + 1)
if (lastIndex == -1 || !suffix.all { it.isDigit() } || suffix.length < 3) {
newUrl
} else {
url.substring(0, lastIndex)
}
}
}
return SAnime.create().apply {
setUrlWithoutDomain(finalUrl)
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.attr("title")
}
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
thumbnail_url = element.selectFirst("img")?.attr("src")
title = element.attr("title")
val slug = element.attr("href").substringAfter(baseUrl)
.trimStart('/')
.substringBefore("-episode-")
setUrlWithoutDomain("/category/$slug")
}
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = GogoAnimeFilters.getSearchParameters(filters)
return when {
params.genre.isNotEmpty() -> GET("$baseUrl/genre/${params.genre}?page=$page", headers)
params.recent.isNotEmpty() -> GET("https://ajax.gogo-load.com/ajax/page-recent-release.html?page=$page&type=${params.recent}", headers)
params.recent.isNotEmpty() -> GET("$AJAX_URL/page-recent-release.html?page=$page&type=${params.recent}", headers)
params.season.isNotEmpty() -> GET("$baseUrl/${params.season}?page=$page", headers)
else -> GET("$baseUrl/filter.html?keyword=$query&${params.filter}&page=$page", headers)
}
@ -120,42 +99,37 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = GogoAnimeFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val infoDocument = document.selectFirst("div.anime-info a[href]")?.let {
client.newCall(GET(it.attr("abs:href"), headers)).execute().asJsoup()
client.newCall(GET(it.absUrl("href"), headers)).execute().asJsoup()
} ?: document
return SAnime.create().apply {
title = infoDocument.select("div.anime_info_body_bg h1").text()
genre = infoDocument.select("p.type:eq(5) a").joinToString("") { it.text() }
description = infoDocument.selectFirst("p.type:eq(4)")!!.ownText()
status = parseStatus(infoDocument.select("p.type:eq(7) a").text())
title = infoDocument.selectFirst("div.anime_info_body_bg > h1")!!.text()
genre = infoDocument.getInfo("Genre:")
status = parseStatus(infoDocument.getInfo("Status:").orEmpty())
// add alternative name to anime description
val altName = "Other name(s): "
infoDocument.selectFirst("p.type:eq(8)")?.ownText()?.let {
if (it.isBlank().not()) {
description = when {
description.isNullOrBlank() -> altName + it
else -> description + "\n\n$altName" + it
}
description = buildString {
infoDocument.getInfo("Plot Summary:")?.also(::append)
// add alternative name to anime description
infoDocument.getInfo("Other name:")?.also {
if (isNotBlank()) append("\n\n")
append("Other name(s): $it")
}
}
}
}
// ============================== Episodes ==============================
private fun episodesRequest(totalEpisodes: String, id: String): List<SEpisode> {
val request = GET("https://ajax.gogo-load.com/ajax/load-list-episode?ep_start=0&ep_end=$totalEpisodes&id=$id", headers)
val request = GET("$AJAX_URL/load-list-episode?ep_start=0&ep_end=$totalEpisodes&id=$id", headers)
val epResponse = client.newCall(request).execute()
val document = epResponse.asJsoup()
return document.select("a").map { episodeFromElement(it) }
return document.select("a").map(::episodeFromElement)
}
override fun episodeListParse(response: Response): List<SEpisode> {
@ -177,8 +151,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
// ============================ Video Links =============================
private val gogoExtractor by lazy { GogoCdnExtractor(client, json) }
private val gogoExtractor by lazy { GogoStreamExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
@ -187,17 +160,15 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val document = response.asJsoup()
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return document.select("div.anime_muti_link > ul > li").parallelMap { server ->
runCatching {
val className = server.className()
if (!hosterSelection.contains(className)) return@runCatching emptyList()
val serverUrl = server.selectFirst("a")
?.attr("abs:data-video")
?: return@runCatching emptyList()
return document.select("div.anime_muti_link > ul > li").parallelCatchingFlatMap { server ->
val className = server.className()
if (!hosterSelection.contains(className)) return@parallelCatchingFlatMap emptyList()
val serverUrl = server.selectFirst("a")
?.attr("abs:data-video")
?: return@parallelCatchingFlatMap emptyList()
getHosterVideos(className, serverUrl)
}.getOrElse { emptyList() }
}.flatten().sort().ifEmpty { throw Exception("Failed to extract videos") }
getHosterVideos(className, serverUrl)
}
}
private fun getHosterVideos(className: String, serverUrl: String): List<Video> {
@ -220,12 +191,18 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document) = throw Exception("not used")
// ============================= Utilities ==============================
private fun Document.getInfo(text: String): String? {
val base = selectFirst("p.type:has(span:containsOwn($text))") ?: return null
return base.select("a").eachText().joinToString("")
.ifBlank { base.ownText() }
.takeUnless(String::isBlank)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
@ -242,13 +219,18 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
// From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
map {
async(Dispatchers.Default) {
runCatching { f(it) }.getOrElse { emptyList() }
}
}.awaitAll().flatten()
}
companion object {
private const val AJAX_URL = "https://ajax.gogo-load.com/ajax"
private val HOSTERS = arrayOf(
"Gogostream",
"Vidstreaming",
@ -266,44 +248,51 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
"filelions",
)
private val PREF_DOMAIN_KEY = "preferred_domain_name_v${BuildConfig.VERSION_CODE}"
private const val PREF_DOMAIN_KEY = "preferred_domain_name_v${BuildConfig.VERSION_CODE}"
private const val PREF_DOMAIN_TITLE = "Override BaseUrl"
private const val PREF_DOMAIN_DEFAULT = "https://gogoanimehd.io"
private const val PREF_DOMAIN_DEFAULT = "https://anitaku.to"
private const val PREF_DOMAIN_SUMMARY = "For temporary uses. Updating the extension will erase this setting."
private const val PREF_DOMAIN_DIALOG_MESSAGE = "Default: $PREF_DOMAIN_DEFAULT"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "Gogostream"
private const val PREF_HOSTER_KEY = "hoster_selection"
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
summary = PREF_DOMAIN_SUMMARY
dialogTitle = PREF_DOMAIN_TITLE
dialogMessage = "Default: $PREF_DOMAIN_DEFAULT"
dialogMessage = PREF_DOMAIN_DIALOG_MESSAGE
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = PREF_DOMAIN_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
val newValueString = newValue as String
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, newValueString.trim()).commit()
runCatching {
val value = (newValue as String).trim().ifEmpty { PREF_DOMAIN_DEFAULT }
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
@ -317,7 +306,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
title = PREF_SERVER_TITLE
entries = HOSTERS
entryValues = HOSTERS
setDefaultValue(PREF_SERVER_DEFAULT)
@ -333,7 +322,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Enable/Disable Hosts"
title = PREF_HOSTER_TITLE
entries = HOSTERS
entryValues = HOSTERS_NAMES
setDefaultValue(PREF_HOSTER_DEFAULT)

View File

@ -1,129 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.lang.Exception
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ExperimentalSerializationApi
class GogoCdnExtractor(private val client: OkHttpClient, private val json: Json) {
fun videosFromUrl(serverUrl: String): List<Video> {
try {
val document = client.newCall(GET(serverUrl)).execute().asJsoup()
val iv = document.select("div.wrapper")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val secretKey = document.select("body[class]")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val decryptionKey = document.select("div.videocontent")
.attr("class").substringAfter("videocontent-")
.filter { it.isDigit() }.toByteArray()
val encryptAjaxParams = cryptoHandler(
document.select("script[data-value]")
.attr("data-value"),
iv,
secretKey,
false,
).substringAfter("&")
val httpUrl = serverUrl.toHttpUrl()
val host = "https://" + httpUrl.host + "/"
val id = httpUrl.queryParameter("id") ?: throw Exception("error getting id")
val encryptedId = cryptoHandler(id, iv, secretKey)
val token = httpUrl.queryParameter("token")
val qualityPrefix = if (token != null) "Gogostream - " else "Vidstreaming - "
val jsonResponse = client.newCall(
GET(
"${host}encrypt-ajax.php?id=$encryptedId&$encryptAjaxParams&alias=$id",
Headers.headersOf(
"X-Requested-With",
"XMLHttpRequest",
),
),
).execute().body.string()
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
val decryptedData = cryptoHandler(data, iv, decryptionKey, false)
val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(decryptedData)["source"]!!.jsonArray
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
val fileURL = array[0].jsonObject["file"].toString().trim('"')
val separator = "#EXT-X-STREAM-INF:"
val masterPlaylist = client.newCall(GET(fileURL)).execute().body.string()
if (masterPlaylist.contains(separator)) {
masterPlaylist.substringAfter(separator)
.split(separator).forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
if (!videoUrl.startsWith("http")) {
videoUrl = fileURL.substringBeforeLast("/") + "/$videoUrl"
}
videoList.add(Video(videoUrl, qualityPrefix + quality, videoUrl))
}
} else {
videoList.add(Video(fileURL, "${qualityPrefix}Original", fileURL))
}
} else {
array.forEach {
val label = it.jsonObject["label"].toString().lowercase(Locale.ROOT)
.trim('"').replace(" ", "")
val fileURL = it.jsonObject["file"].toString().trim('"')
val videoHeaders = Headers.headersOf("Referer", serverUrl)
if (label == "auto") {
autoList.add(
Video(
fileURL,
qualityPrefix + label,
fileURL,
headers = videoHeaders,
),
)
} else {
videoList.add(Video(fileURL, qualityPrefix + label, fileURL, headers = videoHeaders))
}
}
}
return videoList.sortedByDescending {
it.quality.substringAfter(qualityPrefix).substringBefore("p").toIntOrNull() ?: -1
} + autoList
} catch (e: Exception) {
return emptyList()
}
}
private fun cryptoHandler(
string: String,
iv: ByteArray,
secretKeyString: ByteArray,
encrypt: Boolean = true,
): String {
val ivParameterSpec = IvParameterSpec(iv)
val secretKey = SecretKeySpec(secretKeyString, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP)
}
}
}