fix(en/fmovies): Update vrf & vidsrc extractor (#2789)
This commit is contained in:
parent
0ed0ee53d9
commit
3be927b3c5
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'FMovies'
|
||||
extClass = '.FMovies'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -49,7 +49,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val vrfHelper by lazy { FMoviesHelper(client, headers) }
|
||||
private val utils by lazy { FmoviesUtils() }
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
@ -126,8 +126,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
||||
.selectFirst("div[data-id]")!!.attr("data-id")
|
||||
|
||||
val vrf = vrfHelper.getVrf(id)
|
||||
|
||||
val vrf = utils.vrfEncrypt(id)
|
||||
val vrfHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
@ -189,7 +188,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val data = json.decodeFromString<EpisodeInfo>(episode.url)
|
||||
val vrf = vrfHelper.getVrf(data.id)
|
||||
val vrf = utils.vrfEncrypt(data.id)
|
||||
|
||||
val vrfHeaders = headers.newBuilder()
|
||||
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
@ -217,7 +216,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
if (!hosterSelection.contains(name)) return@parallelCatchingFlatMap emptyList()
|
||||
|
||||
// Get decrypted url
|
||||
val vrf = vrfHelper.getVrf(server.attr("data-link-id"))
|
||||
val vrf = utils.vrfEncrypt(server.attr("data-link-id"))
|
||||
|
||||
val vrfHeaders = headers.newBuilder()
|
||||
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
@ -229,8 +228,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
GET("$baseUrl/ajax/server/${server.attr("data-link-id")}?vrf=$vrf", headers = vrfHeaders),
|
||||
).await().parseAs<AjaxServerResponse>().result.url
|
||||
|
||||
val decrypted = vrfHelper.decrypt(encrypted)
|
||||
|
||||
val decrypted = utils.vrfDecrypt(encrypted)
|
||||
when (name) {
|
||||
"Vidplay", "MyCloud" -> vidsrcExtractor.videosFromUrl(decrypted, name)
|
||||
"Filemoon" -> filemoonExtractor.videosFromUrl(decrypted, headers = headers)
|
||||
|
@ -2,12 +2,6 @@ package eu.kanade.tachiyomi.animeextension.en.fmovies
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VrfResponse(
|
||||
val url: String,
|
||||
val vrfQuery: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AjaxResponse(
|
||||
val result: String,
|
||||
@ -36,21 +30,25 @@ data class FMoviesSubs(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RawResponse(
|
||||
val rawURL: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VidsrcResponse(
|
||||
val result: ResultObject,
|
||||
data class MediaResponseBody(
|
||||
val status: Int,
|
||||
val result: Result,
|
||||
) {
|
||||
@Serializable
|
||||
data class ResultObject(
|
||||
val sources: List<SourceObject>,
|
||||
data class Result(
|
||||
val sources: ArrayList<Source>,
|
||||
val tracks: ArrayList<SubTrack> = ArrayList(),
|
||||
) {
|
||||
@Serializable
|
||||
data class SourceObject(
|
||||
data class Source(
|
||||
val file: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubTrack(
|
||||
val file: String,
|
||||
val label: String = "",
|
||||
val kind: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.fmovies
|
||||
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.animeextension.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
class FMoviesHelper(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val userAgent = Headers.headersOf(
|
||||
"User-Agent",
|
||||
"Aniyomi/${AppInfo.getVersionName()} (FMovies; ${BuildConfig.VERSION_CODE})",
|
||||
)
|
||||
|
||||
fun getVrf(id: String): String {
|
||||
val url = API_URL.newBuilder().apply {
|
||||
addPathSegment("fmovies-vrf")
|
||||
addQueryParameter("query", id)
|
||||
addQueryParameter("apikey", API_KEY)
|
||||
}.build().toString()
|
||||
|
||||
return client.newCall(GET(url, userAgent)).execute().parseAs<VrfResponse>().let {
|
||||
URLEncoder.encode(it.url, "utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
fun decrypt(encrypted: String): String {
|
||||
val url = API_URL.newBuilder().apply {
|
||||
addPathSegment("fmovies-decrypt")
|
||||
addQueryParameter("query", encrypted)
|
||||
addQueryParameter("apikey", API_KEY)
|
||||
}.build().toString()
|
||||
|
||||
return client.newCall(GET(url, userAgent)).execute().parseAs<VrfResponse>().url
|
||||
}
|
||||
|
||||
fun getVidSrc(query: String, host: String): String {
|
||||
val url = API_URL.newBuilder().apply {
|
||||
addPathSegment(if (host.contains("mcloud", true)) "rawMcloud" else "rawVizcloud")
|
||||
addQueryParameter("apikey", API_KEY)
|
||||
}.build().toString()
|
||||
|
||||
val futoken = client.newCall(
|
||||
GET("https://$host/futoken", headers),
|
||||
).execute().use { it.body.string() }
|
||||
|
||||
val body = FormBody.Builder().apply {
|
||||
add("query", query)
|
||||
add("futoken", futoken)
|
||||
}.build()
|
||||
|
||||
return client.newCall(
|
||||
POST(url, body = body, headers = userAgent),
|
||||
).execute().parseAs<RawResponse>().rawURL
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val API_KEY = "aniyomi"
|
||||
val API_URL = "https://9anime.eltik.net".toHttpUrl()
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.fmovies
|
||||
|
||||
import android.util.Base64
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class FmoviesUtils {
|
||||
|
||||
fun vrfEncrypt(input: String): String {
|
||||
val rc4Key = SecretKeySpec("FWsfu0KQd9vxYGNB".toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
|
||||
var vrf = cipher.doFinal(input.toByteArray())
|
||||
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
|
||||
vrf = rot13(vrf)
|
||||
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
|
||||
vrf = vrfShift(vrf)
|
||||
val stringVrf = vrf.toString(Charsets.UTF_8)
|
||||
return java.net.URLEncoder.encode(stringVrf, "utf-8")
|
||||
}
|
||||
|
||||
fun vrfDecrypt(input: String): String {
|
||||
var vrf = input.toByteArray()
|
||||
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
||||
|
||||
val rc4Key = SecretKeySpec("8z5Ag5wgagfsOuhz".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")
|
||||
}
|
||||
|
||||
private fun rot13(vrf: ByteArray): ByteArray {
|
||||
for (i in vrf.indices) {
|
||||
val byte = vrf[i]
|
||||
if (byte in 'A'.code..'Z'.code) {
|
||||
vrf[i] = ((byte - 'A'.code + 13) % 26 + 'A'.code).toByte()
|
||||
} else if (byte in 'a'.code..'z'.code) {
|
||||
vrf[i] = ((byte - 'a'.code + 13) % 26 + 'a'.code).toByte()
|
||||
}
|
||||
}
|
||||
return vrf
|
||||
}
|
||||
|
||||
private fun vrfShift(vrf: ByteArray): ByteArray {
|
||||
for (i in vrf.indices) {
|
||||
val shift = arrayOf(-4, -2, -6, 5, -2)[i % 5]
|
||||
vrf[i] = vrf[i].plus(shift).toByte()
|
||||
}
|
||||
return vrf
|
||||
}
|
||||
}
|
@ -1,81 +1,144 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.fmovies.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesHelper
|
||||
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesSubs
|
||||
import eu.kanade.tachiyomi.animeextension.en.fmovies.VidsrcResponse
|
||||
import android.util.Base64
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.animeextension.en.fmovies.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 eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
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 vrfHelper by lazy { FMoviesHelper(client, headers) }
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
fun videosFromUrl(url: String, name: String): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
val host = httpUrl.host
|
||||
val referer = "https://$host/"
|
||||
private val cacheControl = CacheControl.Builder().noStore().build()
|
||||
private val noCacheClient = client.newBuilder()
|
||||
.cache(null)
|
||||
.build()
|
||||
|
||||
val query = buildString {
|
||||
append(httpUrl.pathSegments.last())
|
||||
append("?")
|
||||
append(
|
||||
httpUrl.queryParameterNames.joinToString("&") {
|
||||
"$it=${httpUrl.queryParameter(it)}"
|
||||
},
|
||||
)
|
||||
}
|
||||
private val keys by lazy {
|
||||
noCacheClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
|
||||
).execute().parseAs<List<String>>()
|
||||
}
|
||||
|
||||
val rawUrl = vrfHelper.getVidSrc(query, host).addAutoStart()
|
||||
fun videosFromUrl(embedLink: String, hosterName: String): List<Video> {
|
||||
val host = embedLink.toHttpUrl().host
|
||||
val apiUrl = getApiUrl(embedLink, keys)
|
||||
|
||||
val refererHeaders = headers.newBuilder().apply {
|
||||
val apiHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", host)
|
||||
add("Referer", url.addAutoStart())
|
||||
add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
val infoJson = client.newCall(
|
||||
GET(rawUrl, headers = refererHeaders),
|
||||
).execute().parseAs<VidsrcResponse>()
|
||||
|
||||
val subtitleList = httpUrl.queryParameter("sub.info")?.let {
|
||||
val subtitlesHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", it.toHttpUrl().host)
|
||||
add("Origin", "https://$host")
|
||||
add("Referer", referer)
|
||||
}.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/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
|
||||
).execute().parseAs<List<String>>()
|
||||
val newApiUrL = getApiUrl(embedLink, newKeys)
|
||||
client.newCall(
|
||||
GET(it, headers = subtitlesHeaders),
|
||||
).execute().parseAs<List<FMoviesSubs>>().map {
|
||||
Track(it.file, it.label)
|
||||
GET(newApiUrL, apiHeaders),
|
||||
).execute().parseAs()
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
data.result.sources.first().file,
|
||||
referer = "https://$host/",
|
||||
videoNameGen = { q -> "$hosterName - $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) ?: "")
|
||||
}
|
||||
} ?: emptyList()
|
||||
}
|
||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||
val encodedID = encodeID(vidId, keyList)
|
||||
val apiSlug = callFromFuToken(host, encodedID)
|
||||
|
||||
return infoJson.result.sources.distinctBy { it.file }.flatMap {
|
||||
val url = it.file
|
||||
.toHttpUrl()
|
||||
.newBuilder()
|
||||
.fragment(null)
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
playlistUtils.extractFromHls(url, subtitleList = subtitleList, referer = referer, videoNameGen = { q -> "$name - $q" })
|
||||
return buildString {
|
||||
append("https://")
|
||||
append(host)
|
||||
append("/")
|
||||
append(apiSlug)
|
||||
if (params.isNotEmpty()) {
|
||||
append("?")
|
||||
append(
|
||||
params.joinToString("&") {
|
||||
"${it.first}=${it.second}"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.addAutoStart(): String {
|
||||
return this.toHttpUrl().newBuilder().setQueryParameter("autostart", "true").build().toString()
|
||||
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 fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
|
||||
return filter {
|
||||
it.kind == "captions"
|
||||
}.mapNotNull {
|
||||
runCatching {
|
||||
Track(
|
||||
it.file,
|
||||
it.label,
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user