Fix Aniwave Vidstream & MegaF sources (#108)

* VidSrc-Extractor: Update extraction logic

* Aniwave: Fix server name

* Aniwave: Bump version code
This commit is contained in:
Agung Watanabe 2024-08-05 21:12:06 +07:00 committed by GitHub
parent 802f56295e
commit 93ce6ec717
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 88 deletions

View file

@ -1,18 +1,19 @@
package eu.kanade.tachiyomi.lib.vidsrcextractor package eu.kanade.tachiyomi.lib.vidsrcextractor
import android.util.Base64 import android.util.Base64
import app.cash.quickjs.QuickJs
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.lib.vidsrcextractor.MediaResponseBody.Result
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import okhttp3.CacheControl import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
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
@ -21,54 +22,32 @@ 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 playlistUtils by lazy { PlaylistUtils(client, headers) } private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val json: Json by injectLazy()
private val cacheControl = CacheControl.Builder().noStore().build() fun videosFromUrl(
private val noCacheClient = client.newBuilder() embedLink: String,
.cache(null) hosterName: String,
.build() type: String = "",
subtitleList: List<Track> = emptyList(),
private val keys by lazy { ): List<Video> {
noCacheClient.newCall(
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
).execute().parseAs<List<String>>()
}
fun videosFromUrl(embedLink: String, hosterName: String, type: String = "", subtitleList: List<Track> = emptyList()): List<Video> {
val host = embedLink.toHttpUrl().host val host = embedLink.toHttpUrl().host
val apiUrl = getApiUrl(embedLink, keys) val apiUrl = getApiUrl(embedLink)
val apiHeaders = headers.newBuilder().apply { val response = client.newCall(GET(apiUrl)).execute()
add("Accept", "application/json, text/javascript, */*; q=0.01") val data = response.parseAs<MediaResponseBody>()
add("Host", host)
add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
val response = client.newCall( val decrypted = vrfDecrypt(data.result)
GET(apiUrl, apiHeaders), val result = json.decodeFromString<Result>(decrypted)
).execute()
val data = runCatching {
response.parseAs<MediaResponseBody>()
}.getOrElse { // Keys are out of date
val newKeys = noCacheClient.newCall(
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
).execute().parseAs<List<String>>()
val newApiUrL = getApiUrl(embedLink, newKeys)
client.newCall(
GET(newApiUrL, apiHeaders),
).execute().parseAs()
}
return playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
data.result.sources.first().file, playlistUrl = result.sources.first().file,
referer = "https://$host/", referer = "https://$host/",
videoNameGen = { q -> hosterName + (if (type.isBlank()) "" else " - $type") + " - $q" }, videoNameGen = { q -> hosterName + (if (type.isBlank()) "" else " - $type") + " - $q" },
subtitleList = subtitleList + data.result.tracks.toTracks(), subtitleList = subtitleList + result.tracks.toTracks(),
) )
} }
private fun getApiUrl(embedLink: String, keyList: List<String>): String { private fun getApiUrl(embedLink: String): String {
val host = embedLink.toHttpUrl().host val host = embedLink.toHttpUrl().host
val params = embedLink.toHttpUrl().let { url -> val params = embedLink.toHttpUrl().let { url ->
url.queryParameterNames.map { url.queryParameterNames.map {
@ -76,13 +55,13 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
} }
} }
val vidId = embedLink.substringAfterLast("/").substringBefore("?") val vidId = embedLink.substringAfterLast("/").substringBefore("?")
val encodedID = encodeID(vidId, keyList) val apiSlug = encodeID(vidId, ENCRYPTION_KEY1)
val apiSlug = callFromFuToken(host, encodedID, embedLink) val h = encodeID(vidId, ENCRYPTION_KEY2)
return buildString { return buildString {
append("https://") append("https://")
append(host) append(host)
append("/") append("/mediainfo/")
append(apiSlug) append(apiSlug)
if (params.isNotEmpty()) { if (params.isNotEmpty()) {
append("?") append("?")
@ -91,51 +70,23 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
"${it.first}=${it.second}" "${it.first}=${it.second}"
}, },
) )
append("&h=$h")
} }
} }
} }
private fun encodeID(videoID: String, keyList: List<String>): String { private fun encodeID(videoID: String, key: String): String {
val rc4Key1 = SecretKeySpec(keyList[0].toByteArray(), "RC4") val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val rc4Key2 = SecretKeySpec(keyList[1].toByteArray(), "RC4") val cipher = Cipher.getInstance("RC4")
val cipher1 = Cipher.getInstance("RC4") cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
val cipher2 = Cipher.getInstance("RC4") return Base64.encode(cipher.doFinal(videoID.toByteArray()), Base64.DEFAULT)
cipher1.init(Cipher.DECRYPT_MODE, rc4Key1, cipher1.parameters) .toString(Charsets.UTF_8)
cipher2.init(Cipher.DECRYPT_MODE, rc4Key2, cipher2.parameters) .replace("+", "-")
var encoded = videoID.toByteArray() .replace("/", "_")
.trim()
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, embedLink: String): String { private fun List<Result.SubTrack>.toTracks(): List<Track> {
val refererHeaders = headers.newBuilder().apply {
add("Referer", embedLink)
}.build()
val fuTokenScript = client.newCall(
GET("https://$host/futoken", headers = refererHeaders),
).execute().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 fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
return filter { return filter {
it.kind == "captions" it.kind == "captions"
}.mapNotNull { }.mapNotNull {
@ -147,17 +98,32 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
}.getOrNull() }.getOrNull()
} }
} }
private fun vrfDecrypt(input: String): String {
var vrf = Base64.decode(input.toByteArray(), Base64.URL_SAFE)
val rc4Key = SecretKeySpec(DECRYPTION_KEY.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
vrf = cipher.doFinal(vrf)
return URLDecoder.decode(vrf.toString(Charsets.UTF_8), "utf-8")
}
companion object {
private const val ENCRYPTION_KEY1 = "8Qy3mlM2kod80XIK"
private const val ENCRYPTION_KEY2 = "BgKVSrzpH2Enosgm"
private const val DECRYPTION_KEY = "9jXDYBZUcTcTZveM"
}
} }
@Serializable @Serializable
data class MediaResponseBody( data class MediaResponseBody(
val status: Int, val status: Int,
val result: Result, val result: String,
) { ) {
@Serializable @Serializable
data class Result( data class Result(
val sources: ArrayList<Source>, val sources: List<Source>,
val tracks: ArrayList<SubTrack> = ArrayList(), val tracks: List<SubTrack> = emptyList(),
) { ) {
@Serializable @Serializable
data class Source( data class Source(

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Aniwave' extName = 'Aniwave'
extClass = '.Aniwave' extClass = '.Aniwave'
extVersionCode = 73 extVersionCode = 74
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -282,9 +282,8 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val parsed = response.parseAs<ServerResponse>() val parsed = response.parseAs<ServerResponse>()
val embedLink = utils.vrfDecrypt(DECRYPTION_KEY, parsed.result.url) val embedLink = utils.vrfDecrypt(DECRYPTION_KEY, parsed.result.url)
when (server.serverName) { when (server.serverName) {
"vidstream", "megaf" -> { "vidstream" -> vidsrcExtractor.videosFromUrl(embedLink, "Vidstream", server.type)
vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type) "megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
}
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ") "moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList() "streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
"mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}") "mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")