fix(en/aniwave): Fix vidsrcextractor (#2532)

This commit is contained in:
Secozzi 2023-11-24 19:10:25 +00:00 committed by GitHub
parent c34951783c
commit 3c71aa539f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 91 deletions

View File

@ -8,7 +8,7 @@ ext {
extName = 'Aniwave'
pkgNameSuffix = 'en.nineanime'
extClass = '.Aniwave'
extVersionCode = 58
extVersionCode = 59
libVersion = '13'
}

View File

@ -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} - ")

View File

@ -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,

View File

@ -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)

View File

@ -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)