Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(en/aniwave): fix vidsrcextractor #2532

Merged
merged 3 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/en/aniwave/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ext {
extName = 'Aniwave'
pkgNameSuffix = 'en.nineanime'
extClass = '.Aniwave'
extVersionCode = 58
extVersionCode = 59
libVersion = '13'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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=")

Expand Down Expand Up @@ -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")
Expand All @@ -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} - ")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down