fix(src/es): Fix Metroseries and TioAnime (#2708)

This commit is contained in:
imper1aldev 2024-01-08 19:16:45 -06:00 committed by GitHub
parent 40d30eae8d
commit 2ce73f03b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 222 additions and 66 deletions

View File

@ -25,18 +25,20 @@ class FastreamExtractor(private val client: OkHttpClient, private val headers: H
return runCatching { return runCatching {
val firstDoc = client.newCall(GET(url, videoHeaders)).execute().use { it.asJsoup() } val firstDoc = client.newCall(GET(url, videoHeaders)).execute().use { it.asJsoup() }
if (needsSleep) Thread.sleep(5100L) // 5s is the minimum
val scriptElement = if (firstDoc.select("input[name]").any()) {
val form = FormBody.Builder().apply { val form = FormBody.Builder().apply {
firstDoc.select("input[name]").forEach { firstDoc.select("input[name]").forEach {
add(it.attr("name"), it.attr("value")) add(it.attr("name"), it.attr("value"))
} }
}.build() }.build()
val doc = client.newCall(POST(url, videoHeaders, body = form)).execute().use { it.asJsoup() }
if (needsSleep) Thread.sleep(5100L) // 5s is the minimum doc.selectFirst("script:containsData(jwplayer):containsData(vplayer)") ?: return emptyList()
val doc = client.newCall(POST(url, videoHeaders, body = form)).execute() }
.use { it.asJsoup() } else {
firstDoc.selectFirst("script:containsData(jwplayer):containsData(vplayer)") ?: return emptyList()
val scriptElement = doc.selectFirst("script:containsData(jwplayer):containsData(vplayer)") }
?: return emptyList()
val scriptData = scriptElement.data().let { val scriptData = scriptElement.data().let {
when { when {
@ -45,10 +47,7 @@ class FastreamExtractor(private val client: OkHttpClient, private val headers: H
} }
} ?: return emptyList() } ?: return emptyList()
val videoUrl = scriptData.substringAfter("file:") val videoUrl = scriptData.substringAfter("file:\"").substringBefore("\"").trim()
.substringBefore('}')
.substringBefore(',')
.trim('"', '\'', ' ')
return when { return when {
videoUrl.contains(".m3u8") -> { videoUrl.contains(".m3u8") -> {

View File

@ -6,7 +6,7 @@ ext {
extName = 'MetroSeries' extName = 'MetroSeries'
pkgNameSuffix = 'es.metroseries' pkgNameSuffix = 'es.metroseries'
extClass = '.MetroSeries' extClass = '.MetroSeries'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -30,6 +30,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -80,7 +81,8 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
SAnime.create().apply { SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst(".lnk-blk")?.attr("abs:href") ?: "") setUrlWithoutDomain(element.selectFirst(".lnk-blk")?.attr("abs:href") ?: "")
title = element.selectFirst(".entry-header .entry-title")?.text() ?: "" title = element.selectFirst(".entry-header .entry-title")?.text() ?: ""
thumbnail_url = element.selectFirst(".post-thumbnail figure img")?.attr("abs:src") ?: "" description = element.select(".entry-content p").text() ?: ""
thumbnail_url = element.selectFirst(".post-thumbnail figure img")?.let { getImageUrl(it) }
} }
} }
return AnimesPage(animeList, nextPage) return AnimesPage(animeList, nextPage)
@ -99,12 +101,20 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
return SAnime.create().apply { return SAnime.create().apply {
title = document.selectFirst("main .entry-header .entry-title")?.text() ?: "" title = document.selectFirst("main .entry-header .entry-title")?.text() ?: ""
description = document.select("main .entry-content p").joinToString { it.text() } description = document.select("main .entry-content p").joinToString { it.text() }
thumbnail_url = document.selectFirst("main .post-thumbnail img")?.attr("abs:src") thumbnail_url = document.selectFirst("main .post-thumbnail img")?.let { getImageUrl(it) }
genre = document.select("main .entry-content .tagcloud a").joinToString { it.text() } genre = document.select("main .entry-content .tagcloud a").joinToString { it.text() }
status = SAnime.UNKNOWN status = SAnime.UNKNOWN
} }
} }
private fun getImageUrl(element: Element): String? {
return when {
element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("src") -> element.attr("abs:src")
else -> null
}
}
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.asJsoup()
val referer = response.request.url.toString() val referer = response.request.url.toString()
@ -218,7 +228,7 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
val key = src.split("/").last() val key = src.split("/").last()
src = "https://fastream.to/embed-$key.html" src = "https://fastream.to/embed-$key.html"
} }
FastreamExtractor(client, headers).videosFromUrl(src).also(videoList::addAll) FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false).also(videoList::addAll)
} }
if (src.contains("upstream")) { if (src.contains("upstream")) {

View File

@ -5,13 +5,14 @@ ext {
extName = 'TioanimeH' extName = 'TioanimeH'
pkgNameSuffix = 'es.tioanimeh' pkgNameSuffix = 'es.tioanimeh'
extClass = '.TioanimeHFactory' extClass = '.TioanimeHFactory'
extVersionCode = 13 extVersionCode = 14
libVersion = '13' libVersion = '13'
} }
dependencies { dependencies {
implementation(project(':lib-yourupload-extractor')) implementation(project(':lib-yourupload-extractor'))
implementation(project(':lib-okru-extractor')) implementation(project(':lib-okru-extractor'))
implementation(project(':lib-voe-extractor'))
} }

View File

@ -4,6 +4,7 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.tioanimeh.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -12,6 +13,7 @@ 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.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@ -28,7 +30,7 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
override val lang = "es" override val lang = "es"
override val supportsLatest = false override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
@ -36,6 +38,21 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Voe"
private val SERVER_LIST = arrayOf(
"YourUpload",
"Voe",
"VidGuard",
"Okru",
)
}
override fun popularAnimeSelector(): String = "ul.animes.list-unstyled.row li.col-6.col-sm-4.col-md-3.col-xl-2" override fun popularAnimeSelector(): String = "ul.animes.list-unstyled.row li.col-6.col-sm-4.col-md-3.col-xl-2"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/directorio?p=$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/directorio?p=$page")
@ -84,14 +101,10 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
val serverName = servers[0] val serverName = servers[0]
val serverUrl = servers[1].replace("\\/", "/") val serverUrl = servers[1].replace("\\/", "/")
when (serverName.lowercase()) { when (serverName.lowercase()) {
"okru" -> { "voe" -> VoeExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll)
OkruExtractor(client).videosFromUrl(serverUrl).map { vid -> videoList.add(vid) } "vidguard" -> VidGuardExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll)
} "okru" -> OkruExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll)
"yourupload" -> { "yourupload" -> YourUploadExtractor(client).videoFromUrl(serverUrl, headers = headers).let(videoList::addAll)
videoList.addAll(
YourUploadExtractor(client).videoFromUrl(serverUrl, headers = headers),
)
}
} }
} }
@ -105,24 +118,15 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
override fun videoFromElement(element: Element) = throw Exception("not used") override fun videoFromElement(element: Element) = throw Exception("not used")
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
return try { val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val videoSorted = this.sortedWith( val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
compareBy<Video> { it.quality.replace("[0-9]".toRegex(), "") }.thenByDescending { getNumberFromString(it.quality) }, return this.sortedWith(
).toTypedArray() compareBy(
val userPreferredQuality = preferences.getString("preferred_quality", "Okru:720p") { it.quality.contains(server, true) },
val preferredIdx = videoSorted.indexOfFirst { x -> x.quality == userPreferredQuality } { it.quality.contains(quality) },
if (preferredIdx != -1) { { Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
videoSorted.drop(preferredIdx + 1) ),
videoSorted[0] = videoSorted[preferredIdx] ).reversed()
}
videoSorted.toList()
} catch (e: Exception) {
this
}
}
private fun getNumberFromString(epsStr: String): String {
return epsStr.filter { it.isDigit() }.ifEmpty { "0" }
} }
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
@ -135,6 +139,7 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
else -> GET("$baseUrl/directorio?p=$page ") else -> GET("$baseUrl/directorio?p=$page ")
} }
} }
override fun searchAnimeFromElement(element: Element): SAnime { override fun searchAnimeFromElement(element: Element): SAnime {
return popularAnimeFromElement(element) return popularAnimeFromElement(element)
} }
@ -148,6 +153,7 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
anime.title = document.select("h1.title").text() anime.title = document.select("h1.title").text()
anime.description = document.selectFirst("p.sinopsis")!!.ownText() anime.description = document.selectFirst("p.sinopsis")!!.ownText()
anime.genre = document.select("p.genres span.btn.btn-sm.btn-primary.rounded-pill a").joinToString { it.text() } anime.genre = document.select("p.genres span.btn.btn-sm.btn-primary.rounded-pill a").joinToString { it.text() }
anime.thumbnail_url = document.select(".thumb img").attr("abs:src")
anime.status = parseStatus(document.select("a.btn.btn-success.btn-block.status").text()) anime.status = parseStatus(document.select("a.btn.btn-success.btn-block.status").text())
return anime return anime
} }
@ -160,13 +166,23 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
} }
} }
override fun latestUpdatesNextPageSelector() = throw Exception("not used") override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = throw Exception("not used") override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.title = element.select("article a h3").text()
anime.thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
override fun latestUpdatesRequest(page: Int) = throw Exception("not used") val slug = if (baseUrl.contains("hentai")) "/hentai/" else "/anime/"
val fixUrl = element.select("article a").attr("href").split("-").toTypedArray()
val realUrl = fixUrl.copyOf(fixUrl.size - 1).joinToString("-").replace("/ver/", slug)
anime.setUrlWithoutDomain(realUrl)
return anime
}
override fun latestUpdatesSelector() = throw Exception("not used") override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
override fun latestUpdatesSelector() = ".episodes li"
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"), AnimeFilter.Header("La busqueda por texto ignora el filtro"),
@ -208,21 +224,12 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val qualities = arrayOf( ListPreference(screen.context).apply {
"Okru:1080p", key = PREF_QUALITY_KEY
"Okru:720p",
"Okru:480p",
"Okru:360p",
"Okru:240p",
"Okru:144p", // Okru
"YourUpload", // video servers without resolution
)
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality" title = "Preferred quality"
entries = qualities entries = QUALITY_LIST
entryValues = qualities entryValues = QUALITY_LIST
setDefaultValue("Okru:720p") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -231,7 +238,22 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
} }
screen.addPreference(videoQualityPref) }.also(screen::addPreference)
} }
} }

View File

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.animeextension.es.tioanimeh.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().use { it.asJsoup() }
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.use { it.body.string() }
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard:$resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}