fix(en/fmovies): Update vrf & vidsrc extractor (#2789)

This commit is contained in:
Samfun75 2024-01-19 23:27:34 +03:00 committed by GitHub
parent 0ed0ee53d9
commit 3be927b3c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 187 additions and 144 deletions

View File

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

View File

@ -49,7 +49,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val vrfHelper by lazy { FMoviesHelper(client, headers) } private val utils by lazy { FmoviesUtils() }
// ============================== Popular =============================== // ============================== Popular ===============================
@ -126,8 +126,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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 = vrfHelper.getVrf(id) val vrf = utils.vrfEncrypt(id)
val vrfHeaders = headers.newBuilder().apply { val vrfHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01") add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", baseUrl.toHttpUrl().host) add("Host", baseUrl.toHttpUrl().host)
@ -189,7 +188,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListRequest(episode: SEpisode): Request { override fun videoListRequest(episode: SEpisode): Request {
val data = json.decodeFromString<EpisodeInfo>(episode.url) val data = json.decodeFromString<EpisodeInfo>(episode.url)
val vrf = vrfHelper.getVrf(data.id) val vrf = utils.vrfEncrypt(data.id)
val vrfHeaders = headers.newBuilder() val vrfHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01") .add("Accept", "application/json, text/javascript, */*; q=0.01")
@ -217,7 +216,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (!hosterSelection.contains(name)) return@parallelCatchingFlatMap emptyList() if (!hosterSelection.contains(name)) return@parallelCatchingFlatMap emptyList()
// Get decrypted url // 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() val vrfHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01") .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), GET("$baseUrl/ajax/server/${server.attr("data-link-id")}?vrf=$vrf", headers = vrfHeaders),
).await().parseAs<AjaxServerResponse>().result.url ).await().parseAs<AjaxServerResponse>().result.url
val decrypted = vrfHelper.decrypt(encrypted) val decrypted = utils.vrfDecrypt(encrypted)
when (name) { when (name) {
"Vidplay", "MyCloud" -> vidsrcExtractor.videosFromUrl(decrypted, name) "Vidplay", "MyCloud" -> vidsrcExtractor.videosFromUrl(decrypted, name)
"Filemoon" -> filemoonExtractor.videosFromUrl(decrypted, headers = headers) "Filemoon" -> filemoonExtractor.videosFromUrl(decrypted, headers = headers)

View File

@ -2,12 +2,6 @@ package eu.kanade.tachiyomi.animeextension.en.fmovies
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class VrfResponse(
val url: String,
val vrfQuery: String? = null,
)
@Serializable @Serializable
data class AjaxResponse( data class AjaxResponse(
val result: String, val result: String,
@ -36,21 +30,25 @@ data class FMoviesSubs(
) )
@Serializable @Serializable
data class RawResponse( data class MediaResponseBody(
val rawURL: String, val status: Int,
) val result: Result,
@Serializable
data class VidsrcResponse(
val result: ResultObject,
) { ) {
@Serializable @Serializable
data class ResultObject( data class Result(
val sources: List<SourceObject>, val sources: ArrayList<Source>,
val tracks: ArrayList<SubTrack> = ArrayList(),
) { ) {
@Serializable @Serializable
data class SourceObject( data class Source(
val file: String, val file: String,
) )
@Serializable
data class SubTrack(
val file: String,
val label: String = "",
val kind: String,
)
} }
} }

View File

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

View File

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

View File

@ -1,81 +1,144 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies.extractors package eu.kanade.tachiyomi.animeextension.en.fmovies.extractors
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesHelper import android.util.Base64
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesSubs import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.animeextension.en.fmovies.VidsrcResponse import eu.kanade.tachiyomi.animeextension.en.fmovies.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 eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
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 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 vrfHelper by lazy { FMoviesHelper(client, headers) }
private val playlistUtils by lazy { PlaylistUtils(client, headers) } private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, name: String): List<Video> { private val cacheControl = CacheControl.Builder().noStore().build()
val httpUrl = url.toHttpUrl() private val noCacheClient = client.newBuilder()
val host = httpUrl.host .cache(null)
val referer = "https://$host/" .build()
val query = buildString { private val keys by lazy {
append(httpUrl.pathSegments.last()) noCacheClient.newCall(
append("?") GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
append( ).execute().parseAs<List<String>>()
httpUrl.queryParameterNames.joinToString("&") { }
"$it=${httpUrl.queryParameter(it)}"
},
)
}
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("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", host) add("Host", host)
add("Referer", url.addAutoStart()) add("Referer", URLDecoder.decode(embedLink, "UTF-8"))
add("X-Requested-With", "XMLHttpRequest") add("X-Requested-With", "XMLHttpRequest")
}.build() }.build()
val infoJson = client.newCall( val response = client.newCall(
GET(rawUrl, headers = refererHeaders), GET(apiUrl, apiHeaders),
).execute().parseAs<VidsrcResponse>() ).execute()
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 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( client.newCall(
GET(it, headers = subtitlesHeaders), GET(newApiUrL, apiHeaders),
).execute().parseAs<List<FMoviesSubs>>().map { ).execute().parseAs()
Track(it.file, it.label) }
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 { return buildString {
val url = it.file append("https://")
.toHttpUrl() append(host)
.newBuilder() append("/")
.fragment(null) append(apiSlug)
.build() if (params.isNotEmpty()) {
.toString() append("?")
append(
playlistUtils.extractFromHls(url, subtitleList = subtitleList, referer = referer, videoNameGen = { q -> "$name - $q" }) params.joinToString("&") {
"${it.first}=${it.second}"
},
)
}
} }
} }
private fun String.addAutoStart(): String { private fun encodeID(videoID: String, keyList: List<String>): String {
return this.toHttpUrl().newBuilder().setQueryParameter("autostart", "true").build().toString() 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()
}
} }
} }