From acb0e5380b3df5027149a6b7a91d341243d35a35 Mon Sep 17 00:00:00 2001
From: Nexus <79303560+Nexus-Gits@users.noreply.github.com>
Date: Sun, 13 Aug 2023 17:50:24 +0530
Subject: [PATCH] Add files via upload
---
Crunchyroll/build.gradle.kts | 22 +
Crunchyroll/src/main/AndroidManifest.xml | 2 +
.../kotlin/com/lagradost/CookieInterceptor.kt | 46 ++
.../com/lagradost/CrunchyrollProvider.kt | 513 ++++++++++++++++++
.../lagradost/CrunchyrollProviderPlugin.kt | 14 +
GogoanimeProvider/build.gradle.kts | 27 +
.../src/main/AndroidManifest.xml | 2 +
.../kotlin/com/lagradost/GogoanimeProvider.kt | 412 ++++++++++++++
.../com/lagradost/GogoanimeProviderPlugin.kt | 14 +
9 files changed, 1052 insertions(+)
create mode 100644 Crunchyroll/build.gradle.kts
create mode 100644 Crunchyroll/src/main/AndroidManifest.xml
create mode 100644 Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt
create mode 100644 Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt
create mode 100644 Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt
create mode 100644 GogoanimeProvider/build.gradle.kts
create mode 100644 GogoanimeProvider/src/main/AndroidManifest.xml
create mode 100644 GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProvider.kt
create mode 100644 GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProviderPlugin.kt
diff --git a/Crunchyroll/build.gradle.kts b/Crunchyroll/build.gradle.kts
new file mode 100644
index 00000000..83f90d70
--- /dev/null
+++ b/Crunchyroll/build.gradle.kts
@@ -0,0 +1,22 @@
+// use an integer for version numbers
+version = 5
+
+
+cloudstream {
+ language = "en"
+ // All of these properties are optional, you can safely remove them
+
+ description = "The Crunchyroll provider allows you to watch all the shows that are on Crunchyroll."
+ authors = listOf("Sir Aguacata (KillerDogeEmpire)")
+
+ /**
+ * Status int as the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta only
+ * */
+ status = 0 // will be 3 if unspecified
+ tvTypes = listOf("AnimeMovie", "Anime", "OVA")
+ iconUrl = "https://www.google.com/s2/favicons?domain=crunchyroll.com&sz=%size%"
+}
diff --git a/Crunchyroll/src/main/AndroidManifest.xml b/Crunchyroll/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..29aec9de
--- /dev/null
+++ b/Crunchyroll/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt b/Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt
new file mode 100644
index 00000000..ff11972a
--- /dev/null
+++ b/Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt
@@ -0,0 +1,46 @@
+package com.lagradost
+
+import com.lagradost.nicehttp.Requests
+import okhttp3.*
+import okhttp3.internal.parseCookie
+
+/**
+ * An HTTP session manager.
+ *
+ * This class simply keeps cookies across requests. No security about which site should use which cookies.
+ *
+ */
+
+class CustomSession(
+ client: OkHttpClient
+) : Requests() {
+ var cookies = mutableMapOf()
+
+ init {
+ this.baseClient = client
+ .newBuilder()
+ .addInterceptor {
+ val time = System.currentTimeMillis()
+ val request = it.request()
+ request.headers.forEach { header ->
+ if (header.first.equals("cookie", ignoreCase = true)) {
+ val cookie = parseCookie(time, request.url, header.second) ?: return@forEach
+ cookies += cookie.name to cookie
+ }
+ }
+ it.proceed(request)
+ }
+ .cookieJar(CustomCookieJar())
+ .build()
+ }
+
+ inner class CustomCookieJar : CookieJar {
+ override fun loadForRequest(url: HttpUrl): List {
+ return this@CustomSession.cookies.values.toList()
+ }
+
+ override fun saveFromResponse(url: HttpUrl, cookies: List) {
+ this@CustomSession.cookies += cookies.map { it.name to it }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt
new file mode 100644
index 00000000..ab7a40da
--- /dev/null
+++ b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt
@@ -0,0 +1,513 @@
+package com.lagradost
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.APIHolder.capitalize
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.getQualityFromName
+import com.lagradost.nicehttp.NiceResponse
+import kotlinx.coroutines.delay
+import org.jsoup.Jsoup
+import java.util.*
+
+private fun String.toAscii() = this.map { it.code }.joinToString()
+
+class KrunchyGeoBypasser {
+ companion object {
+ const val BYPASS_SERVER = "https://cr-unblocker.us.to/start_session"
+ val headers = mapOf(
+ "accept" to "*/*",
+// "Accept-Encoding" to "gzip, deflate",
+ "connection" to "keep-alive",
+// "Referer" to "https://google.com/",
+ "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36".toAscii()
+ )
+ var sessionId: String? = null
+
+ // val interceptor = CookieInterceptor()
+ val session = CustomSession(app.baseClient)
+ }
+
+ data class KrunchySession(
+ @JsonProperty("data") var data: DataInfo? = DataInfo(),
+ @JsonProperty("error") var error: Boolean? = null,
+ @JsonProperty("code") var code: String? = null
+ )
+
+ data class DataInfo(
+ @JsonProperty("session_id") var sessionId: String? = null,
+ @JsonProperty("country_code") var countryCode: String? = null,
+ )
+
+ private suspend fun getSessionId(): Boolean {
+ return try {
+ val response = app.get(BYPASS_SERVER, params = mapOf("version" to "1.1")).text
+ val json = parseJson(response)
+ sessionId = json.data?.sessionId
+ true
+ } catch (e: Exception) {
+ sessionId = null
+ false
+ }
+ }
+
+ private suspend fun autoLoadSession(): Boolean {
+ if (sessionId != null) return true
+ getSessionId()
+ // Do not spam the api!
+ delay(3000)
+ return autoLoadSession()
+ }
+
+ suspend fun geoBypassRequest(url: String): NiceResponse {
+ autoLoadSession()
+ return session.get(url, headers = headers, cookies = mapOf("session_id" to sessionId!!))
+ }
+}
+
+class KrunchyProvider : MainAPI() {
+ companion object {
+ val crUnblock = KrunchyGeoBypasser()
+ val episodeNumRegex = Regex("""Episode (\d+)""")
+ }
+
+ // Do not make https! It will fail!
+ override var mainUrl = "http://www.crunchyroll.com"
+ override var name: String = "Crunchyroll"
+ override var lang = "en"
+ override val hasQuickSearch = false
+ override val hasMainPage = true
+
+ override val supportedTypes = setOf(
+ TvType.AnimeMovie,
+ TvType.Anime,
+ TvType.OVA
+ )
+
+ override val mainPage = mainPageOf(
+ "$mainUrl/videos/anime/popular/ajax_page?pg=" to "Popular",
+ "$mainUrl/videos/anime/simulcasts/ajax_page" to "Simulcasts"
+ )
+
+ override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ println("GETMAINPAGE ")
+ val categoryData = request.data
+
+ val paginated = categoryData.endsWith("=")
+ val pagedLink = if (paginated) categoryData + page else categoryData
+ val items = mutableListOf()
+
+ // Only fetch page at first-time load of homepage
+ if (page <= 1 && request.name == "Popular") {
+ val doc = Jsoup.parse(crUnblock.geoBypassRequest(mainUrl).text)
+ val featured = doc.select(".js-featured-show-list > li").mapNotNull { anime ->
+ val url =
+ fixUrlNull(anime?.selectFirst("a")?.attr("href")) ?: return@mapNotNull null
+ val imgEl = anime.selectFirst("img")
+ val name = imgEl?.attr("alt") ?: ""
+ val posterUrl = imgEl?.attr("src")?.replace("small", "full")
+ AnimeSearchResponse(
+ name = name,
+ url = url,
+ apiName = this.name,
+ type = TvType.Anime,
+ posterUrl = posterUrl,
+ dubStatus = EnumSet.of(DubStatus.Subbed)
+ )
+ }
+ val recent =
+ doc.select("div.welcome-countdown-day:contains(Now Showing) li").mapNotNull {
+ val link =
+ fixUrlNull(it.selectFirst("a")?.attr("href")) ?: return@mapNotNull null
+ val name = it.selectFirst("span.welcome-countdown-name")?.text() ?: ""
+ val img = it.selectFirst("img")?.attr("src")?.replace("medium", "full")
+ val dubstat = if (name.contains("Dub)", true)) EnumSet.of(DubStatus.Dubbed) else
+ EnumSet.of(DubStatus.Subbed)
+ val details = it.selectFirst("span.welcome-countdown-details")?.text()
+ val epnum =
+ if (details.isNullOrBlank()) null else episodeNumRegex.find(details)?.value?.replace(
+ "Episode ",
+ ""
+ ) ?: "0"
+ val episodesMap = mutableMapOf()
+ episodesMap[DubStatus.Subbed] = epnum?.toIntOrNull() ?: 0
+ episodesMap[DubStatus.Dubbed] = epnum?.toIntOrNull() ?: 0
+ AnimeSearchResponse(
+ name = "★ $name ★",
+ url = link.replace(Regex("(\\/episode.*)"), ""),
+ apiName = this.name,
+ type = TvType.Anime,
+ posterUrl = fixUrlNull(img),
+ dubStatus = dubstat,
+ episodes = episodesMap
+ )
+ }
+ if (recent.isNotEmpty()) {
+ items.add(
+ HomePageList(
+ name = "Now Showing",
+ list = recent,
+ )
+ )
+ }
+ if (featured.isNotEmpty()) {
+ items.add(HomePageList("Featured", featured))
+ }
+ }
+
+ if (paginated || !paginated && page <= 1) {
+ crUnblock.geoBypassRequest(pagedLink).let { respText ->
+ val soup = Jsoup.parse(respText.text)
+
+ val episodes = soup.select("li").mapNotNull {
+ val innerA = it.selectFirst("a") ?: return@mapNotNull null
+ val urlEps = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null
+ AnimeSearchResponse(
+ name = innerA.attr("title"),
+ url = urlEps,
+ apiName = this.name,
+ type = TvType.Anime,
+ posterUrl = it.selectFirst("img")?.attr("src"),
+ dubStatus = EnumSet.of(DubStatus.Subbed)
+ )
+ }
+ if (episodes.isNotEmpty()) {
+ items.add(
+ HomePageList(
+ name = request.name,
+ list = episodes,
+ )
+ )
+ }
+ }
+ }
+
+ if (items.isNotEmpty()) {
+ return newHomePageResponse(items)
+ }
+ throw ErrorLoadingException()
+ }
+
+ // Maybe fuzzy match in the future
+ private fun getCloseMatches(sequence: String, items: Collection): List {
+ val a = sequence.trim().lowercase()
+
+ return items.mapNotNull { item ->
+ val b = item.trim().lowercase()
+ if (b.contains(a))
+ item
+ else if (a.contains(b))
+ item
+ else null
+ }
+ }
+
+ private data class CrunchyAnimeData(
+ @JsonProperty("name") val name: String,
+ @JsonProperty("img") var img: String,
+ @JsonProperty("link") var link: String
+ )
+
+ private data class CrunchyJson(
+ @JsonProperty("data") val data: List,
+ )
+
+
+ override suspend fun search(query: String): ArrayList {
+ val json =
+ crUnblock.geoBypassRequest("http://www.crunchyroll.com/ajax/?req=RpcApiSearch_GetSearchCandidates").text.split(
+ "*/"
+ )[0].replace("\\/", "/")
+ val data = parseJson(
+ json.split("\n").mapNotNull { if (!it.startsWith("/")) it else null }.joinToString("\n")
+ ).data
+
+ val results = getCloseMatches(query, data.map { it.name })
+ if (results.isEmpty()) return ArrayList()
+ val searchResutls = ArrayList()
+
+ var count = 0
+ for (anime in data) {
+ if (count == results.size) {
+ break
+ }
+ if (anime.name == results[count]) {
+ val dubstat =
+ if (anime.name.contains("Dub)", true)) EnumSet.of(DubStatus.Dubbed) else
+ EnumSet.of(DubStatus.Subbed)
+ anime.link = fixUrl(anime.link)
+ anime.img = anime.img.replace("small", "full")
+ searchResutls.add(
+ AnimeSearchResponse(
+ name = anime.name,
+ url = anime.link,
+ apiName = this.name,
+ type = TvType.Anime,
+ posterUrl = anime.img,
+ dubStatus = dubstat,
+ )
+ )
+ ++count
+ }
+ }
+
+ return searchResutls
+ }
+
+ override suspend fun load(url: String): LoadResponse {
+ val soup = Jsoup.parse(crUnblock.geoBypassRequest(url).text)
+ val title = soup.selectFirst("#showview-content-header .ellipsis")?.text()?.trim()
+ val posterU = soup.selectFirst(".poster")?.attr("src")
+
+ val p = soup.selectFirst(".description")
+ var description = p?.selectFirst(".more")?.text()?.trim()
+ if (description.isNullOrBlank()) {
+ description = p?.selectFirst("span")?.text()?.trim()
+ }
+
+ val genres = soup.select(".large-margin-bottom > ul:nth-child(2) li:nth-child(2) a")
+ .map { it.text().capitalize() }
+ val year = genres.filter { it.toIntOrNull() != null }.map { it.toInt() }.sortedBy { it }
+ .getOrNull(0)
+
+ val subEpisodes = mutableListOf()
+ val dubEpisodes = mutableListOf()
+ val premiumSubEpisodes = mutableListOf()
+ val premiumDubEpisodes = mutableListOf()
+ soup.select(".season").forEach {
+ val seasonName = it.selectFirst("a.season-dropdown")?.text()?.trim()
+ it.select(".episode").forEach { ep ->
+ val epTitle = ep.selectFirst(".short-desc")?.text()
+
+ val epNum = episodeNumRegex.find(
+ ep.selectFirst("span.ellipsis")?.text().toString()
+ )?.destructured?.component1()
+ var poster = ep.selectFirst("img.landscape")?.attr("data-thumbnailurl")
+ val poster2 = ep.selectFirst("img")?.attr("src")
+ if (poster.isNullOrBlank()) {
+ poster = poster2
+ }
+
+ var epDesc =
+ (if (epNum == null) "" else "Episode $epNum") + (if (!seasonName.isNullOrEmpty()) " - $seasonName" else "")
+ val isPremium = poster?.contains("widestar", ignoreCase = true) ?: false
+ if (isPremium) {
+ epDesc = "★ $epDesc ★"
+ }
+
+ val isPremiumDubbed =
+ isPremium && seasonName != null && (seasonName.contains("Dub") || seasonName.contains(
+ "Russian"
+ ) || seasonName.contains("Spanish"))
+
+ val epi = Episode(
+ fixUrl(ep.attr("href")),
+ "$epTitle",
+ posterUrl = poster?.replace("widestar", "full")?.replace("wide", "full"),
+ description = epDesc,
+ season = if (isPremium) -1 else 1
+ )
+ if (isPremiumDubbed) {
+ premiumDubEpisodes.add(epi)
+ } else if (isPremium) {
+ premiumSubEpisodes.add(epi)
+ } else if (seasonName != null && (seasonName.contains("Dub"))) {
+ dubEpisodes.add(epi)
+ } else {
+ subEpisodes.add(epi)
+ }
+ }
+ }
+ val recommendations =
+ soup.select(".other-series > ul li")?.mapNotNull { element ->
+ val recTitle =
+ element.select("span.ellipsis[dir=auto]").text() ?: return@mapNotNull null
+ val image = element.select("img")?.attr("src")
+ val recUrl = fixUrl(element.select("a").attr("href"))
+ AnimeSearchResponse(
+ recTitle,
+ fixUrl(recUrl),
+ this.name,
+ TvType.Anime,
+ fixUrl(image!!),
+ dubStatus =
+ if (recTitle.contains("(DUB)") || recTitle.contains("Dub")) EnumSet.of(
+ DubStatus.Dubbed
+ ) else EnumSet.of(DubStatus.Subbed),
+ )
+ }
+ return newAnimeLoadResponse(title.toString(), url, TvType.Anime) {
+ this.posterUrl = posterU
+ this.engName = title
+ if (subEpisodes.isNotEmpty()) addEpisodes(DubStatus.Subbed, subEpisodes.reversed())
+ if (dubEpisodes.isNotEmpty()) addEpisodes(DubStatus.Dubbed, dubEpisodes.reversed())
+
+ if (premiumDubEpisodes.isNotEmpty()) addEpisodes(
+ DubStatus.Dubbed,
+ premiumDubEpisodes.reversed()
+ )
+ if (premiumSubEpisodes.isNotEmpty()) addEpisodes(
+ DubStatus.Subbed,
+ premiumSubEpisodes.reversed()
+ )
+
+ this.plot = description
+ this.tags = genres
+ this.year = year
+
+ this.recommendations = recommendations
+ this.seasonNames = listOf(
+ SeasonData(
+ 1,
+ "Free",
+ null
+ ),
+ SeasonData(
+ -1,
+ "Premium",
+ null
+ ),
+ )
+ }
+ }
+
+ data class Subtitles(
+ @JsonProperty("language") val language: String,
+ @JsonProperty("url") val url: String,
+ @JsonProperty("title") val title: String?,
+ @JsonProperty("format") val format: String?
+ )
+
+ data class Streams(
+ @JsonProperty("format") val format: String?,
+ @JsonProperty("audio_lang") val audioLang: String?,
+ @JsonProperty("hardsub_lang") val hardsubLang: String?,
+ @JsonProperty("url") val url: String,
+ @JsonProperty("resolution") val resolution: String?,
+ @JsonProperty("title") var title: String?
+ ) {
+ fun title(): String {
+ return when {
+ this.hardsubLang == "enUS" && this.audioLang == "jaJP" -> "Hardsub (English)"
+ this.hardsubLang == "esLA" && this.audioLang == "jaJP" -> "Hardsub (Latino)"
+ this.hardsubLang == "esES" && this.audioLang == "jaJP" -> "Hardsub (Español España)"
+ this.audioLang == "esLA" -> "Latino"
+ this.audioLang == "esES" -> "Español España"
+ this.audioLang == "enUS" -> "English (US)"
+ else -> "RAW"
+ }
+ }
+ }
+
+ data class KrunchyVideo(
+ @JsonProperty("streams") val streams: List,
+ @JsonProperty("subtitles") val subtitles: List,
+ )
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ val contentRegex = Regex("""vilos\.config\.media = (\{.+\})""")
+ val response = crUnblock.geoBypassRequest(data)
+
+ val hlsHelper = M3u8Helper()
+
+ val dat = contentRegex.find(response.text)?.destructured?.component1()
+
+ if (!dat.isNullOrEmpty()) {
+ val json = parseJson(dat)
+ val streams = ArrayList()
+
+ for (stream in json.streams) {
+ if (
+ listOf(
+ "adaptive_hls", "adaptive_dash",
+ "multitrack_adaptive_hls_v2",
+ "vo_adaptive_dash", "vo_adaptive_hls",
+ "trailer_hls",
+ ).contains(stream.format)
+ ) {
+ if (stream.format!!.contains("adaptive") && listOf(
+ "jaJP",
+ "esLA",
+ "esES",
+ "enUS"
+ )
+ .contains(stream.audioLang) && (listOf(
+ "esLA",
+ "esES",
+ "enUS",
+ null
+ ).contains(stream.hardsubLang))
+// && URI(stream.url).path.endsWith(".m3u")
+ ) {
+ stream.title = stream.title()
+ streams.add(stream)
+ }
+ // Premium eps
+ else if (stream.format == "trailer_hls" && listOf(
+ "jaJP",
+ "esLA",
+ "esES",
+ "enUS"
+ ).contains(stream.audioLang) &&
+ (listOf("esLA", "esES", "enUS", null).contains(stream.hardsubLang))
+ ) {
+ stream.title = stream.title()
+ streams.add(stream)
+ }
+ }
+ }
+
+ streams.apmap { stream ->
+ if (stream.url.contains("m3u8") && stream.format!!.contains("adaptive")) {
+// hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(stream.url, null), false)
+// .forEach {
+ callback(
+ ExtractorLink(
+ "Crunchyroll",
+ "Crunchy - ${stream.title}",
+ stream.url,
+ "",
+ getQualityFromName(stream.resolution),
+ true
+ )
+ )
+// }
+ } else if (stream.format == "trailer_hls") {
+ val premiumStream = stream.url
+ .replace("\\/", "/")
+ .replace(Regex("\\/clipFrom.*?index.m3u8"), "").replace("'_,'", "'_'")
+ .replace(stream.url.split("/")[2], "fy.v.vrv.co")
+ callback(
+ ExtractorLink(
+ this.name,
+ "Crunchy - ${stream.title} ★",
+ premiumStream,
+ "",
+ Qualities.Unknown.value,
+ false
+ )
+ )
+ } else null
+ }
+ json.subtitles.forEach {
+ val langclean = it.language.replace("esLA", "Spanish")
+ .replace("enUS", "English")
+ .replace("esES", "Spanish (Spain)")
+ subtitleCallback(
+ SubtitleFile(langclean, it.url)
+ )
+ }
+
+ return true
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt
new file mode 100644
index 00000000..26997ee2
--- /dev/null
+++ b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt
@@ -0,0 +1,14 @@
+
+package com.lagradost
+
+import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
+import com.lagradost.cloudstream3.plugins.Plugin
+import android.content.Context
+
+@CloudstreamPlugin
+class CrunchyrollProviderPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(KrunchyProvider())
+ }
+}
\ No newline at end of file
diff --git a/GogoanimeProvider/build.gradle.kts b/GogoanimeProvider/build.gradle.kts
new file mode 100644
index 00000000..a21f6ba3
--- /dev/null
+++ b/GogoanimeProvider/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("Cloudburst")
+
+ /**
+ * 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=gogoanime.lu&sz=%size%"
+}
\ No newline at end of file
diff --git a/GogoanimeProvider/src/main/AndroidManifest.xml b/GogoanimeProvider/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..29aec9de
--- /dev/null
+++ b/GogoanimeProvider/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProvider.kt b/GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProvider.kt
new file mode 100644
index 00000000..f8037ffc
--- /dev/null
+++ b/GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProvider.kt
@@ -0,0 +1,412 @@
+package com.lagradost
+
+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/GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProviderPlugin.kt b/GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProviderPlugin.kt
new file mode 100644
index 00000000..34e0fb17
--- /dev/null
+++ b/GogoanimeProvider/src/main/kotlin/com/lagradost/GogoanimeProviderPlugin.kt
@@ -0,0 +1,14 @@
+
+package com.lagradost
+
+import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
+import com.lagradost.cloudstream3.plugins.Plugin
+import android.content.Context
+
+@CloudstreamPlugin
+class GogoanimeProviderPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(GogoanimeProvider())
+ }
+}
\ No newline at end of file