fix(en/aniwave): Fix vidsrcextractor (#2532)
This commit is contained in:
parent
c34951783c
commit
3c71aa539f
@ -8,7 +8,7 @@ ext {
|
||||
extName = 'Aniwave'
|
||||
pkgNameSuffix = 'en.nineanime'
|
||||
extClass = '.Aniwave'
|
||||
extVersionCode = 58
|
||||
extVersionCode = 59
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val utils by lazy { AniwaveUtils(client, headers) }
|
||||
private val utils by lazy { AniwaveUtils() }
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
@ -97,7 +97,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filters = AniwaveFilters.getSearchParameters(filters)
|
||||
|
||||
val vrf = if (query.isNotBlank()) utils.callEnimax(query, "vrf") else ""
|
||||
val vrf = if (query.isNotBlank()) utils.vrfEncrypt(query) else ""
|
||||
var url = "$baseUrl/filter?keyword=$query"
|
||||
|
||||
if (filters.genre.isNotBlank()) url += filters.genre
|
||||
@ -147,7 +147,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
||||
.selectFirst("div[data-id]")!!.attr("data-id")
|
||||
val vrf = utils.callEnimax(id, "vrf")
|
||||
val vrf = utils.vrfEncrypt(id)
|
||||
|
||||
val listHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
@ -203,7 +203,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val ids = episode.url.substringBefore("&")
|
||||
val vrf = utils.callEnimax(ids, "vrf")
|
||||
val vrf = utils.vrfEncrypt(ids)
|
||||
val url = "/ajax/server/list/$ids?$vrf"
|
||||
val epurl = episode.url.substringAfter("epurl=")
|
||||
|
||||
@ -257,7 +257,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||
|
||||
private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
|
||||
val vrf = utils.callEnimax(server.serverId, "rawVrf")
|
||||
val vrf = utils.vrfEncrypt(server.serverId)
|
||||
|
||||
val listHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
@ -272,7 +272,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
return runCatching {
|
||||
val parsed = response.parseAs<ServerResponse>()
|
||||
val embedLink = utils.callEnimax(parsed.result.url, "decrypt")
|
||||
val embedLink = utils.vrfDecrypt(parsed.result.url)
|
||||
when (server.serverName) {
|
||||
"vidplay", "mycloud" -> vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type)
|
||||
"filemoon" -> filemoonExtractor.videosFromUrl(embedLink, "Filemoon - ${server.type} - ")
|
||||
|
@ -15,17 +15,6 @@ data class ServerResponse(
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VrfResponse(
|
||||
val url: String,
|
||||
val vrfQuery: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RawResponse(
|
||||
val rawURL: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaResponseBody(
|
||||
val status: Int,
|
||||
|
@ -1,59 +1,13 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.animeextension.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class AniwaveUtils(private val client: OkHttpClient, private val headers: Headers) {
|
||||
class AniwaveUtils {
|
||||
|
||||
val json: Json by injectLazy()
|
||||
|
||||
private val userAgent = Headers.headersOf(
|
||||
"User-Agent",
|
||||
"Aniyomi/${AppInfo.getVersionName()} (AniWave; ${BuildConfig.VERSION_CODE})",
|
||||
)
|
||||
|
||||
fun callEnimax(query: String, action: String): String {
|
||||
return if (action in listOf("rawVizcloud", "rawMcloud")) {
|
||||
val referer = if (action == "rawVizcloud") "https://vidstream.pro/" else "https://mcloud.to/"
|
||||
val futoken = client.newCall(
|
||||
GET(referer + "futoken", headers),
|
||||
).execute().use { it.body.string() }
|
||||
val formBody = FormBody.Builder()
|
||||
.add("query", query)
|
||||
.add("futoken", futoken)
|
||||
.build()
|
||||
client.newCall(
|
||||
POST(
|
||||
url = "https://9anime.eltik.net/$action?apikey=aniyomi",
|
||||
body = formBody,
|
||||
headers = userAgent,
|
||||
),
|
||||
).execute().parseAs<RawResponse>().rawURL
|
||||
} else if (action == "decrypt") {
|
||||
vrfDecrypt(query)
|
||||
} else {
|
||||
"vrf=${java.net.URLEncoder.encode(vrfEncrypt(query), "utf-8")}"
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
val responseBody = use { it.body.string() }
|
||||
return json.decodeFromString(responseBody)
|
||||
}
|
||||
|
||||
private fun vrfEncrypt(input: String): String {
|
||||
fun vrfEncrypt(input: String): String {
|
||||
val rc4Key = SecretKeySpec("ysJhV6U27FVIjjuk".toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
@ -64,11 +18,11 @@ class AniwaveUtils(private val client: OkHttpClient, private val headers: Header
|
||||
vrf = vrfShift(vrf)
|
||||
vrf = Base64.encode(vrf, Base64.DEFAULT)
|
||||
vrf = rot13(vrf)
|
||||
|
||||
return vrf.toString(Charsets.UTF_8)
|
||||
val stringVrf = vrf.toString(Charsets.UTF_8)
|
||||
return "vrf=${java.net.URLEncoder.encode(stringVrf, "utf-8")}"
|
||||
}
|
||||
|
||||
private fun vrfDecrypt(input: String): String {
|
||||
fun vrfDecrypt(input: String): String {
|
||||
var vrf = input.toByteArray()
|
||||
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
||||
|
||||
|
@ -1,52 +1,142 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.nineanime.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.en.nineanime.AniwaveUtils
|
||||
import android.util.Base64
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.animeextension.en.nineanime.MediaResponseBody
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val utils by lazy { AniwaveUtils(client, headers) }
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
private val cacheControl = CacheControl.Builder().noStore().build()
|
||||
private val noCacheClient = client.newBuilder()
|
||||
.cache(null)
|
||||
.build()
|
||||
|
||||
private val keys by lazy {
|
||||
noCacheClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/Claudemirovsky/worstsource-keys/keys/keys.json", cache = cacheControl),
|
||||
).execute().parseAs<List<String>>()
|
||||
}
|
||||
|
||||
fun videosFromUrl(embedLink: String, name: String, type: String): List<Video> {
|
||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||
val (serverName, action) = when (name) {
|
||||
"vidplay" -> Pair("VidPlay", "rawVizcloud")
|
||||
"mycloud" -> Pair("MyCloud", "rawMcloud")
|
||||
else -> return emptyList()
|
||||
val hosterName = when (name) {
|
||||
"vidplay" -> "VidPlay"
|
||||
else -> "MyCloud"
|
||||
}
|
||||
val host = embedLink.toHttpUrl().host
|
||||
val apiUrl = getApiUrl(embedLink, keys)
|
||||
|
||||
val apiHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", host)
|
||||
add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
val response = client.newCall(
|
||||
GET(apiUrl, apiHeaders),
|
||||
).execute()
|
||||
|
||||
val data = runCatching {
|
||||
response.parseAs<MediaResponseBody>()
|
||||
}.getOrElse { // Keys are out of date
|
||||
val newKeys = noCacheClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/Claudemirovsky/worstsource-keys/keys/keys.json", cache = cacheControl),
|
||||
).execute().parseAs<List<String>>()
|
||||
val newApiUrL = getApiUrl(embedLink, newKeys)
|
||||
client.newCall(
|
||||
GET(newApiUrL, apiHeaders),
|
||||
).execute().parseAs()
|
||||
}
|
||||
val rawURL = utils.callEnimax(vidId, action) + "?${embedLink.substringAfter("?")}"
|
||||
val rawReferer = Headers.headersOf(
|
||||
"referer",
|
||||
"$embedLink&autostart=true",
|
||||
"x-requested-with",
|
||||
"XMLHttpRequest",
|
||||
)
|
||||
val rawResponse = client.newCall(GET(rawURL, rawReferer)).execute().parseAs<MediaResponseBody>()
|
||||
val playlistUrl = rawResponse.result.sources.first().file
|
||||
.replace("#.mp4", "")
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
playlistUrl,
|
||||
referer = "https://${embedLink.toHttpUrl().host}/",
|
||||
videoNameGen = { q -> "$serverName - $type - $q" },
|
||||
subtitleList = rawResponse.result.tracks.toTracks(),
|
||||
data.result.sources.first().file,
|
||||
referer = "https://$host/",
|
||||
videoNameGen = { q -> "$hosterName - $type - $q" },
|
||||
subtitleList = data.result.tracks.toTracks(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getApiUrl(embedLink: String, keyList: List<String>): String {
|
||||
val host = embedLink.toHttpUrl().host
|
||||
val params = embedLink.toHttpUrl().let { url ->
|
||||
url.queryParameterNames.map {
|
||||
Pair(it, url.queryParameter(it) ?: "")
|
||||
}
|
||||
}
|
||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||
val encodedID = encodeID(vidId, keyList)
|
||||
val apiSlug = callFromFuToken(host, encodedID)
|
||||
|
||||
return buildString {
|
||||
append("https://")
|
||||
append(host)
|
||||
append("/")
|
||||
append(apiSlug)
|
||||
if (params.isNotEmpty()) {
|
||||
append("?")
|
||||
append(
|
||||
params.joinToString("&") {
|
||||
"${it.first}=${it.second}"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeID(videoID: String, keyList: List<String>): String {
|
||||
val rc4Key1 = SecretKeySpec(keyList[0].toByteArray(), "RC4")
|
||||
val rc4Key2 = SecretKeySpec(keyList[1].toByteArray(), "RC4")
|
||||
val cipher1 = Cipher.getInstance("RC4")
|
||||
val cipher2 = Cipher.getInstance("RC4")
|
||||
cipher1.init(Cipher.DECRYPT_MODE, rc4Key1, cipher1.parameters)
|
||||
cipher2.init(Cipher.DECRYPT_MODE, rc4Key2, cipher2.parameters)
|
||||
var encoded = videoID.toByteArray()
|
||||
|
||||
encoded = cipher1.doFinal(encoded)
|
||||
encoded = cipher2.doFinal(encoded)
|
||||
encoded = Base64.encode(encoded, Base64.DEFAULT)
|
||||
return encoded.toString(Charsets.UTF_8).replace("/", "_").trim()
|
||||
}
|
||||
|
||||
private fun callFromFuToken(host: String, data: String): String {
|
||||
val fuTokenScript = client.newCall(
|
||||
GET("https://$host/futoken"),
|
||||
).execute().use { it.body.string() }
|
||||
|
||||
val js = buildString {
|
||||
append("(function")
|
||||
append(
|
||||
fuTokenScript.substringAfter("window")
|
||||
.substringAfter("function")
|
||||
.replace("jQuery.ajax(", "")
|
||||
.substringBefore("+location.search"),
|
||||
)
|
||||
append("}(\"$data\"))")
|
||||
}
|
||||
|
||||
return QuickJs.create().use {
|
||||
it.evaluate(js)?.toString()!!
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
val responseBody = use { it.body.string() }
|
||||
return json.decodeFromString(responseBody)
|
||||
|
Loading…
x
Reference in New Issue
Block a user