fix(en/aniwave): Fix vidsrcextractor (#2532)
This commit is contained in:
parent
c34951783c
commit
3c71aa539f
@ -8,7 +8,7 @@ ext {
|
|||||||
extName = 'Aniwave'
|
extName = 'Aniwave'
|
||||||
pkgNameSuffix = 'en.nineanime'
|
pkgNameSuffix = 'en.nineanime'
|
||||||
extClass = '.Aniwave'
|
extClass = '.Aniwave'
|
||||||
extVersionCode = 58
|
extVersionCode = 59
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
private val json: Json by injectLazy()
|
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 {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
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 {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val filters = AniwaveFilters.getSearchParameters(filters)
|
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"
|
var url = "$baseUrl/filter?keyword=$query"
|
||||||
|
|
||||||
if (filters.genre.isNotBlank()) url += filters.genre
|
if (filters.genre.isNotBlank()) url += filters.genre
|
||||||
@ -147,7 +147,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
||||||
.selectFirst("div[data-id]")!!.attr("data-id")
|
.selectFirst("div[data-id]")!!.attr("data-id")
|
||||||
val vrf = utils.callEnimax(id, "vrf")
|
val vrf = utils.vrfEncrypt(id)
|
||||||
|
|
||||||
val listHeaders = headers.newBuilder().apply {
|
val listHeaders = headers.newBuilder().apply {
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
@ -203,7 +203,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val ids = episode.url.substringBefore("&")
|
val ids = episode.url.substringBefore("&")
|
||||||
val vrf = utils.callEnimax(ids, "vrf")
|
val vrf = utils.vrfEncrypt(ids)
|
||||||
val url = "/ajax/server/list/$ids?$vrf"
|
val url = "/ajax/server/list/$ids?$vrf"
|
||||||
val epurl = episode.url.substringAfter("epurl=")
|
val epurl = episode.url.substringAfter("epurl=")
|
||||||
|
|
||||||
@ -257,7 +257,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||||
|
|
||||||
private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
|
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 {
|
val listHeaders = headers.newBuilder().apply {
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
@ -272,7 +272,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val parsed = response.parseAs<ServerResponse>()
|
val parsed = response.parseAs<ServerResponse>()
|
||||||
val embedLink = utils.callEnimax(parsed.result.url, "decrypt")
|
val embedLink = utils.vrfDecrypt(parsed.result.url)
|
||||||
when (server.serverName) {
|
when (server.serverName) {
|
||||||
"vidplay", "mycloud" -> vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type)
|
"vidplay", "mycloud" -> vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type)
|
||||||
"filemoon" -> filemoonExtractor.videosFromUrl(embedLink, "Filemoon - ${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
|
@Serializable
|
||||||
data class MediaResponseBody(
|
data class MediaResponseBody(
|
||||||
val status: Int,
|
val status: Int,
|
||||||
|
@ -1,59 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||||
|
|
||||||
import android.util.Base64
|
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 java.net.URLDecoder
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
class AniwaveUtils(private val client: OkHttpClient, private val headers: Headers) {
|
class AniwaveUtils {
|
||||||
|
|
||||||
val json: Json by injectLazy()
|
fun vrfEncrypt(input: String): String {
|
||||||
|
|
||||||
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 {
|
|
||||||
val rc4Key = SecretKeySpec("ysJhV6U27FVIjjuk".toByteArray(), "RC4")
|
val rc4Key = SecretKeySpec("ysJhV6U27FVIjjuk".toByteArray(), "RC4")
|
||||||
val cipher = Cipher.getInstance("RC4")
|
val cipher = Cipher.getInstance("RC4")
|
||||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
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 = vrfShift(vrf)
|
||||||
vrf = Base64.encode(vrf, Base64.DEFAULT)
|
vrf = Base64.encode(vrf, Base64.DEFAULT)
|
||||||
vrf = rot13(vrf)
|
vrf = rot13(vrf)
|
||||||
|
val stringVrf = vrf.toString(Charsets.UTF_8)
|
||||||
return 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()
|
var vrf = input.toByteArray()
|
||||||
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
||||||
|
|
||||||
|
@ -1,52 +1,142 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.nineanime.extractors
|
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.animeextension.en.nineanime.MediaResponseBody
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
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) {
|
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val utils by lazy { AniwaveUtils(client, headers) }
|
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(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> {
|
fun videosFromUrl(embedLink: String, name: String, type: String): List<Video> {
|
||||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
val hosterName = when (name) {
|
||||||
val (serverName, action) = when (name) {
|
"vidplay" -> "VidPlay"
|
||||||
"vidplay" -> Pair("VidPlay", "rawVizcloud")
|
else -> "MyCloud"
|
||||||
"mycloud" -> Pair("MyCloud", "rawMcloud")
|
}
|
||||||
else -> return emptyList()
|
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(
|
return playlistUtils.extractFromHls(
|
||||||
playlistUrl,
|
data.result.sources.first().file,
|
||||||
referer = "https://${embedLink.toHttpUrl().host}/",
|
referer = "https://$host/",
|
||||||
videoNameGen = { q -> "$serverName - $type - $q" },
|
videoNameGen = { q -> "$hosterName - $type - $q" },
|
||||||
subtitleList = rawResponse.result.tracks.toTracks(),
|
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 {
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
val responseBody = use { it.body.string() }
|
val responseBody = use { it.body.string() }
|
||||||
return json.decodeFromString(responseBody)
|
return json.decodeFromString(responseBody)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user