diff --git a/Animixplay/build.gradle.kts b/Animixplay/build.gradle.kts index a72bb316..b7ed96d7 100644 --- a/Animixplay/build.gradle.kts +++ b/Animixplay/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 4 +version = 5 cloudstream { diff --git a/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt b/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt index e692b62a..aade6f8c 100644 --- a/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt +++ b/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt @@ -1,13 +1,13 @@ package com.hexated import com.fasterxml.jackson.annotation.JsonProperty +import com.hexated.GogoExtractor.extractVidstream import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.loadExtractor -import org.json.JSONObject import org.jsoup.Jsoup import java.net.URI @@ -27,14 +27,6 @@ class Animixplay : MainAPI() { ) companion object { - fun getType(t: String?): TvType { - return when { - t?.contains("TV") == true -> TvType.Anime - t?.contains("Movie") == true -> TvType.AnimeMovie - else -> TvType.OVA - } - } - fun getStatus(t: String?): ShowStatus { return when (t) { "Finished Airing" -> ShowStatus.Completed @@ -155,205 +147,237 @@ class Animixplay : MainAPI() { } } - override suspend fun quickSearch(query: String): List? { - return app.post( - "https://cdn.animixplay.to/api/search", - data = mapOf("qfast" to query, "root" to URI(mainUrl).host) - ).parsedSafe()?.result?.let { - Jsoup.parse(it).select("a").map { elem -> - val href = elem.attr("href") - val title = elem.select("p.name").text() - newAnimeSearchResponse(title, href, TvType.Anime) { - this.posterUrl = elem.select("img").attr("src") - addDubStatus(isDub = title.contains("Dub")) - } + override suspend fun quickSearch(query: String): List? { + return app.post( + "https://cdn.animixplay.to/api/search", + data = mapOf("qfast" to query, "root" to URI(mainUrl).host) + ).parsedSafe()?.result?.let { + Jsoup.parse(it).select("a").map { elem -> + val href = elem.attr("href") + val title = elem.select("p.name").text() + newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = elem.select("img").attr("src") + addDubStatus(isDub = title.contains("Dub")) } } } - - - override suspend fun load(url: String): LoadResponse? { - - val (fixUrl, malId) = if (url.contains("/anime/")) { - listOf(url, Regex("anime/([0-9]+)/?").find(url)?.groupValues?.get(1)) - } else { - val malId = app.get(url).text.substringAfterLast("malid = '").substringBefore("';") - listOf("$mainUrl/anime/$malId", malId) - } - - val anilistId = app.post( - "https://graphql.anilist.co/", data = mapOf( - "query" to "{Media(idMal:$malId,type:ANIME){id}}", - ) - ).parsedSafe()?.data?.media?.id - - val res = app.get("$mainUrl/assets/mal/$malId.json").parsedSafe() - ?: throw ErrorLoadingException("No data found") - - val subEpisodes = mutableListOf() - val dubEpisodes = mutableListOf() - - app.post("$mainUrl/api/search", data = mapOf("recomended" to "$malId")) - .parsedSafe()?.data?.filter { it.type == "GOGO" }?.map { item -> - item.items?.apmap { server -> - val dataEps = - app.get(fixUrl(server.url.toString())).document.select("div#epslistplace") - .text().trim() - Regex("\"([0-9]+)\":\"(\\S+?)\"").findAll(dataEps).toList() - .map { it.groupValues[1] to it.groupValues[2] }.map { (ep, link) -> - val episode = Episode(fixUrl(link), episode = ep.toInt() + 1) - if (server.url?.contains("-dub") == true) { - dubEpisodes.add(episode) - } else { - subEpisodes.add(episode) - } - } - } - } - - val recommendations = app.get("$mainUrl/assets/similar/$malId.json") - .parsedSafe()?.recommendations?.mapNotNull { rec -> - newAnimeSearchResponse( - rec.title ?: return@mapNotNull null, - "$mainUrl/anime/${rec.malId}/", - TvType.Anime - ) { - this.posterUrl = rec.imageUrl - addDubStatus(dubExist = false, subExist = true) - } - } - - return newAnimeLoadResponse( - res.title ?: return null, - url, - TvType.Anime - ) { - engName = res.title - posterUrl = res.imageUrl - this.year = res.aired?.from?.split("-")?.firstOrNull()?.toIntOrNull() - showStatus = getStatus(res.status) - plot = res.synopsis - this.tags = res.genres?.mapNotNull { it.name } - this.recommendations = recommendations - addMalId(malId?.toIntOrNull()) - addAniListId(anilistId?.toIntOrNull()) - addTrailer(res.trailerUrl) - if (subEpisodes.isNotEmpty()) addEpisodes(DubStatus.Subbed, subEpisodes) - if (dubEpisodes.isNotEmpty()) addEpisodes(DubStatus.Dubbed, dubEpisodes) - } - - } - - override suspend fun loadLinks( - data: String, - isCasting: Boolean, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ): Boolean { - - val iframe = app.get(data) - val iframeDoc = iframe.document - - argamap({ - iframeDoc.select(".list-server-items > .linkserver") - .forEach { element -> - val status = element.attr("data-status") ?: return@forEach - if (status != "1") return@forEach - val extractorData = element.attr("data-video") ?: return@forEach - loadExtractor(extractorData, iframe.url, subtitleCallback, callback) - } - }, { - val iv = "3134003223491201" - val secretKey = "37911490979715163134003223491201" - val secretDecryptKey = "54674138327930866480207815084989" - GogoanimeProvider.extractVidstream( - iframe.url, - this.name, - callback, - iv, - secretKey, - secretDecryptKey, - isUsingAdaptiveKeys = false, - isUsingAdaptiveData = true, - iframeDocument = iframeDoc - ) - }) - return true } - private data class IdAni( - @JsonProperty("id") val id: String? = null, - ) + private suspend fun loadMissingAnime(url: String): LoadResponse? { + val doc = app.get(url).document - private data class MediaAni( - @JsonProperty("Media") val media: IdAni? = null, - ) + val title = doc.selectFirst("span.animetitle")?.text() + val image = fixUrlNull(doc.selectFirst("meta[property=og:image]")?.attr("content")) + val genres = doc.selectFirst("span#genredata")?.text()?.split(",")?.map { it.trim() } - private data class DataAni( - @JsonProperty("data") val data: MediaAni? = null, - ) + val subEpisodes = mutableListOf() + val dubEpisodes = mutableListOf() + val dataEps = doc.select("div#epslistplace") + .text().trim() + Regex("\"([0-9]+)\":\"(\\S+?)\"").findAll(dataEps).toList() + .map { it.groupValues[1] to it.groupValues[2] }.map { (ep, link) -> + val episode = Episode(fixUrl(link), episode = ep.toInt() + 1) + if (url.contains("-dub")) { + dubEpisodes.add(episode) + } else { + subEpisodes.add(episode) + } + } - private data class Items( - @JsonProperty("url") val url: String? = null, - @JsonProperty("title") val title: String? = null, - ) + return newAnimeLoadResponse( + title ?: return null, + url, + TvType.Anime + ) { + this.posterUrl = image + this.tags = genres + if (subEpisodes.isNotEmpty()) addEpisodes(DubStatus.Subbed, subEpisodes) + if (dubEpisodes.isNotEmpty()) addEpisodes(DubStatus.Dubbed, dubEpisodes) + } + } - private data class Episodes( - @JsonProperty("type") val type: String? = null, - @JsonProperty("items") val items: ArrayList? = arrayListOf(), - ) + override suspend fun load(url: String): LoadResponse? { - private data class Data( - @JsonProperty("data") val data: ArrayList? = arrayListOf(), - ) + val (fixUrl, malId) = if (url.contains("/anime/")) { + listOf(url, Regex("anime/([0-9]+)/?").find(url)?.groupValues?.get(1)) + } else { + val malId = app.get(url).text.substringAfterLast("malid = '").substringBefore("';") + listOf("$mainUrl/anime/$malId", malId) + } - private data class Aired( - @JsonProperty("from") val from: String? = null, - ) + val anilistId = app.post( + "https://graphql.anilist.co/", data = mapOf( + "query" to "{Media(idMal:$malId,type:ANIME){id}}", + ) + ).parsedSafe()?.data?.media?.id - private data class Genres( - @JsonProperty("name") val name: String? = null, - ) + val res = app.get("$mainUrl/assets/mal/$malId.json").parsedSafe() + ?: return loadMissingAnime(url) - private data class RecResult( - @JsonProperty("recommendations") val recommendations: ArrayList? = arrayListOf(), - ) + val subEpisodes = mutableListOf() + val dubEpisodes = mutableListOf() - private data class Recommendations( - @JsonProperty("mal_id") val malId: String? = null, - @JsonProperty("image_url") val imageUrl: String? = null, - @JsonProperty("title") val title: String? = null, - ) + app.post("$mainUrl/api/search", data = mapOf("recomended" to "$malId")) + .parsedSafe()?.data?.filter { it.type == "GOGO" }?.map { item -> + item.items?.apmap { server -> + val dataEps = + app.get(fixUrl(server.url.toString())).document.select("div#epslistplace") + .text().trim() + Regex("\"([0-9]+)\":\"(\\S+?)\"").findAll(dataEps).toList() + .map { it.groupValues[1] to it.groupValues[2] }.map { (ep, link) -> + val episode = Episode(fixUrl(link), episode = ep.toInt() + 1) + if (server.url?.contains("-dub") == true) { + dubEpisodes.add(episode) + } else { + subEpisodes.add(episode) + } + } + } + } - private data class AnimeDetail( - @JsonProperty("title") val title: String? = null, - @JsonProperty("image_url") val imageUrl: String? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("aired") val aired: Aired? = null, - @JsonProperty("status") val status: String? = null, - @JsonProperty("synopsis") val synopsis: String? = null, - @JsonProperty("trailer_url") val trailerUrl: String? = null, - @JsonProperty("genres") val genres: ArrayList? = arrayListOf(), - ) + val recommendations = app.get("$mainUrl/assets/similar/$malId.json") + .parsedSafe()?.recommendations?.mapNotNull { rec -> + newAnimeSearchResponse( + rec.title ?: return@mapNotNull null, + "$mainUrl/anime/${rec.malId}/", + TvType.Anime + ) { + this.posterUrl = rec.imageUrl + addDubStatus(dubExist = false, subExist = true) + } + } - private data class Search( - @JsonProperty("result") val result: String? = null, - ) + return newAnimeLoadResponse( + res.title ?: return null, + url, + TvType.Anime + ) { + engName = res.title + posterUrl = res.imageUrl + this.year = res.aired?.from?.split("-")?.firstOrNull()?.toIntOrNull() + showStatus = getStatus(res.status) + plot = res.synopsis + this.tags = res.genres?.mapNotNull { it.name } + this.recommendations = recommendations + addMalId(malId?.toIntOrNull()) + addAniListId(anilistId?.toIntOrNull()) + addTrailer(res.trailerUrl) + if (subEpisodes.isNotEmpty()) addEpisodes(DubStatus.Subbed, subEpisodes) + if (dubEpisodes.isNotEmpty()) addEpisodes(DubStatus.Dubbed, dubEpisodes) + } - private data class Result( - @JsonProperty("result") val result: ArrayList = arrayListOf(), - @JsonProperty("last") val last: Any? = null, - ) + } - private data class Anime( - @JsonProperty("title") val title: String? = null, - @JsonProperty("url") val url: String? = null, - @JsonProperty("img") val img: String? = null, - @JsonProperty("picture") val picture: String? = null, - @JsonProperty("infotext") val infotext: String? = null, - ) + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { - private data class FullSearch( + val iframe = app.get(data) + val iframeDoc = iframe.document + + argamap({ + iframeDoc.select(".list-server-items > .linkserver") + .forEach { element -> + val status = element.attr("data-status") ?: return@forEach + if (status != "1") return@forEach + val extractorData = element.attr("data-video") ?: return@forEach + loadExtractor(extractorData, iframe.url, subtitleCallback, callback) + } + }, { + val iv = "3134003223491201" + val secretKey = "37911490979715163134003223491201" + val secretDecryptKey = "54674138327930866480207815084989" + extractVidstream( + iframe.url, + this.name, + callback, + iv, + secretKey, + secretDecryptKey, + isUsingAdaptiveKeys = false, + isUsingAdaptiveData = true, + iframeDocument = iframeDoc + ) + }) + return true + } + + private data class IdAni( + @JsonProperty("id") val id: String? = null, + ) + + private data class MediaAni( + @JsonProperty("Media") val media: IdAni? = null, + ) + + private data class DataAni( + @JsonProperty("data") val data: MediaAni? = null, + ) + + private data class Items( + @JsonProperty("url") val url: String? = null, + @JsonProperty("title") val title: String? = null, + ) + + private data class Episodes( + @JsonProperty("type") val type: String? = null, + @JsonProperty("items") val items: ArrayList? = arrayListOf(), + ) + + private data class Data( + @JsonProperty("data") val data: ArrayList? = arrayListOf(), + ) + + private data class Aired( + @JsonProperty("from") val from: String? = null, + ) + + private data class Genres( + @JsonProperty("name") val name: String? = null, + ) + + private data class RecResult( + @JsonProperty("recommendations") val recommendations: ArrayList? = arrayListOf(), + ) + + private data class Recommendations( + @JsonProperty("mal_id") val malId: String? = null, + @JsonProperty("image_url") val imageUrl: String? = null, + @JsonProperty("title") val title: String? = null, + ) + + private data class AnimeDetail( + @JsonProperty("title") val title: String? = null, + @JsonProperty("image_url") val imageUrl: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("aired") val aired: Aired? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("synopsis") val synopsis: String? = null, + @JsonProperty("trailer_url") val trailerUrl: String? = null, + @JsonProperty("genres") val genres: ArrayList? = arrayListOf(), + ) + + private data class Search( + @JsonProperty("result") val result: String? = null, + ) + + private data class Result( + @JsonProperty("result") val result: ArrayList = arrayListOf(), + @JsonProperty("last") val last: Any? = null, + ) + + private data class Anime( + @JsonProperty("title") val title: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("img") val img: String? = null, + @JsonProperty("picture") val picture: String? = null, + @JsonProperty("infotext") val infotext: String? = null, + ) + + private data class FullSearch( @JsonProperty("result") val result: String? = null, ) diff --git a/Animixplay/src/main/kotlin/com/hexated/GogoExtractor.kt b/Animixplay/src/main/kotlin/com/hexated/GogoExtractor.kt new file mode 100644 index 00000000..ed19fd25 --- /dev/null +++ b/Animixplay/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/Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt b/Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt deleted file mode 100644 index d5b1fba9..00000000 --- a/Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt +++ /dev/null @@ -1,414 +0,0 @@ -package com.hexated - -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.utils.* -import org.jsoup.Jsoup -import org.jsoup.nodes.Document -import java.net.URI -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -class GogoanimeProvider : MainAPI() { - companion object { - fun getType(t: String): TvType { - return if (t.contains("OVA") || t.contains("Special")) TvType.OVA - else if (t.contains("Movie")) TvType.AnimeMovie - else TvType.Anime - } - - fun getStatus(t: String): ShowStatus { - return when (t) { - "Completed" -> ShowStatus.Completed - "Ongoing" -> ShowStatus.Ongoing - else -> ShowStatus.Completed - } - } - - /** - * @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) - } - } - - val qualityRegex = Regex("(\\d+)P") - - // 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())) - } - } - - private fun String.decodeHex(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - .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) - } - } - } - - override var mainUrl = "https://gogoanime.lu" - override var name = "GogoAnime" - override val hasQuickSearch = false - override val hasMainPage = true - - override val supportedTypes = setOf( - TvType.AnimeMovie, - TvType.Anime, - TvType.OVA - ) - - val headers = mapOf( - "authority" to "ajax.gogo-load.com", - "sec-ch-ua" to "\"Google Chrome\";v=\"89\", \"Chromium\";v=\"89\", \";Not A Brand\";v=\"99\"", - "accept" to "text/html, */*; q=0.01", - "dnt" to "1", - "sec-ch-ua-mobile" to "?0", - "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36", - "origin" to mainUrl, - "sec-fetch-site" to "cross-site", - "sec-fetch-mode" to "cors", - "sec-fetch-dest" to "empty", - "referer" to "$mainUrl/" - ) - val parseRegex = - Regex("""
  • \s*\n.*\n.*\n.*?img src="(.*?)"""") - - override val mainPage = mainPageOf( - Pair("1", "Recent Release - Sub"), - Pair("2", "Recent Release - Dub"), - Pair("3", "Recent Release - Chinese"), - ) - - override suspend fun getMainPage( - page: Int, - request: MainPageRequest - ): HomePageResponse { - val params = mapOf("page" to page.toString(), "type" to request.data) - val html = app.get( - "https://ajax.gogo-load.com/ajax/page-recent-release.html", - headers = headers, - params = params - ) - val isSub = listOf(1, 3).contains(request.data.toInt()) - - val home = parseRegex.findAll(html.text).map { - val (link, epNum, title, poster) = it.destructured - newAnimeSearchResponse(title, link) { - this.posterUrl = poster - addDubStatus(!isSub, epNum.toIntOrNull()) - } - }.toList() - - return newHomePageResponse(request.name, home) - } - - override suspend fun search(query: String): ArrayList { - val link = "$mainUrl/search.html?keyword=$query" - val html = app.get(link).text - val doc = Jsoup.parse(html) - - val episodes = doc.select(""".last_episodes li""").mapNotNull { - AnimeSearchResponse( - it.selectFirst(".name")?.text()?.replace(" (Dub)", "") ?: return@mapNotNull null, - fixUrl(it.selectFirst(".name > a")?.attr("href") ?: return@mapNotNull null), - this.name, - TvType.Anime, - it.selectFirst("img")?.attr("src"), - it.selectFirst(".released")?.text()?.split(":")?.getOrNull(1)?.trim() - ?.toIntOrNull(), - if (it.selectFirst(".name")?.text() - ?.contains("Dub") == true - ) EnumSet.of(DubStatus.Dubbed) else EnumSet.of( - DubStatus.Subbed - ), - ) - } - - return ArrayList(episodes) - } - - private fun getProperAnimeLink(uri: String): String { - if (uri.contains("-episode")) { - val split = uri.split("/") - val slug = split[split.size - 1].split("-episode")[0] - return "$mainUrl/category/$slug" - } - return uri - } - - override suspend fun load(url: String): LoadResponse { - val link = getProperAnimeLink(url) - val episodeloadApi = "https://ajax.gogo-load.com/ajax/load-list-episode" - val doc = app.get(link).document - - val animeBody = doc.selectFirst(".anime_info_body_bg") - val title = animeBody?.selectFirst("h1")!!.text() - val poster = animeBody.selectFirst("img")?.attr("src") - var description: String? = null - val genre = ArrayList() - var year: Int? = null - var status: String? = null - var nativeName: String? = null - var type: String? = null - - animeBody.select("p.type").forEach { pType -> - when (pType.selectFirst("span")?.text()?.trim()) { - "Plot Summary:" -> { - description = pType.text().replace("Plot Summary:", "").trim() - } - "Genre:" -> { - genre.addAll(pType.select("a").map { - it.attr("title") - }) - } - "Released:" -> { - year = pType.text().replace("Released:", "").trim().toIntOrNull() - } - "Status:" -> { - status = pType.text().replace("Status:", "").trim() - } - "Other name:" -> { - nativeName = pType.text().replace("Other name:", "").trim() - } - "Type:" -> { - type = pType.text().replace("type:", "").trim() - } - } - } - - val animeId = doc.selectFirst("#movie_id")!!.attr("value") - val params = mapOf("ep_start" to "0", "ep_end" to "2000", "id" to animeId) - - val episodes = app.get(episodeloadApi, params = params).document.select("a").map { - Episode( - fixUrl(it.attr("href").trim()), - "Episode " + it.selectFirst(".name")?.text()?.replace("EP", "")?.trim() - ) - }.reversed() - - return newAnimeLoadResponse(title, link, getType(type.toString())) { - japName = nativeName - engName = title - posterUrl = poster - this.year = year - addEpisodes(DubStatus.Subbed, episodes) // TODO CHECK - plot = description - tags = genre - - showStatus = getStatus(status.toString()) - } - } - - 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 - ) - - private suspend fun extractVideos( - uri: String, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val doc = app.get(uri).document - - val iframe = fixUrlNull(doc.selectFirst("div.play-video > iframe")?.attr("src")) ?: return - - argamap( - { - val link = iframe.replace("streaming.php", "download") - val page = app.get(link, headers = mapOf("Referer" to iframe)) - - page.document.select(".dowload > a").apmap { - if (it.hasAttr("download")) { - val qual = if (it.text() - .contains("HDP") - ) "1080" else qualityRegex.find(it.text())?.destructured?.component1() - .toString() - callback( - ExtractorLink( - "Gogoanime", - "Gogoanime", - it.attr("href"), - page.url, - getQualityFromName(qual), - it.attr("href").contains(".m3u8") - ) - ) - } else { - val url = it.attr("href") - loadExtractor(url, null, subtitleCallback, callback) - } - } - }, { - val streamingResponse = app.get(iframe, headers = mapOf("Referer" to iframe)) - val streamingDocument = streamingResponse.document - argamap({ - streamingDocument.select(".list-server-items > .linkserver") - .forEach { element -> - val status = element.attr("data-status") ?: return@forEach - if (status != "1") return@forEach - val data = element.attr("data-video") ?: return@forEach - loadExtractor(data, streamingResponse.url, subtitleCallback, callback) - } - }, { - val iv = "3134003223491201" - val secretKey = "37911490979715163134003223491201" - val secretDecryptKey = "54674138327930866480207815084989" - extractVidstream( - iframe, - this.name, - callback, - iv, - secretKey, - secretDecryptKey, - isUsingAdaptiveKeys = false, - isUsingAdaptiveData = true - ) - }) - } - ) - } - - override suspend fun loadLinks( - data: String, - isCasting: Boolean, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ): Boolean { - extractVideos(data, subtitleCallback, callback) - return true - } -} diff --git a/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt b/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt index 79f439c4..9b6bfe98 100644 --- a/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt +++ b/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt @@ -9,8 +9,7 @@ import org.jsoup.nodes.Element class LayarKacaProvider : MainAPI() { override var mainUrl = "https://lk21official.org" - - // private val redirectUrl = "https://nd21x1.github.io" +// private val redirectUrl = "https://nd21x1.github.io" private val seriesUrl = "https://nontondrama.icu" override var name = "LayarKaca" override val hasMainPage = true