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.BloggerJWPlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.GoAnimesExtractor 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.JsDecoder
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.LinkfunBypasser
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.PlaylistExtractor import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.PlaylistExtractor
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup 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 okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -20,6 +25,9 @@ class GoAnimes : DooPlay(
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeSelector() = "div#featured-titles article.item.tvshows > div.poster" override fun popularAnimeSelector() = "div#featured-titles article.item.tvshows > div.poster"
// =============================== Latest ===============================
override val latestUpdatesPath = "lancamentos"
// ============================== Episodes ============================== // ============================== Episodes ==============================
override val seasonListSelector = "div#seasons > *" override val seasonListSelector = "div#seasons > *"
@ -31,9 +39,9 @@ class GoAnimes : DooPlay(
// Episodes are listed at another page // Episodes are listed at another page
val url = season.attr("href") val url = season.attr("href")
return client.newCall(GET(url)) return client.newCall(GET(url, headers))
.execute() .execute()
.asJsoup() .use { it.asJsoup() }
.let(::getSeasonEpisodes) .let(::getSeasonEpisodes)
} }
@ -60,20 +68,26 @@ class GoAnimes : DooPlay(
override val prefQualityValues = arrayOf("240p", "360p", "480p", "720p", "1080p") override val prefQualityValues = arrayOf("240p", "360p", "480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues 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> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.use { it.asJsoup() }
val players = document.select("ul#playeroptionsul li") 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> { private fun getPlayerVideos(player: Element): List<Video> {
val url = getPlayerUrl(player) val url = getPlayerUrl(player)
return when { return when {
"player5.goanimes.net" in url -> "player5.goanimes.net" in url -> goanimesExtractor.videosFromUrl(url)
GoAnimesExtractor(client).videosFromUrl(url)
listOf("/bloggerjwplayer", "/m3u8", "/multivideo").any { it in url } -> { listOf("/bloggerjwplayer", "/m3u8", "/multivideo").any { it in url } -> {
val script = client.newCall(GET(url)).execute() val script = client.newCall(GET(url)).execute()
.body.string() .use { it.body.string() }
.let(JsDecoder::decodeScript) .let(JsDecoder::decodeScript)
when { when {
"/bloggerjwplayer" in url -> "/bloggerjwplayer" in url ->
@ -84,7 +98,7 @@ class GoAnimes : DooPlay(
script.substringAfter("attr") script.substringAfter("attr")
.substringAfter(" \"") .substringAfter(" \"")
.substringBefore('"') .substringBefore('"')
.let { GoAnimesExtractor(client).videosFromUrl(it) } .let(goanimesExtractor::videosFromUrl)
else -> emptyList<Video>() else -> emptyList<Video>()
} }
} }
@ -96,14 +110,29 @@ class GoAnimes : DooPlay(
val type = player.attr("data-type") val type = player.attr("data-type")
val id = player.attr("data-post") val id = player.attr("data-post")
val num = player.attr("data-nume") 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() .execute()
.body.string() .use { it.body.string() }
.substringAfter("\"embed_url\":\"") .substringAfter("\"embed_url\":\"")
.substringBefore("\",") .substringBefore("\",")
.replace("\\", "") .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 =============================== // ============================= Utilities ==============================
override val latestUpdatesPath = "lancamentos" 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 package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class GoAnimesExtractor(private val client: OkHttpClient) { class GoAnimesExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val regexPlayer = Regex("""player\('(\S+?)','\S+'\)""") private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String): List<Video> { fun videosFromUrl(url: String): List<Video> {
val playlistUrl = client.newCall(GET(url)).execute() val body = client.newCall(GET(url, headers)).execute()
.body.string() .use { it.body.string() }
.let(JsUnpacker::unpack) return when {
.let(regexPlayer::find) "/profix/player.php" in url ->
?.groupValues PlaylistExtractor.videosFromScript(body, PLAYER_NAME)
?.get(1) "/proxy/v.php" in url -> {
?: return emptyList<Video>() val playlistUrl = JsUnpacker.unpackAndCombine(body)
?.substringAfterLast("player(\\'", "")
?.substringBefore("\\'", "")
?.takeIf(String::isNotEmpty)
?: return emptyList()
val playlistData = client.newCall(GET(playlistUrl)).execute() playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "$PLAYER_NAME - $it" })
.body.string() }
"/proxy/api3/" in url -> {
val separator = "#EXT-X-STREAM-INF:" val playlistUrl = body.substringAfter("sources:", "")
return playlistData.substringAfter(separator).split(separator).map { .substringAfter("file:", "")
val quality = it.substringAfter("RESOLUTION=") .substringAfter("'", "")
.substringAfter("x") .substringBefore("'", "")
.substringBefore(",") + "p" .takeIf(String::isNotEmpty)
val videoUrl = it.substringAfter("\n").substringBefore("\n") ?: return emptyList()
Video(videoUrl, "GoAnimes - $quality", videoUrl) 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 package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import android.util.Base64
import kotlin.math.pow import kotlin.math.pow
object JsDecoder { object JsDecoder {
@ -24,7 +25,11 @@ object JsDecoder {
}.joinToString("") }.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() val regex = """\}\("(\w+)",.*?"(\w+)",(\d+),(\d+),.*?\)""".toRegex()
return regex.find(script) return regex.find(script)
?.run { ?.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 import eu.kanade.tachiyomi.animesource.model.Video
object PlaylistExtractor { object PlaylistExtractor {
fun videosFromScript(script: String): List<Video> { fun videosFromScript(script: String, prefix: String = "Playlist"): List<Video> {
val sources = script.substringAfter("sources: [").substringBefore("],") val sources = script.substringAfter("sources: [").substringBefore("],")
return sources.split("file:\"").drop(1).mapNotNull { source -> return sources.split("{").drop(1).mapNotNull { source ->
val url = source.substringBefore("\"").ifEmpty { return@mapNotNull null } val url = source.substringAfter("file:").substringAfter('"').substringBefore('"')
val label = source.substringAfter("label:\"").substringBefore("\"") .ifEmpty { return@mapNotNull null }
val label = source.substringAfter("label:").substringAfter('"').substringBefore('"')
.replace("FHD", "1080p") .replace("FHD", "1080p")
.replace("HD", "720p") .replace("HD", "720p")
.replace("SD", "480p") .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("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("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, overrideVersionCode = 1),
SingleLang("AnimePlayer", "https://animeplayer.com.br", "pt-BR", isNsfw = true),
SingleLang("AnimeSync", "https://animesync.org", "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("AnimeSAGA", "https://www.animesaga.in", "hi", isNsfw = false, overrideVersionCode = 6),
SingleLang("AnimesFox BR", "https://animesfox.net", "pt-BR", isNsfw = false, overrideVersionCode = 2), 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("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 5),
SingleLang("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 16), SingleLang("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 16),
SingleLang("DonghuaX", "https://donghuax.com", "pt-BR", isNsfw = false, overrideVersionCode = 1), 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("JetAnime", "https://ssl.jetanimes.com", "fr", isNsfw = false),
SingleLang("Kinoking", "https://kinoking.cc", "de", isNsfw = false, overrideVersionCode = 17), SingleLang("Kinoking", "https://kinoking.cc", "de", isNsfw = false, overrideVersionCode = 17),
SingleLang("Multimovies", "https://multimovies.shop", "en", isNsfw = false, overrideVersionCode = 10), SingleLang("Multimovies", "https://multimovies.shop", "en", isNsfw = false, overrideVersionCode = 10),