1570 lines
52 KiB
Kotlin
1570 lines
52 KiB
Kotlin
package com.hexated
|
|
|
|
import android.util.Base64
|
|
import com.hexated.DumpUtils.queryApi
|
|
import com.hexated.SoraStream.Companion.anilistAPI
|
|
import com.hexated.SoraStream.Companion.crunchyrollAPI
|
|
import com.hexated.SoraStream.Companion.filmxyAPI
|
|
import com.hexated.SoraStream.Companion.gdbot
|
|
import com.hexated.SoraStream.Companion.hdmovies4uAPI
|
|
import com.hexated.SoraStream.Companion.malsyncAPI
|
|
import com.hexated.SoraStream.Companion.tvMoviesAPI
|
|
import com.lagradost.cloudstream3.*
|
|
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
|
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
|
import com.lagradost.cloudstream3.mvvm.logError
|
|
import com.lagradost.cloudstream3.utils.*
|
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|
import com.lagradost.nicehttp.NiceResponse
|
|
import com.lagradost.nicehttp.RequestBodyTypes
|
|
import com.lagradost.nicehttp.requestCreator
|
|
import kotlinx.coroutines.delay
|
|
import okhttp3.FormBody
|
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
import org.jsoup.nodes.Document
|
|
import java.math.BigInteger
|
|
import java.net.*
|
|
import java.nio.charset.StandardCharsets
|
|
import java.security.*
|
|
import java.security.spec.PKCS8EncodedKeySpec
|
|
import java.security.spec.X509EncodedKeySpec
|
|
import java.text.SimpleDateFormat
|
|
import java.util.*
|
|
import javax.crypto.Cipher
|
|
import javax.crypto.spec.GCMParameterSpec
|
|
import javax.crypto.spec.IvParameterSpec
|
|
import javax.crypto.spec.SecretKeySpec
|
|
import kotlin.collections.ArrayList
|
|
import kotlin.math.min
|
|
|
|
var filmxyCookies: Map<String, String>? = null
|
|
var sfServer: String? = null
|
|
|
|
val encodedIndex = arrayOf(
|
|
"GamMovies",
|
|
"JSMovies",
|
|
"BlackMovies",
|
|
"CodexMovies",
|
|
"RinzryMovies",
|
|
"EdithxMovies",
|
|
"XtremeMovies",
|
|
"PapaonMovies[1]",
|
|
"PapaonMovies[2]",
|
|
"JmdkhMovies",
|
|
"RubyMovies",
|
|
"ShinobiMovies",
|
|
"VitoenMovies",
|
|
)
|
|
|
|
val lockedIndex = arrayOf(
|
|
"CodexMovies",
|
|
"EdithxMovies",
|
|
)
|
|
|
|
val mkvIndex = arrayOf(
|
|
"EdithxMovies",
|
|
"JmdkhMovies",
|
|
)
|
|
|
|
val untrimmedIndex = arrayOf(
|
|
"PapaonMovies[1]",
|
|
"PapaonMovies[2]",
|
|
"EdithxMovies",
|
|
)
|
|
|
|
val needRefererIndex = arrayOf(
|
|
"ShinobiMovies",
|
|
)
|
|
|
|
val ddomainIndex = arrayOf(
|
|
"RinzryMovies",
|
|
"ShinobiMovies"
|
|
)
|
|
|
|
val mimeType = arrayOf(
|
|
"video/x-matroska",
|
|
"video/mp4",
|
|
"video/x-msvideo"
|
|
)
|
|
|
|
fun Document.getMirrorLink(): String? {
|
|
return this.select("div.mb-4 a").randomOrNull()
|
|
?.attr("href")
|
|
}
|
|
|
|
fun Document.getMirrorServer(server: Int): String {
|
|
return this.select("div.text-center a:contains(Server $server)").attr("href")
|
|
}
|
|
|
|
suspend fun extractMirrorUHD(url: String, ref: String): String? {
|
|
var baseDoc = app.get(fixUrl(url, ref)).document
|
|
var downLink = baseDoc.getMirrorLink()
|
|
run lit@{
|
|
(1..2).forEach {
|
|
if (downLink != null) return@lit
|
|
val server = baseDoc.getMirrorServer(it.plus(1))
|
|
baseDoc = app.get(fixUrl(server, ref)).document
|
|
downLink = baseDoc.getMirrorLink()
|
|
}
|
|
}
|
|
return if (downLink?.contains("workers.dev") == true) downLink else base64Decode(
|
|
downLink?.substringAfter(
|
|
"download?url="
|
|
) ?: return null
|
|
)
|
|
}
|
|
|
|
suspend fun extractInstantUHD(url: String): String? {
|
|
val host = getBaseUrl(url)
|
|
val body = FormBody.Builder()
|
|
.addEncoded("keys", url.substringAfter("url="))
|
|
.build()
|
|
return app.post(
|
|
"$host/api", requestBody = body, headers = mapOf(
|
|
"x-token" to URI(url).host
|
|
), referer = "$host/"
|
|
).parsedSafe<Map<String, String>>()?.get("url")
|
|
}
|
|
|
|
suspend fun extractDirectUHD(url: String, niceResponse: NiceResponse): String? {
|
|
val document = niceResponse.document
|
|
val script = document.selectFirst("script:containsData(cf_token)")?.data() ?: return null
|
|
val actionToken = script.substringAfter("\"key\", \"").substringBefore("\");")
|
|
val cfToken = script.substringAfter("cf_token = \"").substringBefore("\";")
|
|
val body = FormBody.Builder()
|
|
.addEncoded("action", "direct")
|
|
.addEncoded("key", actionToken)
|
|
.addEncoded("action_token", cfToken)
|
|
.build()
|
|
val cookies = mapOf("PHPSESSID" to "${niceResponse.cookies["PHPSESSID"]}")
|
|
val direct = app.post(
|
|
url,
|
|
requestBody = body,
|
|
cookies = cookies,
|
|
referer = url,
|
|
headers = mapOf(
|
|
"x-token" to "driveleech.org"
|
|
)
|
|
).parsedSafe<Map<String, String>>()?.get("url")
|
|
|
|
return app.get(
|
|
direct ?: return null, cookies = cookies,
|
|
referer = url
|
|
).text.substringAfter("worker_url = '").substringBefore("';")
|
|
|
|
}
|
|
|
|
suspend fun extractBackupUHD(url: String): String? {
|
|
val resumeDoc = app.get(url)
|
|
|
|
val script = resumeDoc.document.selectFirst("script:containsData(FormData.)")?.data()
|
|
|
|
val ssid = resumeDoc.cookies["PHPSESSID"]
|
|
val baseIframe = getBaseUrl(url)
|
|
val fetchLink =
|
|
script?.substringAfter("fetch('")?.substringBefore("',")?.let { fixUrl(it, baseIframe) }
|
|
val token = script?.substringAfter("'token', '")?.substringBefore("');")
|
|
|
|
val body = FormBody.Builder()
|
|
.addEncoded("token", "$token")
|
|
.build()
|
|
val cookies = mapOf("PHPSESSID" to "$ssid")
|
|
|
|
val result = app.post(
|
|
fetchLink ?: return null,
|
|
requestBody = body,
|
|
headers = mapOf(
|
|
"Accept" to "*/*",
|
|
"Origin" to baseIframe,
|
|
"Sec-Fetch-Site" to "same-origin"
|
|
),
|
|
cookies = cookies,
|
|
referer = url
|
|
).text
|
|
return tryParseJson<UHDBackupUrl>(result)?.url
|
|
}
|
|
|
|
suspend fun extractGdbot(url: String): String? {
|
|
val headers = mapOf(
|
|
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
)
|
|
val res = app.get(
|
|
"$gdbot/", headers = headers
|
|
)
|
|
val token = res.document.selectFirst("input[name=_token]")?.attr("value")
|
|
val cookiesSet = res.headers.filter { it.first == "set-cookie" }
|
|
val xsrf =
|
|
cookiesSet.find { it.second.contains("XSRF-TOKEN") }?.second?.substringAfter("XSRF-TOKEN=")
|
|
?.substringBefore(";")
|
|
val session =
|
|
cookiesSet.find { it.second.contains("gdtot_proxy_session") }?.second?.substringAfter("gdtot_proxy_session=")
|
|
?.substringBefore(";")
|
|
|
|
val cookies = mapOf(
|
|
"gdtot_proxy_session" to "$session",
|
|
"XSRF-TOKEN" to "$xsrf"
|
|
)
|
|
val requestFile = app.post(
|
|
"$gdbot/file", data = mapOf(
|
|
"link" to url,
|
|
"_token" to "$token"
|
|
), headers = headers, referer = "$gdbot/", cookies = cookies
|
|
).document
|
|
|
|
return requestFile.selectFirst("div.mt-8 a.float-right")?.attr("href")
|
|
}
|
|
|
|
suspend fun extractDirectDl(url: String): String? {
|
|
val iframe = app.get(url).document.selectFirst("li.flex.flex-col.py-6 a:contains(Direct DL)")
|
|
?.attr("href")
|
|
val request = app.get(iframe ?: return null)
|
|
val driveDoc = request.document
|
|
val token = driveDoc.select("section#generate_url").attr("data-token")
|
|
val uid = driveDoc.select("section#generate_url").attr("data-uid")
|
|
|
|
val ssid = request.cookies["PHPSESSID"]
|
|
val body =
|
|
"""{"type":"DOWNLOAD_GENERATE","payload":{"uid":"$uid","access_token":"$token"}}""".toRequestBody(
|
|
RequestBodyTypes.JSON.toMediaTypeOrNull()
|
|
)
|
|
|
|
val json = app.post(
|
|
"https://rajbetmovies.com/action", requestBody = body, headers = mapOf(
|
|
"Accept" to "application/json, text/plain, */*",
|
|
"Cookie" to "PHPSESSID=$ssid",
|
|
"X-Requested-With" to "xmlhttprequest"
|
|
), referer = request.url
|
|
).text
|
|
return tryParseJson<DirectDl>(json)?.download_url
|
|
}
|
|
|
|
suspend fun extractDrivebot(url: String): String? {
|
|
val iframeDrivebot =
|
|
app.get(url).document.selectFirst("li.flex.flex-col.py-6 a:contains(Drivebot)")
|
|
?.attr("href") ?: return null
|
|
return getDrivebotLink(iframeDrivebot)
|
|
}
|
|
|
|
suspend fun extractGdflix(url: String): String? {
|
|
val iframeGdflix =
|
|
if (!url.contains("gdflix")) app.get(url).document.selectFirst("li.flex.flex-col.py-6 a:contains(GDFlix Direct)")
|
|
?.attr("href") ?: return null else url
|
|
val base = getBaseUrl(iframeGdflix)
|
|
|
|
val req = app.get(iframeGdflix).document.selectFirst("script:containsData(replace)")?.data()
|
|
?.substringAfter("replace(\"")
|
|
?.substringBefore("\")")?.let {
|
|
app.get(fixUrl(it, base))
|
|
} ?: return null
|
|
|
|
val iframeDrivebot2 = req.document.selectFirst("a.btn.btn-outline-warning")?.attr("href")
|
|
return getDrivebotLink(iframeDrivebot2)
|
|
|
|
// val reqUrl = req.url
|
|
// val ssid = req.cookies["PHPSESSID"]
|
|
// val script = req.document.selectFirst("script:containsData(formData =)")?.data()
|
|
// val key = Regex("append\\(\"key\", \"(\\S+?)\"\\);").find(script ?: return null)?.groupValues?.get(1)
|
|
//
|
|
// val body = FormBody.Builder()
|
|
// .addEncoded("action", "direct")
|
|
// .addEncoded("key", "$key")
|
|
// .addEncoded("action_token", "cf_token")
|
|
// .build()
|
|
//
|
|
// val gdriveUrl = app.post(
|
|
// reqUrl, requestBody = body,
|
|
// cookies = mapOf("PHPSESSID" to "$ssid"),
|
|
// headers = mapOf(
|
|
// "x-token" to URI(reqUrl).host
|
|
// )
|
|
// ).parsedSafe<Gdflix>()?.url
|
|
//
|
|
// return getDirectGdrive(gdriveUrl ?: return null)
|
|
|
|
}
|
|
|
|
suspend fun getDrivebotLink(url: String?): String? {
|
|
val driveDoc = app.get(url ?: return null)
|
|
|
|
val ssid = driveDoc.cookies["PHPSESSID"]
|
|
val script = driveDoc.document.selectFirst("script:containsData(var formData)")?.data()
|
|
|
|
val baseUrl = getBaseUrl(url)
|
|
val token = script?.substringAfter("'token', '")?.substringBefore("');")
|
|
val link =
|
|
script?.substringAfter("fetch('")?.substringBefore("',").let { "$baseUrl$it" }
|
|
|
|
val body = FormBody.Builder()
|
|
.addEncoded("token", "$token")
|
|
.build()
|
|
val cookies = mapOf("PHPSESSID" to "$ssid")
|
|
|
|
val file = app.post(
|
|
link,
|
|
requestBody = body,
|
|
headers = mapOf(
|
|
"Accept" to "*/*",
|
|
"Origin" to baseUrl,
|
|
"Sec-Fetch-Site" to "same-origin"
|
|
),
|
|
cookies = cookies,
|
|
referer = url
|
|
).parsedSafe<DriveBotLink>()?.url ?: return null
|
|
|
|
return if (file.startsWith("http")) file else app.get(
|
|
fixUrl(
|
|
file,
|
|
baseUrl
|
|
)
|
|
).document.selectFirst("script:containsData(window.open)")
|
|
?.data()?.substringAfter("window.open('")?.substringBefore("')")
|
|
}
|
|
|
|
suspend fun extractOiya(url: String): String? {
|
|
return app.get(url).document.selectFirst("div.wp-block-button a")?.attr("href")
|
|
}
|
|
|
|
fun deobfstr(hash: String, index: String): String {
|
|
var result = ""
|
|
for (i in hash.indices step 2) {
|
|
val j = hash.substring(i, i + 2)
|
|
result += (j.toInt(16) xor index[(i / 2) % index.length].code).toChar()
|
|
}
|
|
return result
|
|
}
|
|
|
|
suspend fun extractCovyn(url: String?): Pair<String?, String?>? {
|
|
val request = session.get(url ?: return null, referer = "${tvMoviesAPI}/")
|
|
val filehosting = session.baseClient.cookieJar.loadForRequest(url.toHttpUrl())
|
|
.find { it.name == "filehosting" }?.value
|
|
val headers = mapOf(
|
|
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
"Connection" to "keep-alive",
|
|
"Cookie" to "filehosting=$filehosting",
|
|
)
|
|
|
|
val iframe = request.document.findTvMoviesIframe()
|
|
delay(10500)
|
|
val request2 = session.get(
|
|
iframe ?: return null, referer = url, headers = headers
|
|
)
|
|
|
|
val iframe2 = request2.document.findTvMoviesIframe()
|
|
delay(10500)
|
|
val request3 = session.get(
|
|
iframe2 ?: return null, referer = iframe, headers = headers
|
|
)
|
|
|
|
val response = request3.document
|
|
val videoLink = response.selectFirst("button.btn.btn--primary")?.attr("onclick")
|
|
?.substringAfter("location = '")?.substringBefore("';")?.let {
|
|
app.get(
|
|
it, referer = iframe2, headers = headers
|
|
).url
|
|
}
|
|
val size = response.selectFirst("ul.row--list li:contains(Filesize) span:last-child")
|
|
?.text()
|
|
|
|
return Pair(videoLink, size)
|
|
}
|
|
|
|
suspend fun getDirectGdrive(url: String): String {
|
|
val fixUrl = if (url.contains("&export=download")) {
|
|
url
|
|
} else {
|
|
"https://drive.google.com/uc?id=${
|
|
Regex("(?:\\?id=|/d/)(\\S+)/").find("$url/")?.groupValues?.get(1)
|
|
}&export=download"
|
|
}
|
|
|
|
val doc = app.get(fixUrl).document
|
|
val form = doc.select("form#download-form").attr("action")
|
|
val uc = doc.select("input#uc-download-link").attr("value")
|
|
return app.post(
|
|
form, data = mapOf(
|
|
"uc-download-link" to uc
|
|
)
|
|
).url
|
|
|
|
}
|
|
|
|
suspend fun invokeSmashyFfix(
|
|
name: String,
|
|
url: String,
|
|
ref: String,
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
callback: (ExtractorLink) -> Unit,
|
|
) {
|
|
val json = app.get(url, referer = ref, headers = mapOf("X-Requested-With" to "XMLHttpRequest"))
|
|
.parsedSafe<SmashySources>()
|
|
json?.sourceUrls?.map {
|
|
M3u8Helper.generateM3u8(
|
|
"Smashy [$name]",
|
|
it,
|
|
""
|
|
).forEach(callback)
|
|
}
|
|
|
|
json?.subtitleUrls?.split(",")?.map { sub ->
|
|
val lang = "\\[(.*)]".toRegex().find(sub)?.groupValues?.get(1)
|
|
val subUrl = sub.replace("[$lang]", "").trim()
|
|
subtitleCallback.invoke(
|
|
SubtitleFile(
|
|
lang ?: return@map,
|
|
subUrl
|
|
)
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
suspend fun invokeSmashySu(
|
|
name: String,
|
|
url: String,
|
|
ref: String,
|
|
callback: (ExtractorLink) -> Unit,
|
|
) {
|
|
val json = app.get(url, referer = ref, headers = mapOf("X-Requested-With" to "XMLHttpRequest"))
|
|
.parsedSafe<SmashySources>()
|
|
json?.sourceUrls?.firstOrNull()?.removeSuffix(",")?.split(",")?.forEach { links ->
|
|
val quality = Regex("\\[(\\S+)]").find(links)?.groupValues?.getOrNull(1) ?: return@forEach
|
|
val trimmedLink = links.removePrefix("[$quality]").trim()
|
|
callback.invoke(
|
|
ExtractorLink(
|
|
"Smashy [$name]",
|
|
"Smashy [$name]",
|
|
trimmedLink,
|
|
"",
|
|
getQualityFromName(quality),
|
|
INFER_TYPE
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
suspend fun getDumpIdAndType(title: String?, year: Int?, season: Int?): Pair<String?, Int?> {
|
|
val res = tryParseJson<DumpQuickSearchData>(
|
|
queryApi(
|
|
"POST",
|
|
"${BuildConfig.DUMP_API}/search/searchWithKeyWord",
|
|
mapOf(
|
|
"searchKeyWord" to "$title",
|
|
"size" to "50",
|
|
)
|
|
)
|
|
)?.searchResults
|
|
|
|
val media = if (res?.size == 1) {
|
|
res.firstOrNull()
|
|
} else {
|
|
res?.find {
|
|
when (season) {
|
|
null -> {
|
|
it.name.equals(
|
|
title,
|
|
true
|
|
) && it.releaseTime == "$year" && it.domainType == 0
|
|
}
|
|
|
|
1 -> {
|
|
it.name?.contains(
|
|
"$title",
|
|
true
|
|
) == true && (it.releaseTime == "$year" || it.name.contains(
|
|
"Season $season",
|
|
true
|
|
)) && it.domainType == 1
|
|
}
|
|
|
|
else -> {
|
|
it.name?.contains(Regex("(?i)$title\\s?($season|${season.toRomanNumeral()}|Season\\s$season)")) == true && it.releaseTime == "$year" && it.domainType == 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return media?.id to media?.domainType
|
|
|
|
}
|
|
|
|
suspend fun fetchDumpEpisodes(id: String, type: String, episode: Int?): EpisodeVo? {
|
|
return tryParseJson<DumpMediaDetail>(
|
|
queryApi(
|
|
"GET",
|
|
"${BuildConfig.DUMP_API}/movieDrama/get",
|
|
mapOf(
|
|
"category" to type,
|
|
"id" to id,
|
|
)
|
|
)
|
|
)?.episodeVo?.find {
|
|
it.seriesNo == (episode ?: 0)
|
|
}
|
|
}
|
|
|
|
suspend fun invokeDrivetot(
|
|
url: String,
|
|
tags: String? = null,
|
|
size: String? = null,
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
callback: (ExtractorLink) -> Unit,
|
|
) {
|
|
val res = app.get(url)
|
|
val data = res.document.select("form input").associate { it.attr("name") to it.attr("value") }
|
|
app.post(res.url, data = data, cookies = res.cookies).document.select("div.card-body a")
|
|
.apmap { ele ->
|
|
val href = base64Decode(ele.attr("href").substringAfterLast("/")).let {
|
|
if (it.contains("hubcloud.lol")) it.replace("hubcloud.lol", "hubcloud.in") else it
|
|
}
|
|
loadExtractor(href, "$hdmovies4uAPI/", subtitleCallback) { link ->
|
|
callback.invoke(
|
|
ExtractorLink(
|
|
link.source,
|
|
"${link.name} $tags [$size]",
|
|
link.url,
|
|
link.referer,
|
|
link.quality,
|
|
link.type,
|
|
link.headers,
|
|
link.extractorData
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun bypassBqrecipes(url: String): String? {
|
|
var res = app.get(url)
|
|
var location = res.text.substringAfter(".replace('").substringBefore("');")
|
|
var cookies = res.cookies
|
|
res = app.get(location, cookies = cookies)
|
|
cookies = cookies + res.cookies
|
|
val document = res.document
|
|
location = document.select("form#recaptcha").attr("action")
|
|
val data =
|
|
document.select("form#recaptcha input").associate { it.attr("name") to it.attr("value") }
|
|
res = app.post(location, data = data, cookies = cookies)
|
|
location = res.document.selectFirst("a#messagedown")?.attr("href") ?: return null
|
|
cookies = (cookies + res.cookies).minus("var")
|
|
return app.get(location, cookies = cookies, allowRedirects = false).headers["location"]
|
|
}
|
|
|
|
suspend fun bypassOuo(url: String?): String? {
|
|
var res = session.get(url ?: return null)
|
|
run lit@{
|
|
(1..2).forEach { _ ->
|
|
if (res.headers["location"] != null) return@lit
|
|
val document = res.document
|
|
val nextUrl = document.select("form").attr("action")
|
|
val data = document.select("form input").mapNotNull {
|
|
it.attr("name") to it.attr("value")
|
|
}.toMap().toMutableMap()
|
|
val captchaKey =
|
|
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
|
.attr("src").substringAfter("render=")
|
|
val token = getCaptchaToken(url, captchaKey)
|
|
data["x-token"] = token ?: ""
|
|
res = session.post(
|
|
nextUrl,
|
|
data = data,
|
|
headers = mapOf("content-type" to "application/x-www-form-urlencoded"),
|
|
allowRedirects = false
|
|
)
|
|
}
|
|
}
|
|
|
|
return res.headers["location"]
|
|
}
|
|
|
|
suspend fun bypassFdAds(url: String?): String? {
|
|
val directUrl =
|
|
app.get(url ?: return null, verify = false).document.select("a#link").attr("href")
|
|
.substringAfter("/go/")
|
|
.let { base64Decode(it) }
|
|
val doc = app.get(directUrl, verify = false).document
|
|
val lastDoc = app.post(
|
|
doc.select("form#landing").attr("action"),
|
|
data = mapOf("go" to doc.select("form#landing input").attr("value")),
|
|
verify = false
|
|
).document
|
|
val json = lastDoc.select("form#landing input[name=newwpsafelink]").attr("value")
|
|
.let { base64Decode(it) }
|
|
val finalJson =
|
|
tryParseJson<FDAds>(json)?.linkr?.substringAfter("redirect=")?.let { base64Decode(it) }
|
|
return tryParseJson<Safelink>(finalJson)?.safelink
|
|
}
|
|
|
|
suspend fun bypassHrefli(url: String): String? {
|
|
fun Document.getFormUrl(): String {
|
|
return this.select("form#landing").attr("action")
|
|
}
|
|
|
|
fun Document.getFormData(): Map<String, String> {
|
|
return this.select("form#landing input").associate { it.attr("name") to it.attr("value") }
|
|
}
|
|
|
|
val host = getBaseUrl(url)
|
|
var res = app.get(url).document
|
|
var formUrl = res.getFormUrl()
|
|
var formData = res.getFormData()
|
|
|
|
res = app.post(formUrl, data = formData).document
|
|
formUrl = res.getFormUrl()
|
|
formData = res.getFormData()
|
|
|
|
res = app.post(formUrl, data = formData).document
|
|
val skToken = res.selectFirst("script:containsData(?go=)")?.data()?.substringAfter("?go=")
|
|
?.substringBefore("\"") ?: return null
|
|
val driveUrl = app.get(
|
|
"$host?go=$skToken", cookies = mapOf(
|
|
skToken to "${formData["_wp_http2"]}"
|
|
)
|
|
).document.selectFirst("meta[http-equiv=refresh]")?.attr("content")?.substringAfter("url=")
|
|
val path = app.get(driveUrl ?: return null).text.substringAfter("replace(\"")
|
|
.substringBefore("\")")
|
|
if (path == "/404") return null
|
|
return fixUrl(path, getBaseUrl(driveUrl))
|
|
}
|
|
|
|
suspend fun getTvMoviesServer(url: String, season: Int?, episode: Int?): Pair<String, String?>? {
|
|
|
|
val req = app.get(url)
|
|
if (!req.isSuccessful) return null
|
|
val doc = req.document
|
|
|
|
return if (season == null) {
|
|
doc.select("table.wp-block-table tr:last-child td:first-child").text() to
|
|
doc.selectFirst("table.wp-block-table tr a")?.attr("href").let { link ->
|
|
app.get(link ?: return null).document.select("div#text-url a")
|
|
.mapIndexed { index, element ->
|
|
element.attr("href") to element.parent()?.textNodes()?.getOrNull(index)
|
|
?.text()
|
|
}.filter { it.second?.contains("Subtitles", true) == false }
|
|
.map { it.first }
|
|
}.lastOrNull()
|
|
} else {
|
|
doc.select("div.vc_tta-panels div#Season-$season table.wp-block-table tr:last-child td:first-child")
|
|
.text() to
|
|
doc.select("div.vc_tta-panels div#Season-$season table.wp-block-table tr a")
|
|
.mapNotNull { ele ->
|
|
app.get(ele.attr("href")).document.select("div#text-url a")
|
|
.mapIndexed { index, element ->
|
|
element.attr("href") to element.parent()?.textNodes()
|
|
?.getOrNull(index)?.text()
|
|
}.find { it.second?.contains("Episode $episode", true) == true }?.first
|
|
}.lastOrNull()
|
|
}
|
|
}
|
|
|
|
suspend fun getSfServer() = sfServer ?: fetchSfServer().also { sfServer = it }
|
|
|
|
suspend fun fetchSfServer(): String {
|
|
return app.get("https://raw.githubusercontent.com/hexated/cloudstream-resources/main/sfmovies_server").text
|
|
}
|
|
|
|
suspend fun getFilmxyCookies(url: String) =
|
|
filmxyCookies ?: fetchFilmxyCookies(url).also { filmxyCookies = it }
|
|
|
|
suspend fun fetchFilmxyCookies(url: String): Map<String, String> {
|
|
|
|
val defaultCookies =
|
|
mutableMapOf("G_ENABLED_IDPS" to "google", "true_checker" to "1", "XID" to "1")
|
|
session.get(
|
|
url,
|
|
headers = mapOf(
|
|
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
|
|
),
|
|
cookies = defaultCookies,
|
|
)
|
|
val phpsessid = session.baseClient.cookieJar.loadForRequest(url.toHttpUrl())
|
|
.first { it.name == "PHPSESSID" }.value
|
|
defaultCookies["PHPSESSID"] = phpsessid
|
|
|
|
val userNonce =
|
|
app.get(
|
|
"$filmxyAPI/login/?redirect_to=$filmxyAPI/",
|
|
cookies = defaultCookies
|
|
).document.select("script")
|
|
.find { it.data().contains("var userNonce") }?.data()?.let {
|
|
Regex("var\\suserNonce.*?\"(\\S+?)\";").find(it)?.groupValues?.get(1)
|
|
}
|
|
|
|
val cookieUrl = "${filmxyAPI}/wp-admin/admin-ajax.php"
|
|
|
|
session.post(
|
|
cookieUrl,
|
|
data = mapOf(
|
|
"action" to "guest_login",
|
|
"nonce" to "$userNonce",
|
|
),
|
|
headers = mapOf(
|
|
"X-Requested-With" to "XMLHttpRequest",
|
|
),
|
|
cookies = defaultCookies
|
|
)
|
|
val cookieJar = session.baseClient.cookieJar.loadForRequest(cookieUrl.toHttpUrl())
|
|
.associate { it.name to it.value }.toMutableMap()
|
|
|
|
return cookieJar.plus(defaultCookies)
|
|
}
|
|
|
|
fun Document.findTvMoviesIframe(): String? {
|
|
return this.selectFirst("script:containsData(var seconds)")?.data()?.substringAfter("href='")
|
|
?.substringBefore("'>")
|
|
}
|
|
|
|
//modified code from https://github.com/jmir1/aniyomi-extensions/blob/master/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt
|
|
suspend fun getCrunchyrollToken(): CrunchyrollAccessToken {
|
|
val client = app.baseClient.newBuilder()
|
|
.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("cr-unblocker.us.to", 1080)))
|
|
.build()
|
|
|
|
Authenticator.setDefault(object : Authenticator() {
|
|
override fun getPasswordAuthentication(): PasswordAuthentication {
|
|
return PasswordAuthentication("crunblocker", "crunblocker".toCharArray())
|
|
}
|
|
})
|
|
|
|
val request = requestCreator(
|
|
method = "POST",
|
|
url = "$crunchyrollAPI/auth/v1/token",
|
|
headers = mapOf(
|
|
"User-Agent" to "Crunchyroll/3.26.1 Android/11 okhttp/4.9.2",
|
|
"Content-Type" to "application/x-www-form-urlencoded",
|
|
"Authorization" to "Basic ${BuildConfig.CRUNCHYROLL_BASIC_TOKEN}"
|
|
),
|
|
data = mapOf(
|
|
"refresh_token" to app.get(BuildConfig.CRUNCHYROLL_REFRESH_TOKEN).text,
|
|
"grant_type" to "refresh_token",
|
|
"scope" to "offline_access"
|
|
)
|
|
)
|
|
|
|
val token = tryParseJson<CrunchyrollToken>(client.newCall(request).execute().body.string())
|
|
val headers = mapOf("Authorization" to "${token?.tokenType} ${token?.accessToken}")
|
|
val cms =
|
|
app.get("$crunchyrollAPI/index/v2", headers = headers).parsedSafe<CrunchyrollToken>()?.cms
|
|
return CrunchyrollAccessToken(
|
|
token?.accessToken,
|
|
token?.tokenType,
|
|
cms?.bucket,
|
|
cms?.policy,
|
|
cms?.signature,
|
|
cms?.key_pair_id,
|
|
)
|
|
}
|
|
|
|
suspend fun getCrunchyrollId(aniId: String?): String? {
|
|
val query = """
|
|
query media(${'$'}id: Int, ${'$'}type: MediaType, ${'$'}isAdult: Boolean) {
|
|
Media(id: ${'$'}id, type: ${'$'}type, isAdult: ${'$'}isAdult) {
|
|
id
|
|
externalLinks {
|
|
id
|
|
site
|
|
url
|
|
type
|
|
}
|
|
}
|
|
}
|
|
""".trimIndent().trim()
|
|
|
|
val variables = mapOf(
|
|
"id" to aniId,
|
|
"isAdult" to false,
|
|
"type" to "ANIME",
|
|
)
|
|
|
|
val data = mapOf(
|
|
"query" to query,
|
|
"variables" to variables
|
|
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
|
|
|
val externalLinks = app.post(anilistAPI, requestBody = data)
|
|
.parsedSafe<AnilistResponses>()?.data?.Media?.externalLinks
|
|
|
|
return (externalLinks?.find { it.site == "VRV" }
|
|
?: externalLinks?.find { it.site == "Crunchyroll" })?.url?.let {
|
|
app.get(it).url.substringAfter("/series/").substringBefore("/")
|
|
}
|
|
}
|
|
|
|
suspend fun getCrunchyrollIdFromMalSync(aniId: String?): String? {
|
|
val res = app.get("$malsyncAPI/mal/anime/$aniId").parsedSafe<MalSyncRes>()?.Sites
|
|
val vrv = res?.get("Vrv")?.map { it.value }?.firstOrNull()?.get("url")
|
|
val crunchyroll = res?.get("Vrv")?.map { it.value }?.firstOrNull()?.get("url")
|
|
val regex = Regex("series/(\\w+)/?")
|
|
return regex.find("$vrv")?.groupValues?.getOrNull(1)
|
|
?: regex.find("$crunchyroll")?.groupValues?.getOrNull(1)
|
|
}
|
|
|
|
suspend fun String.haveDub(referer: String) : Boolean {
|
|
return app.get(this,referer=referer).text.contains("TYPE=AUDIO")
|
|
}
|
|
|
|
suspend fun convertTmdbToAnimeId(
|
|
title: String?,
|
|
date: String?,
|
|
airedDate: String?,
|
|
type: TvType
|
|
): AniIds {
|
|
val sDate = date?.split("-")
|
|
val sAiredDate = airedDate?.split("-")
|
|
|
|
val year = sDate?.firstOrNull()?.toIntOrNull()
|
|
val airedYear = sAiredDate?.firstOrNull()?.toIntOrNull()
|
|
val season = getSeason(sDate?.get(1)?.toIntOrNull())
|
|
val airedSeason = getSeason(sAiredDate?.get(1)?.toIntOrNull())
|
|
|
|
return if (type == TvType.AnimeMovie) {
|
|
tmdbToAnimeId(title, airedYear, "", type)
|
|
} else {
|
|
val ids = tmdbToAnimeId(title, year, season, type)
|
|
if (ids.id == null && ids.idMal == null) tmdbToAnimeId(
|
|
title,
|
|
airedYear,
|
|
airedSeason,
|
|
type
|
|
) else ids
|
|
}
|
|
}
|
|
|
|
suspend fun tmdbToAnimeId(title: String?, year: Int?, season: String?, type: TvType): AniIds {
|
|
val query = """
|
|
query (
|
|
${'$'}page: Int = 1
|
|
${'$'}search: String
|
|
${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]
|
|
${'$'}type: MediaType
|
|
${'$'}season: MediaSeason
|
|
${'$'}seasonYear: Int
|
|
${'$'}format: [MediaFormat]
|
|
) {
|
|
Page(page: ${'$'}page, perPage: 20) {
|
|
media(
|
|
search: ${'$'}search
|
|
sort: ${'$'}sort
|
|
type: ${'$'}type
|
|
season: ${'$'}season
|
|
seasonYear: ${'$'}seasonYear
|
|
format_in: ${'$'}format
|
|
) {
|
|
id
|
|
idMal
|
|
}
|
|
}
|
|
}
|
|
""".trimIndent().trim()
|
|
|
|
val variables = mapOf(
|
|
"search" to title,
|
|
"sort" to "SEARCH_MATCH",
|
|
"type" to "ANIME",
|
|
"season" to season?.uppercase(),
|
|
"seasonYear" to year,
|
|
"format" to listOf(if (type == TvType.AnimeMovie) "MOVIE" else "TV")
|
|
).filterValues { value -> value != null && value.toString().isNotEmpty() }
|
|
|
|
val data = mapOf(
|
|
"query" to query,
|
|
"variables" to variables
|
|
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
|
|
|
val res = app.post(anilistAPI, requestBody = data)
|
|
.parsedSafe<AniSearch>()?.data?.Page?.media?.firstOrNull()
|
|
return AniIds(res?.id, res?.idMal)
|
|
|
|
}
|
|
|
|
fun generateWpKey(r: String, m: String): String {
|
|
val rList = r.split("\\x").toTypedArray()
|
|
var n = ""
|
|
val decodedM = String(base64Decode(m.split("").reversed().joinToString("")).toCharArray())
|
|
for (s in decodedM.split("|")) {
|
|
n += "\\x" + rList[Integer.parseInt(s) + 1]
|
|
}
|
|
return n
|
|
}
|
|
|
|
suspend fun loadCustomTagExtractor(
|
|
tag: String? = null,
|
|
url: String,
|
|
referer: String? = null,
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
callback: (ExtractorLink) -> Unit,
|
|
quality: Int? = null,
|
|
) {
|
|
loadExtractor(url, referer, subtitleCallback) { link ->
|
|
callback.invoke(
|
|
ExtractorLink(
|
|
link.source,
|
|
"${link.name} $tag",
|
|
link.url,
|
|
link.referer,
|
|
when (link.type) {
|
|
ExtractorLinkType.M3U8 -> link.quality
|
|
else -> quality ?: link.quality
|
|
},
|
|
link.type,
|
|
link.headers,
|
|
link.extractorData
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
suspend fun loadCustomExtractor(
|
|
name: String? = null,
|
|
url: String,
|
|
referer: String? = null,
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
callback: (ExtractorLink) -> Unit,
|
|
quality: Int? = null,
|
|
) {
|
|
loadExtractor(url, referer, subtitleCallback) { link ->
|
|
callback.invoke(
|
|
ExtractorLink(
|
|
name ?: link.source,
|
|
name ?: link.name,
|
|
link.url,
|
|
link.referer,
|
|
when {
|
|
link.name == "VidSrc" -> Qualities.P1080.value
|
|
link.type == ExtractorLinkType.M3U8 -> link.quality
|
|
else -> quality ?: link.quality
|
|
},
|
|
link.type,
|
|
link.headers,
|
|
link.extractorData
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun getSeason(month: Int?): String? {
|
|
val seasons = arrayOf(
|
|
"Winter", "Winter", "Spring", "Spring", "Spring", "Summer",
|
|
"Summer", "Summer", "Fall", "Fall", "Fall", "Winter"
|
|
)
|
|
if (month == null) return null
|
|
return seasons[month - 1]
|
|
}
|
|
|
|
fun getEpisodeSlug(
|
|
season: Int? = null,
|
|
episode: Int? = null,
|
|
): Pair<String, String> {
|
|
return if (season == null && episode == null) {
|
|
"" to ""
|
|
} else {
|
|
(if (season!! < 10) "0$season" else "$season") to (if (episode!! < 10) "0$episode" else "$episode")
|
|
}
|
|
}
|
|
|
|
fun getTitleSlug(title: String? = null): Pair<String?, String?> {
|
|
val slug = title.createSlug()
|
|
return slug?.replace("-", "\\W") to title?.replace(" ", "_")
|
|
}
|
|
|
|
fun getIndexQuery(
|
|
title: String? = null,
|
|
year: Int? = null,
|
|
season: Int? = null,
|
|
episode: Int? = null
|
|
): String {
|
|
val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode)
|
|
return (if (season == null) {
|
|
"$title ${year ?: ""}"
|
|
} else {
|
|
"$title S${seasonSlug}E${episodeSlug}"
|
|
}).trim()
|
|
}
|
|
|
|
fun searchIndex(
|
|
title: String? = null,
|
|
season: Int? = null,
|
|
episode: Int? = null,
|
|
year: Int? = null,
|
|
response: String,
|
|
isTrimmed: Boolean = true,
|
|
): List<IndexMedia>? {
|
|
val files = tryParseJson<IndexSearch>(response)?.data?.files?.filter { media ->
|
|
matchingIndex(
|
|
media.name ?: return null,
|
|
media.mimeType ?: return null,
|
|
title ?: return null,
|
|
year,
|
|
season,
|
|
episode
|
|
)
|
|
}?.distinctBy { it.name }?.sortedByDescending { it.size?.toLongOrNull() ?: 0 } ?: return null
|
|
|
|
return if (isTrimmed) {
|
|
files.let { file ->
|
|
listOfNotNull(
|
|
file.find { it.name?.contains("2160p", true) == true },
|
|
file.find { it.name?.contains("1080p", true) == true }
|
|
)
|
|
}
|
|
} else {
|
|
files
|
|
}
|
|
}
|
|
|
|
fun matchingIndex(
|
|
mediaName: String?,
|
|
mediaMimeType: String?,
|
|
title: String?,
|
|
year: Int?,
|
|
season: Int?,
|
|
episode: Int?,
|
|
include720: Boolean = false
|
|
): Boolean {
|
|
val (wSlug, dwSlug) = getTitleSlug(title)
|
|
val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode)
|
|
return (if (season == null) {
|
|
mediaName?.contains(Regex("(?i)(?:$wSlug|$dwSlug).*$year")) == true
|
|
} else {
|
|
mediaName?.contains(Regex("(?i)(?:$wSlug|$dwSlug).*S${seasonSlug}.?E${episodeSlug}")) == true
|
|
}) && mediaName?.contains(
|
|
if (include720) Regex("(?i)(2160p|1080p|720p)") else Regex("(?i)(2160p|1080p)")
|
|
) == true && ((mediaMimeType in mimeType) || mediaName.contains(Regex("\\.mkv|\\.mp4|\\.avi")))
|
|
}
|
|
|
|
fun decodeIndexJson(json: String): String {
|
|
val slug = json.reversed().substring(24)
|
|
return base64Decode(slug.substring(0, slug.length - 20))
|
|
}
|
|
|
|
fun String.xorDecrypt(key: String): String {
|
|
val sb = StringBuilder()
|
|
var i = 0
|
|
while (i < this.length) {
|
|
var j = 0
|
|
while (j < key.length && i < this.length) {
|
|
sb.append((this[i].code xor key[j].code).toChar())
|
|
j++
|
|
i++
|
|
}
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
fun vidsrctoDecrypt(text: String): String {
|
|
val parse = Base64.decode(text.toByteArray(), Base64.URL_SAFE)
|
|
val cipher = Cipher.getInstance("RC4")
|
|
cipher.init(
|
|
Cipher.DECRYPT_MODE,
|
|
SecretKeySpec("8z5Ag5wgagfsOuhz".toByteArray(), "RC4"),
|
|
cipher.parameters
|
|
)
|
|
return decode(cipher.doFinal(parse).toString(Charsets.UTF_8))
|
|
}
|
|
|
|
fun String?.createSlug(): String? {
|
|
return this?.filter { it.isWhitespace() || it.isLetterOrDigit() }
|
|
?.trim()
|
|
?.replace("\\s+".toRegex(), "-")
|
|
?.lowercase()
|
|
}
|
|
|
|
fun getLanguage(str: String): String {
|
|
return if (str.contains("(in_ID)")) "Indonesian" else str
|
|
}
|
|
|
|
fun bytesToGigaBytes(number: Double): Double = number / 1024000000
|
|
|
|
fun getKisskhTitle(str: String?): String? {
|
|
return str?.replace(Regex("[^a-zA-Z\\d]"), "-")
|
|
}
|
|
|
|
fun String.getFileSize(): Float? {
|
|
val size = Regex("(?i)(\\d+\\.?\\d+\\sGB|MB)").find(this)?.groupValues?.get(0)?.trim()
|
|
val num = Regex("(\\d+\\.?\\d+)").find(size ?: return null)?.groupValues?.get(0)?.toFloat()
|
|
?: return null
|
|
return when {
|
|
size.contains("GB") -> num * 1000000
|
|
else -> num * 1000
|
|
}
|
|
}
|
|
|
|
fun getUhdTags(str: String?): String {
|
|
return Regex("\\d{3,4}[Pp]\\.?(.*?)\\[").find(str ?: "")?.groupValues?.getOrNull(1)
|
|
?.replace(".", " ")?.trim()
|
|
?: str ?: ""
|
|
}
|
|
|
|
fun getIndexQualityTags(str: String?, fullTag: Boolean = false): String {
|
|
return if (fullTag) Regex("(?i)(.*)\\.(?:mkv|mp4|avi)").find(str ?: "")?.groupValues?.get(1)
|
|
?.trim() ?: str ?: "" else Regex("(?i)\\d{3,4}[pP]\\.?(.*?)\\.(mkv|mp4|avi)").find(
|
|
str ?: ""
|
|
)?.groupValues?.getOrNull(1)
|
|
?.replace(".", " ")?.trim() ?: str ?: ""
|
|
}
|
|
|
|
fun getIndexQuality(str: String?): Int {
|
|
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
|
?: Qualities.Unknown.value
|
|
}
|
|
|
|
fun getIndexSize(str: String?): String? {
|
|
return Regex("(?i)([\\d.]+\\s*(?:gb|mb))").find(str ?: "")?.groupValues?.getOrNull(1)?.trim()
|
|
}
|
|
|
|
fun getQuality(str: String): Int {
|
|
return when (str) {
|
|
"360p" -> Qualities.P240.value
|
|
"480p" -> Qualities.P360.value
|
|
"720p" -> Qualities.P480.value
|
|
"1080p" -> Qualities.P720.value
|
|
"1080p Ultra" -> Qualities.P1080.value
|
|
else -> getQualityFromName(str)
|
|
}
|
|
}
|
|
|
|
fun getGMoviesQuality(str: String): Int {
|
|
return when {
|
|
str.contains("480P", true) -> Qualities.P480.value
|
|
str.contains("720P", true) -> Qualities.P720.value
|
|
str.contains("1080P", true) -> Qualities.P1080.value
|
|
str.contains("4K", true) -> Qualities.P2160.value
|
|
else -> Qualities.Unknown.value
|
|
}
|
|
}
|
|
|
|
fun getFDoviesQuality(str: String): String {
|
|
return when {
|
|
str.contains("1080P", true) -> "1080P"
|
|
str.contains("4K", true) -> "4K"
|
|
else -> ""
|
|
}
|
|
}
|
|
|
|
fun getVipLanguage(str: String): String {
|
|
return when (str) {
|
|
"in_ID" -> "Indonesian"
|
|
"pt" -> "Portuguese"
|
|
else -> str.split("_").first().let {
|
|
SubtitleHelper.fromTwoLettersToLanguage(it).toString()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun fixCrunchyrollLang(language: String?): String? {
|
|
return SubtitleHelper.fromTwoLettersToLanguage(language ?: return null)
|
|
?: SubtitleHelper.fromTwoLettersToLanguage(language.substringBefore("-"))
|
|
}
|
|
|
|
fun getDeviceId(length: Int = 16): String {
|
|
val allowedChars = ('a'..'f') + ('0'..'9')
|
|
return (1..length)
|
|
.map { allowedChars.random() }
|
|
.joinToString("")
|
|
}
|
|
|
|
fun String.encodeUrl(): String {
|
|
val url = URL(this)
|
|
val uri = URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)
|
|
return uri.toURL().toString()
|
|
}
|
|
|
|
fun getBaseUrl(url: String): String {
|
|
return URI(url).let {
|
|
"${it.scheme}://${it.host}"
|
|
}
|
|
}
|
|
|
|
fun String.getHost(): String {
|
|
return fixTitle(URI(this).host.substringBeforeLast(".").substringAfterLast("."))
|
|
}
|
|
|
|
fun isUpcoming(dateString: String?): Boolean {
|
|
return try {
|
|
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
|
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
|
unixTimeMS < dateTime
|
|
} catch (t: Throwable) {
|
|
logError(t)
|
|
false
|
|
}
|
|
}
|
|
|
|
fun getDate(): TmdbDate {
|
|
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
|
val calender = Calendar.getInstance()
|
|
val today = formatter.format(calender.time)
|
|
calender.add(Calendar.WEEK_OF_YEAR, 1)
|
|
val nextWeek = formatter.format(calender.time)
|
|
return TmdbDate(today, nextWeek)
|
|
}
|
|
|
|
fun decode(input: String): String = URLDecoder.decode(input, "utf-8")
|
|
|
|
fun encode(input: String): String = URLEncoder.encode(input, "utf-8").replace("+", "%20")
|
|
|
|
fun base64DecodeAPI(api: String): String {
|
|
return api.chunked(4).map { base64Decode(it) }.reversed().joinToString("")
|
|
}
|
|
|
|
fun fixUrl(url: String, domain: String): String {
|
|
if (url.startsWith("http")) {
|
|
return url
|
|
}
|
|
if (url.isEmpty()) {
|
|
return ""
|
|
}
|
|
|
|
val startsWithNoHttp = url.startsWith("//")
|
|
if (startsWithNoHttp) {
|
|
return "https:$url"
|
|
} else {
|
|
if (url.startsWith('/')) {
|
|
return domain + url
|
|
}
|
|
return "$domain/$url"
|
|
}
|
|
}
|
|
|
|
fun Int.toRomanNumeral(): String = Symbol.closestBelow(this)
|
|
.let { symbol ->
|
|
if (symbol != null) {
|
|
"$symbol${(this - symbol.decimalValue).toRomanNumeral()}"
|
|
} else {
|
|
""
|
|
}
|
|
}
|
|
|
|
private enum class Symbol(val decimalValue: Int) {
|
|
I(1),
|
|
IV(4),
|
|
V(5),
|
|
IX(9),
|
|
X(10);
|
|
|
|
companion object {
|
|
fun closestBelow(value: Int) =
|
|
entries.toTypedArray()
|
|
.sortedByDescending { it.decimalValue }
|
|
.firstOrNull { value >= it.decimalValue }
|
|
}
|
|
}
|
|
|
|
// steal from https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/aniwave/src/eu/kanade/tachiyomi/animeextension/en/nineanime/AniwaveUtils.kt
|
|
// credits to @samfundev
|
|
object AniwaveUtils {
|
|
|
|
fun encodeVrf(input: String): String {
|
|
val rc4Key = SecretKeySpec("ysJhV6U27FVIjjuk".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 = Base64.encode(vrf, Base64.DEFAULT or Base64.NO_WRAP)
|
|
vrf = vrfShift(vrf)
|
|
vrf = Base64.encode(vrf, Base64.DEFAULT)
|
|
vrf = rot13(vrf)
|
|
val stringVrf = vrf.toString(Charsets.UTF_8)
|
|
return encode(stringVrf)
|
|
}
|
|
|
|
fun decodeVrf(input: String): String {
|
|
var vrf = input.toByteArray()
|
|
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
|
val rc4Key = SecretKeySpec("hlPeNwkncH0fq9so".toByteArray(), "RC4")
|
|
val cipher = Cipher.getInstance("RC4")
|
|
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
|
vrf = cipher.doFinal(vrf)
|
|
return decode(vrf.toString(Charsets.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(-3, 3, -4, 2, -2, 5, 4, 5)[i % 8]
|
|
vrf[i] = vrf[i].plus(shift).toByte()
|
|
}
|
|
return vrf
|
|
}
|
|
}
|
|
|
|
object DumpUtils {
|
|
|
|
private val deviceId = getDeviceId()
|
|
|
|
suspend fun queryApi(method: String, url: String, params: Map<String, String>): String {
|
|
return app.custom(
|
|
method,
|
|
url,
|
|
requestBody = if (method == "POST") params.toJson()
|
|
.toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) else null,
|
|
params = if (method == "GET") params else emptyMap(),
|
|
headers = createHeaders(params)
|
|
).parsedSafe<HashMap<String, String>>()?.get("data").let {
|
|
cryptoHandler(
|
|
it.toString(),
|
|
deviceId,
|
|
false
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun createHeaders(
|
|
params: Map<String, String>,
|
|
currentTime: String = System.currentTimeMillis().toString(),
|
|
): Map<String, String> {
|
|
return mapOf(
|
|
"lang" to "en",
|
|
"currentTime" to currentTime,
|
|
"sign" to getSign(currentTime, params).toString(),
|
|
"aesKey" to getAesKey().toString(),
|
|
)
|
|
}
|
|
|
|
private fun cryptoHandler(
|
|
string: String,
|
|
secretKeyString: String,
|
|
encrypt: Boolean = true
|
|
): String {
|
|
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
|
|
val cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING")
|
|
return if (!encrypt) {
|
|
cipher.init(Cipher.DECRYPT_MODE, secretKey)
|
|
String(cipher.doFinal(base64DecodeArray(string)))
|
|
} else {
|
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
|
base64Encode(cipher.doFinal(string.toByteArray()))
|
|
}
|
|
}
|
|
|
|
private fun getAesKey(): String? {
|
|
val publicKey =
|
|
RSAEncryptionHelper.getPublicKeyFromString(BuildConfig.DUMP_KEY) ?: return null
|
|
return RSAEncryptionHelper.encryptText(deviceId, publicKey)
|
|
}
|
|
|
|
private fun getSign(currentTime: String, params: Map<String, String>): String? {
|
|
val chipper = listOf(
|
|
currentTime,
|
|
params.map { it.value }.reversed().joinToString("")
|
|
.let { base64Encode(it.toByteArray()) }).joinToString("")
|
|
val enc = cryptoHandler(chipper, deviceId)
|
|
return md5(enc)
|
|
}
|
|
|
|
private fun md5(input: String): String {
|
|
val md = MessageDigest.getInstance("MD5")
|
|
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
|
|
}
|
|
|
|
}
|
|
|
|
object RSAEncryptionHelper {
|
|
|
|
private const val RSA_ALGORITHM = "RSA"
|
|
private const val CIPHER_TYPE_FOR_RSA = "RSA/ECB/PKCS1Padding"
|
|
|
|
private val keyFactory = KeyFactory.getInstance(RSA_ALGORITHM)
|
|
private val cipher = Cipher.getInstance(CIPHER_TYPE_FOR_RSA)
|
|
|
|
fun getPublicKeyFromString(publicKeyString: String): PublicKey? =
|
|
try {
|
|
val keySpec =
|
|
X509EncodedKeySpec(Base64.decode(publicKeyString.toByteArray(), Base64.NO_WRAP))
|
|
keyFactory.generatePublic(keySpec)
|
|
} catch (exception: Exception) {
|
|
exception.printStackTrace()
|
|
null
|
|
}
|
|
|
|
fun getPrivateKeyFromString(privateKeyString: String): PrivateKey? =
|
|
try {
|
|
val keySpec =
|
|
PKCS8EncodedKeySpec(Base64.decode(privateKeyString.toByteArray(), Base64.DEFAULT))
|
|
keyFactory.generatePrivate(keySpec)
|
|
} catch (exception: Exception) {
|
|
exception.printStackTrace()
|
|
null
|
|
}
|
|
|
|
fun encryptText(plainText: String, publicKey: PublicKey): String? =
|
|
try {
|
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
|
Base64.encodeToString(cipher.doFinal(plainText.toByteArray()), Base64.NO_WRAP)
|
|
} catch (exception: Exception) {
|
|
exception.printStackTrace()
|
|
null
|
|
}
|
|
|
|
fun decryptText(encryptedText: String, privateKey: PrivateKey): String? =
|
|
try {
|
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
|
String(cipher.doFinal(Base64.decode(encryptedText, Base64.DEFAULT)))
|
|
} catch (exception: Exception) {
|
|
exception.printStackTrace()
|
|
null
|
|
}
|
|
}
|
|
|
|
// code found on https://stackoverflow.com/a/63701411
|
|
|
|
/**
|
|
* Conforming with CryptoJS AES method
|
|
*/
|
|
// see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f
|
|
@Suppress("unused", "FunctionName", "SameParameterValue")
|
|
object CryptoJS {
|
|
|
|
private const val KEY_SIZE = 256
|
|
private const val IV_SIZE = 128
|
|
private const val HASH_CIPHER = "AES/CBC/PKCS7Padding"
|
|
private const val AES = "AES"
|
|
private const val KDF_DIGEST = "MD5"
|
|
|
|
// Seriously crypto-js, what's wrong with you?
|
|
private const val APPEND = "Salted__"
|
|
|
|
/**
|
|
* Encrypt
|
|
* @param password passphrase
|
|
* @param plainText plain string
|
|
*/
|
|
fun encrypt(password: String, plainText: String): String {
|
|
val saltBytes = generateSalt(8)
|
|
val key = ByteArray(KEY_SIZE / 8)
|
|
val iv = ByteArray(IV_SIZE / 8)
|
|
EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv)
|
|
val keyS = SecretKeySpec(key, AES)
|
|
val cipher = Cipher.getInstance(HASH_CIPHER)
|
|
val ivSpec = IvParameterSpec(iv)
|
|
cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec)
|
|
val cipherText = cipher.doFinal(plainText.toByteArray())
|
|
// Thanks kientux for this: https://gist.github.com/kientux/bb48259c6f2133e628ad
|
|
// Create CryptoJS-like encrypted!
|
|
val sBytes = APPEND.toByteArray()
|
|
val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size)
|
|
System.arraycopy(sBytes, 0, b, 0, sBytes.size)
|
|
System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size)
|
|
System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size)
|
|
val bEncode = Base64.encode(b, Base64.NO_WRAP)
|
|
return String(bEncode)
|
|
}
|
|
|
|
/**
|
|
* Decrypt
|
|
* Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051
|
|
* @param password passphrase
|
|
* @param cipherText encrypted string
|
|
*/
|
|
fun decrypt(password: String, cipherText: String): String {
|
|
val ctBytes = Base64.decode(cipherText.toByteArray(), Base64.NO_WRAP)
|
|
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
|
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
|
val key = ByteArray(KEY_SIZE / 8)
|
|
val iv = ByteArray(IV_SIZE / 8)
|
|
EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv)
|
|
val cipher = Cipher.getInstance(HASH_CIPHER)
|
|
val keyS = SecretKeySpec(key, AES)
|
|
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv))
|
|
val plainText = cipher.doFinal(cipherTextBytes)
|
|
return String(plainText)
|
|
}
|
|
|
|
private fun EvpKDF(
|
|
password: ByteArray,
|
|
keySize: Int,
|
|
ivSize: Int,
|
|
salt: ByteArray,
|
|
resultKey: ByteArray,
|
|
resultIv: ByteArray
|
|
): ByteArray {
|
|
return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv)
|
|
}
|
|
|
|
@Suppress("NAME_SHADOWING")
|
|
private fun EvpKDF(
|
|
password: ByteArray,
|
|
keySize: Int,
|
|
ivSize: Int,
|
|
salt: ByteArray,
|
|
iterations: Int,
|
|
hashAlgorithm: String,
|
|
resultKey: ByteArray,
|
|
resultIv: ByteArray
|
|
): ByteArray {
|
|
val keySize = keySize / 32
|
|
val ivSize = ivSize / 32
|
|
val targetKeySize = keySize + ivSize
|
|
val derivedBytes = ByteArray(targetKeySize * 4)
|
|
var numberOfDerivedWords = 0
|
|
var block: ByteArray? = null
|
|
val hash = MessageDigest.getInstance(hashAlgorithm)
|
|
while (numberOfDerivedWords < targetKeySize) {
|
|
if (block != null) {
|
|
hash.update(block)
|
|
}
|
|
hash.update(password)
|
|
block = hash.digest(salt)
|
|
hash.reset()
|
|
// Iterations
|
|
for (i in 1 until iterations) {
|
|
block = hash.digest(block!!)
|
|
hash.reset()
|
|
}
|
|
System.arraycopy(
|
|
block!!, 0, derivedBytes, numberOfDerivedWords * 4,
|
|
min(block.size, (targetKeySize - numberOfDerivedWords) * 4)
|
|
)
|
|
numberOfDerivedWords += block.size / 4
|
|
}
|
|
System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4)
|
|
System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4)
|
|
return derivedBytes // key + iv
|
|
}
|
|
|
|
private fun generateSalt(length: Int): ByteArray {
|
|
return ByteArray(length).apply {
|
|
SecureRandom().nextBytes(this)
|
|
}
|
|
}
|
|
}
|
|
|
|
object AESGCM {
|
|
fun ByteArray.decrypt(pass: String): String {
|
|
val (key, iv) = generateKeyAndIv(pass)
|
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, iv))
|
|
return String(cipher.doFinal(this), StandardCharsets.UTF_8)
|
|
}
|
|
|
|
private fun generateKeyAndIv(pass: String): Pair<ByteArray, ByteArray> {
|
|
val datePart = getCurrentUTCDateString().take(16)
|
|
val hexString = datePart + pass
|
|
val byteArray = hexString.toByteArray(StandardCharsets.UTF_8)
|
|
val digest = MessageDigest.getInstance("SHA-256").digest(byteArray)
|
|
return digest.copyOfRange(0, digest.size / 2) to digest.copyOfRange(
|
|
digest.size / 2,
|
|
digest.size
|
|
)
|
|
}
|
|
|
|
private fun getCurrentUTCDateString(): String {
|
|
val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.getDefault())
|
|
dateFormat.timeZone = TimeZone.getTimeZone("GMT")
|
|
return dateFormat.format(Date())
|
|
}
|
|
}
|