From 73b775d29f79e7a0ce281640684d8c4b26f5fde2 Mon Sep 17 00:00:00 2001 From: hexated Date: Sat, 1 Oct 2022 01:02:50 +0700 Subject: [PATCH] added Animixplay --- Animixplay/build.gradle.kts | 27 ++ Animixplay/src/main/AndroidManifest.xml | 2 + .../src/main/kotlin/com/hexated/Animixplay.kt | 335 ++++++++++++++ .../kotlin/com/hexated/AnimixplayPlugin.kt | 14 + .../kotlin/com/hexated/GogoanimeProvider.kt | 412 ++++++++++++++++++ IdlixProvider/build.gradle.kts | 2 +- .../main/kotlin/com/hexated/IdlixProvider.kt | 5 +- 7 files changed, 794 insertions(+), 3 deletions(-) create mode 100644 Animixplay/build.gradle.kts create mode 100644 Animixplay/src/main/AndroidManifest.xml create mode 100644 Animixplay/src/main/kotlin/com/hexated/Animixplay.kt create mode 100644 Animixplay/src/main/kotlin/com/hexated/AnimixplayPlugin.kt create mode 100644 Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt diff --git a/Animixplay/build.gradle.kts b/Animixplay/build.gradle.kts new file mode 100644 index 00000000..d3a2ba0c --- /dev/null +++ b/Animixplay/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=animixplay.to&sz=%size%" +} \ No newline at end of file diff --git a/Animixplay/src/main/AndroidManifest.xml b/Animixplay/src/main/AndroidManifest.xml new file mode 100644 index 00000000..874740e3 --- /dev/null +++ b/Animixplay/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt b/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt new file mode 100644 index 00000000..6406336e --- /dev/null +++ b/Animixplay/src/main/kotlin/com/hexated/Animixplay.kt @@ -0,0 +1,335 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +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.jsoup.Jsoup +import java.net.URI + + +class Animixplay : MainAPI() { + override var mainUrl = "https://animixplay.to" + override var name = "Animixplay" + override val hasMainPage = true + override var lang = "en" + override val hasQuickSearch = true + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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 + "Currently Airing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/api/search" to "Sub", + "$mainUrl/api/search" to "Dub", + "$mainUrl/a/XsWgdGCnKJfNvDFAM28EV" to "All", + "$mainUrl/api/search" to "Movie", + ) + + private var newPagination : String? = null + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val items = mutableListOf() + val paged = page.toString() + val pagination = if (request.name == "Movie") { + paged.replace(paged, "99999999") + } else { + paged.replace(paged, "3020-05-06 00:00:00") + } + + if (page <= 1) { + val headers = when (request.name) { + "Sub" -> mapOf("seasonal" to pagination) + "Dub" -> mapOf("seasonaldub" to pagination) + "All" -> mapOf("recent" to pagination) + "Movie" -> mapOf("movie" to pagination) + else -> mapOf() + } + val res = app.post( + request.data, + data = headers, + referer = mainUrl, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsedSafe() + newPagination = res?.last.toString() + val home = res?.result?.mapNotNull { + it.toSearchResponse() + } ?: throw ErrorLoadingException("No media found") + items.add( + HomePageList( + name = request.name, + list = home, + ) + ) + } else { + val headers = when (request.name) { + "Sub" -> mapOf("seasonal" to "$newPagination") + "Dub" -> mapOf("seasonaldub" to "$newPagination") + "All" -> mapOf("recent" to "$newPagination") + "Movie" -> mapOf("movie" to "$newPagination") + else -> mapOf() + } + val res = app.post( + request.data, + data = headers, + referer = mainUrl, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsedSafe() + newPagination = res?.last.toString() + val home = res?.result?.mapNotNull { + it.toSearchResponse() + } ?: throw ErrorLoadingException("No media found") + items.add( + HomePageList( + name = request.name, + list = home, + ) + ) + } + + return newHomePageResponse(items) + } + + private fun Anime.toSearchResponse(): AnimeSearchResponse? { + return newAnimeSearchResponse( + title ?: return null, + fixUrl(url ?: return null), + TvType.TvSeries, + ) { + this.posterUrl = img ?: picture + addDubStatus( + isDub = title.contains("Dub"), + episodes = Regex("EP\\s([0-9]+)/").find( + infotext ?: "" + )?.groupValues?.getOrNull(1) + ?.toIntOrNull() + ) + } + } + + override suspend fun quickSearch(query: String) = search(query) + + override suspend fun search(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("Invalid json responses") + + 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/${rec.malId}", + TvType.Anime + ) { + this.posterUrl = rec.imageUrl + addDubStatus(dubExist = false, subExist = true) + } + } + + return newAnimeLoadResponse( + res.title ?: return null, + url, + getType(res.type) + ) { + 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 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, + ) + +} \ No newline at end of file diff --git a/Animixplay/src/main/kotlin/com/hexated/AnimixplayPlugin.kt b/Animixplay/src/main/kotlin/com/hexated/AnimixplayPlugin.kt new file mode 100644 index 00000000..e759a515 --- /dev/null +++ b/Animixplay/src/main/kotlin/com/hexated/AnimixplayPlugin.kt @@ -0,0 +1,14 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class AnimixplayPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Animixplay()) + } +} \ No newline at end of file diff --git a/Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt b/Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt new file mode 100644 index 00000000..6d2b323f --- /dev/null +++ b/Animixplay/src/main/kotlin/com/hexated/GogoanimeProvider.kt @@ -0,0 +1,412 @@ +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.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +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) + + fun invokeGogoSource( + source: GogoSource, + sourceCallback: (ExtractorLink) -> Unit + ) { + sourceCallback.invoke( + ExtractorLink( + mainApiName, + mainApiName, + source.file, + mainUrl, + getQualityFromName(source.label), + isM3u8 = source.type == "hls" || source.label?.contains( + "auto", + ignoreCase = true + ) == true + ) + ) + } + + 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/IdlixProvider/build.gradle.kts b/IdlixProvider/build.gradle.kts index 96ed11bd..4daf0235 100644 --- a/IdlixProvider/build.gradle.kts +++ b/IdlixProvider/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 3 +version = 4 cloudstream { diff --git a/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt b/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt index e47d2596..27ba243e 100644 --- a/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt +++ b/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt @@ -39,12 +39,13 @@ class IdlixProvider : MainAPI() { request: MainPageRequest ): HomePageResponse { val url = request.data.split("?") - val document = if (request.name == "Featured") { + val nonPaged = request.name == "Featured" && page <= 1 + val document = if (nonPaged) { app.get(request.data).document } else { app.get("${url.first()}$page/?${url.lastOrNull()}").document } - val home = (if (request.name == "Featured") { + val home = (if (nonPaged) { document.select("div.items.featured article") } else { document.select("div.items.full article, div#archive-content article")