fix(pt/goanimes): Fix video extractor (#2296)

This commit is contained in:
Claudemirovsky
2023-10-02 07:24:03 -03:00
committed by GitHub
parent be0ddc3ef4
commit 1a4c98f704
8 changed files with 140 additions and 79 deletions

View File

@ -0,0 +1,4 @@
dependencies {
implementation(project(":lib-playlist-utils"))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}

View File

@ -3,12 +3,17 @@ package eu.kanade.tachiyomi.animeextension.pt.goanimes
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.BloggerJWPlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.GoAnimesExtractor
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.JsDecoder
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.LinkfunBypasser
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.PlaylistExtractor
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import okhttp3.Response
import org.jsoup.nodes.Element
@ -20,6 +25,9 @@ class GoAnimes : DooPlay(
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div#featured-titles article.item.tvshows > div.poster"
// =============================== Latest ===============================
override val latestUpdatesPath = "lancamentos"
// ============================== Episodes ==============================
override val seasonListSelector = "div#seasons > *"
@ -31,9 +39,9 @@ class GoAnimes : DooPlay(
// Episodes are listed at another page
val url = season.attr("href")
return client.newCall(GET(url))
return client.newCall(GET(url, headers))
.execute()
.asJsoup()
.use { it.asJsoup() }
.let(::getSeasonEpisodes)
}
@ -60,20 +68,26 @@ class GoAnimes : DooPlay(
override val prefQualityValues = arrayOf("240p", "360p", "480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
private val goanimesExtractor by lazy { GoAnimesExtractor(client, headers) }
private val linkfunBypasser by lazy { LinkfunBypasser(client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val document = response.use { it.asJsoup() }
val players = document.select("ul#playeroptionsul li")
return players.flatMap(::getPlayerVideos)
return players.parallelMap {
runCatching {
getPlayerVideos(it)
}.getOrElse { emptyList() }
}.flatten().ifEmpty { throw Exception("Nenhum vídeo encontrado.") }
}
private fun getPlayerVideos(player: Element): List<Video> {
val url = getPlayerUrl(player)
return when {
"player5.goanimes.net" in url ->
GoAnimesExtractor(client).videosFromUrl(url)
"player5.goanimes.net" in url -> goanimesExtractor.videosFromUrl(url)
listOf("/bloggerjwplayer", "/m3u8", "/multivideo").any { it in url } -> {
val script = client.newCall(GET(url)).execute()
.body.string()
.use { it.body.string() }
.let(JsDecoder::decodeScript)
when {
"/bloggerjwplayer" in url ->
@ -84,7 +98,7 @@ class GoAnimes : DooPlay(
script.substringAfter("attr")
.substringAfter(" \"")
.substringBefore('"')
.let { GoAnimesExtractor(client).videosFromUrl(it) }
.let(goanimesExtractor::videosFromUrl)
else -> emptyList<Video>()
}
}
@ -96,14 +110,29 @@ class GoAnimes : DooPlay(
val type = player.attr("data-type")
val id = player.attr("data-post")
val num = player.attr("data-nume")
return client.newCall(GET("$baseUrl/wp-json/dooplayer/v2/$id/$type/$num"))
val url = client.newCall(GET("$baseUrl/wp-json/dooplayer/v2/$id/$type/$num"))
.execute()
.body.string()
.use { it.body.string() }
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
return when {
"/protetorlinks/" in url -> {
val link = client.newCall(GET(url)).execute()
.use { it.asJsoup() }
.selectFirst("a[href]")!!.attr("href")
client.newCall(GET(link)).execute()
.use(linkfunBypasser::getIframeUrl)
}
else -> url
}
}
// =============================== Latest ===============================
override val latestUpdatesPath = "lancamentos"
// ============================= Utilities ==============================
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
}

View File

@ -1,31 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class GoAnimesExtractor(private val client: OkHttpClient) {
private val regexPlayer = Regex("""player\('(\S+?)','\S+'\)""")
class GoAnimesExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String): List<Video> {
val playlistUrl = client.newCall(GET(url)).execute()
.body.string()
.let(JsUnpacker::unpack)
.let(regexPlayer::find)
?.groupValues
?.get(1)
?: return emptyList<Video>()
val body = client.newCall(GET(url, headers)).execute()
.use { it.body.string() }
return when {
"/profix/player.php" in url ->
PlaylistExtractor.videosFromScript(body, PLAYER_NAME)
"/proxy/v.php" in url -> {
val playlistUrl = JsUnpacker.unpackAndCombine(body)
?.substringAfterLast("player(\\'", "")
?.substringBefore("\\'", "")
?.takeIf(String::isNotEmpty)
?: return emptyList()
val playlistData = client.newCall(GET(playlistUrl)).execute()
.body.string()
val separator = "#EXT-X-STREAM-INF:"
return playlistData.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, "GoAnimes - $quality", videoUrl)
playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "$PLAYER_NAME - $it" })
}
"/proxy/api3/" in url -> {
val playlistUrl = body.substringAfter("sources:", "")
.substringAfter("file:", "")
.substringAfter("'", "")
.substringBefore("'", "")
.takeIf(String::isNotEmpty)
?: return emptyList()
playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "$PLAYER_NAME - $it" })
}
else -> emptyList()
}
}
}
private const val PLAYER_NAME = "GoAnimes"

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import android.util.Base64
import kotlin.math.pow
object JsDecoder {
@ -24,7 +25,11 @@ object JsDecoder {
}.joinToString("")
}
fun decodeScript(script: String): String {
fun decodeScript(html: String): String {
val script = html.substringAfter(";base64,")
.substringBefore('"')
.let { String(Base64.decode(it, Base64.DEFAULT)) }
val regex = """\}\("(\w+)",.*?"(\w+)",(\d+),(\d+),.*?\)""".toRegex()
return regex.find(script)
?.run {

View File

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
object JsUnpacker {
private val REGEX_REPLACE = "\\b\\w+\\b".toRegex()
private val REGEX_EVAL = """\}\('(.*)',(\d+),(\d+),'(.*)'\.split""".toRegex()
private fun hasPacker(js: String): Boolean = REGEX_EVAL.containsMatchIn(js)
private fun getPackerArgs(js: String): List<String> = REGEX_EVAL.findAll(js)
.last().groupValues
private fun convert(base: Int, num: Int): String {
val firstPart = if (num < base) "" else (num / base).toString()
val calc = num % base
if (calc > 35) {
return firstPart + (calc + 29).toChar().toString()
}
return firstPart + calc.toString(36)
}
fun unpack(js: String): String {
if (!hasPacker(js)) return js
val args = getPackerArgs(js)
val origJS = args[1]
val base = args[2].toInt()
val count = args[3].toInt()
val origList = args[4].split("|")
val replaceMap = (0..(count - 1)).map {
val key = convert(base, it)
key to try { origList[it] } catch (e: Exception) { key }
}.toMap()
val result = origJS.replace(REGEX_REPLACE) {
replaceMap.get(it.value) ?: it.value
}.replace("\\", "")
return unpack(result)
}
}

View File

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import android.util.Base64
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 LinkfunBypasser(private val client: OkHttpClient) {
fun getIframeUrl(response: Response): String {
return response.use { page ->
val document = page.asJsoup(decodeAtob(page.body.string()))
val newHeaders = Headers.headersOf("Referer", document.location())
val iframe = document.selectFirst("iframe")
if (iframe != null) {
iframe.attr("src")
} else {
val formBody = FormBody.Builder().apply {
document.select("input[name]").forEach {
add(it.attr("name"), it.attr("value"))
}
}.build()
val formUrl = document.selectFirst("form")!!.attr("action")
client.newCall(POST(formUrl, newHeaders, formBody))
.execute()
.use(::getIframeUrl)
}
}
}
companion object {
fun decodeAtob(html: String): String {
val atobContent = html.substringAfter("atob(\"").substringBefore("\"));")
val hexAtob = atobContent.replace("\\x", "").decodeHex()
val decoded = Base64.decode(hexAtob, Base64.DEFAULT)
return String(decoded)
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
}
}

View File

@ -3,16 +3,17 @@ package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import eu.kanade.tachiyomi.animesource.model.Video
object PlaylistExtractor {
fun videosFromScript(script: String): List<Video> {
fun videosFromScript(script: String, prefix: String = "Playlist"): List<Video> {
val sources = script.substringAfter("sources: [").substringBefore("],")
return sources.split("file:\"").drop(1).mapNotNull { source ->
val url = source.substringBefore("\"").ifEmpty { return@mapNotNull null }
val label = source.substringAfter("label:\"").substringBefore("\"")
return sources.split("{").drop(1).mapNotNull { source ->
val url = source.substringAfter("file:").substringAfter('"').substringBefore('"')
.ifEmpty { return@mapNotNull null }
val label = source.substringAfter("label:").substringAfter('"').substringBefore('"')
.replace("FHD", "1080p")
.replace("HD", "720p")
.replace("SD", "480p")
Video(url, "Playlist - $label", url)
Video(url, "$prefix - $label", url)
}
}
}

View File

@ -15,14 +15,13 @@ class DooPlayGenerator : ThemeSourceGenerator {
SingleLang("AnimeOnline.Ninja", "https://ww3.animeonline.ninja", "es", className = "AnimeOnlineNinja", isNsfw = false, overrideVersionCode = 33),
SingleLang("AnimesOnline", "https://animesonline.nz", "pt-BR", isNsfw = false, overrideVersionCode = 5, pkgName = "animesgratis"),
SingleLang("AnimePlayer", "https://animeplayer.com.br", "pt-BR", isNsfw = true, overrideVersionCode = 1),
SingleLang("AnimePlayer", "https://animeplayer.com.br", "pt-BR", isNsfw = true),
SingleLang("AnimeSync", "https://animesync.org", "pt-BR", isNsfw = true),
SingleLang("AnimeSAGA", "https://www.animesaga.in", "hi", isNsfw = false, overrideVersionCode = 6),
SingleLang("AnimesFox BR", "https://animesfox.net", "pt-BR", isNsfw = false, overrideVersionCode = 2),
SingleLang("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 5),
SingleLang("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 16),
SingleLang("DonghuaX", "https://donghuax.com", "pt-BR", isNsfw = false, overrideVersionCode = 1),
SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true, overrideVersionCode = 2),
SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true, overrideVersionCode = 3),
SingleLang("JetAnime", "https://ssl.jetanimes.com", "fr", isNsfw = false),
SingleLang("Kinoking", "https://kinoking.cc", "de", isNsfw = false, overrideVersionCode = 17),
SingleLang("Multimovies", "https://multimovies.shop", "en", isNsfw = false, overrideVersionCode = 10),