* fix(pt/vizer): Fixed pt/Vizer videos empty (fix #256) * feat: Added new lib extractor: fireplayer-extractor
This commit is contained in:
parent
53856f9275
commit
8f7d9267d4
8 changed files with 177 additions and 72 deletions
10
lib/fireplayer-extractor/build.gradle.kts
Normal file
10
lib/fireplayer-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1") {
|
||||||
|
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
|
||||||
|
}
|
||||||
|
implementation(project(":lib:playlist-utils"))
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.fireplayerextractor
|
||||||
|
|
||||||
|
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 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 FireplayerExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val defaultHost: String? = null,
|
||||||
|
) {
|
||||||
|
fun videosFromUrl(
|
||||||
|
url: String,
|
||||||
|
videoNameGen: (String) -> String = { quality -> quality },
|
||||||
|
videoHost: String? = null,
|
||||||
|
): List<Video> {
|
||||||
|
val host = videoHost ?: defaultHost ?: "https://${url.toHttpUrl().host}"
|
||||||
|
|
||||||
|
val headers = Headers.Builder()
|
||||||
|
.set("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.set("Referer", host)
|
||||||
|
.set("Origin", "https://${host.toHttpUrl().host}")
|
||||||
|
.set("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
var id = url.substringAfterLast("/")
|
||||||
|
|
||||||
|
if (id.length < 32) {
|
||||||
|
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||||
|
|
||||||
|
val script =
|
||||||
|
doc.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")?.data()
|
||||||
|
?.let(JsUnpacker::unpackAndCombine)
|
||||||
|
?: doc.selectFirst("script:containsData(FirePlayer)")?.data()
|
||||||
|
|
||||||
|
if (script?.contains("FirePlayer(") == true) {
|
||||||
|
id = script.substringAfter("FirePlayer(\"").substringBefore('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val postUrl = "$host/player/index.php?data=$id&do=getVideo"
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("hash", id)
|
||||||
|
.add("r", "")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val masterUrl = client.newCall(POST(postUrl, headers, body = body)).execute()
|
||||||
|
.body.string()
|
||||||
|
.substringAfter("securedLink\":\"")
|
||||||
|
.substringBefore('"')
|
||||||
|
.replace("\\", "")
|
||||||
|
|
||||||
|
val playlistUtils = PlaylistUtils(client, headers)
|
||||||
|
|
||||||
|
return playlistUtils.extractFromHls(masterUrl, videoNameGen = videoNameGen)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,12 @@ class MixDropExtractor(private val client: OkHttpClient) {
|
||||||
externalSubs: List<Track> = emptyList(),
|
externalSubs: List<Track> = emptyList(),
|
||||||
referer: String = DEFAULT_REFERER,
|
referer: String = DEFAULT_REFERER,
|
||||||
): List<Video> {
|
): List<Video> {
|
||||||
val headers = Headers.headersOf("Referer", referer)
|
val headers = Headers.headersOf(
|
||||||
|
"Referer",
|
||||||
|
referer,
|
||||||
|
"User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||||
|
)
|
||||||
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||||
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
|
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
|
||||||
?.data()
|
?.data()
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Vizer.tv'
|
extName = 'Vizer.tv'
|
||||||
extClass = '.Vizer'
|
extClass = '.Vizer'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(':lib:fireplayer-extractor'))
|
||||||
implementation(project(':lib:mixdrop-extractor'))
|
implementation(project(':lib:mixdrop-extractor'))
|
||||||
|
implementation(project(':lib:playlist-utils'))
|
||||||
implementation(project(':lib:streamtape-extractor'))
|
implementation(project(':lib:streamtape-extractor'))
|
||||||
|
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchItemDto
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchResultDto
|
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchResultDto
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoDto
|
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoDto
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoListDto
|
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoListDto
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.extractors.WarezExtractor
|
import eu.kanade.tachiyomi.animeextension.pt.vizer.interceptor.WebViewResolver
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
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.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.fireplayerextractor.FireplayerExtractor
|
||||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
@ -41,7 +42,7 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "Vizer.tv"
|
override val name = "Vizer.tv"
|
||||||
|
|
||||||
override val baseUrl = "https://vizertv.in"
|
override val baseUrl = "https://novizer.com"
|
||||||
private val apiUrl = "$baseUrl/includes/ajax"
|
private val apiUrl = "$baseUrl/includes/ajax"
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
override val lang = "pt-BR"
|
||||||
|
@ -58,6 +59,8 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val webViewResolver by lazy { WebViewResolver(headers) }
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
val pageType = preferences.getString(PREF_POPULAR_PAGE_KEY, PREF_POPULAR_PAGE_DEFAULT)!!
|
val pageType = preferences.getString(PREF_POPULAR_PAGE_KEY, PREF_POPULAR_PAGE_DEFAULT)!!
|
||||||
|
@ -176,7 +179,7 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val response = episodesClient.newCall(apiRequest("getEpisodes=$id")).execute()
|
val response = episodesClient.newCall(apiRequest("getEpisodes=$id")).execute()
|
||||||
val episodes = response.parseAs<EpisodeListDto>().episodes
|
val episodes = response.parseAs<EpisodeListDto>().episodes
|
||||||
.values
|
.values
|
||||||
.filter { it.released }
|
.filter { it.released === true }
|
||||||
.map {
|
.map {
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
name = "$sname: Ep ${it.name}".run {
|
name = "$sname: Ep ${it.name}".run {
|
||||||
|
@ -243,7 +246,7 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
||||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||||
private val warezExtractor by lazy { WarezExtractor(client, headers) }
|
private val fireplayerExtractor by lazy { FireplayerExtractor(client) }
|
||||||
|
|
||||||
private fun getVideosFromObject(videoObj: VideoDto): List<Video> {
|
private fun getVideosFromObject(videoObj: VideoDto): List<Video> {
|
||||||
val hosters = videoObj.hosters ?: return emptyList()
|
val hosters = videoObj.hosters ?: return emptyList()
|
||||||
|
@ -251,12 +254,16 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val langPrefix = if (videoObj.lang == "1") "LEG" else "DUB"
|
val langPrefix = if (videoObj.lang == "1") "LEG" else "DUB"
|
||||||
|
|
||||||
return hosters.iterator().flatMap { (name, status) ->
|
return hosters.iterator().flatMap { (name, status) ->
|
||||||
if (status != 3) return@flatMap emptyList()
|
// Always try the warezcdn
|
||||||
|
if (status != 3 && name != "warezcdn") return@flatMap emptyList()
|
||||||
val url = getPlayerUrl(videoObj.id, name)
|
val url = getPlayerUrl(videoObj.id, name)
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
when (name) {
|
when (name) {
|
||||||
"mixdrop" -> mixdropExtractor.videosFromUrl(url, langPrefix)
|
"mixdrop" -> mixdropExtractor.videosFromUrl(url, langPrefix)
|
||||||
"streamtape" -> streamtapeExtractor.videosFromUrl(url, "StreamTape($langPrefix)")
|
"streamtape" -> streamtapeExtractor.videosFromUrl(url, "StreamTape($langPrefix)")
|
||||||
"warezcdn" -> warezExtractor.videosFromUrl(url, langPrefix)
|
"warezcdn" -> fireplayerExtractor.videosFromUrl(url, videoNameGen = { "WarezCDN($langPrefix) - $it" })
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,17 +302,8 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
private val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
private val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||||
|
|
||||||
private fun getPlayerUrl(id: String, name: String): String {
|
private fun getPlayerUrl(id: String, name: String): String? {
|
||||||
val req = GET("$baseUrl/embed/getPlay.php?id=$id&sv=$name", headers)
|
return webViewResolver.getUrl("$baseUrl/embed/getEmbed.php?id=$id&sv=$name", "$baseUrl/termos")
|
||||||
return if (name == "warezcdn") {
|
|
||||||
val res = noRedirectClient.newCall(req).execute()
|
|
||||||
res.close()
|
|
||||||
res.headers["location"]!!
|
|
||||||
} else {
|
|
||||||
val res = client.newCall(req).execute()
|
|
||||||
val body = res.body.string()
|
|
||||||
body.substringAfter("location.href=\"", "").substringBefore("\";", "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun apiRequest(body: String): Request {
|
private fun apiRequest(body: String): Request {
|
||||||
|
|
|
@ -83,7 +83,7 @@ class HostersDto(
|
||||||
object BooleanSerializer : JsonTransformingSerializer<Boolean>(Boolean.serializer()) {
|
object BooleanSerializer : JsonTransformingSerializer<Boolean>(Boolean.serializer()) {
|
||||||
override fun transformDeserialize(element: JsonElement): JsonElement {
|
override fun transformDeserialize(element: JsonElement): JsonElement {
|
||||||
require(element is JsonPrimitive)
|
require(element is JsonPrimitive)
|
||||||
return if (element.jsonPrimitive.isString) {
|
return if (element.jsonPrimitive.isString && element.jsonPrimitive.content == "true") {
|
||||||
JsonPrimitive(true)
|
JsonPrimitive(true)
|
||||||
} else {
|
} else {
|
||||||
JsonPrimitive(element.jsonPrimitive.booleanOrNull ?: false)
|
JsonPrimitive(element.jsonPrimitive.booleanOrNull ?: false)
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.vizer.extractors
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
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 WarezExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String, lang: String): List<Video> {
|
|
||||||
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
|
||||||
val httpUrl = doc.location().toHttpUrl()
|
|
||||||
val videoId = httpUrl.queryParameter("id") ?: return emptyList()
|
|
||||||
val script = doc.selectFirst("script:containsData(allowanceKey)")?.data()
|
|
||||||
?: return emptyList()
|
|
||||||
val key = script.substringAfter("allowanceKey").substringAfter('"').substringBefore('"')
|
|
||||||
val cdn = script.substringAfter("cdnListing").substringAfter('[').substringBefore(']')
|
|
||||||
.split(',')
|
|
||||||
.random()
|
|
||||||
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("getVideo", videoId)
|
|
||||||
.add("key", key)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val host = "https://" + httpUrl.host
|
|
||||||
val reqHeaders = headers.newBuilder()
|
|
||||||
.set("Origin", host)
|
|
||||||
.set("Referer", url)
|
|
||||||
.set("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val req = client.newCall(POST("$host/player/functions.php", reqHeaders, body)).execute()
|
|
||||||
val id = req.body.string().substringAfter("id\":\"", "").substringBefore('"', "")
|
|
||||||
.ifBlank { return emptyList() }
|
|
||||||
val decrypted = decryptorium(id)
|
|
||||||
val videoUrl = "https://workerproxy.warezcdn.workers.dev/?url=https://cloclo$cdn.cloud.mail.ru/weblink/view/$decrypted"
|
|
||||||
return listOf(Video(videoUrl, "WarezCDN - $lang", videoUrl, headers))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decryptorium(enc: String): String {
|
|
||||||
val b64dec = String(Base64.decode(enc, Base64.DEFAULT)).trim()
|
|
||||||
val start = b64dec.reversed().dropLast(5)
|
|
||||||
val end = b64dec.substring(0, 5)
|
|
||||||
return start + end
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.pt.vizer.interceptor
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import okhttp3.Headers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class WebViewResolver(private val globalHeaders: Headers) {
|
||||||
|
private val context: Application by injectLazy()
|
||||||
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
|
private val tag by lazy { javaClass.simpleName }
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
fun getUrl(origRequestUrl: String, baseUrl: String): String? {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
var webView: WebView? = null
|
||||||
|
var result: String? = null
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
val webview = WebView(context)
|
||||||
|
webView = webview
|
||||||
|
with(webview.settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
useWideViewPort = false
|
||||||
|
loadWithOverviewMode = false
|
||||||
|
userAgentString = globalHeaders["User-Agent"]
|
||||||
|
}
|
||||||
|
webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest,
|
||||||
|
): WebResourceResponse? {
|
||||||
|
val url = request.url.toString()
|
||||||
|
Log.d(tag, "Checking url $url")
|
||||||
|
if (VIDEO_REGEX.containsMatchIn(url)) {
|
||||||
|
result = url
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
Log.d(tag, "onPageFinished $url")
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
|
||||||
|
view?.evaluateJavascript("document.body.innerHTML += '<iframe src=\"" + origRequestUrl + "\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" webkitallowfullscreen=\"\" mozallowfullscreen=\"\"></iframe>'") {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webView?.loadUrl(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
webView?.stopLoading()
|
||||||
|
webView?.destroy()
|
||||||
|
webView = null
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TIMEOUT_SEC: Long = 25
|
||||||
|
private val VIDEO_REGEX by lazy { Regex("//(mixdrop|streamtape|warezcdn)|/video/") }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue