update few providers

This commit is contained in:
hexated 2023-09-09 19:16:04 +07:00
parent 471a1703a3
commit dd612e12bc
32 changed files with 370 additions and 248 deletions

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 1
version = 2
cloudstream {

View file

@ -150,8 +150,8 @@ class Animasu : MainAPI() {
link.name,
link.url,
link.referer,
if(!link.isM3u8) getIndexQuality(quality) else link.quality,
link.isM3u8,
if(link.type != ExtractorLinkType.M3U8) getIndexQuality(quality) else link.quality,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 8
version = 9
cloudstream {

View file

@ -3,6 +3,7 @@ package com.hexated
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.nicehttp.NiceResponse
@ -144,7 +145,7 @@ class AnimeSailProvider : MainAPI() {
Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src")
?: throw ErrorLoadingException("No iframe found")
)
val quality = getIndexQuality(it.text())
when {
iframe.startsWith("$mainUrl/utils/player/arch/") || iframe.startsWith(
"$mainUrl/utils/player/race/"
@ -156,15 +157,13 @@ class AnimeSailProvider : MainAPI() {
iframe.contains("/race/") -> "Race"
else -> this.name
}
val quality =
Regex("\\.(\\d{3,4})\\.").find(link)?.groupValues?.get(1)
callback.invoke(
ExtractorLink(
source = source,
name = source,
url = link,
referer = mainUrl,
quality = quality?.toIntOrNull() ?: Qualities.Unknown.value
quality = quality
)
)
}
@ -175,16 +174,16 @@ class AnimeSailProvider : MainAPI() {
val link = "https://rasa-cintaku-semakin-berantai.xyz/v/${
iframe.substringAfter("id=").substringBefore("&token")
}"
loadExtractor(link, mainUrl, subtitleCallback, callback)
loadFixedExtractor(link, quality, mainUrl, subtitleCallback, callback)
}
iframe.startsWith("$mainUrl/utils/player/framezilla/") || iframe.startsWith("https://uservideo.xyz") -> {
request(iframe, ref = data).document.select("iframe").attr("src")
.let { link ->
loadExtractor(fixUrl(link), mainUrl, subtitleCallback, callback)
loadFixedExtractor(fixUrl(link), quality, mainUrl, subtitleCallback, callback)
}
}
else -> {
loadExtractor(iframe, mainUrl, subtitleCallback, callback)
loadFixedExtractor(iframe, quality, mainUrl, subtitleCallback, callback)
}
}
}
@ -193,4 +192,32 @@ class AnimeSailProvider : MainAPI() {
return true
}
private suspend fun loadFixedExtractor(
url: String,
quality: Int?,
referer: String? = null,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
loadExtractor(url, referer, subtitleCallback) { link ->
callback.invoke(
ExtractorLink(
link.name,
link.name,
link.url,
link.referer,
if(link.type == ExtractorLinkType.M3U8) link.quality else quality ?: Qualities.Unknown.value,
link.type,
link.headers,
link.extractorData
)
)
}
}
private fun getIndexQuality(str: String): Int {
return Regex("(\\d{3,4})[pP]").find(str)?.groupValues?.getOrNull(1)?.toIntOrNull()
?: Qualities.Unknown.value
}
}

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 5
version = 6
cloudstream {

View file

@ -158,7 +158,7 @@ open class Aniworld : MainAPI() {
link.url,
link.referer,
link.quality,
link.isM3u8,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 10
version = 11
cloudstream {

View file

@ -121,8 +121,8 @@ class Nodrakorid : DutaMovie() {
link.name,
link.url,
link.referer,
if(link.isM3u8) link.quality else quality ?: Qualities.Unknown.value,
link.isM3u8,
if(link.type == ExtractorLinkType.M3U8) link.quality else quality ?: Qualities.Unknown.value,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 10
version = 11
cloudstream {

View file

@ -213,7 +213,7 @@ class Hdfilmcehennemi : MainAPI() {
link.url,
link.referer,
link.quality,
link.isM3u8,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 14
version = 15
cloudstream {

View file

@ -3,6 +3,7 @@ package com.hexated
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.extractors.helper.AesHelper
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
@ -10,12 +11,7 @@ import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.nodes.Element
import java.net.URI
import java.security.DigestException
import java.security.MessageDigest
import java.util.ArrayList
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class KuronimeProvider : MainAPI() {
override var mainUrl = "https://45.12.2.26"
@ -186,10 +182,11 @@ class KuronimeProvider : MainAPI() {
argamap(
{
val decrypt = cryptoAES(
servers?.src ?: return@argamap,
val decrypt = AesHelper.cryptoAESHandler(
base64Decode(servers?.src ?: return@argamap),
KEY.toByteArray(),
false
false,
"AES/CBC/NoPadding"
)
val source =
tryParseJson<Sources>(decrypt?.toJsonFormat())?.src?.replace("\\", "")
@ -206,10 +203,11 @@ class KuronimeProvider : MainAPI() {
)
},
{
val decrypt = cryptoAES(
servers?.mirror ?: return@argamap,
val decrypt = AesHelper.cryptoAESHandler(
base64Decode(servers?.mirror ?: return@argamap),
KEY.toByteArray(),
false
false,
"AES/CBC/NoPadding"
)
tryParseJson<Mirrors>(decrypt)?.embed?.map { embed ->
embed.value.apmap {
@ -249,7 +247,7 @@ class KuronimeProvider : MainAPI() {
link.url,
link.referer,
getQualityFromName(quality),
link.isM3u8,
link.type,
link.headers,
link.extractorData
)
@ -263,86 +261,6 @@ class KuronimeProvider : MainAPI() {
}
}
// https://stackoverflow.com/a/41434590/8166854
private fun generateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
return listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
)
} catch (e: DigestException) {
return null
}
}
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
private fun cryptoAES(
data: String,
pass: ByteArray,
encrypt: Boolean = true
): String? {
val json = tryParseJson<AesData>(base64Decode(data))
?: throw ErrorLoadingException("No Data Found")
val (key, iv) = generateKeyAndIv(pass, json.s.decodeHex()) ?: return null
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(json.ct)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(json.ct.toByteArray()))
}
}
data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
data class Mirrors(
@JsonProperty("embed") val embed: Map<String, Map<String, String>> = emptyMap(),
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 7
version = 8
cloudstream {

View file

@ -17,7 +17,7 @@ class Paistream : Streampai() {
}
class TvMinioppai : Streampai() {
override val name = "Tv.Minioppai"
override val name = "Minioppai"
override val mainUrl = "https://tv.minioppai.org"
}

View file

@ -47,7 +47,7 @@ class Minioppai : MainAPI() {
override val mainPage = mainPageOf(
"$mainUrl/watch" to "New Episode",
"$mainUrl/popular" to "Popular Hentai",
"$mainUrl/populars" to "Popular Hentai",
)
override suspend fun getMainPage(

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 5
version = 6
cloudstream {

View file

@ -170,8 +170,8 @@ class Nekopoi : MainAPI() {
link.name,
link.url,
link.referer,
if (link.isM3u8) link.quality else it.first,
link.isM3u8,
if (link.type == ExtractorLinkType.M3U8) link.quality else it.first,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 3
version = 4
cloudstream {

View file

@ -164,7 +164,7 @@ class Nimegami : MainAPI() {
link.url,
link.referer,
getQualityFromName(quality),
link.isM3u8,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 22
version = 23
cloudstream {

View file

@ -195,7 +195,7 @@ class OploverzProvider : MainAPI() {
link.url,
link.referer,
name.fixQuality(),
link.isM3u8,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 13
version = 14
cloudstream {

View file

@ -236,7 +236,7 @@ class OtakudesuProvider : MainAPI() {
link.url,
link.referer,
quality,
link.isM3u8,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,5 +1,5 @@
// use an integer for version numbers
version = 13
version = 14
cloudstream {

View file

@ -203,7 +203,7 @@ class Samehadaku : MainAPI() {
link.url,
link.referer,
name.fixQuality(),
link.isM3u8,
link.type,
link.headers,
link.extractorData
)

View file

@ -1,7 +1,7 @@
import org.jetbrains.kotlin.konan.properties.Properties
// use an integer for version numbers
version = 163
version = 164
android {
defaultConfig {

View file

@ -0,0 +1,162 @@
package com.hexated
import com.lagradost.cloudstream3.extractors.Filesim
import com.lagradost.cloudstream3.extractors.GMPlayer
import com.lagradost.cloudstream3.extractors.StreamSB
import com.lagradost.cloudstream3.extractors.Voe
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import java.math.BigInteger
import java.security.MessageDigest
open class Playm4u : ExtractorApi() {
override val name = "Playm4u"
override val mainUrl = "https://play9str.playm4u.xyz"
override val requiresReferer = true
private val password = "plhq@@@22"
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url, referer = referer).document
val script = document.selectFirst("script:containsData(idfile =)")?.data() ?: return
val passScript = document.selectFirst("script:containsData(domain_ref =)")?.data() ?: return
val pass = passScript.substringAfter("CryptoJS.MD5('").substringBefore("')")
val amount = passScript.substringAfter(".toString()), ").substringBefore("));").toInt()
val idFile = "idfile\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
val idUser = "idUser\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
val domainApi = "DOMAIN_API\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
val nameKeyV3 = "NameKeyV3\\s*=\\s*[\"'](\\S+)[\"'];".findIn(script)
val dataEnc = caesarShift(
mahoa(
"Win32|$idUser|$idFile|$referer",
md5(pass)
), amount
).toHex()
val captchaKey =
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
.attr("src").substringAfter("render=")
val token = getCaptchaToken(
url,
captchaKey,
referer = referer
)
val source = app.post(
domainApi, data = mapOf(
"namekey" to nameKeyV3,
"token" to "$token",
"referrer" to "$referer",
"data" to "$dataEnc|${md5(dataEnc + password)}",
), referer = "$mainUrl/"
).parsedSafe<Source>()
callback.invoke(
ExtractorLink(
this.name,
this.name,
source?.data ?: return,
"$mainUrl/",
Qualities.P1080.value,
INFER_TYPE
)
)
subtitleCallback.invoke(
SubtitleFile(
source.sub?.substringBefore("|")?.toLanguage() ?: return,
source.sub.substringAfter("|"),
)
)
}
private fun caesarShift(str: String, amount: Int): String {
var output = ""
val adjustedAmount = if (amount < 0) amount + 26 else amount
for (element in str) {
var c = element
if (c.isLetter()) {
val code = c.code
c = when (code) {
in 65..90 -> ((code - 65 + adjustedAmount) % 26 + 65).toChar()
in 97..122 -> ((code - 97 + adjustedAmount) % 26 + 97).toChar()
else -> c
}
}
output += c
}
return output
}
private fun mahoa(input: String, key: String): String {
val a = CryptoJS.encrypt(key, input)
return a.replace("U2FsdGVkX1", "")
.replace("/", "|a")
.replace("+", "|b")
.replace("=", "|c")
.replace("|", "-z")
}
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
private fun String.toHex(): String {
return this.toByteArray().joinToString("") { "%02x".format(it) }
}
private fun String.findIn(data: String): String {
return this.toRegex().find(data)?.groupValues?.get(1) ?: ""
}
private fun String.toLanguage() : String {
return if(this == "EN") "English" else this
}
data class Source(
@JsonProperty("data") val data: String? = null,
@JsonProperty("sub") val sub: String? = null,
)
}
class TravelR : GMPlayer() {
override val name = "TravelR"
override val mainUrl = "https://travel-russia.xyz"
}
class Mwish : Filesim() {
override val name = "Mwish"
override var mainUrl = "https://mwish.pro"
}
class Animefever : Filesim() {
override val name = "Animefever"
override var mainUrl = "https://animefever.fun"
}
class Multimovies : Filesim() {
override val name = "Multimovies"
override var mainUrl = "https://multimovies.cloud"
}
class MultimoviesSB : StreamSB() {
override var name = "Multimovies"
override var mainUrl = "https://multimovies.website"
}
class Yipsu : Voe() {
override val name = "Yipsu"
override var mainUrl = "https://yip.su"
}

View file

@ -1,6 +1,5 @@
package com.hexated
import com.hexated.AesHelper.cryptoAESHandler
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
@ -10,6 +9,7 @@ import com.lagradost.cloudstream3.extractors.Filesim
import com.lagradost.cloudstream3.extractors.GMPlayer
import com.lagradost.cloudstream3.extractors.StreamSB
import com.lagradost.cloudstream3.extractors.Voe
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
import com.lagradost.cloudstream3.extractors.helper.GogoHelper
import com.lagradost.cloudstream3.network.CloudflareKiller
import com.lagradost.nicehttp.RequestBodyTypes
@ -127,7 +127,7 @@ object SoraExtractor : SoraStream() {
link.url,
link.referer,
if (link.name == "VidSrc") Qualities.P1080.value else link.quality,
link.isM3u8,
link.type,
link.headers,
link.extractorData
)
@ -273,7 +273,7 @@ object SoraExtractor : SoraStream() {
video.url,
video.referer,
Qualities.P1080.value,
video.isM3u8,
video.type,
video.headers,
video.extractorData
)
@ -415,7 +415,7 @@ object SoraExtractor : SoraStream() {
} else {
"$idlixAPI/episode/$fixTitle-season-$season-episode-$episode"
}
invokeWpmovies(url, subtitleCallback, callback, encrypt = true, key = "\\x5a\\x6d\\x5a\\x6c\\x4e\\x7a\\x55\\x79\\x4d\\x54\\x56\\x6a\\x5a\\x47\\x52\\x69\\x5a\\x44\\x55\\x30\\x5a\\x6d\\x59\\x35\\x4f\\x57\\x45\\x33\\x4d\\x44\\x4a\\x69\\x4e\\x32\\x4a\\x6c\\x4f\\x54\\x42\\x6c\\x4e\\x7a\\x49\\x3d")
invokeWpmovies(url, subtitleCallback, callback, encrypt = true)
}
suspend fun invokeMultimovies(
@ -480,7 +480,7 @@ object SoraExtractor : SoraStream() {
)
val source = tryParseJson<ResponseHash>(json.text)?.let {
when {
encrypt -> cryptoAESHandler(it.embed_url,(key ?: return@apmap).toByteArray(), false)?.fixBloat()
encrypt -> cryptoAESHandler(it.embed_url,(it.key ?: return@apmap).toByteArray(), false)?.fixBloat()
fixIframe -> Jsoup.parse(it.embed_url).select("IFRAME").attr("SRC")
else -> it.embed_url
}
@ -711,10 +711,10 @@ object SoraExtractor : SoraStream() {
link.url,
link.referer,
when {
link.isM3u8 -> link.quality
link.type == ExtractorLinkType.M3U8 -> link.quality
else -> getQualityFromName(it.first)
},
link.isM3u8,
link.type,
link.headers,
link.extractorData
)
@ -1039,10 +1039,10 @@ object SoraExtractor : SoraStream() {
link.url,
link.referer,
when {
link.isM3u8 -> link.quality
link.type == ExtractorLinkType.M3U8 -> link.quality
else -> it.third
},
link.isM3u8,
link.type,
link.headers,
link.extractorData
)
@ -1517,7 +1517,9 @@ object SoraExtractor : SoraStream() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val res = app.get("$m4uhdAPI/search/${title.createSlug()}.html").document
val req = app.get("$m4uhdAPI/search/${title.createSlug()}.html")
val referer = getBaseUrl(req.url)
val res = req.document
val scriptData = res.select("div.row div.item").map {
Triple(
it.selectFirst("img.imagecover")?.attr("title"),
@ -1538,7 +1540,7 @@ object SoraExtractor : SoraStream() {
}
}
val link = fixUrl(script?.third ?: return, m4uhdAPI)
val link = fixUrl(script?.third ?: return, referer)
val request = app.get(link)
var cookiesSet = request.headers.filter { it.first == "set-cookie" }
var xsrf =
@ -1558,7 +1560,7 @@ object SoraExtractor : SoraStream() {
?: return
val idepisode = episodeData.select("button").attr("idepisode") ?: return
val requestEmbed = app.post(
"$m4uhdAPI/ajaxtv", data = mapOf(
"$referer/ajaxtv", data = mapOf(
"idepisode" to idepisode, "_token" to "$token"
), referer = link, headers = mapOf(
"X-Requested-With" to "XMLHttpRequest",
@ -1572,14 +1574,16 @@ object SoraExtractor : SoraStream() {
cookiesSet.find { it.second.contains("XSRF-TOKEN") }?.second?.substringAfter("XSRF-TOKEN=")
?.substringBefore(";")
session =
cookiesSet.find { it.second.contains("laravel_session") }?.second?.substringAfter("laravel_session=")
cookiesSet.find { it.second.contains("laravel_session") }?.second?.substringAfter(
"laravel_session="
)
?.substringBefore(";")
requestEmbed.document.select("div.le-server span").map { it.attr("data") }
}
m4uData.apmap { data ->
val iframe = app.post(
"$m4uhdAPI/ajax",
"$referer/ajax",
data = mapOf(
"m4u" to data, "_token" to "$token"
),
@ -1594,7 +1598,7 @@ object SoraExtractor : SoraStream() {
),
).document.select("iframe").attr("src")
loadExtractor(iframe, m4uhdAPI, subtitleCallback, callback)
loadExtractor(iframe, referer, subtitleCallback, callback)
}
}
@ -2902,33 +2906,3 @@ object SoraExtractor : SoraStream() {
}
class TravelR : GMPlayer() {
override val name = "TravelR"
override val mainUrl = "https://travel-russia.xyz"
}
class Mwish : Filesim() {
override val name = "Mwish"
override var mainUrl = "https://mwish.pro"
}
class Animefever : Filesim() {
override val name = "Animefever"
override var mainUrl = "https://animefever.fun"
}
class Multimovies : Filesim() {
override val name = "Multimovies"
override var mainUrl = "https://multimovies.cloud"
}
class MultimoviesSB : StreamSB() {
override var name = "Multimovies"
override var mainUrl = "https://multimovies.website"
}
class Yipsu : Voe() {
override val name = "Yipsu"
override var mainUrl = "https://yip.su"
}

View file

@ -126,7 +126,7 @@ open class SoraStream : TmdbProvider() {
const val emoviesAPI = "https://emovies.si"
const val pobmoviesAPI = "https://pobmovies.cam"
const val fourCartoonAPI = "https://4cartoon.net"
const val multimoviesAPI = "https://multimovies.xyz"
const val multimoviesAPI = "https://multi-movies.xyz"
const val netmoviesAPI = "https://netmovies.to"
const val momentAPI = "https://moment-explanation-i-244.site"
const val doomoviesAPI = "https://doomovies.net"
@ -539,7 +539,7 @@ open class SoraStream : TmdbProvider() {
)
},
{
invokeM4uhd(
if(!res.isAnime) invokeM4uhd(
res.title,
res.year,
res.season,

View file

@ -257,7 +257,7 @@ class SoraStreamLite : SoraStream() {
invokeFwatayako(res.imdbId, res.season, res.episode, callback)
},
{
invokeM4uhd(
if(!res.isAnime) invokeM4uhd(
res.title,
res.year,
res.season,

View file

@ -17,5 +17,6 @@ class SoraStreamPlugin: Plugin() {
registerExtractorAPI(Yipsu())
registerExtractorAPI(Mwish())
registerExtractorAPI(TravelR())
registerExtractorAPI(Playm4u())
}
}

View file

@ -1,7 +1,6 @@
package com.hexated
import android.util.Base64
import com.fasterxml.jackson.annotation.JsonProperty
import com.hexated.DumpUtils.queryApi
import com.hexated.SoraStream.Companion.anilistAPI
import com.hexated.SoraStream.Companion.base64DecodeAPI
@ -41,6 +40,7 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.collections.ArrayList
import kotlin.math.min
val bflixChipperKey = base64DecodeAPI("Yjc=ejM=TzA=YTk=WHE=WnU=bXU=RFo=")
const val bflixKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
@ -1143,10 +1143,10 @@ suspend fun loadCustomExtractor(
link.url,
link.referer,
when {
link.isM3u8 -> link.quality
link.type == ExtractorLinkType.M3U8 -> link.quality
else -> quality ?: link.quality
},
link.isM3u8,
link.type,
link.headers,
link.extractorData
)
@ -1725,85 +1725,125 @@ object RSAEncryptionHelper {
}
}
object AesHelper {
// code found on https://stackoverflow.com/a/63701411
fun cryptoAESHandler(
data: String,
pass: ByteArray,
encrypt: Boolean = true,
padding: String = "AES/CBC/PKCS5PADDING",
): String? {
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: throw ErrorLoadingException("failed to generate key")
val cipher = Cipher.getInstance(padding)
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(parse.ct)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(parse.ct.toByteArray()))
}
/**
* 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)
}
// https://stackoverflow.com/a/41434590/8166854
private fun generateKeyAndIv(
/**
* 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,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): List<ByteArray>? {
resultKey: ByteArray,
resultIv: ByteArray
): ByteArray {
return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv)
}
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
@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)
}
return listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
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)
)
} catch (e: DigestException) {
return null
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)
}
}
private fun String.hexToByteArray(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
private data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
}