diff --git a/SoraStream/src/main/kotlin/com/hexated/CryptoAES.kt b/SoraStream/src/main/kotlin/com/hexated/CryptoAES.kt new file mode 100644 index 00000000..ca29b4b8 --- /dev/null +++ b/SoraStream/src/main/kotlin/com/hexated/CryptoAES.kt @@ -0,0 +1,133 @@ +package com.hexated + +import android.util.Base64 +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.math.min + +// 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 CryptoAES { + + private const val KEY_SIZE = 256 + private const val IV_SIZE = 128 + private const val HASH_CIPHER = "AES/CBC/PKCS5Padding" + 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) + } + } +} \ No newline at end of file diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index b27af6ea..b6a0340a 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -1,5 +1,6 @@ package com.hexated +import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* @@ -14,6 +15,7 @@ import com.lagradost.nicehttp.RequestBodyTypes import kotlinx.coroutines.delay import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody +import okio.ByteString.Companion.encode import org.jsoup.Jsoup val session = Session(Requests().baseClient) @@ -2022,6 +2024,135 @@ object SoraExtractor : SoraStream() { } } + //TODO only subs + suspend fun invokeWatchsomuch( + imdbId: String? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + ) { + val watchSomuchAPI = "https://watchsomuch.tv" + val id = imdbId?.removePrefix("tt") + val epsId = app.post( + "$watchSomuchAPI/Watch/ajMovieTorrents.aspx", + data = mapOf( + "index" to "0", + "mid" to "$id", + "wsk" to "f6ea6cde-e42b-4c26-98d3-b4fe48cdd4fb", + "lid" to "", + "liu" to "" + ), headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsedSafe()?.movie?.torrents?.let { eps -> + if (season == null) { + eps.firstOrNull()?.id + } else { + eps.find { it.episode == episode && it.season == season }?.id + } + } ?: return + + val subUrl = if (season == null) { + "$watchSomuchAPI/Watch/ajMovieSubtitles.aspx?mid=$id&tid=$epsId&part=" + } else { + "$watchSomuchAPI/Watch/ajMovieSubtitles.aspx?mid=$id&tid=$epsId&part=S0${season}E0${episode}" + } + + app.get(subUrl) + .parsedSafe()?.subtitles + ?.filter { it.url?.startsWith("https") == true } + ?.map { sub -> + Log.i("hexated", "${sub.label} => ${sub.url}") + subtitleCallback.invoke( + SubtitleFile( + sub.label ?: "", + sub.url ?: return@map null + ) + ) + } + + + } + + suspend fun invokeBaymovies( + title: String? = null, + year: Int? = null, + season: Int? = null, + episode: Int? = null, + callback: (ExtractorLink) -> Unit, + ) { + val key = base64DecodeAPI("ZW0=c3Q=c3k=b28=YWQ=Ymg=") + val headers = mapOf( + "Referer" to "$baymovies/", + "Origin" to baymovies, + "cf_cache_token" to "UKsVpQqBMxB56gBfhYKbfCVkRIXMh42pk6G4DdkXXoVh7j4BjV" + ) + + val titleSlug = title.fixTitle()?.replace("-", ".") ?: return + val (episodeSlug, seasonSlug) = if (season == null) { + listOf("", "") + } else { + listOf( + if (episode!! < 10) "0$episode" else episode, + if (season < 10) "0$season" else season + ) + } + + val query = if (season == null) { + "$title $year" + } else { + "$title S${episodeSlug}E${seasonSlug}" + } + + val media = + app.get("$baymoviesAPI//0:search?q=$query&page_token=&page_index=0", headers = headers) + .parsedSafe()?.data?.files?.filter { media -> + (if (season == null) { + media.name?.contains("$year") == true + } else { + media.name?.contains(Regex("(?i)S${episodeSlug}E${seasonSlug}")) == true + }) && media.name?.contains( + "720p", + true + ) == false && (media.mimeType == "video/x-matroska" || media.mimeType == "video/mp4") && (media.name.contains( + titleSlug, + true + ) || media.name.contains(title ?: return, true)) + }?.distinctBy { it.name } ?: return + + media.apmap { file -> + val expiry = (System.currentTimeMillis() + 345600000).toString() + val hmacSign = "${file.id}@$expiry".encode() + .hmacSha256(key.encode()).base64().replace("+", "-") + val encryptedId = + base64Encode(CryptoAES.encrypt(key, file.id ?: return@apmap null).toByteArray()) + val encryptedExpiry = base64Encode(CryptoAES.encrypt(key, expiry).toByteArray()) + val worker = getConfig().workers.randomOrNull() ?: return@apmap null + + val link = "https://api.$worker.workers.dev/download.aspx?file=$encryptedId&expiry=$encryptedExpiry&mac=$hmacSign" + val size = file.size?.toDouble() ?: return@apmap null + val sizeFile = "%.2f GB".format(bytesToGigaBytes(size)) + val tags = Regex("\\d{3,4}[pP]\\.?(.*?)\\.(mkv|mp4)").find( + file.name ?: return@apmap null + )?.groupValues?.getOrNull(1)?.replace(".", " ")?.trim() + ?: "" + val quality = + Regex("(\\d{3,4})[pP]").find(file.name)?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + + callback.invoke( + ExtractorLink( + "Baymovies $tags [$sizeFile]", + "Baymovies $tags [$sizeFile]", + link, + "", + quality, + ) + ) + + } + + + } + } class StreamM4u : XStreamCdn() { @@ -2041,6 +2172,12 @@ data class FDMovieIFrame( val type: String, ) +data class BaymoviesConfig( + val country: String, + val downloadTime: String, + val workers: List +) + data class Movie123Media( @JsonProperty("url") val url: String? = null, ) @@ -2300,4 +2437,45 @@ data class Smashy1Tracks( data class Smashy1Source( @JsonProperty("file") val file: String? = null, @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), +) + +data class WatchsomuchTorrents( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("movieId") val movieId: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, +) + +data class WatchsomuchMovies( + @JsonProperty("torrents") val torrents: ArrayList? = arrayListOf(), +) + +data class WatchsomuchResponses( + @JsonProperty("movie") val movie: WatchsomuchMovies? = null, +) + +data class WatchsomuchSubtitles( + @JsonProperty("url") val url: String? = null, + @JsonProperty("label") val label: String? = null, +) + +data class WatchsomuchSubResponses( + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), +) + +data class Baymovies( + @JsonProperty("id") val id: String? = null, + @JsonProperty("driveId") val driveId: String? = null, + @JsonProperty("mimeType") val mimeType: String? = null, + @JsonProperty("size") val size: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("modifiedTime") val modifiedTime: String? = null, +) + +data class BaymoviesData( + @JsonProperty("files") val files: ArrayList? = arrayListOf(), +) + +data class BaymoviesSearch( + @JsonProperty("data") val data: BaymoviesData? = null, ) \ No newline at end of file diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt index cfdc659e..ee99741a 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -3,6 +3,7 @@ package com.hexated import com.fasterxml.jackson.annotation.JsonProperty import com.hexated.SoraExtractor.invoke123Movie import com.hexated.SoraExtractor.invokeAnimes +import com.hexated.SoraExtractor.invokeBaymovies import com.hexated.SoraExtractor.invokeBollyMaza import com.hexated.SoraExtractor.invokeDbgo import com.hexated.SoraExtractor.invokeFilmxy @@ -65,12 +66,11 @@ open class SoraStream : TmdbProvider() { const val consumetAnilistAPI = "https://api.consumet.org/meta/anilist" const val kamyrollAPI = "https://api.kamyroll.tech" - private val mainAPI = - base64DecodeAPI("cHA=LmE=ZWw=cmM=dmU=aC4=dGM=d2E=eHA=Ly8=czo=dHA=aHQ=") - + private val mainAPI = base64DecodeAPI("cHA=LmE=ZWw=cmM=dmU=aC4=dGM=d2E=eHA=Ly8=czo=dHA=aHQ=") + var baymovies = base64DecodeAPI("Zw==b3I=dS4=LmU=ZXg=bmQ=emk=aS4=YXA=dXA=cm8=Y2c=bGk=dWI=eHA=ZGU=aW4=YXk=ZWI=dGg=Ly8=czo=dHA=aHQ=") // private var mainServerAPI = base64DecodeAPI("cA==YXA=bC4=Y2U=ZXI=LnY=aWU=b3Y=LW0=cmE=c28=Ly8=czo=dHA=aHQ=") - var netMoviesAPI = - base64DecodeAPI("aQ==YXA=cC8=YXA=bC4=Y2U=ZXI=LnY=bG0=Zmk=dC0=bmU=Ly8=czo=dHA=aHQ=") + var netMoviesAPI = base64DecodeAPI("aQ==YXA=cC8=YXA=bC4=Y2U=ZXI=LnY=bG0=Zmk=dC0=bmU=Ly8=czo=dHA=aHQ=") + const val twoEmbedAPI = "https://www.2embed.to" const val vidSrcAPI = "https://v2.vidsrc.me" const val dbgoAPI = "https://dbgo.fun" @@ -105,6 +105,7 @@ open class SoraStream : TmdbProvider() { const val animeKaizokuAPI = "https://animekaizoku.com" const val movie123NetAPI = "https://ww7.0123movie.net" const val smashyStreamAPI = "https://embed.smashystream.com" + const val baymoviesAPI = "https://thebayindexpublicgroupapi.zindex.eu.org" fun getType(t: String?): TvType { return when (t) { @@ -529,6 +530,15 @@ open class SoraStream : TmdbProvider() { }, { invokeSmashyStream(res.id, res.season, res.episode, subtitleCallback, callback) + }, + { + if(!res.isAnime) invokeBaymovies( + res.title, + res.year, + res.season, + res.episode, + callback + ) } ) diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index 453287da..0f8fc705 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -1,15 +1,13 @@ package com.hexated +import com.hexated.SoraStream.Companion.baymovies import com.hexated.SoraStream.Companion.consumetCrunchyrollAPI import com.hexated.SoraStream.Companion.filmxyAPI import com.hexated.SoraStream.Companion.gdbot import com.hexated.SoraStream.Companion.kamyrollAPI import com.hexated.SoraStream.Companion.tvMoviesAPI +import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getCaptchaToken -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Decode -import com.lagradost.cloudstream3.base64Encode import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -600,6 +598,23 @@ fun List>?.matchingEpisode(episode: Int?): String? { }?.get("id") } +suspend fun getConfig(): BaymoviesConfig { + val regex = """const country = "(.*?)"; +const downloadtime = "(.*?)"; +var arrayofworkers = (.*)""".toRegex() + val js = app.get( + "https://geolocation.zindex.eu.org/api.js", + referer = "$baymovies/", + ).text + val match = regex.find(js) ?: throw ErrorLoadingException() + val country = match.groupValues[1] + val downloadTime = match.groupValues[2] + val workers = tryParseJson>(match.groupValues[3]) + ?: throw ErrorLoadingException() + + return BaymoviesConfig(country, downloadTime, workers) +} + fun String?.fixTitle(): String? { return this?.replace(Regex("[!%:'?,]|( &)"), "")?.replace(" ", "-")?.lowercase() ?.replace("-–-", "-") @@ -609,6 +624,8 @@ 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-Z0-9]"), "-") }