fix(pt/pobreflix): Fix video extractor + add more extractors + parallelize (#2610)

This commit is contained in:
Claudemirovsky 2023-12-05 06:45:38 -03:00 committed by GitHub
parent dadd535639
commit 34b01b16f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 87 deletions

View File

@ -1,4 +1,6 @@
dependencies {
implementation(project(":lib-filemoon-extractor"))
implementation(project(":lib-streamwish-extractor"))
implementation(project(":lib-streamtape-extractor"))
implementation(project(":lib-playlist-utils"))
}

View File

@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.animeextension.pt.pobreflix
import android.util.Base64
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.EplayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.MyStreamExtractor
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.PainelfxExtractor
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.SuperFlixExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
@ -24,10 +26,12 @@ class Pobreflix : DooPlay(
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/series/page/$page/", headers)
// ============================ Video Links =============================
private val painelfxExtractor by lazy { PainelfxExtractor(client, headers, ::genericExtractor) }
private val eplayerExtractor by lazy { EplayerExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val mystreamExtractor by lazy { MyStreamExtractor(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val superflixExtractor by lazy { SuperFlixExtractor(client, headers, ::genericExtractor) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.use { it.asJsoup() }
@ -39,8 +43,8 @@ class Pobreflix : DooPlay(
?: return@flatMap emptyList()
val url = data.replace("\\", "").substringAfter("url\":\"").substringBefore('"')
when {
url.contains("painelfx") ->
painelfxExtractor.videosFromUrl(url)
url.contains("superflix") ->
superflixExtractor.videosFromUrl(url)
else -> genericExtractor(url)
}
}.getOrElse { emptyList() }
@ -48,11 +52,20 @@ class Pobreflix : DooPlay(
}
private fun genericExtractor(url: String, language: String = ""): List<Video> {
val langSubstr = "[$language]"
return when {
url.contains("filemoon") -> filemoonExtractor.videosFromUrl(url, headers = headers)
url.contains("filemoon") ->
filemoonExtractor.videosFromUrl(url, "$langSubstr Filemoon - ", headers = headers)
url.contains("watch.brplayer") || url.contains("/watch?v=") ->
mystreamExtractor.videosFromUrl(url, language)
url.contains("embedplayer") -> eplayerExtractor.videosFromUrl(url, language)
url.contains("embedplayer") ->
eplayerExtractor.videosFromUrl(url, language)
url.contains("streamtape") ->
streamtapeExtractor.videosFromUrl(url, "$langSubstr Streamtape")
url.contains("filelions") ->
streamwishExtractor.videosFromUrl(url, videoNameGen = { "$langSubstr FileLions - $it" })
url.contains("streamwish") ->
streamwishExtractor.videosFromUrl(url, videoNameGen = { "$langSubstr Streamwish - $it" })
else -> emptyList()
}
}

View File

@ -1,80 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
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.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class PainelfxExtractor(
private val client: OkHttpClient,
private val headers: Headers,
private val genericExtractor: (String, String) -> List<Video>,
) {
fun videosFromUrl(url: String): List<Video> {
val docHeaders = headers.newBuilder().set("Referer", "https://gastronomiabrasileira.net/").build()
val doc = client.newCall(GET(url, docHeaders)).execute().use { it.asJsoup() }
val lang = when (url.substringAfterLast("/")) {
"leg" -> "Legendado"
else -> "Dublado"
}
val host = url.toHttpUrl().host
val videoHeaders = headers.newBuilder().set("Referer", "https://$host/").build()
val buttons = doc.select("div.panel-body > button")
val encodedHexList = when {
buttons.isNotEmpty() -> {
buttons.map { elem ->
val form = FormBody.Builder()
.add("idS", elem.attr("idS"))
.build()
client.newCall(POST("https://$host/CallEpi", body = form)).execute()
.use { it.body.string() }
}
}
else -> {
val script = doc.selectFirst("script:containsData(idS:)")?.data() ?: return emptyList()
val idList = script.split("idS:").drop(1).map { it.substringAfter('"').substringBefore('"') }
idList.map { idS ->
val form = FormBody.Builder()
.add("id", idS)
.build()
client.newCall(POST("https://$host/CallPlayer", body = form)).execute()
.use { it.body.string() }
}
}
}
return encodedHexList.flatMap {
videosFromHex(it, lang, videoHeaders)
}
}
private fun videosFromHex(hex: String, lang: String, videoHeaders: Headers): List<Video> {
val decoded = hex.decodeHex().let(::String).replace("\\", "")
return if (decoded.contains("video\"")) {
decoded.substringAfter("video").split("{").drop(1).map {
val videoUrl = it.substringAfter("file\":\"").substringBefore('"')
val quality = it.substringAfter("label\":\"").substringBefore('"')
Video(videoUrl, "$lang - $quality", videoUrl, videoHeaders)
}
} else {
val url = decoded.substringAfter("\"url\":\"").substringBefore('"')
genericExtractor(url, lang)
}
}
// 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

@ -0,0 +1,139 @@
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class SuperFlixExtractor(
private val client: OkHttpClient,
private val defaultHeaders: Headers,
private val genericExtractor: (String, String) -> List<Video>,
) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String): List<Video> {
val links = linksFromUrl(url)
val fixedLinks = links.parallelCatchingFlatMap {
val (language, linkUrl) = it
when {
linkUrl.contains("?vid=") -> linksFromPlayer(linkUrl, language)
else -> listOf(it)
}
}
return fixedLinks.parallelCatchingFlatMap { genericExtractor(it.second, it.first) }
}
private fun linksFromPlayer(url: String, language: String): List<Pair<String, String>> {
val httpUrl = url.toHttpUrl()
val id = httpUrl.queryParameter("vid")!!
val headers = defaultHeaders.newBuilder()
.set("referer", "$API_DOMAIN/")
.set("origin", API_DOMAIN)
.build()
val doc = client.newCall(GET(url, headers)).execute().use { it.asJsoup() }
val baseUrl = "https://" + httpUrl.host
val apiUrl = "$baseUrl/ajax_sources.php"
val apiHeaders = headers.newBuilder()
.set("referer", url)
.set("origin", baseUrl)
.set("X-Requested-With", "XMLHttpRequest")
.build()
return doc.select("ul > li[data-order-value]").mapNotNull {
val name = it.attr("data-dropdown-value")
val order = it.attr("data-order-value")
val formBody = FormBody.Builder()
.add("vid", id)
.add("alternative", name)
.add("ord", order)
.build()
val req = client.newCall(POST(apiUrl, apiHeaders, formBody)).execute()
.use { it.body.string() }
runCatching {
val iframeUrl = json.decodeFromString<PlayerLinkDto>(req).iframe!!
val iframeServer = iframeUrl.toHttpUrl().queryParameter("sv")!!
language to when (name) {
"1xbet" -> "https://watch.brplayer.site/watch?v=${iframeServer.trim('/')}"
else -> iframeServer
}
}.getOrNull()
}
}
@Serializable
data class PlayerLinkDto(val iframe: String? = null)
private fun linksFromUrl(url: String): List<Pair<String, String>> {
val doc = client.newCall(GET(url, defaultHeaders)).execute().use { it.asJsoup() }
val items = doc.select("div.select_language").mapNotNull {
val target = it.attr("data-target")
val id = doc.selectFirst("div.players_select div[data-target=$target] div[data-id]")
?.attr("data-id")
?: return@mapNotNull null
it.text() to id // (Language, videoId)
}
val headers = defaultHeaders.newBuilder()
.set("Origin", API_DOMAIN)
.set("Referer", url)
.set("X-Requested-With", "XMLHttpRequest")
.build()
return items.mapNotNull {
runCatching {
it.first to getLink(it.second, headers)!!
}.getOrNull()
}
}
private fun getLink(id: String, headers: Headers): String? {
val body = FormBody.Builder()
.add("action", "getPlayer")
.add("video_id", id)
.build()
val res = client.newCall(POST("$API_DOMAIN/api", headers, body)).execute()
.use { it.body.string() }
return json.decodeFromString<ApiResponseDto>(res).data?.video_url
}
@Serializable
data class ApiResponseDto(val data: DataDto? = null)
@Serializable
data class DataDto(val video_url: String? = null)
private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
runBlocking {
map {
async(Dispatchers.Default) {
runCatching { f(it) }.getOrElse { emptyList() }
}
}.awaitAll().flatten()
}
}
private const val API_DOMAIN = "https://superflixapi.top"

View File

@ -24,7 +24,7 @@ class DooPlayGenerator : ThemeSourceGenerator {
SingleLang("Kinoking", "https://kinoking.cc", "de", isNsfw = false, overrideVersionCode = 19),
SingleLang("Multimovies", "https://multimovies.live", "en", isNsfw = false, overrideVersionCode = 13),
SingleLang("Pi Fansubs", "https://pifansubs.org", "pt-BR", isNsfw = true, overrideVersionCode = 17),
SingleLang("Pobreflix", "https://pobreflix.biz", "pt-BR", isNsfw = true, overrideVersionCode = 2),
SingleLang("Pobreflix", "https://pobreflix.biz", "pt-BR", isNsfw = true, overrideVersionCode = 3),
SingleLang("UniqueStream", "https://uniquestream.net", "en", isNsfw = false, overrideVersionCode = 2),
)