diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index 175bd10b..79e94c36 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 119 +version = 120 cloudstream { diff --git a/SoraStream/src/main/kotlin/com/hexated/GogoExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/GogoExtractor.kt new file mode 100644 index 00000000..ed19fd25 --- /dev/null +++ b/SoraStream/src/main/kotlin/com/hexated/GogoExtractor.kt @@ -0,0 +1,166 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.nodes.Document +import java.net.URI +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object GogoExtractor { + + /** + * @param id base64Decode(show_id) + IV + * @return the encryption key + * */ + private fun getKey(id: String): String? { + return normalSafeApiCall { + id.map { + it.code.toString(16) + }.joinToString("").substring(0, 32) + } + } + + // https://github.com/saikou-app/saikou/blob/3e756bd8e876ad7a9318b17110526880525a5cd3/app/src/main/java/ani/saikou/anime/source/extractors/GogoCDN.kt#L60 + // No Licence on the function + private fun cryptoHandler( + string: String, + iv: String, + secretKeyString: String, + encrypt: Boolean = true + ): String { + //println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") + val ivParameterSpec = IvParameterSpec(iv.toByteArray()) + val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES") + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) + String(cipher.doFinal(base64DecodeArray(string))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) + base64Encode(cipher.doFinal(string.toByteArray())) + } + } + + /** + * @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX + * @param mainApiName used for ExtractorLink names and source + * @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off + * @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off + * @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off + * @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey() + * @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value + * */ + suspend fun extractVidstream( + iframeUrl: String, + mainApiName: String, + callback: (ExtractorLink) -> Unit, + iv: String?, + secretKey: String?, + secretDecryptKey: String?, + // This could be removed, but i prefer it verbose + isUsingAdaptiveKeys: Boolean, + isUsingAdaptiveData: Boolean, + // If you don't want to re-fetch the document + iframeDocument: Document? = null + ) = safeApiCall { + // https://github.com/saikou-app/saikou/blob/3e756bd8e876ad7a9318b17110526880525a5cd3/app/src/main/java/ani/saikou/anime/source/extractors/GogoCDN.kt + // No Licence on the following code + // Also modified of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/gogoanime/src/eu/kanade/tachiyomi/animeextension/en/gogoanime/extractors/GogoCdnExtractor.kt + // License on the code above https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE + + if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys) + return@safeApiCall + + val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=") + + var document: Document? = iframeDocument + val foundIv = + iv ?: (document ?: app.get(iframeUrl).document.also { document = it }) + .select("""div.wrapper[class*=container]""") + .attr("class").split("-").lastOrNull() ?: return@safeApiCall + val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall + val foundDecryptKey = secretDecryptKey ?: foundKey + + val uri = URI(iframeUrl) + val mainUrl = "https://" + uri.host + + val encryptedId = cryptoHandler(id, foundIv, foundKey) + val encryptRequestData = if (isUsingAdaptiveData) { + // Only fetch the document if necessary + val realDocument = document ?: app.get(iframeUrl).document + val dataEncrypted = + realDocument.select("script[data-name='episode']").attr("data-value") + val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false) + "id=$encryptedId&alias=$id&" + headers.substringAfter("&") + } else { + "id=$encryptedId&alias=$id" + } + + val jsonResponse = + app.get( + "$mainUrl/encrypt-ajax.php?$encryptRequestData", + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ) + val dataencrypted = + jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}") + val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false) + val sources = AppUtils.parseJson(datadecrypted) + + suspend fun invokeGogoSource( + source: GogoSource, + sourceCallback: (ExtractorLink) -> Unit + ) { + if (source.file.contains(".m3u8")) { + M3u8Helper.generateM3u8( + mainApiName, + source.file, + mainUrl, + headers = mapOf("Origin" to "https://plyr.link") + ).forEach(sourceCallback) + } else { + sourceCallback.invoke( + ExtractorLink( + mainApiName, + mainApiName, + source.file, + mainUrl, + getQualityFromName(source.label), + ) + ) + } + } + + sources.source?.forEach { + invokeGogoSource(it, callback) + } + sources.sourceBk?.forEach { + invokeGogoSource(it, callback) + } + } + + data class GogoSources( + @JsonProperty("source") val source: List?, + @JsonProperty("sourceBk") val sourceBk: List?, + //val track: List, + //val advertising: List, + //val linkiframe: String + ) + + data class GogoSource( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("default") val default: String? = null + ) +} diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index 8ba3a0d8..5e2b30b7 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -322,46 +322,72 @@ object SoraExtractor : SoraStream() { suspend fun invokeSeries9( title: String? = null, + year: Int? = null, season: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { val fixTitle = title.createSlug() - val url = if (season == null) { - "$series9API/film/$fixTitle/watching.html" + val doc = if (season == null) { + val res = app.get("$series9API/film/$fixTitle/watching.html") + if (!res.isSuccessful) app.get("$series9API/film/$fixTitle-$year/watching.html").document else res.document } else { - "$series9API/film/$fixTitle-season-$season/watching.html" + app.get("$series9API/film/$fixTitle-season-$season/watching.html").document } - val request = app.get(url) - if (!request.isSuccessful) return - val res = request.document - val sources: ArrayList = arrayListOf() + doc.select("div#list-eps div.le-server").apmap { ele -> + val server = if (season == null) { + ele.select("a").attr("player-data") + } else { + ele.select("a[episode-data=$episode]").attr("player-data") + }.let { httpsify(it) } - if (season == null) { - val xstreamcdn = - res.selectFirst("div#list-eps div#server-29 a")?.attr("player-data")?.let { - Regex("(.*?)((\\?cap)|(\\?sub)|(#cap)|(#sub))").find(it)?.groupValues?.get(1) - } - val streamsb = res.selectFirst("div#list-eps div#server-13 a")?.attr("player-data") - val doodstream = res.selectFirst("div#list-eps div#server-14 a")?.attr("player-data") - sources.addAll(listOf(xstreamcdn, streamsb, doodstream)) - } else { - val xstreamcdn = res.selectFirst("div#list-eps div#server-29 a[episode-data=$episode]") - ?.attr("player-data")?.let { - Regex("(.*?)((\\?cap)|(\\?sub)|(#cap)|(#sub))").find(it)?.groupValues?.get(1) - } - val streamsb = res.selectFirst("div#list-eps div#server-13 a[episode-data=$episode]") - ?.attr("player-data") - val doodstream = res.selectFirst("div#list-eps div#server-14 a[episode-data=$episode]") - ?.attr("player-data") - sources.addAll(listOf(xstreamcdn, streamsb, doodstream)) + if (server.startsWith("https://movembed.cc")) { + val iv = "9225679083961858" + val secretKey = "25742532592138496744665879883281" + val secretDecryptKey = secretKey + GogoExtractor.extractVidstream( + server, + "Vidstream", + callback, + iv, + secretKey, + secretDecryptKey, + isUsingAdaptiveKeys = false, + isUsingAdaptiveData = true, + iframeDocument = app.get(server).document + ) + } else { + loadExtractor(server, series9API, subtitleCallback, callback) + } } - sources.apmap { link -> - loadExtractor(link ?: return@apmap null, url, subtitleCallback, callback) - } +// val sources: ArrayList = arrayListOf() +// +// if (season == null) { +// val xstreamcdn = +// res.selectFirst("div#list-eps div#server-29 a")?.attr("player-data")?.let { +// Regex("(.*?)((\\?cap)|(\\?sub)|(#cap)|(#sub))").find(it)?.groupValues?.get(1) +// } +// val streamsb = res.selectFirst("div#list-eps div#server-13 a")?.attr("player-data") +// val doodstream = res.selectFirst("div#list-eps div#server-14 a")?.attr("player-data") +// sources.addAll(listOf(xstreamcdn, streamsb, doodstream)) +// } else { +// val xstreamcdn = res.selectFirst("div#list-eps div#server-29 a[episode-data=$episode]") +// ?.attr("player-data")?.let { +// Regex("(.*?)((\\?cap)|(\\?sub)|(#cap)|(#sub))").find(it)?.groupValues?.get(1) +// } +// val streamsb = res.selectFirst("div#list-eps div#server-13 a[episode-data=$episode]") +// ?.attr("player-data") +// val doodstream = res.selectFirst("div#list-eps div#server-14 a[episode-data=$episode]") +// ?.attr("player-data") +// sources.addAll(listOf(xstreamcdn, streamsb, doodstream)) +// } +// +// sources.apmap { link -> +// loadExtractor(link ?: return@apmap null, null, subtitleCallback, callback) +// } } suspend fun invokeIdlix( diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index 5923c615..650519f9 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -41,7 +41,7 @@ import kotlin.collections.ArrayList import kotlin.math.min val soraAPI = base64DecodeAPI("cA==YXA=cy8=Y20=di8=LnQ=b2s=a2w=bG8=aS4=YXA=ZS0=aWw=b2I=LW0=Z2E=Ly8=czo=dHA=aHQ=") -val soraBackupAPI = base64DecodeAPI("dg==LnQ=bGw=aGk=dGM=dXM=Lmo=b2s=a2w=bG8=Ly8=czo=dHA=aHQ=") +val soraBackupAPI = base64DecodeAPI("dHY=bC4=aWw=Y2g=c3Q=anU=MS4=b2s=a2w=bG8=Ly8=czo=dHA=aHQ=") val soraHeaders = mapOf( "lang" to "en",