diff --git a/AA_BLANK/build.gradle.kts b/AA_BLANK/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AA_BLANK/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AA_BLANK/src/main/AndroidManifest.xml b/AA_BLANK/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AA_BLANK/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AllAnimeProvider/build.gradle.kts b/AllAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AllAnimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AllAnimeProvider/src/main/AndroidManifest.xml b/AllAnimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AllAnimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AllAnimeProvider/src/main/kotlin/com/lagradost/AllAnimeProvider.kt b/AllAnimeProvider/src/main/kotlin/com/lagradost/AllAnimeProvider.kt new file mode 100644 index 0000000..6a340e5 --- /dev/null +++ b/AllAnimeProvider/src/main/kotlin/com/lagradost/AllAnimeProvider.kt @@ -0,0 +1,404 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.mvvm.safeApiCall +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 org.jsoup.Jsoup +import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable +import java.net.URI +import java.net.URLDecoder + + +class AllAnimeProvider : MainAPI() { + override var mainUrl = "https://allanime.site" + override var name = "AllAnime" + override val hasQuickSearch = false + override val hasMainPage = true + + private fun getStatus(t: String): ShowStatus { + return when (t) { + "Finished" -> ShowStatus.Completed + "Releasing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie) + + private data class Data( + @JsonProperty("shows") val shows: Shows + ) + + private data class Shows( + @JsonProperty("pageInfo") val pageInfo: PageInfo, + @JsonProperty("edges") val edges: List, + @JsonProperty("__typename") val _typename: String + ) + + private data class Edges( + @JsonProperty("_id") val Id: String?, + @JsonProperty("name") val name: String, + @JsonProperty("englishName") val englishName: String?, + @JsonProperty("nativeName") val nativeName: String?, + @JsonProperty("thumbnail") val thumbnail: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("season") val season: Season?, + @JsonProperty("score") val score: Double?, + @JsonProperty("airedStart") val airedStart: AiredStart?, + @JsonProperty("availableEpisodes") val availableEpisodes: AvailableEpisodes?, + @JsonProperty("availableEpisodesDetail") val availableEpisodesDetail: AvailableEpisodesDetail?, + @JsonProperty("studios") val studios: List?, + @JsonProperty("description") val description: String?, + @JsonProperty("status") val status: String?, + ) + + private data class AvailableEpisodes( + @JsonProperty("sub") val sub: Int, + @JsonProperty("dub") val dub: Int, + @JsonProperty("raw") val raw: Int + ) + + private data class AiredStart( + @JsonProperty("year") val year: Int, + @JsonProperty("month") val month: Int, + @JsonProperty("date") val date: Int + ) + + private data class Season( + @JsonProperty("quarter") val quarter: String, + @JsonProperty("year") val year: Int + ) + + private data class PageInfo( + @JsonProperty("total") val total: Int, + @JsonProperty("__typename") val _typename: String + ) + + private data class AllAnimeQuery( + @JsonProperty("data") val data: Data + ) + + data class RandomMain( + @JsonProperty("data") var data: DataRan? = DataRan() + ) + + data class DataRan( + @JsonProperty("queryRandomRecommendation") var queryRandomRecommendation: ArrayList = arrayListOf() + ) + + data class QueryRandomRecommendation( + @JsonProperty("_id") val Id: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("englishName") val englishName: String? = null, + @JsonProperty("nativeName") val nativeName: String? = null, + @JsonProperty("thumbnail") val thumbnail: String? = null, + @JsonProperty("airedStart") val airedStart: String? = null, + @JsonProperty("availableChapters") val availableChapters: String? = null, + @JsonProperty("availableEpisodes") val availableEpisodes: String? = null, + @JsonProperty("__typename") val _typename: String? = null + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val urls = listOf( +// Pair( +// "Top Anime", +// """$mainUrl/graphql?variables={"type":"anime","size":30,"dateRange":30}&extensions={"persistedQuery":{"version":1,"sha256Hash":"276d52ba09ca48ce2b8beb3affb26d9d673b22f9d1fd4892aaa39524128bc745"}}""" +// ), + // "countryOrigin":"JP" for Japanese only + Pair( + "Recently updated", + """$mainUrl/graphql?variables={"search":{"allowAdult":false,"allowUnknown":false},"limit":30,"page":1,"translationType":"dub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"d2670e3e27ee109630991152c8484fce5ff5e280c523378001f9a23dc1839068"}}""" + ), + ) + + val random = + """$mainUrl/graphql?variables={"format":"anime"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"21ac672633498a3698e8f6a93ce6c2b3722b29a216dcca93363bf012c360cd54"}}""" + val ranlink = app.get(random).text + val jsonran = parseJson(ranlink) + val ranhome = jsonran.data?.queryRandomRecommendation?.map { + newAnimeSearchResponse(it.name!!, "$mainUrl/anime/${it.Id}", fix = false) { + this.posterUrl = it.thumbnail + this.otherName = it.nativeName + } + } + + items.add(HomePageList("Random", ranhome!!)) + + urls.apmap { (HomeName, url) -> + val test = app.get(url).text + val json = parseJson(test) + val home = ArrayList() + val results = json.data.shows.edges.filter { + // filtering in case there is an anime with 0 episodes available on the site. + !(it.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0) + } + results.map { + home.add( + newAnimeSearchResponse(it.name, "$mainUrl/anime/${it.Id}", fix = false) { + this.posterUrl = it.thumbnail + this.year = it.airedStart?.year + this.otherName = it.englishName + addDub(it.availableEpisodes?.dub) + addSub(it.availableEpisodes?.sub) + }) + } + items.add(HomePageList(HomeName, home)) + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List { + val link = + """ $mainUrl/graphql?variables={"search":{"allowAdult":false,"allowUnknown":false,"query":"$query"},"limit":26,"page":1,"translationType":"dub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"d2670e3e27ee109630991152c8484fce5ff5e280c523378001f9a23dc1839068"}}""" + var res = app.get(link).text + if (res.contains("PERSISTED_QUERY_NOT_FOUND")) { + res = app.get(link).text + if (res.contains("PERSISTED_QUERY_NOT_FOUND")) return emptyList() + } + val response = parseJson(res) + + val results = response.data.shows.edges.filter { + // filtering in case there is an anime with 0 episodes available on the site. + !(it.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0) + } + + return results.map { + newAnimeSearchResponse(it.name, "$mainUrl/anime/${it.Id}", fix = false) { + this.posterUrl = it.thumbnail + this.year = it.airedStart?.year + this.otherName = it.englishName + addDub(it.availableEpisodes?.dub) + addSub(it.availableEpisodes?.sub) + } + } + } + + private data class AvailableEpisodesDetail( + @JsonProperty("sub") val sub: List, + @JsonProperty("dub") val dub: List, + @JsonProperty("raw") val raw: List + ) + + + override suspend fun load(url: String): LoadResponse? { + val rhino = Context.enter() + rhino.initStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initStandardObjects() + + val html = app.get(url).text + val soup = Jsoup.parse(html) + + val script = soup.select("script").firstOrNull { + it.html().contains("window.__NUXT__") + } ?: return null + + val js = """ + const window = {} + ${script.html()} + const returnValue = JSON.stringify(window.__NUXT__.fetch[0].show) + """.trimIndent() + + rhino.evaluateString(scope, js, "JavaScript", 1, null) + val jsEval = scope.get("returnValue", scope) ?: return null + val showData = parseJson(jsEval as String) + + val title = showData.name + val description = showData.description + val poster = showData.thumbnail + + val episodes = showData.availableEpisodes.let { + if (it == null) return@let Pair(null, null) + Pair(if (it.sub != 0) ((1..it.sub).map { epNum -> + Episode( + "$mainUrl/anime/${showData.Id}/episodes/sub/$epNum", episode = epNum + ) + }) else null, if (it.dub != 0) ((1..it.dub).map { epNum -> + Episode( + "$mainUrl/anime/${showData.Id}/episodes/dub/$epNum", episode = epNum + ) + }) else null) + } + + val characters = soup.select("div.character > div.card-character-box").mapNotNull { + val img = it?.selectFirst("img")?.attr("src") ?: return@mapNotNull null + val name = it.selectFirst("div > a")?.ownText() ?: return@mapNotNull null + val role = when (it.selectFirst("div > .text-secondary")?.text()?.trim()) { + "Main" -> ActorRole.Main + "Supporting" -> ActorRole.Supporting + "Background" -> ActorRole.Background + else -> null + } + Pair(Actor(name, img), role) + } + + // bruh, they use graphql + //val recommendations = soup.select("#suggesction > div > div.p > .swipercard")?.mapNotNull { + // val recTitle = it?.selectFirst(".showname > a") ?: return@mapNotNull null + // val recName = recTitle.text() ?: return@mapNotNull null + // val href = fixUrlNull(recTitle.attr("href")) ?: return@mapNotNull null + // val img = it.selectFirst(".image > img").attr("src") ?: return@mapNotNull null + // AnimeSearchResponse(recName, href, this.name, TvType.Anime, img) + //} + + return newAnimeLoadResponse(title, url, TvType.Anime) { + posterUrl = poster + year = showData.airedStart?.year + + addEpisodes(DubStatus.Subbed, episodes.first) + addEpisodes(DubStatus.Dubbed, episodes.second) + addActors(characters) + //this.recommendations = recommendations + + showStatus = getStatus(showData.status.toString()) + + plot = description?.replace(Regex("""<(.*?)>"""), "") + } + } + + private val embedBlackList = listOf( + "https://mp4upload.com/", + "https://streamsb.net/", + "https://dood.to/", + "https://videobin.co/", + "https://ok.ru", + "https://streamlare.com", + ) + + private fun embedIsBlacklisted(url: String): Boolean { + embedBlackList.forEach { + if (it.javaClass.name == "kotlin.text.Regex") { + if ((it as Regex).matches(url)) { + return true + } + } else { + if (url.contains(it)) { + return true + } + } + } + return false + } + + private fun String.sanitize(): String { + var out = this + listOf(Pair("\\u002F", "/")).forEach { + out = out.replace(it.first, it.second) + } + return out + } + + private data class Links( + @JsonProperty("link") val link: String, + @JsonProperty("hls") val hls: Boolean?, + @JsonProperty("resolutionStr") val resolutionStr: String, + @JsonProperty("src") val src: String? + ) + + private data class AllAnimeVideoApiResponse( + @JsonProperty("links") val links: List + ) + + private data class ApiEndPoint( + @JsonProperty("episodeIframeHead") val episodeIframeHead: String + ) + + private suspend fun getM3u8Qualities( + m3u8Link: String, + referer: String, + qualityName: String, + ): List { + return M3u8Helper.generateM3u8( + this.name, + m3u8Link, + referer, + name = "${this.name} - $qualityName" + ) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + var apiEndPoint = + parseJson(app.get("$mainUrl/getVersion").text).episodeIframeHead + if (apiEndPoint.endsWith("/")) apiEndPoint = + apiEndPoint.slice(0 until apiEndPoint.length - 1) + + val html = app.get(data).text + + val sources = Regex("""sourceUrl[:=]"(.+?)"""").findAll(html).toList() + .map { URLDecoder.decode(it.destructured.component1().sanitize(), "UTF-8") } + sources.apmap { + safeApiCall { + var link = it.replace(" ", "%20") + if (URI(link).isAbsolute || link.startsWith("//")) { + if (link.startsWith("//")) link = "https:$it" + + if (Regex("""streaming\.php\?""").matches(link)) { + // for now ignore + } else if (!embedIsBlacklisted(link)) { + if (URI(link).path.contains(".m3u")) { + getM3u8Qualities(link, data, URI(link).host).forEach(callback) + } else { + callback( + ExtractorLink( + "AllAnime - " + URI(link).host, + "", + link, + data, + Qualities.P1080.value, + false + ) + ) + } + } + } else { + link = apiEndPoint + URI(link).path + ".json?" + URI(link).query + val response = app.get(link) + + if (response.code < 400) { + val links = parseJson(response.text).links + links.forEach { server -> + if (server.hls != null && server.hls) { + getM3u8Qualities( + server.link, + "$apiEndPoint/player?uri=" + (if (URI(server.link).host.isNotEmpty()) server.link else apiEndPoint + URI( + server.link + ).path), + server.resolutionStr + ).forEach(callback) + } else { + callback( + ExtractorLink( + "AllAnime - " + URI(server.link).host, + server.resolutionStr, + server.link, + "$apiEndPoint/player?uri=" + (if (URI(server.link).host.isNotEmpty()) server.link else apiEndPoint + URI( + server.link + ).path), + Qualities.P1080.value, + false + ) + ) + } + } + } + } + } + } + return true + } + +} diff --git a/AllAnimeProvider/src/main/kotlin/com/lagradost/AllAnimeProviderPlugin.kt b/AllAnimeProvider/src/main/kotlin/com/lagradost/AllAnimeProviderPlugin.kt new file mode 100644 index 0000000..8b2169a --- /dev/null +++ b/AllAnimeProvider/src/main/kotlin/com/lagradost/AllAnimeProviderPlugin.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 AllAnimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AllAnimeProvider()) + } +} \ No newline at end of file diff --git a/AniPlayProvider/build.gradle.kts b/AniPlayProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AniPlayProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AniPlayProvider/src/main/AndroidManifest.xml b/AniPlayProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AniPlayProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProvider.kt b/AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProvider.kt new file mode 100644 index 0000000..15eb860 --- /dev/null +++ b/AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProvider.kt @@ -0,0 +1,215 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.Qualities + +class AniPlayProvider : MainAPI() { + override var mainUrl = "https://aniplay.it" + override var name = "AniPlay" + override var lang = "it" + override val hasMainPage = true + private val dubIdentifier = " (ITA)" + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getStatus(t: String?): ShowStatus? { + return when (t?.lowercase()) { + "completato" -> ShowStatus.Completed + "in corso" -> ShowStatus.Ongoing + else -> null // "annunciato" + } + } + fun getType(t: String?): TvType { + return when (t?.lowercase()) { + "ona" -> TvType.OVA + "movie" -> TvType.AnimeMovie + else -> TvType.Anime //"serie", "special" + } + } + } + + private fun isDub(title: String): Boolean{ + return title.contains(dubIdentifier) + } + + data class ApiPoster( + @JsonProperty("imageFull") val posterUrl: String + ) + + data class ApiMainPageAnime( + @JsonProperty("animeId") val id: Int, + @JsonProperty("episodeNumber") val episode: String?, + @JsonProperty("animeTitle") val title: String, + @JsonProperty("animeType") val type: String, + @JsonProperty("fullHd") val fullHD: Boolean, + @JsonProperty("animeVerticalImages") val posters: List + ) + + data class ApiSearchResult( + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String, + @JsonProperty("status") val status: String, + @JsonProperty("type") val type: String, + @JsonProperty("verticalImages") val posters: List + ) + + data class ApiGenres( + @JsonProperty("description") val name: String + ) + data class ApiWebsite( + @JsonProperty("listWebsiteId") val websiteId: Int, + @JsonProperty("url") val url: String + ) + + data class ApiEpisode( + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String?, + @JsonProperty("episodeNumber") val number: String, + ) + + private fun ApiEpisode.toEpisode() : Episode? { + val number = this.number.toIntOrNull() ?: return null + return Episode( + data = "$mainUrl/api/episode/${this.id}", + episode = number, + name = this.title + ) + } + + data class ApiSeason( + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String + ) + + private suspend fun ApiSeason.toEpisodeList(url: String) : List { + return app.get("$url/season/${this.id}").parsed>().mapNotNull { it.toEpisode() } + } + + data class ApiAnime( + @JsonProperty("title") val title: String, + @JsonProperty("alternativeTitle") val japTitle: String?, + @JsonProperty("episodeDuration") val duration: Int, + @JsonProperty("storyline") val plot: String, + @JsonProperty("type") val type: String, + @JsonProperty("status") val status: String, + @JsonProperty("genres") val genres: List, + @JsonProperty("verticalImages") val posters: List, + @JsonProperty("listWebsites") val websites: List, + @JsonProperty("episodes") val episodes: List, + @JsonProperty("seasons") val seasons: List? + ) + + data class ApiEpisodeUrl( + @JsonProperty("videoUrl") val url: String + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val response = app.get("$mainUrl/api/home/latest-episodes?page=0").parsed>() + + val results = response.map{ + val isDub = isDub(it.title) + newAnimeSearchResponse( + name = if (isDub) it.title.replace(dubIdentifier, "") else it.title, + url = "$mainUrl/api/anime/${it.id}", + type = getType(it.type), + ){ + addDubStatus(isDub, it.episode?.toIntOrNull()) + this.posterUrl = it.posters.first().posterUrl + this.quality = if (it.fullHD) SearchQuality.HD else null + } + } + return HomePageResponse(listOf(HomePageList("Ultime uscite",results))) + } + + override suspend fun search(query: String): List { + val response = app.get("$mainUrl/api/anime/advanced-search?page=0&size=36&query=$query").parsed>() + + return response.map { + val isDub = isDub(it.title) + + newAnimeSearchResponse( + name = if (isDub) it.title.replace(dubIdentifier, "") else it.title, + url = "$mainUrl/api/anime/${it.id}", + type = getType(it.type), + ){ + addDubStatus(isDub) + this.posterUrl = it.posters.first().posterUrl + } + } + } + + override suspend fun load(url: String): LoadResponse { + + val response = app.get(url).parsed() + + val tags: List = response.genres.map { it.name } + + val malId: Int? = response.websites.find { it.websiteId == 1 }?.url?.removePrefix("https://myanimelist.net/anime/")?.split("/")?.first()?.toIntOrNull() + val aniListId: Int? = response.websites.find { it.websiteId == 4 }?.url?.removePrefix("https://anilist.co/anime/")?.split("/")?.first()?.toIntOrNull() + + val episodes = if (response.seasons.isNullOrEmpty()) response.episodes.mapNotNull { it.toEpisode() } else response.seasons.map{ it.toEpisodeList(url) }.flatten() + val isDub = isDub(response.title) + + return newAnimeLoadResponse(response.title, url, getType(response.type)) { + this.name = if (isDub) response.title.replace(dubIdentifier, "") else response.title + this.japName = response.japTitle + this.plot = response.plot + this.tags = tags + this.showStatus = getStatus(response.status) + addPoster(response.posters.first().posterUrl) + addEpisodes(if (isDub) DubStatus.Dubbed else DubStatus.Subbed, episodes) + addMalId(malId) + addAniListId(aniListId) + addDuration(response.duration.toString()) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val episode = app.get(data).parsed() + + if(episode.url.contains(".m3u8")){ + val m3u8Helper = M3u8Helper() + val streams = m3u8Helper.m3u8Generation(M3u8Helper.M3u8Stream(episode.url,Qualities.Unknown.value), false) + + streams.forEach { + callback.invoke( + ExtractorLink( + name, + name, + it.streamUrl, + referer = mainUrl, + quality = it.quality ?: Qualities.Unknown.value, + isM3u8 = it.streamUrl.contains(".m3u8"))) } + return true + } + + callback.invoke( + ExtractorLink( + name, + name, + episode.url, + referer = mainUrl, + quality = Qualities.Unknown.value, + isM3u8 = false, + ) + ) + return true + } +} \ No newline at end of file diff --git a/AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProviderPlugin.kt b/AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProviderPlugin.kt new file mode 100644 index 0000000..de3dccb --- /dev/null +++ b/AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProviderPlugin.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 AniPlayProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AniPlayProvider()) + } +} \ No newline at end of file diff --git a/AniflixProvider/build.gradle.kts b/AniflixProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AniflixProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AniflixProvider/src/main/AndroidManifest.xml b/AniflixProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AniflixProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AniflixProvider/src/main/kotlin/com/lagradost/AniflixProvider.kt b/AniflixProvider/src/main/kotlin/com/lagradost/AniflixProvider.kt new file mode 100644 index 0000000..c62d891 --- /dev/null +++ b/AniflixProvider/src/main/kotlin/com/lagradost/AniflixProvider.kt @@ -0,0 +1,274 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URLDecoder + +class AniflixProvider : MainAPI() { + override var mainUrl = "https://aniflix.pro" + override var name = "Aniflix" + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + companion object { + var token: String? = null + } + + private suspend fun getToken(): String { + return token ?: run { + Regex("([^/]*)/_buildManifest\\.js").find(app.get(mainUrl).text)?.groupValues?.getOrNull( + 1 + ) + ?.also { + token = it + } + ?: throw ErrorLoadingException("No token found") + } + } + + private fun Anime.toSearchResponse(): SearchResponse? { + return newAnimeSearchResponse( + title?.english ?: title?.romaji ?: return null, + "$mainUrl/anime/${id ?: return null}" + ) { + posterUrl = coverImage?.large ?: coverImage?.medium + } + } + + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val soup = app.get(mainUrl).document + val elements = listOf( + Pair("Trending Now", "div:nth-child(3) > div a"), + Pair("Popular", "div:nth-child(4) > div a"), + Pair("Top Rated", "div:nth-child(5) > div a"), + ) + + elements.map { (name, element) -> + val home = soup.select(element).map { + val href = it.attr("href") + val title = it.selectFirst("p.mt-2")!!.text() + val image = it.selectFirst("img.rounded-md[sizes]")!!.attr("src").replace("/_next/image?url=","") + .replace(Regex("\\&.*\$"),"") + val realposter = URLDecoder.decode(image, "UTF-8") + newAnimeSearchResponse(title, fixUrl(href)) { + this.posterUrl = realposter + } + } + items.add(HomePageList(name, home)) + } + + return HomePageResponse(items) + } + + override suspend fun search(query: String): List? { + val token = getToken() + val url = "$mainUrl/_next/data/$token/search.json?keyword=$query" + val response = app.get(url) + val searchResponse = + response.parsedSafe() + ?: throw ErrorLoadingException("No Media") + return searchResponse.pageProps?.searchResults?.Page?.media?.mapNotNull { media -> + media.toSearchResponse() + } + } + + override suspend fun load(url: String): LoadResponse { + val token = getToken() + + val id = Regex("$mainUrl/anime/([0-9]*)").find(url)?.groupValues?.getOrNull(1) + ?: throw ErrorLoadingException("Error parsing link for id") + + val res = app.get("https://aniflix.pro/_next/data/$token/anime/$id.json?id=$id") + .parsedSafe()?.pageProps + ?: throw ErrorLoadingException("Invalid Json reponse") + val isMovie = res.anime.format == "MOVIE" + return newAnimeLoadResponse( + res.anime.title?.english ?: res.anime.title?.romaji + ?: throw ErrorLoadingException("Invalid title reponse"), + url, if (isMovie) TvType.AnimeMovie else TvType.Anime + ) { + recommendations = res.recommended.mapNotNull { it.toSearchResponse() } + tags = res.anime.genres + posterUrl = res.anime.coverImage?.large ?: res.anime.coverImage?.medium + plot = res.anime.description + showStatus = when (res.anime.status) { + "FINISHED" -> ShowStatus.Completed + "RELEASING" -> ShowStatus.Ongoing + else -> null + } + addAniListId(id.toIntOrNull()) + + // subbed because they are both subbed and dubbed + if (isMovie) + addEpisodes( + DubStatus.Subbed, + listOf(newEpisode("$mainUrl/api/anime/?id=$id&episode=1")) + ) + else + addEpisodes(DubStatus.Subbed, res.episodes.episodes?.nodes?.mapIndexed { index, node -> + val episodeIndex = node?.number ?: (index + 1) + //"$mainUrl/_next/data/$token/watch/$id.json?episode=${node.number ?: return@mapNotNull null}&id=$id" + newEpisode("$mainUrl/api/anime?id=$id&episode=${episodeIndex}") { + episode = episodeIndex + posterUrl = node?.thumbnail?.original?.url + name = node?.titles?.canonical + } + }) + } + } + + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + return app.get(data).parsed().let { res -> + val dubReferer = res.dub?.Referer ?: "" + res.dub?.sources?.forEach { source -> + callback( + ExtractorLink( + name, + "${source.label ?: name} (DUB)", + source.file ?: return@forEach, + dubReferer, + getQualityFromName(source.label), + source.type == "hls" + ) + ) + } + + val subReferer = res.dub?.Referer ?: "" + res.sub?.sources?.forEach { source -> + callback( + ExtractorLink( + name, + "${source.label ?: name} (SUB)", + source.file ?: return@forEach, + subReferer, + getQualityFromName(source.label), + source.type == "hls" + ) + ) + } + + !res.dub?.sources.isNullOrEmpty() && !res.sub?.sources.isNullOrEmpty() + } + } + + data class AniLoadResponse( + @JsonProperty("sub") val sub: DubSubSource?, + @JsonProperty("dub") val dub: DubSubSource?, + @JsonProperty("episodes") val episodes: Int? + ) + + data class Sources( + @JsonProperty("file") val file: String?, + @JsonProperty("label") val label: String?, + @JsonProperty("type") val type: String? + ) + + data class DubSubSource( + @JsonProperty("Referer") var Referer: String?, + @JsonProperty("sources") var sources: ArrayList = arrayListOf() + ) + + data class PageProps( + @JsonProperty("searchResults") val searchResults: SearchResults? + ) + + data class SearchResults( + @JsonProperty("Page") val Page: Page? + ) + + data class Page( + @JsonProperty("media") val media: ArrayList = arrayListOf() + ) + + data class CoverImage( + @JsonProperty("color") val color: String?, + @JsonProperty("medium") val medium: String?, + @JsonProperty("large") val large: String?, + ) + + data class Title( + @JsonProperty("english") val english: String?, + @JsonProperty("romaji") val romaji: String?, + ) + + data class Search( + @JsonProperty("pageProps") val pageProps: PageProps?, + @JsonProperty("__N_SSP") val _NSSP: Boolean? + ) + + data class Anime( + @JsonProperty("status") val status: String?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: Title?, + @JsonProperty("coverImage") val coverImage: CoverImage?, + @JsonProperty("format") val format: String?, + @JsonProperty("duration") val duration: Int?, + @JsonProperty("meanScore") val meanScore: Int?, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: String?, + @JsonProperty("bannerImage") val bannerImage: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("genres") val genres: ArrayList? = null, + @JsonProperty("season") val season: String?, + @JsonProperty("startDate") val startDate: StartDate?, + ) + + data class StartDate( + @JsonProperty("year") val year: Int? + ) + + data class AnimeResponsePage( + @JsonProperty("pageProps") val pageProps: AnimeResponse?, + ) + + data class AnimeResponse( + @JsonProperty("anime") val anime: Anime, + @JsonProperty("recommended") val recommended: ArrayList, + @JsonProperty("episodes") val episodes: EpisodesParent, + ) + + data class EpisodesParent( + @JsonProperty("id") val id: String?, + @JsonProperty("season") val season: String?, + @JsonProperty("startDate") val startDate: String?, + @JsonProperty("episodeCount") val episodeCount: Int?, + @JsonProperty("episodes") val episodes: Episodes?, + ) + + data class Episodes( + @JsonProperty("nodes") val nodes: ArrayList = arrayListOf() + ) + + data class Nodes( + @JsonProperty("number") val number: Int? = null, + @JsonProperty("titles") val titles: Titles?, + @JsonProperty("thumbnail") val thumbnail: Thumbnail?, + ) + + data class Titles( + @JsonProperty("canonical") val canonical: String?, + ) + + data class Original( + @JsonProperty("url") val url: String?, + ) + + data class Thumbnail( + @JsonProperty("original") val original: Original?, + ) +} \ No newline at end of file diff --git a/AniflixProvider/src/main/kotlin/com/lagradost/AniflixProviderPlugin.kt b/AniflixProvider/src/main/kotlin/com/lagradost/AniflixProviderPlugin.kt new file mode 100644 index 0000000..54d3391 --- /dev/null +++ b/AniflixProvider/src/main/kotlin/com/lagradost/AniflixProviderPlugin.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 AniflixProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AniflixProvider()) + } +} \ No newline at end of file diff --git a/AnimeFlickProvider/build.gradle.kts b/AnimeFlickProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeFlickProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeFlickProvider/src/main/AndroidManifest.xml b/AnimeFlickProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeFlickProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeFlickProvider/src/main/kotlin/com/lagradost/AnimeFlickProvider.kt b/AnimeFlickProvider/src/main/kotlin/com/lagradost/AnimeFlickProvider.kt new file mode 100644 index 0000000..4c76bfa --- /dev/null +++ b/AnimeFlickProvider/src/main/kotlin/com/lagradost/AnimeFlickProvider.kt @@ -0,0 +1,119 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.extractorApis +import org.jsoup.Jsoup +import java.util.* + +class AnimeFlickProvider : 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 + } + } + + override var mainUrl = "https://animeflick.net" + override var name = "AnimeFlick" + override val hasQuickSearch = false + override val hasMainPage = false + + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.Anime, + TvType.OVA + ) + + override suspend fun search(query: String): List { + val link = "https://animeflick.net/search.php?search=$query" + val html = app.get(link).text + val doc = Jsoup.parse(html) + + return doc.select(".row.mt-2").mapNotNull { + val href = mainUrl + it.selectFirst("a")?.attr("href") + val title = it.selectFirst("h5 > a")?.text() ?: return@mapNotNull null + val poster = mainUrl + it.selectFirst("img")?.attr("src")?.replace("70x110", "225x320") + AnimeSearchResponse( + title, + href, + this.name, + getType(title), + poster, + null, + EnumSet.of(DubStatus.Subbed), + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val html = app.get(url).text + val doc = Jsoup.parse(html) + + val poster = mainUrl + doc.selectFirst("img.rounded")?.attr("src") + val title = doc.selectFirst("h2.title")!!.text() + + val yearText = doc.selectFirst(".trending-year")?.text() + val year = + if (yearText != null) Regex("""(\d{4})""").find(yearText)?.destructured?.component1() + ?.toIntOrNull() else null + val description = doc.selectFirst("p")?.text() + + val genres = doc.select("a[href*=\"genre-\"]").map { it.text() } + + val episodes = doc.select("#collapseOne .block-space > .row > div:nth-child(2)").map { + val name = it.selectFirst("a")?.text() + val link = mainUrl + it.selectFirst("a")?.attr("href") + Episode(link, name) + }.reversed() + + return newAnimeLoadResponse(title, url, getType(title)) { + posterUrl = poster + this.year = year + + addEpisodes(DubStatus.Subbed, episodes) + + plot = description + tags = genres + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val html = app.get(data).text + + val episodeRegex = Regex("""(https://.*?\.mp4)""") + val links = episodeRegex.findAll(html).map { + it.value + }.toList() + for (link in links) { + var alreadyAdded = false + for (extractor in extractorApis) { + if (link.startsWith(extractor.mainUrl)) { + extractor.getSafeUrl(link, data, subtitleCallback, callback) + alreadyAdded = true + break + } + } + if (!alreadyAdded) { + callback( + ExtractorLink( + this.name, + "${this.name} - Auto", + link, + "", + Qualities.P1080.value + ) + ) + } + } + + return true + } +} diff --git a/AnimeFlickProvider/src/main/kotlin/com/lagradost/AnimeFlickProviderPlugin.kt b/AnimeFlickProvider/src/main/kotlin/com/lagradost/AnimeFlickProviderPlugin.kt new file mode 100644 index 0000000..0e9d53c --- /dev/null +++ b/AnimeFlickProvider/src/main/kotlin/com/lagradost/AnimeFlickProviderPlugin.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 AnimeFlickProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeFlickProvider()) + } +} \ No newline at end of file diff --git a/AnimeIndoProvider/build.gradle.kts b/AnimeIndoProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeIndoProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeIndoProvider/src/main/AndroidManifest.xml b/AnimeIndoProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeIndoProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeIndoProvider/src/main/kotlin/com/lagradost/AnimeIndoProvider.kt b/AnimeIndoProvider/src/main/kotlin/com/lagradost/AnimeIndoProvider.kt new file mode 100644 index 0000000..fb132b7 --- /dev/null +++ b/AnimeIndoProvider/src/main/kotlin/com/lagradost/AnimeIndoProvider.kt @@ -0,0 +1,192 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.getCaptchaToken +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import com.lagradost.nicehttp.NiceResponse +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class AnimeIndoProvider : MainAPI() { + override var mainUrl = "https://animeindo.sbs" + override var name = "AnimeIndo" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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) { + "Finished Airing" -> ShowStatus.Completed + "Currently Airing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private suspend fun request(url: String): NiceResponse { + val req = app.get( + url, + cookies = mapOf("recaptcha_cookie" to "#Asia/Jakarta#-420#win32#Windows#0,false,false#Google Inc. (Intel)~ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0)") + ) + if (req.isSuccessful) { + return req + } else { + val document = app.get(url).document + val captchaKey = + document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]") + .attr("src").substringAfter("render=").substringBefore("&") + val token = getCaptchaToken(url, captchaKey) + return app.post( + url, + data = mapOf( + "action" to "recaptcha_for_all", + "token" to "$token", + "sitekey" to captchaKey + ) + ) + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/anime-terbaru/page/" to "Anime Terbaru", + "$mainUrl/donghua-terbaru/page/" to "Donghua Terbaru" + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = request(request.data + page).document + val home = document.select("div.post-show > article").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> Regex("(.+)-episode").find( + title + )?.groupValues?.get(1).toString() + (title.contains("-movie")) -> Regex("(.+)-movie").find(title)?.groupValues?.get( + 1 + ).toString() + else -> title + } + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("div.title")?.text()?.trim() ?: return null + val href = getProperAnimeLink(this.selectFirst("a")!!.attr("href")) + val posterUrl = this.select("img[itemprop=image]").attr("src").toString() + val type = getType(this.select("div.type").text().trim()) + val epNum = + this.selectFirst("span.episode")?.ownText()?.replace(Regex("[^0-9]"), "")?.trim() + ?.toIntOrNull() + return newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = request(link).document + + return document.select(".site-main.relat > article").map { + val title = it.selectFirst("div.title > h2")!!.ownText().trim() + val href = it.selectFirst("a")!!.attr("href") + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + val type = getType(it.select("div.type").text().trim()) + newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = request(url).document + + val title = document.selectFirst("h1.entry-title")?.text().toString().trim() + val poster = document.selectFirst("div.thumb > img[itemprop=image]")?.attr("src") + val tags = document.select("div.genxed > a").map { it.text() } + val type = getType( + document.selectFirst("div.info-content > div.spe > span:nth-child(6)")?.ownText() + .toString() + ) + val year = Regex("\\d, ([0-9]*)").find( + document.select("div.info-content > div.spe > span:nth-child(9) > time").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.selectFirst("div.info-content > div.spe > span:nth-child(1)")!!.ownText() + .trim() + ) + val description = document.select("div[itemprop=description] > p").text() + val trailer = document.selectFirst("div.player-embed iframe")?.attr("src") + val episodes = document.select("div.lstepsiode.listeps ul li").mapNotNull { + val header = it.selectFirst("span.lchx > a") ?: return@mapNotNull null + val name = header.text().trim() + val episode = header.text().trim().replace("Episode", "").trim().toIntOrNull() + val link = fixUrl(header.attr("href")) + Episode(link, name = name, episode = episode) + }.reversed() + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + addTrailer(trailer) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = request(data).document + document.select("div.itemleft > .mirror > option").mapNotNull { + fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("src")) + }.apmap { + if (it.startsWith("https://uservideo.xyz")) { + app.get(it, referer = "$mainUrl/").document.select("iframe").attr("src") + } else { + it + } + }.apmap { + loadExtractor(it, data, subtitleCallback, callback) + } + + return true + } + + +} \ No newline at end of file diff --git a/AnimeIndoProvider/src/main/kotlin/com/lagradost/AnimeIndoProviderPlugin.kt b/AnimeIndoProvider/src/main/kotlin/com/lagradost/AnimeIndoProviderPlugin.kt new file mode 100644 index 0000000..d1dab81 --- /dev/null +++ b/AnimeIndoProvider/src/main/kotlin/com/lagradost/AnimeIndoProviderPlugin.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 AnimeIndoProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeIndoProvider()) + } +} \ No newline at end of file diff --git a/AnimePaheProvider/build.gradle.kts b/AnimePaheProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimePaheProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimePaheProvider/src/main/AndroidManifest.xml b/AnimePaheProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimePaheProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimePaheProvider/src/main/kotlin/com/lagradost/AnimePaheProvider.kt b/AnimePaheProvider/src/main/kotlin/com/lagradost/AnimePaheProvider.kt new file mode 100644 index 0000000..56c0e4a --- /dev/null +++ b/AnimePaheProvider/src/main/kotlin/com/lagradost/AnimePaheProvider.kt @@ -0,0 +1,562 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +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.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.nicehttp.NiceResponse +import org.jsoup.Jsoup +import kotlin.math.pow + +class AnimePaheProvider : MainAPI() { + // credit to https://github.com/justfoolingaround/animdl/tree/master/animdl/core/codebase/providers/animepahe + companion object { + const val MAIN_URL = "https://animepahe.com" + + var cookies: Map = mapOf() + private 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 + } + + suspend fun generateSession(): Boolean { + if (cookies.isNotEmpty()) return true + return try { + val response = app.get("$MAIN_URL/") + cookies = response.cookies + true + } catch (e: Exception) { + false + } + } + + val YTSM = Regex("ysmm = '([^']+)") + + val KWIK_PARAMS_RE = Regex("""\("(\w+)",\d+,"(\w+)",(\d+),(\d+),\d+\)""") + val KWIK_D_URL = Regex("action=\"([^\"]+)\"") + val KWIK_D_TOKEN = Regex("value=\"([^\"]+)\"") + val YOUTUBE_VIDEO_LINK = + Regex("""(^(?:https?:)?(?://)?(?:www\.)?(?:youtu\.be/|youtube(?:-nocookie)?\.(?:[A-Za-z]{2,4}|[A-Za-z]{2,3}\.[A-Za-z]{2})/)(?:watch|embed/|vi?/)*(?:\?[\w=&]*vi?=)?[^#&?/]{11}.*${'$'})""") + } + + override var mainUrl = MAIN_URL + override var name = "AnimePahe" + override val hasQuickSearch = false + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.Anime, + TvType.OVA + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + data class Data( + @JsonProperty("id") val id: Int, + @JsonProperty("anime_id") val animeId: Int, + @JsonProperty("anime_title") val animeTitle: String, + @JsonProperty("anime_slug") val animeSlug: String, + @JsonProperty("episode") val episode: Int, + @JsonProperty("snapshot") val snapshot: String, + @JsonProperty("created_at") val createdAt: String, + @JsonProperty("anime_session") val animeSession: String, + ) + + data class AnimePaheLatestReleases( + @JsonProperty("total") val total: Int, + @JsonProperty("data") val data: List + ) + + val urls = listOf( + Pair("$mainUrl/api?m=airing&page=1", "Latest Releases"), + ) + + val items = ArrayList() + for (i in urls) { + try { + val response = app.get(i.first).text + val episodes = parseJson(response).data.map { + newAnimeSearchResponse( + it.animeTitle, + "https://pahe.win/a/${it.animeId}?slug=${it.animeTitle}", + fix = false + ) { + this.posterUrl = it.snapshot + addDubStatus(DubStatus.Subbed, it.episode) + } + } + + items.add(HomePageList(i.second, episodes)) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + data class AnimePaheSearchData( + @JsonProperty("id") val id: Int, + @JsonProperty("slug") val slug: String, + @JsonProperty("title") val title: String, + @JsonProperty("type") val type: String, + @JsonProperty("episodes") val episodes: Int, + @JsonProperty("status") val status: String, + @JsonProperty("season") val season: String, + @JsonProperty("year") val year: Int, + @JsonProperty("score") val score: Double, + @JsonProperty("poster") val poster: String, + @JsonProperty("session") val session: String, + @JsonProperty("relevance") val relevance: String + ) + + data class AnimePaheSearch( + @JsonProperty("total") val total: Int, + @JsonProperty("data") val data: List + ) + + private suspend fun getAnimeByIdAndTitle(title: String, animeId: Int): String? { + val url = "$mainUrl/api?m=search&l=8&q=$title" + val headers = mapOf("referer" to "$mainUrl/") + + val req = app.get(url, headers = headers).text + val data = parseJson(req) + for (anime in data.data) { + if (anime.id == animeId) { + return "https://animepahe.com/anime/${anime.session}" + } + } + return null + } + + + override suspend fun search(query: String): List { + val url = "$mainUrl/api?m=search&l=8&q=$query" + val headers = mapOf("referer" to "$mainUrl/") + + val req = app.get(url, headers = headers).text + val data = parseJson(req) + + return data.data.map { + newAnimeSearchResponse( + it.title, + "https://pahe.win/a/${it.id}?slug=${it.title}", + fix = false + ) { + this.posterUrl = it.poster + addDubStatus(DubStatus.Subbed, it.episodes) + } + } + } + + private data class AnimeData( + @JsonProperty("id") val id: Int, + @JsonProperty("anime_id") val animeId: Int, + @JsonProperty("episode") val episode: Int, + @JsonProperty("title") val title: String, + @JsonProperty("snapshot") val snapshot: String, + @JsonProperty("session") val session: String, + @JsonProperty("filler") val filler: Int, + @JsonProperty("created_at") val createdAt: String + ) + + private data class AnimePaheAnimeData( + @JsonProperty("total") val total: Int, + @JsonProperty("per_page") val perPage: Int, + @JsonProperty("current_page") val currentPage: Int, + @JsonProperty("last_page") val lastPage: Int, + @JsonProperty("next_page_url") val nextPageUrl: String?, + @JsonProperty("prev_page_url") val prevPageUrl: String?, + @JsonProperty("from") val from: Int, + @JsonProperty("to") val to: Int, + @JsonProperty("data") val data: List + ) + + private suspend fun generateListOfEpisodes(link: String): ArrayList { + try { + val attrs = link.split('/') + val id = attrs[attrs.size - 1].split("?")[0] + + val uri = "$mainUrl/api?m=release&id=$id&sort=episode_asc&page=1" + val headers = mapOf("referer" to "$mainUrl/") + + val req = app.get(uri, headers = headers).text + val data = parseJson(req) + + val lastPage = data.lastPage + val perPage = data.perPage + val total = data.total + var ep = 1 + val episodes = ArrayList() + + fun getEpisodeTitle(k: AnimeData): String { + return k.title.ifEmpty { + "Episode ${k.episode}" + } + } + + if (lastPage == 1 && perPage > total) { + data.data.forEach { + episodes.add( + newEpisode("$mainUrl/api?m=links&id=${it.animeId}&session=${it.session}&p=kwik!!TRUE!!") { + addDate(it.createdAt) + this.name = getEpisodeTitle(it) + this.posterUrl = it.snapshot + } + ) + } + } else { + for (page in 0 until lastPage) { + for (i in 0 until perPage) { + if (ep <= total) { + episodes.add( + Episode( + "$mainUrl/api?m=release&id=${id}&sort=episode_asc&page=${page + 1}&ep=${ep}!!FALSE!!" + ) + ) + ++ep + } + } + } + } + return episodes + } catch (e: Exception) { + return ArrayList() + } + } + + override suspend fun load(url: String): LoadResponse? { + return suspendSafeApiCall { + val regex = Regex("""a/(\d+)\?slug=(.+)""") + val (animeId, animeTitle) = regex.find(url)!!.destructured + val link = getAnimeByIdAndTitle(animeTitle, animeId.toInt())!! + + val html = app.get(link).text + val doc = Jsoup.parse(html) + + val japTitle = doc.selectFirst("h2.japanese")?.text() + val poster = doc.selectFirst(".anime-poster a")?.attr("href") + + val tvType = doc.selectFirst("""a[href*="/anime/type/"]""")?.text() + + val trailer: String? = if (html.contains("https://www.youtube.com/watch")) { + YOUTUBE_VIDEO_LINK.find(html)?.destructured?.component1() + } else { + null + } + + val episodes = generateListOfEpisodes(url) + val year = Regex("""Aired:[^,]*, (\d+)""") + .find(html)!!.destructured.component1() + .toIntOrNull() + val status = + when (Regex("""Status:[^a]*a href=["']/anime/(.*?)["']""") + .find(html)!!.destructured.component1()) { + "airing" -> ShowStatus.Ongoing + "completed" -> ShowStatus.Completed + else -> null + } + val synopsis = doc.selectFirst(".anime-synopsis")?.text() + + var anilistId: Int? = null + var malId: Int? = null + + doc.select(".external-links > a").forEach { aTag -> + val split = aTag.attr("href").split("/") + + if (aTag.attr("href").contains("anilist.co")) { + anilistId = split[split.size - 1].toIntOrNull() + } else if (aTag.attr("href").contains("myanimelist.net")) { + malId = split[split.size - 1].toIntOrNull() + } + } + + newAnimeLoadResponse(animeTitle, url, getType(tvType.toString())) { + engName = animeTitle + japName = japTitle + + this.posterUrl = poster + this.year = year + + addEpisodes(DubStatus.Subbed, episodes) + this.showStatus = status + plot = synopsis + tags = if (!doc.select(".anime-genre > ul a").isEmpty()) { + ArrayList(doc.select(".anime-genre > ul a").map { it.text().toString() }) + } else { + null + } + + addMalId(malId) + addAniListId(anilistId) + addTrailer(trailer) + } + } + } + + + private fun isNumber(s: String?): Boolean { + return s?.toIntOrNull() != null + } + + private fun cookieStrToMap(cookie: String): Map { + val cookies = mutableMapOf() + for (string in cookie.split("; ")) { + val split = string.split("=").toMutableList() + val name = split.removeFirst().trim() + val value = if (split.size == 0) { + "true" + } else { + split.joinToString("=") + } + cookies[name] = value + } + return cookies.toMap() + } + + private fun getString(content: String, s1: Int, s2: Int): String { + val characterMap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/" + + val slice2 = characterMap.slice(0 until s2) + var acc: Long = 0 + + for ((n, i) in content.reversed().withIndex()) { + acc += (when (isNumber("$i")) { + true -> "$i".toLong() + false -> "0".toLong() + }) * s1.toDouble().pow(n.toDouble()).toInt() + } + + var k = "" + + while (acc > 0) { + k = slice2[(acc % s2).toInt()] + k + acc = (acc - (acc % s2)) / s2 + } + + return when (k != "") { + true -> k + false -> "0" + } + } + + private fun decrypt(fullString: String, key: String, v1: Int, v2: Int): String { + var r = "" + var i = 0 + + while (i < fullString.length) { + var s = "" + + while (fullString[i] != key[v2]) { + s += fullString[i] + ++i + } + var j = 0 + + while (j < key.length) { + s = s.replace(key[j].toString(), j.toString()) + ++j + } + r += (getString(s, v2, 10).toInt() - v1).toChar() + ++i + } + return r + } + + private fun zipGen(gen: Sequence>): ArrayList, Pair>> { + val allItems = gen.toList().toMutableList() + val newList = ArrayList, Pair>>() + + while (allItems.size > 1) { + newList.add(Pair(allItems[0], allItems[1])) + allItems.removeAt(0) + allItems.removeAt(0) + } + return newList + } + + private fun decodeAdfly(codedKey: String): String { + var r = "" + var j = "" + + for ((n, l) in codedKey.withIndex()) { + if (n % 2 != 0) { + j = l + j + } else { + r += l + } + } + + val encodedUri = ((r + j).toCharArray().map { it.toString() }).toMutableList() + val numbers = sequence { + for ((i, n) in encodedUri.withIndex()) { + if (isNumber(n)) { + yield(Pair(i, n.toInt())) + } + } + } + + for ((first, second) in zipGen(numbers)) { + val xor = first.second.xor(second.second) + if (xor < 10) { + encodedUri[first.first] = xor.toString() + } + } + var returnValue = String(encodedUri.joinToString("").toByteArray(), Charsets.UTF_8) + returnValue = base64Decode(returnValue) + return returnValue.slice(16..returnValue.length - 17) + } + + private data class VideoQuality( + @JsonProperty("id") val id: Int?, + @JsonProperty("audio") val audio: String?, + @JsonProperty("kwik") val kwik: String?, + @JsonProperty("kwik_pahewin") val kwikPahewin: String + ) + + private data class AnimePaheEpisodeLoadLinks( + @JsonProperty("data") val data: List> + ) + + private suspend fun bypassAdfly(adflyUri: String): String { + if (!generateSession()) { + return bypassAdfly(adflyUri) + } + + var responseCode = 302 + var adflyContent: NiceResponse? = null + var tries = 0 + + while (responseCode != 200 && tries < 20) { + adflyContent = app.get( + app.get(adflyUri, cookies = cookies, allowRedirects = false).url, + cookies = cookies, + allowRedirects = false + ) + cookies = cookies + adflyContent.cookies + responseCode = adflyContent.code + ++tries + } + if (tries > 19) { + throw Exception("Failed to bypass adfly.") + } + return decodeAdfly(YTSM.find(adflyContent?.text.toString())!!.destructured.component1()) + } + + private suspend fun getStreamUrlFromKwik(url: String?): String? { + if (url == null) return null + val response = + app.get( + url, + headers = mapOf("referer" to mainUrl), + cookies = cookies + ).text + Regex("eval((.|\\n)*?)").find(response)?.groupValues?.get(1)?.let { jsEval -> + JsUnpacker("eval$jsEval").unpack()?.let { unPacked -> + Regex("source=\'(.*?)\'").find(unPacked)?.groupValues?.get(1)?.let { link -> + return link + } + } + } + return null + } + + private suspend fun getStreamUrlFromKwikAdfly(adflyUri: String): String { + val fContent = + app.get( + bypassAdfly(adflyUri), + headers = mapOf("referer" to "https://kwik.cx/"), + cookies = cookies + ) + cookies = cookies + fContent.cookies + + val (fullString, key, v1, v2) = KWIK_PARAMS_RE.find(fContent.text)!!.destructured + val decrypted = decrypt(fullString, key, v1.toInt(), v2.toInt()) + val uri = KWIK_D_URL.find(decrypted)!!.destructured.component1() + val tok = KWIK_D_TOKEN.find(decrypted)!!.destructured.component1() + var content: NiceResponse? = null + + var code = 419 + var tries = 0 + + while (code != 302 && tries < 20) { + content = app.post( + uri, + allowRedirects = false, + data = mapOf("_token" to tok), + headers = mapOf("referer" to fContent.url), + cookies = fContent.cookies + ) + code = content.code + ++tries + } + if (tries > 19) { + throw Exception("Failed to extract the stream uri from kwik.") + } + return content?.headers?.values("location").toString() + } + + private suspend fun extractVideoLinks( + episodeLink: String, + callback: (ExtractorLink) -> Unit + ) { + var link = episodeLink + val headers = mapOf("referer" to "$mainUrl/") + + if (link.contains("!!TRUE!!")) { + link = link.replace("!!TRUE!!", "") + } else { + val regex = """&ep=(\d+)!!FALSE!!""".toRegex() + val episodeNum = regex.find(link)?.destructured?.component1()?.toIntOrNull() + link = link.replace(regex, "") + + val req = app.get(link, headers = headers).text + val jsonResponse = parseJson(req) + val ep = ((jsonResponse.data.map { + if (it.episode == episodeNum) { + it + } else { + null + } + }).filterNotNull())[0] + link = "$mainUrl/api?m=links&id=${ep.animeId}&session=${ep.session}&p=kwik" + } + val req = app.get(link, headers = headers).text + val data = mapper.readValue(req) + + data.data.forEach { + it.entries.toList().apmap { quality -> + getStreamUrlFromKwik(quality.value.kwik)?.let { link -> + callback( + ExtractorLink( + "KWIK", + "KWIK - ${quality.key} [${quality.value.audio ?: "jpn"}]", + link, + "https://kwik.cx/", + getQualityFromName(quality.key), + link.contains(".m3u8") + ) + ) + } + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + extractVideoLinks(data, callback) + return true + } +} diff --git a/AnimePaheProvider/src/main/kotlin/com/lagradost/AnimePaheProviderPlugin.kt b/AnimePaheProvider/src/main/kotlin/com/lagradost/AnimePaheProviderPlugin.kt new file mode 100644 index 0000000..e0ae1f3 --- /dev/null +++ b/AnimePaheProvider/src/main/kotlin/com/lagradost/AnimePaheProviderPlugin.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 AnimePaheProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimePaheProvider()) + } +} \ No newline at end of file diff --git a/AnimeSailProvider/build.gradle.kts b/AnimeSailProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeSailProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeSailProvider/src/main/AndroidManifest.xml b/AnimeSailProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeSailProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeSailProvider/src/main/kotlin/com/lagradost/AnimeSailProvider.kt b/AnimeSailProvider/src/main/kotlin/com/lagradost/AnimeSailProvider.kt new file mode 100644 index 0000000..b190fd8 --- /dev/null +++ b/AnimeSailProvider/src/main/kotlin/com/lagradost/AnimeSailProvider.kt @@ -0,0 +1,191 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import com.lagradost.nicehttp.NiceResponse +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class AnimeSailProvider : MainAPI() { + override var mainUrl = "https://111.90.143.42" + override var name = "AnimeSail" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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 + } + } + } + + private suspend fun request(url: String, ref: String? = null): NiceResponse { + return app.get( + url, + headers = mapOf("Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"), + cookies = mapOf("_as_ipin_ct" to "ID"), + referer = ref + ) + } + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "Episode Terbaru", + "$mainUrl/movie-terbaru/page/" to "Movie Terbaru", + "$mainUrl/genres/donghua/page/" to "Donghua" + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = request(request.data + page).document + val home = document.select("article").map { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> title.substringBefore( + "-episode" + ) + (title.contains("-movie")) -> title.substringBefore("-movie") + else -> title + } + + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString()) + val title = this.select(".tt > h2").text().trim() + val posterUrl = fixUrlNull(this.selectFirst("div.limit img")?.attr("src")) + val epNum = this.selectFirst(".tt > h2")?.text()?.let { + Regex("Episode\\s?([0-9]+)").find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = request(link).document + + return document.select("div.listupd article").map { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = request(url).document + + val title = document.selectFirst("h1.entry-title")?.text().toString().trim() + val type = getType( + document.select("tbody th:contains(Tipe)").next().text() + ) + val episodes = document.select("ul.daftar > li").map { + val header = it.select("a").text().trim() + val name = + Regex("(Episode\\s?[0-9]+)").find(header)?.groupValues?.getOrNull(0) ?: header + val link = fixUrl(it.select("a").attr("href")) + Episode(link, name = name) + }.reversed() + + return newAnimeLoadResponse(title, url, type) { + posterUrl = document.selectFirst("div.entry-content > img")?.attr("src") + this.year = + document.select("tbody th:contains(Dirilis)").next().text().trim().toIntOrNull() + addEpisodes(DubStatus.Subbed, episodes) + showStatus = + getStatus(document.select("tbody th:contains(Status)").next().text().trim()) + plot = document.selectFirst("div.entry-content > p")?.text() + this.tags = + document.select("tbody th:contains(Genre)").next().select("a").map { it.text() } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = request(data).document + + document.select(".mobius > .mirror > option").apmap { + safeApiCall { + val iframe = fixUrl( + Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src") + ?: throw ErrorLoadingException("No iframe found") + ) + + when { + iframe.startsWith("$mainUrl/utils/player/arch/") || iframe.startsWith( + "$mainUrl/utils/player/race/" + ) -> request(iframe, ref = data).document.select("source").attr("src") + .let { link -> + val source = + when { + iframe.contains("/arch/") -> "Arch" + iframe.contains("/race/") -> "Race" + else -> this.name + } + val quality = + Regex("\\.([0-9]{3,4})\\.").find(link)?.groupValues?.get(1) + callback.invoke( + ExtractorLink( + source = source, + name = source, + url = link, + referer = mainUrl, + quality = quality?.toIntOrNull() ?: Qualities.Unknown.value + ) + ) + } +// skip for now +// iframe.startsWith("$mainUrl/utils/player/fichan/") -> "" +// iframe.startsWith("$mainUrl/utils/player/blogger/") -> "" + iframe.startsWith("$mainUrl/utils/player/framezilla/") || iframe.startsWith("https://uservideo.xyz") -> { + request(iframe, ref = data).document.select("iframe").attr("src") + .let { link -> + loadExtractor(fixUrl(link), mainUrl, subtitleCallback, callback) + } + } + else -> { + loadExtractor(iframe, mainUrl, subtitleCallback, callback) + } + } + } + } + + return true + } + + +} \ No newline at end of file diff --git a/AnimeSailProvider/src/main/kotlin/com/lagradost/AnimeSailProviderPlugin.kt b/AnimeSailProvider/src/main/kotlin/com/lagradost/AnimeSailProviderPlugin.kt new file mode 100644 index 0000000..6b2b8a9 --- /dev/null +++ b/AnimeSailProvider/src/main/kotlin/com/lagradost/AnimeSailProviderPlugin.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 AnimeSailProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeSailProvider()) + } +} \ No newline at end of file diff --git a/AnimeSaturnProvider/build.gradle.kts b/AnimeSaturnProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeSaturnProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeSaturnProvider/src/main/AndroidManifest.xml b/AnimeSaturnProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeSaturnProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeSaturnProvider/src/main/kotlin/com/lagradost/AnimeSaturnProvider.kt b/AnimeSaturnProvider/src/main/kotlin/com/lagradost/AnimeSaturnProvider.kt new file mode 100644 index 0000000..dadb2b5 --- /dev/null +++ b/AnimeSaturnProvider/src/main/kotlin/com/lagradost/AnimeSaturnProvider.kt @@ -0,0 +1,201 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addRating +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.jsoup.nodes.Element + +class AnimeSaturnProvider : MainAPI() { + override var mainUrl = "https://www.animesaturn.cc" + override var name = "AnimeSaturn" + override var lang = "it" + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getStatus(t: String?): ShowStatus? { + return when (t?.lowercase()) { + "finito" -> ShowStatus.Completed + "in corso" -> ShowStatus.Ongoing + else -> null + } + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + + var title = this.select("a.badge-archivio").first()!!.text() + var isDubbed = false + + if (title.contains(" (ITA)")){ + title = title.replace(" (ITA)", "") + isDubbed = true + } + + val url = this.select("a.badge-archivio").first()!!.attr("href") + + val posterUrl = this.select("img.locandina-archivio[src]").first()!!.attr("src") + + return newAnimeSearchResponse(title, url, TvType.Anime) { + addDubStatus(isDubbed) + this.posterUrl = posterUrl + } + } + + private fun Element.toEpisode(): Episode? { + var episode = this.text().split(" ")[1] + if(episode.contains(".")) return null + if(episode.contains("-")) + episode = episode.split("-")[0] + + return Episode( + data = this.attr("href"), + episode = episode.toInt() + ) + + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val document = app.get(mainUrl).document + val list = ArrayList() + document.select("div.container:has(span.badge-saturn)").forEach { + val tabName = it.select("span.badge-saturn").first()!!.text() + if (tabName.equals("Ultimi episodi")) return@forEach + val results = ArrayList() + it.select(".main-anime-card").forEach { card -> + var title = card.select("a[title]").first()!!.attr("title") + var isDubbed = false + if(title.contains(" (ITA)")){ + title = title.replace(" (ITA)", "") + isDubbed = true + } + val posterUrl = card.select("img.new-anime").first()!!.attr("src") + val url = card.select("a").first()!!.attr("href") + + results.add(newAnimeSearchResponse(title, url, TvType.Anime){ + addDubStatus(isDubbed) + this.posterUrl = posterUrl + }) + } + list.add(HomePageList(tabName, results)) + } + return HomePageResponse(list) + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/animelist?search=$query").document + return document.select("div.item-archivio").map { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + + val document = app.get(url).document + + val title = document.select("img.cover-anime").first()!!.attr("alt") + val japTitle = document.select("div.box-trasparente-alternativo").first()!!.text() + val posterUrl = document.select("img.cover-anime[src]").first()!!.attr("src") + var malId : Int? = null + var aniListId : Int? = null + + document.select("[rel=\"noopener noreferrer\"]").forEach { + if(it.attr("href").contains("myanimelist")) + malId = it.attr("href").removeSuffix("/").split('/').last().toIntOrNull() + else + aniListId = it.attr("href").removeSuffix("/").split('/').last().toIntOrNull() + } + + val plot = document.select("div#shown-trama").first()?.text() + + val tags = document.select("a.generi-as").map { it.text() } + + val details : List? = document.select("div.container:contains(Stato: )").first()?.text()?.split(" ") + var status : String? = null + var duration : String? = null + var year : String? = null + var score : String? = null + + val isDubbed = document.select("div.anime-title-as").first()!!.text().contains("(ITA)") + + if (!details.isNullOrEmpty()) { + details.forEach { + val index = details.indexOf(it) +1 + when (it) { + "Stato:" -> status = details[index] + "episodi:" -> duration = details[index] + "uscita:" -> year = details[index + 2] + "Voto:" -> score = details[index].split("/")[0] + else -> return@forEach + } + } + } + + val episodes = document.select("a.bottone-ep").mapNotNull{ it.toEpisode() } + + return newAnimeLoadResponse(title, url, TvType.Anime) { + this.engName = title + this.japName = japTitle + this.year = year?.toIntOrNull() + this.plot = plot + this.tags = tags + this.showStatus = getStatus(status) + addPoster(posterUrl) + addRating(score) + addEpisodes(if (isDubbed) DubStatus.Dubbed else DubStatus.Subbed, episodes) + addMalId(malId) + addAniListId(aniListId) + addDuration(duration) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val page = app.get(data).document + val episodeLink = page.select("div.card-body > a[href]").find {it1 -> + it1.attr("href").contains("watch?") + }?.attr("href") + + val episodePage = app.get(episodeLink!!).document + val episodeUrl: String? + var isM3U8 = false + + if(episodePage.select("video.afterglow > source").isNotEmpty()) //Old player + episodeUrl = episodePage.select("video.afterglow > source").first()!!.attr("src") + + else{ //New player + val script = episodePage.select("script").find { + it.toString().contains("jwplayer('player_hls').setup({") + }!!.toString() + episodeUrl = script.split(" ").find { it.contains(".m3u8") and !it.contains(".replace") }!!.replace("\"","").replace(",", "") + isM3U8 = true + } + + + callback.invoke( + ExtractorLink( + name, + name, + episodeUrl!!, + isM3u8 = isM3U8, + referer = "https://www.animesaturn.io/", //Some servers need the old host as referer, and the new ones accept it too + quality = Qualities.Unknown.value + ) + ) + return true + } +} diff --git a/AnimeSaturnProvider/src/main/kotlin/com/lagradost/AnimeSaturnProviderPlugin.kt b/AnimeSaturnProvider/src/main/kotlin/com/lagradost/AnimeSaturnProviderPlugin.kt new file mode 100644 index 0000000..048741d --- /dev/null +++ b/AnimeSaturnProvider/src/main/kotlin/com/lagradost/AnimeSaturnProviderPlugin.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 AnimeSaturnProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeSaturnProvider()) + } +} \ No newline at end of file diff --git a/AnimeWorldProvider/build.gradle.kts b/AnimeWorldProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeWorldProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeWorldProvider/src/main/AndroidManifest.xml b/AnimeWorldProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeWorldProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeWorldProvider/src/main/kotlin/com/lagradost/AnimeWorldProvider.kt b/AnimeWorldProvider/src/main/kotlin/com/lagradost/AnimeWorldProvider.kt new file mode 100644 index 0000000..3d454cb --- /dev/null +++ b/AnimeWorldProvider/src/main/kotlin/com/lagradost/AnimeWorldProvider.kt @@ -0,0 +1,265 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addRating +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.nicehttp.NiceResponse +import org.jsoup.nodes.Element +import org.mozilla.javascript.ConsString +import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable + +class AnimeWorldProvider : MainAPI() { + override var mainUrl = "https://www.animeworld.tv" + override var name = "AnimeWorld" + override var lang = "it" + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + private var cookies = emptyMap() + + // Disabled authentication as site did + private suspend fun request(url: String): NiceResponse { +// if (cookies.isEmpty()) { +// cookies = getCookies(url) +// } + return app.get(url +// , cookies = cookies + ) + } + + private suspend fun getCookies(url: String): Map { + val rhino = Context.enter() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + + val slowAes = app.get("https://www.animeworld.tv/aes.min.js").text +// val slowAes = """ +// var _0x2465=["\x72\x6F\x74\x61\x74\x65","\x73\x62\x6F\x78","\x52\x63\x6F\x6E","\x6E\x75\x6D\x62\x65\x72\x4F\x66\x52\x6F\x75\x6E\x64\x73","\x63\x6F\x72\x65","\x53\x49\x5A\x45\x5F\x32\x35\x36","\x6B\x65\x79\x53\x69\x7A\x65","\x72\x73\x62\x6F\x78","\x73\x68\x69\x66\x74\x52\x6F\x77","\x6D\x69\x78\x43\x6F\x6C\x75\x6D\x6E","\x67\x61\x6C\x6F\x69\x73\x5F\x6D\x75\x6C\x74\x69\x70\x6C\x69\x63\x61\x74\x69\x6F\x6E","\x73\x75\x62\x42\x79\x74\x65\x73","\x73\x68\x69\x66\x74\x52\x6F\x77\x73","\x6D\x69\x78\x43\x6F\x6C\x75\x6D\x6E\x73","\x61\x64\x64\x52\x6F\x75\x6E\x64\x4B\x65\x79","\x63\x72\x65\x61\x74\x65\x52\x6F\x75\x6E\x64\x4B\x65\x79","\x72\x6F\x75\x6E\x64","\x69\x6E\x76\x52\x6F\x75\x6E\x64","\x53\x49\x5A\x45\x5F\x31\x32\x38","\x53\x49\x5A\x45\x5F\x31\x39\x32","\x65\x78\x70\x61\x6E\x64\x4B\x65\x79","\x6D\x61\x69\x6E","\x69\x6E\x76\x4D\x61\x69\x6E","\x73\x6C\x69\x63\x65","\x6C\x65\x6E\x67\x74\x68","\x69\x76\x20\x6C\x65\x6E\x67\x74\x68\x20\x6D\x75\x73\x74\x20\x62\x65\x20\x31\x32\x38\x20\x62\x69\x74\x73\x2E","\x43\x42\x43","\x6D\x6F\x64\x65\x4F\x66\x4F\x70\x65\x72\x61\x74\x69\x6F\x6E","\x70\x61\x64\x42\x79\x74\x65\x73\x49\x6E","\x63\x65\x69\x6C","\x67\x65\x74\x42\x6C\x6F\x63\x6B","\x43\x46\x42","\x65\x6E\x63\x72\x79\x70\x74","\x61\x65\x73","\x70\x75\x73\x68","\x4F\x46\x42","\x64\x65\x63\x72\x79\x70\x74","\x75\x6E\x70\x61\x64\x42\x79\x74\x65\x73\x4F\x75\x74","\x73\x70\x6C\x69\x63\x65"];var slowAES={aes:{keySize:{SIZE_128:16,SIZE_192:24,SIZE_256:32},sbox:[0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16],rsbox:[0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d],rotate:function(_0x85f6x2){var _0x85f6x3=_0x85f6x2[0];for(var _0x85f6x4=0;_0x85f6x4< 3;_0x85f6x4++){_0x85f6x2[_0x85f6x4]= _0x85f6x2[_0x85f6x4+ 1]};_0x85f6x2[3]= _0x85f6x3;return _0x85f6x2},Rcon:[0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb],G2X:[0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5],G3X:[0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a],G9X:[0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46],GBX:[0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3],GDX:[0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97],GEX:[0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d],core:function(_0x85f6x2,_0x85f6x5){_0x85f6x2= this[_0x2465[0]](_0x85f6x2);for(var _0x85f6x4=0;_0x85f6x4< 4;++_0x85f6x4){_0x85f6x2[_0x85f6x4]= this[_0x2465[1]][_0x85f6x2[_0x85f6x4]]};_0x85f6x2[0]= _0x85f6x2[0]^ this[_0x2465[2]][_0x85f6x5];return _0x85f6x2},expandKey:function(_0x85f6x6,_0x85f6x7){var _0x85f6x8=(16* (this[_0x2465[3]](_0x85f6x7)+ 1));var _0x85f6x9=0;var _0x85f6xa=1;var _0x85f6xb=[];var _0x85f6xc=[];for(var _0x85f6x4=0;_0x85f6x4< _0x85f6x8;_0x85f6x4++){_0x85f6xc[_0x85f6x4]= 0};for(var _0x85f6xd=0;_0x85f6xd< _0x85f6x7;_0x85f6xd++){_0x85f6xc[_0x85f6xd]= _0x85f6x6[_0x85f6xd]};_0x85f6x9+= _0x85f6x7;while(_0x85f6x9< _0x85f6x8){for(var _0x85f6xe=0;_0x85f6xe< 4;_0x85f6xe++){_0x85f6xb[_0x85f6xe]= _0x85f6xc[(_0x85f6x9- 4)+ _0x85f6xe]};if(_0x85f6x9% _0x85f6x7== 0){_0x85f6xb= this[_0x2465[4]](_0x85f6xb,_0x85f6xa++)};if(_0x85f6x7== this[_0x2465[6]][_0x2465[5]]&& ((_0x85f6x9% _0x85f6x7)== 16)){for(var _0x85f6xf=0;_0x85f6xf< 4;_0x85f6xf++){_0x85f6xb[_0x85f6xf]= this[_0x2465[1]][_0x85f6xb[_0x85f6xf]]}};for(var _0x85f6x10=0;_0x85f6x10< 4;_0x85f6x10++){_0x85f6xc[_0x85f6x9]= _0x85f6xc[_0x85f6x9- _0x85f6x7]^ _0x85f6xb[_0x85f6x10];_0x85f6x9++}};return _0x85f6xc},addRoundKey:function(_0x85f6x11,_0x85f6x12){for(var _0x85f6x4=0;_0x85f6x4< 16;_0x85f6x4++){_0x85f6x11[_0x85f6x4]^= _0x85f6x12[_0x85f6x4]};return _0x85f6x11},createRoundKey:function(_0x85f6xc,_0x85f6x13){var _0x85f6x12=[];for(var _0x85f6x4=0;_0x85f6x4< 4;_0x85f6x4++){for(var _0x85f6xd=0;_0x85f6xd< 4;_0x85f6xd++){_0x85f6x12[_0x85f6xd* 4+ _0x85f6x4]= _0x85f6xc[_0x85f6x13+ _0x85f6x4* 4+ _0x85f6xd]}};return _0x85f6x12},subBytes:function(_0x85f6x11,_0x85f6x14){for(var _0x85f6x4=0;_0x85f6x4< 16;_0x85f6x4++){_0x85f6x11[_0x85f6x4]= _0x85f6x14?this[_0x2465[7]][_0x85f6x11[_0x85f6x4]]:this[_0x2465[1]][_0x85f6x11[_0x85f6x4]]};return _0x85f6x11},shiftRows:function(_0x85f6x11,_0x85f6x14){for(var _0x85f6x4=0;_0x85f6x4< 4;_0x85f6x4++){_0x85f6x11= this[_0x2465[8]](_0x85f6x11,_0x85f6x4* 4,_0x85f6x4,_0x85f6x14)};return _0x85f6x11},shiftRow:function(_0x85f6x11,_0x85f6x15,_0x85f6x16,_0x85f6x14){for(var _0x85f6x4=0;_0x85f6x4< _0x85f6x16;_0x85f6x4++){if(_0x85f6x14){var _0x85f6x17=_0x85f6x11[_0x85f6x15+ 3];for(var _0x85f6xd=3;_0x85f6xd> 0;_0x85f6xd--){_0x85f6x11[_0x85f6x15+ _0x85f6xd]= _0x85f6x11[_0x85f6x15+ _0x85f6xd- 1]};_0x85f6x11[_0x85f6x15]= _0x85f6x17}else {var _0x85f6x17=_0x85f6x11[_0x85f6x15];for(var _0x85f6xd=0;_0x85f6xd< 3;_0x85f6xd++){_0x85f6x11[_0x85f6x15+ _0x85f6xd]= _0x85f6x11[_0x85f6x15+ _0x85f6xd+ 1]};_0x85f6x11[_0x85f6x15+ 3]= _0x85f6x17}};return _0x85f6x11},galois_multiplication:function(_0x85f6x18,_0x85f6x19){var _0x85f6x1a=0;for(var _0x85f6x1b=0;_0x85f6x1b< 8;_0x85f6x1b++){if((_0x85f6x19& 1)== 1){_0x85f6x1a^= _0x85f6x18};if(_0x85f6x1a> 0x100){_0x85f6x1a^= 0x100};var _0x85f6x1c=(_0x85f6x18& 0x80);_0x85f6x18<<= 1;if(_0x85f6x18> 0x100){_0x85f6x18^= 0x100};if(_0x85f6x1c== 0x80){_0x85f6x18^= 0x1b};if(_0x85f6x18> 0x100){_0x85f6x18^= 0x100};_0x85f6x19>>= 1;if(_0x85f6x19> 0x100){_0x85f6x19^= 0x100}};return _0x85f6x1a},mixColumns:function(_0x85f6x11,_0x85f6x14){var _0x85f6x1d=[];for(var _0x85f6x4=0;_0x85f6x4< 4;_0x85f6x4++){for(var _0x85f6xd=0;_0x85f6xd< 4;_0x85f6xd++){_0x85f6x1d[_0x85f6xd]= _0x85f6x11[(_0x85f6xd* 4)+ _0x85f6x4]};_0x85f6x1d= this[_0x2465[9]](_0x85f6x1d,_0x85f6x14);for(var _0x85f6xe=0;_0x85f6xe< 4;_0x85f6xe++){_0x85f6x11[(_0x85f6xe* 4)+ _0x85f6x4]= _0x85f6x1d[_0x85f6xe]}};return _0x85f6x11},mixColumn:function(_0x85f6x1d,_0x85f6x14){var _0x85f6x1e=[];if(_0x85f6x14){_0x85f6x1e= [14,9,13,11]}else {_0x85f6x1e= [2,1,1,3]};var _0x85f6x1f=[];for(var _0x85f6x4=0;_0x85f6x4< 4;_0x85f6x4++){_0x85f6x1f[_0x85f6x4]= _0x85f6x1d[_0x85f6x4]};_0x85f6x1d[0]= this[_0x2465[10]](_0x85f6x1f[0],_0x85f6x1e[0])^ this[_0x2465[10]](_0x85f6x1f[3],_0x85f6x1e[1])^ this[_0x2465[10]](_0x85f6x1f[2],_0x85f6x1e[2])^ this[_0x2465[10]](_0x85f6x1f[1],_0x85f6x1e[3]);_0x85f6x1d[1]= this[_0x2465[10]](_0x85f6x1f[1],_0x85f6x1e[0])^ this[_0x2465[10]](_0x85f6x1f[0],_0x85f6x1e[1])^ this[_0x2465[10]](_0x85f6x1f[3],_0x85f6x1e[2])^ this[_0x2465[10]](_0x85f6x1f[2],_0x85f6x1e[3]);_0x85f6x1d[2]= this[_0x2465[10]](_0x85f6x1f[2],_0x85f6x1e[0])^ this[_0x2465[10]](_0x85f6x1f[1],_0x85f6x1e[1])^ this[_0x2465[10]](_0x85f6x1f[0],_0x85f6x1e[2])^ this[_0x2465[10]](_0x85f6x1f[3],_0x85f6x1e[3]);_0x85f6x1d[3]= this[_0x2465[10]](_0x85f6x1f[3],_0x85f6x1e[0])^ this[_0x2465[10]](_0x85f6x1f[2],_0x85f6x1e[1])^ this[_0x2465[10]](_0x85f6x1f[1],_0x85f6x1e[2])^ this[_0x2465[10]](_0x85f6x1f[0],_0x85f6x1e[3]);return _0x85f6x1d},round:function(_0x85f6x11,_0x85f6x12){_0x85f6x11= this[_0x2465[11]](_0x85f6x11,false);_0x85f6x11= this[_0x2465[12]](_0x85f6x11,false);_0x85f6x11= this[_0x2465[13]](_0x85f6x11,false);_0x85f6x11= this[_0x2465[14]](_0x85f6x11,_0x85f6x12);return _0x85f6x11},invRound:function(_0x85f6x11,_0x85f6x12){_0x85f6x11= this[_0x2465[12]](_0x85f6x11,true);_0x85f6x11= this[_0x2465[11]](_0x85f6x11,true);_0x85f6x11= this[_0x2465[14]](_0x85f6x11,_0x85f6x12);_0x85f6x11= this[_0x2465[13]](_0x85f6x11,true);return _0x85f6x11},main:function(_0x85f6x11,_0x85f6xc,_0x85f6x20){_0x85f6x11= this[_0x2465[14]](_0x85f6x11,this[_0x2465[15]](_0x85f6xc,0));for(var _0x85f6x4=1;_0x85f6x4< _0x85f6x20;_0x85f6x4++){_0x85f6x11= this[_0x2465[16]](_0x85f6x11,this[_0x2465[15]](_0x85f6xc,16* _0x85f6x4))};_0x85f6x11= this[_0x2465[11]](_0x85f6x11,false);_0x85f6x11= this[_0x2465[12]](_0x85f6x11,false);_0x85f6x11= this[_0x2465[14]](_0x85f6x11,this[_0x2465[15]](_0x85f6xc,16* _0x85f6x20));return _0x85f6x11},invMain:function(_0x85f6x11,_0x85f6xc,_0x85f6x20){_0x85f6x11= this[_0x2465[14]](_0x85f6x11,this[_0x2465[15]](_0x85f6xc,16* _0x85f6x20));for(var _0x85f6x4=_0x85f6x20- 1;_0x85f6x4> 0;_0x85f6x4--){_0x85f6x11= this[_0x2465[17]](_0x85f6x11,this[_0x2465[15]](_0x85f6xc,16* _0x85f6x4))};_0x85f6x11= this[_0x2465[12]](_0x85f6x11,true);_0x85f6x11= this[_0x2465[11]](_0x85f6x11,true);_0x85f6x11= this[_0x2465[14]](_0x85f6x11,this[_0x2465[15]](_0x85f6xc,0));return _0x85f6x11},numberOfRounds:function(_0x85f6x7){var _0x85f6x20;switch(_0x85f6x7){case this[_0x2465[6]][_0x2465[18]]:_0x85f6x20= 10;break;case this[_0x2465[6]][_0x2465[19]]:_0x85f6x20= 12;break;case this[_0x2465[6]][_0x2465[5]]:_0x85f6x20= 14;break;default:return null;break};return _0x85f6x20},encrypt:function(_0x85f6x21,_0x85f6x6,_0x85f6x7){var _0x85f6x22=[];var _0x85f6x23=[];var _0x85f6x20=this[_0x2465[3]](_0x85f6x7);for(var _0x85f6x4=0;_0x85f6x4< 4;_0x85f6x4++){for(var _0x85f6xd=0;_0x85f6xd< 4;_0x85f6xd++){_0x85f6x23[(_0x85f6x4+ (_0x85f6xd* 4))]= _0x85f6x21[(_0x85f6x4* 4)+ _0x85f6xd]}};var _0x85f6xc=this[_0x2465[20]](_0x85f6x6,_0x85f6x7);_0x85f6x23= this[_0x2465[21]](_0x85f6x23,_0x85f6xc,_0x85f6x20);for(var _0x85f6xe=0;_0x85f6xe< 4;_0x85f6xe++){for(var _0x85f6xf=0;_0x85f6xf< 4;_0x85f6xf++){_0x85f6x22[(_0x85f6xe* 4)+ _0x85f6xf]= _0x85f6x23[(_0x85f6xe+ (_0x85f6xf* 4))]}};return _0x85f6x22},decrypt:function(_0x85f6x21,_0x85f6x6,_0x85f6x7){var _0x85f6x22=[];var _0x85f6x23=[];var _0x85f6x20=this[_0x2465[3]](_0x85f6x7);for(var _0x85f6x4=0;_0x85f6x4< 4;_0x85f6x4++){for(var _0x85f6xd=0;_0x85f6xd< 4;_0x85f6xd++){_0x85f6x23[(_0x85f6x4+ (_0x85f6xd* 4))]= _0x85f6x21[(_0x85f6x4* 4)+ _0x85f6xd]}};var _0x85f6xc=this[_0x2465[20]](_0x85f6x6,_0x85f6x7);_0x85f6x23= this[_0x2465[22]](_0x85f6x23,_0x85f6xc,_0x85f6x20);for(var _0x85f6xe=0;_0x85f6xe< 4;_0x85f6xe++){for(var _0x85f6xf=0;_0x85f6xf< 4;_0x85f6xf++){_0x85f6x22[(_0x85f6xe* 4)+ _0x85f6xf]= _0x85f6x23[(_0x85f6xe+ (_0x85f6xf* 4))]}};return _0x85f6x22}},modeOfOperation:{OFB:0,CFB:1,CBC:2},getBlock:function(_0x85f6x24,_0x85f6x25,_0x85f6x26,_0x85f6x27){if(_0x85f6x26- _0x85f6x25> 16){_0x85f6x26= _0x85f6x25+ 16};return _0x85f6x24[_0x2465[23]](_0x85f6x25,_0x85f6x26)},encrypt:function(_0x85f6x24,_0x85f6x27,_0x85f6x6,_0x85f6x28){var _0x85f6x7=_0x85f6x6[_0x2465[24]];if(_0x85f6x28[_0x2465[24]]% 16){throw _0x2465[25]};var _0x85f6x29=[];var _0x85f6x21=[];var _0x85f6x22=[];var _0x85f6x2a=[];var _0x85f6x2b=[];var _0x85f6x2c=true;if(_0x85f6x27== this[_0x2465[27]][_0x2465[26]]){this[_0x2465[28]](_0x85f6x24)};if(_0x85f6x24!== null){for(var _0x85f6xd=0;_0x85f6xd< Math[_0x2465[29]](_0x85f6x24[_0x2465[24]]/ 16);_0x85f6xd++){var _0x85f6x25=_0x85f6xd* 16;var _0x85f6x26=_0x85f6xd* 16+ 16;if(_0x85f6xd* 16+ 16> _0x85f6x24[_0x2465[24]]){_0x85f6x26= _0x85f6x24[_0x2465[24]]};_0x85f6x29= this[_0x2465[30]](_0x85f6x24,_0x85f6x25,_0x85f6x26,_0x85f6x27);if(_0x85f6x27== this[_0x2465[27]][_0x2465[31]]){if(_0x85f6x2c){_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x28,_0x85f6x6,_0x85f6x7);_0x85f6x2c= false}else {_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x21,_0x85f6x6,_0x85f6x7)};for(var _0x85f6x4=0;_0x85f6x4< 16;_0x85f6x4++){_0x85f6x2a[_0x85f6x4]= _0x85f6x29[_0x85f6x4]^ _0x85f6x22[_0x85f6x4]};for(var _0x85f6xe=0;_0x85f6xe< _0x85f6x26- _0x85f6x25;_0x85f6xe++){_0x85f6x2b[_0x2465[34]](_0x85f6x2a[_0x85f6xe])};_0x85f6x21= _0x85f6x2a}else {if(_0x85f6x27== this[_0x2465[27]][_0x2465[35]]){if(_0x85f6x2c){_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x28,_0x85f6x6,_0x85f6x7);_0x85f6x2c= false}else {_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x21,_0x85f6x6,_0x85f6x7)};for(var _0x85f6x4=0;_0x85f6x4< 16;_0x85f6x4++){_0x85f6x2a[_0x85f6x4]= _0x85f6x29[_0x85f6x4]^ _0x85f6x22[_0x85f6x4]};for(var _0x85f6xe=0;_0x85f6xe< _0x85f6x26- _0x85f6x25;_0x85f6xe++){_0x85f6x2b[_0x2465[34]](_0x85f6x2a[_0x85f6xe])};_0x85f6x21= _0x85f6x22}else {if(_0x85f6x27== this[_0x2465[27]][_0x2465[26]]){for(var _0x85f6x4=0;_0x85f6x4< 16;_0x85f6x4++){_0x85f6x21[_0x85f6x4]= _0x85f6x29[_0x85f6x4]^ ((_0x85f6x2c)?_0x85f6x28[_0x85f6x4]:_0x85f6x2a[_0x85f6x4])};_0x85f6x2c= false;_0x85f6x2a= this[_0x2465[33]][_0x2465[32]](_0x85f6x21,_0x85f6x6,_0x85f6x7);for(var _0x85f6xe=0;_0x85f6xe< 16;_0x85f6xe++){_0x85f6x2b[_0x2465[34]](_0x85f6x2a[_0x85f6xe])}}}}}};return _0x85f6x2b},decrypt:function(_0x85f6x2d,_0x85f6x27,_0x85f6x6,_0x85f6x28){var _0x85f6x7=_0x85f6x6[_0x2465[24]];if(_0x85f6x28[_0x2465[24]]% 16){throw _0x2465[25]};var _0x85f6x2a=[];var _0x85f6x21=[];var _0x85f6x22=[];var _0x85f6x29=[];var _0x85f6x2e=[];var _0x85f6x2c=true;if(_0x85f6x2d!== null){for(var _0x85f6xd=0;_0x85f6xd< Math[_0x2465[29]](_0x85f6x2d[_0x2465[24]]/ 16);_0x85f6xd++){var _0x85f6x25=_0x85f6xd* 16;var _0x85f6x26=_0x85f6xd* 16+ 16;if(_0x85f6xd* 16+ 16> _0x85f6x2d[_0x2465[24]]){_0x85f6x26= _0x85f6x2d[_0x2465[24]]};_0x85f6x2a= this[_0x2465[30]](_0x85f6x2d,_0x85f6x25,_0x85f6x26,_0x85f6x27);if(_0x85f6x27== this[_0x2465[27]][_0x2465[31]]){if(_0x85f6x2c){_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x28,_0x85f6x6,_0x85f6x7);_0x85f6x2c= false}else {_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x21,_0x85f6x6,_0x85f6x7)};for(i= 0;i< 16;i++){_0x85f6x29[i]= _0x85f6x22[i]^ _0x85f6x2a[i]};for(var _0x85f6xe=0;_0x85f6xe< _0x85f6x26- _0x85f6x25;_0x85f6xe++){_0x85f6x2e[_0x2465[34]](_0x85f6x29[_0x85f6xe])};_0x85f6x21= _0x85f6x2a}else {if(_0x85f6x27== this[_0x2465[27]][_0x2465[35]]){if(_0x85f6x2c){_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x28,_0x85f6x6,_0x85f6x7);_0x85f6x2c= false}else {_0x85f6x22= this[_0x2465[33]][_0x2465[32]](_0x85f6x21,_0x85f6x6,_0x85f6x7)};for(i= 0;i< 16;i++){_0x85f6x29[i]= _0x85f6x22[i]^ _0x85f6x2a[i]};for(var _0x85f6xe=0;_0x85f6xe< _0x85f6x26- _0x85f6x25;_0x85f6xe++){_0x85f6x2e[_0x2465[34]](_0x85f6x29[_0x85f6xe])};_0x85f6x21= _0x85f6x22}else {if(_0x85f6x27== this[_0x2465[27]][_0x2465[26]]){_0x85f6x22= this[_0x2465[33]][_0x2465[36]](_0x85f6x2a,_0x85f6x6,_0x85f6x7);for(i= 0;i< 16;i++){_0x85f6x29[i]= ((_0x85f6x2c)?_0x85f6x28[i]:_0x85f6x21[i])^ _0x85f6x22[i]};_0x85f6x2c= false;for(var _0x85f6xe=0;_0x85f6xe< _0x85f6x26- _0x85f6x25;_0x85f6xe++){_0x85f6x2e[_0x2465[34]](_0x85f6x29[_0x85f6xe])};_0x85f6x21= _0x85f6x2a}}}};if(_0x85f6x27== this[_0x2465[27]][_0x2465[26]]){this[_0x2465[37]](_0x85f6x2e)}};return _0x85f6x2e},padBytesIn:function(_0x85f6x2f){var _0x85f6x30=_0x85f6x2f[_0x2465[24]];var _0x85f6x31=16- (_0x85f6x30% 16);for(var _0x85f6x4=0;_0x85f6x4< _0x85f6x31;_0x85f6x4++){_0x85f6x2f[_0x2465[34]](_0x85f6x31)}},unpadBytesOut:function(_0x85f6x2f){var _0x85f6x32=0;var _0x85f6x31=-1;var _0x85f6x33=16;if(_0x85f6x2f[_0x2465[24]]> 16){for(var _0x85f6x4=_0x85f6x2f[_0x2465[24]]- 1;_0x85f6x4>= _0x85f6x2f[_0x2465[24]]- 1- _0x85f6x33;_0x85f6x4--){if(_0x85f6x2f[_0x85f6x4]<= _0x85f6x33){if(_0x85f6x31== -1){_0x85f6x31= _0x85f6x2f[_0x85f6x4]};if(_0x85f6x2f[_0x85f6x4]!= _0x85f6x31){_0x85f6x32= 0;break};_0x85f6x32++}else {break};if(_0x85f6x32== _0x85f6x31){break}};if(_0x85f6x32> 0){_0x85f6x2f[_0x2465[38]](_0x85f6x2f[_0x2465[24]]- _0x85f6x32,_0x85f6x32)}}}} +// """.trimIndent() + val decodeBase64 = "atob = function(s) {\n" + + " var e={},i,b=0,c,x,l=0,a,r='',w=String.fromCharCode,L=s.length;\n" + + " var A=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n" + + " for(i=0;i<64;i++){e[A.charAt(i)]=i;}\n" + + " for(x=0;x=8){((a=(b>>>(l-=8))&0xff)||(x<(L-2)))&&(r+=w(a));}\n" + + " }\n" + + " return r;\n" + + "};" + val doc = "var document = {};" + + val siteScriptRegex = Regex("""script>(.*)\+";\s*path""") + val html = app.get(url).text + val siteScript = siteScriptRegex.find(html)?.groupValues?.getOrNull(1) + + rhino.evaluateString( + scope, + "$doc$decodeBase64$slowAes;$siteScript;", + "JavaScript", + 1, + null + ) + val jsEval = scope.get("document", scope) as? Scriptable + val cookies = jsEval?.get("cookie", jsEval) as? ConsString + return cookies?.split(";")?.associate { + val split = it.split("=") + (split.getOrNull(0)?.trim() ?: "") to (split.getOrNull(1)?.trim() ?: "") + }?.filter { it.key.isNotBlank() && it.value.isNotBlank() } ?: emptyMap() + } + + fun getType(t: String?): TvType { + return when (t?.lowercase()) { + "movie" -> TvType.AnimeMovie + "ova" -> TvType.OVA + else -> TvType.Anime + } + } + + fun getStatus(t: String?): ShowStatus? { + return when (t?.lowercase()) { + "finito" -> ShowStatus.Completed + "in corso" -> ShowStatus.Ongoing + else -> null + } + } + } + + private fun Element.toSearchResult(showEpisode: Boolean = true): AnimeSearchResponse { + fun String.parseHref(): String { + val h = this.split('.').toMutableList() + h[1] = h[1].substringBeforeLast('/') + return h.joinToString(".") + } + + val anchor = this.select("a.name").firstOrNull() ?: throw ErrorLoadingException("Error") + val title = anchor.text().removeSuffix(" (ITA)") + val otherTitle = anchor.attr("data-jtitle").removeSuffix(" (ITA)") + + val url = fixUrl(anchor.attr("href").parseHref()) + val poster = this.select("a.poster img").attr("src") + + val statusElement = this.select("div.status") // .first() + val dub = statusElement.select(".dub").isNotEmpty() + + val episode = if (showEpisode) statusElement.select(".ep").text().split(' ').last() + .toIntOrNull() else null + val type = when { + statusElement.select(".movie").isNotEmpty() -> TvType.AnimeMovie + statusElement.select(".ova").isNotEmpty() -> TvType.OVA + else -> TvType.Anime + } + + return newAnimeSearchResponse(title, url, type) { + addDubStatus(dub, episode) + this.otherName = otherTitle + this.posterUrl = poster + } + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val document = request(mainUrl).document + val list = ArrayList() + + val widget = document.select(".widget.hotnew") + widget.select(".tabs [data-name=\"sub\"], .tabs [data-name=\"dub\"]").forEach { tab -> + val tabId = tab.attr("data-name") + val tabName = tab.text().removeSuffix("-ITA") + val animeList = widget.select("[data-name=\"$tabId\"] .film-list .item").map { + it.toSearchResult() + } + list.add(HomePageList(tabName, animeList)) + } + widget.select(".tabs [data-name=\"trending\"]").forEach { tab -> + val tabId = tab.attr("data-name") + val tabName = tab.text() + val animeList = widget.select("[data-name=\"$tabId\"] .film-list .item").map { + it.toSearchResult(showEpisode = false) + }.distinctBy { it.url } + list.add(HomePageList(tabName, animeList)) + } + return HomePageResponse(list) + } + + override suspend fun search(query: String): List { + val document = request("$mainUrl/search?keyword=$query").document + return document.select(".film-list > .item").map { + it.toSearchResult(showEpisode = false) + } + } + + override suspend fun load(url: String): LoadResponse { + val document = request(url).document + + val widget = document.select("div.widget.info") + val title = widget.select(".info .title").text().removeSuffix(" (ITA)") + val otherTitle = widget.select(".info .title").attr("data-jtitle").removeSuffix(" (ITA)") + val description = + widget.select(".desc .long").first()?.text() ?: widget.select(".desc").text() + val poster = document.select(".thumb img").attr("src") + + val type: TvType = getType(widget.select("dd").first()?.text()) + val genres = widget.select(".meta").select("a[href*=\"/genre/\"]").map { it.text() } + val rating = widget.select("#average-vote").text() + + val trailerUrl = document.select(".trailer[data-url]").attr("data-url") + val malId = document.select("#mal-button").attr("href") + .split('/').last().toIntOrNull() + val anlId = document.select("#anilist-button").attr("href") + .split('/').last().toIntOrNull() + + var dub = false + var year: Int? = null + var status: ShowStatus? = null + var duration: String? = null + + for (meta in document.select(".meta dt, .meta dd")) { + val text = meta.text() + if (text.contains("Audio")) + dub = meta.nextElementSibling()?.text() == "Italiano" + else if (year == null && text.contains("Data")) + year = meta.nextElementSibling()?.text()?.split(' ')?.last()?.toIntOrNull() + else if (status == null && text.contains("Stato")) + status = getStatus(meta.nextElementSibling()?.text()) + else if (status == null && text.contains("Durata")) + duration = meta.nextElementSibling()?.text() + } + + val servers = document.select(".widget.servers") + val episodes = servers.select(".server[data-name=\"9\"] .episode").map { + val id = it.select("a").attr("data-id") + val number = it.select("a").attr("data-episode-num").toIntOrNull() + Episode( + "$mainUrl/api/episode/info?id=$id", + episode = number + ) + } + val comingSoon = episodes.isEmpty() + + val recommendations = document.select(".film-list.interesting .item").map { + it.toSearchResult(showEpisode = false) + } + + return newAnimeLoadResponse(title, url, type) { + engName = title + japName = otherTitle + addPoster(poster) + this.year = year + addEpisodes(if (dub) DubStatus.Dubbed else DubStatus.Subbed, episodes) + showStatus = status + plot = description + tags = genres + addMalId(malId) + addAniListId(anlId) + addRating(rating) + addDuration(duration) + addTrailer(trailerUrl) + this.recommendations = recommendations + this.comingSoon = comingSoon + } + } + + data class Json( + @JsonProperty("grabber") val grabber: String, + @JsonProperty("name") val name: String, + @JsonProperty("target") val target: String, + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val url = tryParseJson( + request(data).text + )?.grabber + + if (url.isNullOrEmpty()) + return false + + callback.invoke( + ExtractorLink( + name, + name, + url, + referer = mainUrl, + quality = Qualities.Unknown.value + ) + ) + return true + } +} diff --git a/AnimeWorldProvider/src/main/kotlin/com/lagradost/AnimeWorldProviderPlugin.kt b/AnimeWorldProvider/src/main/kotlin/com/lagradost/AnimeWorldProviderPlugin.kt new file mode 100644 index 0000000..07f9f19 --- /dev/null +++ b/AnimeWorldProvider/src/main/kotlin/com/lagradost/AnimeWorldProviderPlugin.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 AnimeWorldProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeWorldProvider()) + } +} \ No newline at end of file diff --git a/AnimefenixProvider/build.gradle.kts b/AnimefenixProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimefenixProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimefenixProvider/src/main/AndroidManifest.xml b/AnimefenixProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimefenixProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimefenixProvider/src/main/kotlin/com/lagradost/AnimefenixProvider.kt b/AnimefenixProvider/src/main/kotlin/com/lagradost/AnimefenixProvider.kt new file mode 100644 index 0000000..203fa8d --- /dev/null +++ b/AnimefenixProvider/src/main/kotlin/com/lagradost/AnimefenixProvider.kt @@ -0,0 +1,249 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import java.util.* + + +class AnimefenixProvider:MainAPI() { + + override var mainUrl = "https://animefenix.com" + override var name = "Animefenix" + override var lang = "es" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + fun getDubStatus(title: String): DubStatus { + return if (title.contains("Latino") || title.contains("Castellano")) + DubStatus.Dubbed + else DubStatus.Subbed + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("$mainUrl/", "Animes"), + Pair("$mainUrl/animes?type[]=movie&order=default", "Peliculas", ), + Pair("$mainUrl/animes?type[]=ova&order=default", "OVA's", ), + ) + + val items = ArrayList() + + items.add( + HomePageList( + "Últimos episodios", + app.get(mainUrl).document.select(".capitulos-grid div.item").map { + val title = it.selectFirst("div.overtitle")?.text() + val poster = it.selectFirst("a img")?.attr("src") + val epRegex = Regex("(-(\\d+)\$|-(\\d+)\\.(\\d+))") + val url = it.selectFirst("a")?.attr("href")?.replace(epRegex,"") + ?.replace("/ver/","/") + val epNum = it.selectFirst(".is-size-7")?.text()?.replace("Episodio ","")?.toIntOrNull() + newAnimeSearchResponse(title!!, url!!) { + this.posterUrl = poster + addDubStatus(getDubStatus(title), epNum) + } + }) + ) + + urls.apmap { (url, name) -> + val response = app.get(url) + val soup = Jsoup.parse(response.text) + val home = soup.select(".list-series article").map { + val title = it.selectFirst("h3 a")?.text() + val poster = it.selectFirst("figure img")?.attr("src") + AnimeSearchResponse( + title!!, + it.selectFirst("a")?.attr("href") ?: "", + this.name, + TvType.Anime, + poster, + null, + if (title.contains("Latino")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed), + ) + } + + items.add(HomePageList(name, home)) + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/animes?q=$query").document.select(".list-series article").map { + val title = it.selectFirst("h3 a")?.text() + val href = it.selectFirst("a")?.attr("href") + val image = it.selectFirst("figure img")?.attr("src") + AnimeSearchResponse( + title!!, + href!!, + this.name, + TvType.Anime, + fixUrl(image ?: ""), + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of( + DubStatus.Subbed), + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = Jsoup.parse(app.get(url, timeout = 120).text) + val poster = doc.selectFirst(".image > img")?.attr("src") + val title = doc.selectFirst("h1.title.has-text-orange")?.text() + val description = doc.selectFirst("p.has-text-light")?.text() + val genres = doc.select(".genres a").map { it.text() } + val status = when (doc.selectFirst(".is-narrow-desktop a.button")?.text()) { + "Emisión" -> ShowStatus.Ongoing + "Finalizado" -> ShowStatus.Completed + else -> null + } + val episodes = doc.select(".anime-page__episode-list li").map { + val name = it.selectFirst("span")?.text() + val link = it.selectFirst("a")?.attr("href") + Episode(link!!, name) + }.reversed() + val type = if (doc.selectFirst("ul.has-text-light")?.text() + !!.contains("Película") && episodes.size == 1 + ) TvType.AnimeMovie else TvType.Anime + return newAnimeLoadResponse(title!!, url, type) { + japName = null + engName = title + posterUrl = poster + addEpisodes(DubStatus.Subbed, episodes) + plot = description + tags = genres + showStatus = status + } + } + + private fun cleanStreamID(input: String): String = input.replace(Regex("player=.*&code=|&"),"") + + data class Amazon ( + @JsonProperty("file") var file : String? = null, + @JsonProperty("type") var type : String? = null, + @JsonProperty("label") var label : String? = null + ) + + private fun cleanExtractor( + source: String, + name: String, + url: String, + callback: (ExtractorLink) -> Unit + ): Boolean { + callback( + ExtractorLink( + source, + name, + url, + "", + Qualities.Unknown.value, + false + ) + ) + return true + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val soup = app.get(data).document + val script = soup.selectFirst(".player-container script")?.data() + if (script!!.contains("var tabsArray =")) { + val sourcesRegex = Regex("player=.*&code(.*)&") + val test = sourcesRegex.findAll(script).toList() + test.apmap { + val codestream = it.value + val links = when { + codestream.contains("player=2&") -> "https://embedsito.com/v/"+cleanStreamID(codestream) + codestream.contains("player=3&") -> "https://www.mp4upload.com/embed-"+cleanStreamID(codestream)+".html" + codestream.contains("player=6&") -> "https://www.yourupload.com/embed/"+cleanStreamID(codestream) + codestream.contains("player=12&") -> "http://ok.ru/videoembed/"+cleanStreamID(codestream) + codestream.contains("player=4&") -> "https://sendvid.com/"+cleanStreamID(codestream) + codestream.contains("player=9&") -> "AmaNormal https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream) + codestream.contains("player=11&") -> "AmazonES https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream) + codestream.contains("player=22&") -> "Fireload https://www.animefenix.com/stream/fl.php?v="+cleanStreamID(codestream) + + else -> "" + } + loadExtractor(links, data, subtitleCallback, callback) + + argamap({ + if (links.contains("AmaNormal")) { + val doc = app.get(links.replace("AmaNormal ","")).document + doc.select("script").map { script -> + if (script.data().contains("sources: [{\"file\"")) { + val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","") + val json = parseJson(text) + if (json.file != null) { + cleanExtractor( + "Amazon", + "Amazon ${json.label}", + json.file!!, + callback + ) + } + } + } + } + + if (links.contains("AmazonES")) { + val amazonES = links.replace("AmazonES ", "") + val doc = app.get("$amazonES&ext=es").document + doc.select("script").map { script -> + if (script.data().contains("sources: [{\"file\"")) { + val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","") + val json = parseJson(text) + if (json.file != null) { + cleanExtractor( + "AmazonES", + "AmazonES ${json.label}", + json.file!!, + callback + ) + } + } + } + } + if (links.contains("Fireload")) { + val doc = app.get(links.replace("Fireload ", "")).document + doc.select("script").map { script -> + if (script.data().contains("sources: [{\"file\"")) { + val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","") + val json = parseJson(text) + val testurl = if (json.file?.contains("fireload") == true) { + app.get("https://${json.file}").text + } else null + if (testurl?.contains("error") == true) { + // + } else if (json.file?.contains("fireload") == true) { + cleanExtractor( + "Fireload", + "Fireload ${json.label}", + "https://"+json.file!!, + callback + ) + } + } + } + } + }) + } + } + return true + } +} \ No newline at end of file diff --git a/AnimefenixProvider/src/main/kotlin/com/lagradost/AnimefenixProviderPlugin.kt b/AnimefenixProvider/src/main/kotlin/com/lagradost/AnimefenixProviderPlugin.kt new file mode 100644 index 0000000..459bd29 --- /dev/null +++ b/AnimefenixProvider/src/main/kotlin/com/lagradost/AnimefenixProviderPlugin.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 AnimefenixProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimefenixProvider()) + } +} \ No newline at end of file diff --git a/AnimeflvIOProvider/build.gradle.kts b/AnimeflvIOProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeflvIOProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeflvIOProvider/src/main/AndroidManifest.xml b/AnimeflvIOProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeflvIOProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeflvIOProvider/src/main/kotlin/com/lagradost/AnimeflvIOProvider.kt b/AnimeflvIOProvider/src/main/kotlin/com/lagradost/AnimeflvIOProvider.kt new file mode 100644 index 0000000..5b74053 --- /dev/null +++ b/AnimeflvIOProvider/src/main/kotlin/com/lagradost/AnimeflvIOProvider.kt @@ -0,0 +1,239 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.* + +class AnimeflvIOProvider : MainAPI() { + override var mainUrl = "https://animeflv.io" //Also scrapes from animeid.to + override var name = "Animeflv.io" + override var lang = "es" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val items = ArrayList() + val urls = listOf( + Pair("$mainUrl/series", "Series actualizadas"), + Pair("$mainUrl/peliculas", "Peliculas actualizadas"), + ) + items.add( + HomePageList( + "Estrenos", + app.get(mainUrl).document.select("div#owl-demo-premiere-movies .pull-left").map { + val title = it.selectFirst("p")?.text() ?: "" + AnimeSearchResponse( + title, + fixUrl(it.selectFirst("a")?.attr("href") ?: ""), + this.name, + TvType.Anime, + it.selectFirst("img")?.attr("src"), + it.selectFirst("span.year").toString().toIntOrNull(), + EnumSet.of(DubStatus.Subbed), + ) + }) + ) + urls.apmap { (url, name) -> + val soup = app.get(url).document + val home = soup.select("div.item-pelicula").map { + val title = it.selectFirst(".item-detail p")?.text() ?: "" + val poster = it.selectFirst("figure img")?.attr("src") + AnimeSearchResponse( + title, + fixUrl(it.selectFirst("a")?.attr("href") ?: ""), + this.name, + TvType.Anime, + poster, + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + + items.add(HomePageList(name, home)) + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List { + val headers = mapOf( + "Host" to "animeflv.io", + "User-Agent" to USER_AGENT, + "X-Requested-With" to "XMLHttpRequest", + "DNT" to "1", + "Alt-Used" to "animeflv.io", + "Connection" to "keep-alive", + "Referer" to "https://animeflv.io", + ) + val url = "$mainUrl/search.html?keyword=$query" + val document = app.get( + url, + headers = headers + ).document + return document.select(".item-pelicula.pull-left").map { + val title = it.selectFirst("div.item-detail p")?.text() ?: "" + val href = fixUrl(it.selectFirst("a")?.attr("href") ?: "") + var image = it.selectFirst("figure img")?.attr("src") ?: "" + val isMovie = href.contains("/pelicula/") + if (image.contains("/static/img/picture.png")) { + image = "" + } + if (isMovie) { + MovieSearchResponse( + title, + href, + this.name, + TvType.AnimeMovie, + image, + null + ) + } else { + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + image, + null, + EnumSet.of(DubStatus.Subbed), + ) + } + } + } + + override suspend fun load(url: String): LoadResponse? { + // Gets the url returned from searching. + val soup = app.get(url).document + val title = soup.selectFirst(".info-content h1")?.text() + val description = soup.selectFirst("span.sinopsis")?.text()?.trim() + val poster: String? = soup.selectFirst(".poster img")?.attr("src") + val episodes = soup.select(".item-season-episodes a").map { li -> + val href = fixUrl(li.selectFirst("a")?.attr("href") ?: "") + val name = li.selectFirst("a")?.text() ?: "" + Episode( + href, name, + ) + }.reversed() + + val year = Regex("(\\d*)").find(soup.select(".info-half").text()) + + val tvType = if (url.contains("/pelicula/")) TvType.AnimeMovie else TvType.Anime + val genre = soup.select(".content-type-a a") + .map { it?.text()?.trim().toString().replace(", ", "") } + val duration = Regex("""(\d*)""").find( + soup.select("p.info-half:nth-child(4)").text() + ) + + return when (tvType) { + TvType.Anime -> { + return newAnimeLoadResponse(title ?: "", url, tvType) { + japName = null + engName = title + posterUrl = poster + this.year = null + addEpisodes(DubStatus.Subbed, episodes) + plot = description + tags = genre + + showStatus = null + } + } + TvType.AnimeMovie -> { + MovieLoadResponse( + title ?: "", + url, + this.name, + tvType, + url, + poster, + year.toString().toIntOrNull(), + description, + null, + genre, + duration.toString().toIntOrNull(), + ) + } + else -> null + } + } + + data class MainJson( + @JsonProperty("source") val source: List, + @JsonProperty("source_bk") val sourceBk: String?, + @JsonProperty("track") val track: List?, + @JsonProperty("advertising") val advertising: List?, + @JsonProperty("linkiframe") val linkiframe: String? + ) + + data class Source( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("default") val default: String, + @JsonProperty("type") val type: String + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("li.tab-video").apmap { + val url = fixUrl(it.attr("data-video")) + if (url.contains("animeid")) { + val ajaxurl = url.replace("streaming.php", "ajax.php") + val ajaxurltext = app.get(ajaxurl).text + val json = parseJson(ajaxurltext) + json.source.forEach { source -> + if (source.file.contains("m3u8")) { + generateM3u8( + "Animeflv.io", + source.file, + "https://animeid.to", + headers = mapOf("Referer" to "https://animeid.to") + ).apmap { + callback( + ExtractorLink( + "Animeflv.io", + "Animeflv.io", + it.url, + "https://animeid.to", + getQualityFromName(it.quality.toString()), + it.url.contains("m3u8") + ) + ) + } + } else { + callback( + ExtractorLink( + name, + "$name ${source.label}", + source.file, + "https://animeid.to", + Qualities.Unknown.value, + isM3u8 = source.file.contains("m3u8") + ) + ) + } + } + } + loadExtractor(url, data, subtitleCallback, callback) + } + return true + } +} \ No newline at end of file diff --git a/AnimeflvIOProvider/src/main/kotlin/com/lagradost/AnimeflvIOProviderPlugin.kt b/AnimeflvIOProvider/src/main/kotlin/com/lagradost/AnimeflvIOProviderPlugin.kt new file mode 100644 index 0000000..f0fa600 --- /dev/null +++ b/AnimeflvIOProvider/src/main/kotlin/com/lagradost/AnimeflvIOProviderPlugin.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 AnimeflvIOProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeflvIOProvider()) + } +} \ No newline at end of file diff --git a/AnimeflvnetProvider/build.gradle.kts b/AnimeflvnetProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimeflvnetProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimeflvnetProvider/src/main/AndroidManifest.xml b/AnimeflvnetProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimeflvnetProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimeflvnetProvider/src/main/kotlin/com/lagradost/AnimeflvnetProvider.kt b/AnimeflvnetProvider/src/main/kotlin/com/lagradost/AnimeflvnetProvider.kt new file mode 100644 index 0000000..c30076f --- /dev/null +++ b/AnimeflvnetProvider/src/main/kotlin/com/lagradost/AnimeflvnetProvider.kt @@ -0,0 +1,182 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.* + +class AnimeflvnetProvider : MainAPI() { + companion object { + fun getType(t: String): TvType { + return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA + else if (t.contains("Película")) TvType.AnimeMovie + else TvType.Anime + } + + fun getDubStatus(title: String): DubStatus { + return if (title.contains("Latino") || title.contains("Castellano")) + DubStatus.Dubbed + else DubStatus.Subbed + } + } + + override var mainUrl = "https://www3.animeflv.net" + override var name = "Animeflv.net" + override var lang = "es" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("$mainUrl/browse?type[]=movie&order=updated", "Películas"), + Pair("$mainUrl/browse?status[]=2&order=default", "Animes"), + Pair("$mainUrl/browse?status[]=1&order=rating", "En emision"), + ) + val items = ArrayList() + items.add( + HomePageList( + "Últimos episodios", + app.get(mainUrl).document.select("main.Main ul.ListEpisodios li").mapNotNull { + val title = it.selectFirst("strong.Title")?.text() ?: return@mapNotNull null + val poster = it.selectFirst("span img")?.attr("src") ?: return@mapNotNull null + val epRegex = Regex("(-(\\d+)\$)") + val url = it.selectFirst("a")?.attr("href")?.replace(epRegex, "") + ?.replace("ver/", "anime/") ?: return@mapNotNull null + val epNum = + it.selectFirst("span.Capi")?.text()?.replace("Episodio ", "")?.toIntOrNull() + newAnimeSearchResponse(title, url) { + this.posterUrl = fixUrl(poster) + addDubStatus(getDubStatus(title), epNum) + } + }) + ) + for ((url, name) in urls) { + try { + val doc = app.get(url).document + val home = doc.select("ul.ListAnimes li article").mapNotNull { + val title = it.selectFirst("h3.Title")?.text() ?: return@mapNotNull null + val poster = it.selectFirst("figure img")?.attr("src") ?: return@mapNotNull null + newAnimeSearchResponse( + title, + fixUrl(it.selectFirst("a")?.attr("href") ?: return@mapNotNull null) + ) { + this.posterUrl = fixUrl(poster) + addDubStatus(getDubStatus(title)) + } + } + + items.add(HomePageList(name, home)) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + data class SearchObject( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String, + @JsonProperty("type") val type: String, + @JsonProperty("last_id") val lastId: String, + @JsonProperty("slug") val slug: String + ) + + override suspend fun search(query: String): List { + val response = app.post( + "https://www3.animeflv.net/api/animes/search", + data = mapOf(Pair("value", query)) + ).text + val json = parseJson>(response) + return json.map { searchr -> + val title = searchr.title + val href = "$mainUrl/anime/${searchr.slug}" + val image = "$mainUrl/uploads/animes/covers/${searchr.id}.jpg" + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + fixUrl(image), + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of( + DubStatus.Subbed + ), + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url).document + val episodes = ArrayList() + val title = doc.selectFirst("h1.Title")!!.text() + val poster = doc.selectFirst("div.AnimeCover div.Image figure img")?.attr("src")!! + val description = doc.selectFirst("div.Description p")?.text() + val type = doc.selectFirst("span.Type")?.text() ?: "" + val status = when (doc.selectFirst("p.AnmStts span")?.text()) { + "En emision" -> ShowStatus.Ongoing + "Finalizado" -> ShowStatus.Completed + else -> null + } + val genre = doc.select("nav.Nvgnrs a") + .map { it?.text()?.trim().toString() } + + doc.select("script").map { script -> + if (script.data().contains("var episodes = [")) { + val data = script.data().substringAfter("var episodes = [").substringBefore("];") + data.split("],").forEach { + val epNum = it.removePrefix("[").substringBefore(",") + // val epthumbid = it.removePrefix("[").substringAfter(",").substringBefore("]") + val animeid = doc.selectFirst("div.Strs.RateIt")?.attr("data-id") + val epthumb = "https://cdn.animeflv.net/screenshots/$animeid/$epNum/th_3.jpg" + val link = url.replace("/anime/", "/ver/") + "-$epNum" + episodes.add( + Episode( + link, + null, + posterUrl = epthumb, + episode = epNum.toIntOrNull() + ) + ) + } + } + } + return newAnimeLoadResponse(title, url, getType(type)) { + posterUrl = fixUrl(poster) + addEpisodes(DubStatus.Subbed, episodes.reversed()) + showStatus = status + plot = description + tags = genre + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("script").apmap { script -> + if (script.data().contains("var videos = {") || script.data() + .contains("var anime_id =") || script.data().contains("server") + ) { + val videos = script.data().replace("\\/", "/") + fetchUrls(videos).map { + it.replace("https://embedsb.com/e/", "https://watchsb.com/e/") + .replace("https://ok.ru", "http://ok.ru") + }.apmap { + loadExtractor(it, data, subtitleCallback, callback) + } + } + } + return true + } +} diff --git a/AnimeflvnetProvider/src/main/kotlin/com/lagradost/AnimeflvnetProviderPlugin.kt b/AnimeflvnetProvider/src/main/kotlin/com/lagradost/AnimeflvnetProviderPlugin.kt new file mode 100644 index 0000000..9f45880 --- /dev/null +++ b/AnimeflvnetProvider/src/main/kotlin/com/lagradost/AnimeflvnetProviderPlugin.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 AnimeflvnetProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimeflvnetProvider()) + } +} \ No newline at end of file diff --git a/AnimekisaProvider/build.gradle.kts b/AnimekisaProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/AnimekisaProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/AnimekisaProvider/src/main/AndroidManifest.xml b/AnimekisaProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AnimekisaProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AnimekisaProvider/src/main/kotlin/com/lagradost/AnimekisaProvider.kt b/AnimekisaProvider/src/main/kotlin/com/lagradost/AnimekisaProvider.kt new file mode 100644 index 0000000..0a5ca2f --- /dev/null +++ b/AnimekisaProvider/src/main/kotlin/com/lagradost/AnimekisaProvider.kt @@ -0,0 +1,131 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import java.util.* + + +class AnimekisaProvider : MainAPI() { + override var mainUrl = "https://animekisa.in" + override var name = "Animekisa" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + data class Response( + @JsonProperty("html") val html: String + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("$mainUrl/ajax/list/views?type=all", "All animes"), + Pair("$mainUrl/ajax/list/views?type=day", "Trending now"), + Pair("$mainUrl/ajax/list/views?type=week", "Trending by week"), + Pair("$mainUrl/ajax/list/views?type=month", "Trending by month"), + ) + + val items = urls.mapNotNull { + suspendSafeApiCall { + val home = Jsoup.parse( + parseJson( + app.get( + it.first + ).text + ).html + ).select("div.flw-item").mapNotNull secondMap@ { + val title = it.selectFirst("h3.title a")?.text() ?: return@secondMap null + val link = it.selectFirst("a")?.attr("href") ?: return@secondMap null + val poster = it.selectFirst("img.lazyload")?.attr("data-src") + AnimeSearchResponse( + title, + link, + this.name, + TvType.Anime, + poster, + null, + if (title.contains("(DUB)") || title.contains("(Dub)")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + HomePageList(name, home) + } + } + + if (items.isEmpty()) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/search/?keyword=$query").document.select("div.flw-item") + .mapNotNull { + val title = it.selectFirst("h3 a")?.text() ?: "" + val url = it.selectFirst("a.film-poster-ahref")?.attr("href") + ?.replace("watch/", "anime/")?.replace( + Regex("(-episode-(\\d+)/\$|-episode-(\\d+)\$|-episode-full|-episode-.*-.(/|))"), + "" + ) ?: return@mapNotNull null + val poster = it.selectFirst(".film-poster img")?.attr("data-src") + AnimeSearchResponse( + title, + url, + this.name, + TvType.Anime, + poster, + null, + if (title.contains("(DUB)") || title.contains("(Dub)")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + }.toList() + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url, timeout = 120).document + val poster = doc.selectFirst(".mb-2 img")?.attr("src") + ?: doc.selectFirst("head meta[property=og:image]")?.attr("content") + val title = doc.selectFirst("h1.heading-name a")!!.text() + val description = doc.selectFirst("div.description p")?.text()?.trim() + val genres = doc.select("div.row-line a").map { it.text() } + val test = if (doc.selectFirst("div.dp-i-c-right").toString() + .contains("Airing") + ) ShowStatus.Ongoing else ShowStatus.Completed + val episodes = doc.select("div.tab-content ul li.nav-item").mapNotNull { + val link = it.selectFirst("a")?.attr("href") ?: return@mapNotNull null + Episode(link) + } + val type = if (doc.selectFirst(".dp-i-stats").toString() + .contains("Movies") + ) TvType.AnimeMovie else TvType.Anime + return newAnimeLoadResponse(title, url, type) { + posterUrl = poster + addEpisodes(DubStatus.Subbed, episodes) + showStatus = test + plot = description + tags = genres + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("#servers-list ul.nav li a").apmap { + val server = it.attr("data-embed") + loadExtractor(server, data, subtitleCallback, callback) + } + return true + } +} \ No newline at end of file diff --git a/AnimekisaProvider/src/main/kotlin/com/lagradost/AnimekisaProviderPlugin.kt b/AnimekisaProvider/src/main/kotlin/com/lagradost/AnimekisaProviderPlugin.kt new file mode 100644 index 0000000..058cee7 --- /dev/null +++ b/AnimekisaProvider/src/main/kotlin/com/lagradost/AnimekisaProviderPlugin.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 AnimekisaProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AnimekisaProvider()) + } +} \ No newline at end of file diff --git a/DoramasYTProvider/src/main/kotlin/com/lagradost/DoramasYTProvider.kt b/DoramasYTProvider/src/main/kotlin/com/lagradost/DoramasYTProvider.kt index f4d0e99..8f3b5a6 100644 --- a/DoramasYTProvider/src/main/kotlin/com/lagradost/DoramasYTProvider.kt +++ b/DoramasYTProvider/src/main/kotlin/com/lagradost/DoramasYTProvider.kt @@ -141,14 +141,14 @@ class DoramasYTProvider : MainAPI() { val encodedurl = it.select("p").attr("data-player") val urlDecoded = base64Decode(encodedurl) val url = (urlDecoded).replace("https://doramasyt.com/reproductor?url=", "") -// if (url.startsWith("https://www.fembed.com")) { -// val extractor = FEmbed() -// extractor.getUrl(url).forEach { link -> -// callback.invoke(link) -// } -// } else { + if (url.startsWith("https://www.fembed.com")) { + val extractor = FEmbed() + extractor.getUrl(url).forEach { link -> + callback.invoke(link) + } + } else { loadExtractor(url, mainUrl, subtitleCallback, callback) -// } + } } return true } diff --git a/DoramasYTProvider/src/main/kotlin/com/lagradost/XStreamCdn.kt b/DoramasYTProvider/src/main/kotlin/com/lagradost/XStreamCdn.kt new file mode 100644 index 0000000..866aee0 --- /dev/null +++ b/DoramasYTProvider/src/main/kotlin/com/lagradost/XStreamCdn.kt @@ -0,0 +1,67 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName + +class FEmbed: XStreamCdn() { + override val name: String = "FEmbed" + override val mainUrl: String = "https://www.fembed.com" +} + +open class XStreamCdn : ExtractorApi() { + override val name: String = "XStreamCdn" + override val mainUrl: String = "https://embedsito.com" + override val requiresReferer = false + open var domainUrl: String = "embedsito.com" + + private data class ResponseData( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + //val type: String // Mp4 + ) + + private data class ResponseJson( + @JsonProperty("success") val success: Boolean, + @JsonProperty("data") val data: List? + ) + + override fun getExtractorUrl(id: String): String { + return "$domainUrl/api/source/$id" + } + + override suspend fun getUrl(url: String, referer: String?): List { + val headers = mapOf( + "Referer" to url, + "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0", + ) + val id = url.trimEnd('/').split("/").last() + val newUrl = "https://${domainUrl}/api/source/${id}" + val extractedLinksList: MutableList = mutableListOf() + with(app.post(newUrl, headers = headers)) { + if (this.code != 200) return listOf() + val text = this.text + if (text.isEmpty()) return listOf() + if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf() + AppUtils.parseJson(text)?.let { + if (it.success && it.data != null) { + it.data.forEach { data -> + extractedLinksList.add( + ExtractorLink( + name, + name = name, + data.file, + url, + getQualityFromName(data.label), + ) + ) + } + } + } + } + return extractedLinksList + } +} \ No newline at end of file diff --git a/DubbedAnimeProvider/build.gradle.kts b/DubbedAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/DubbedAnimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/DubbedAnimeProvider/src/main/AndroidManifest.xml b/DubbedAnimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/DubbedAnimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/DubbedAnimeProvider/src/main/kotlin/com/lagradost/DubbedAnimeProvider.kt b/DubbedAnimeProvider/src/main/kotlin/com/lagradost/DubbedAnimeProvider.kt new file mode 100644 index 0000000..bb59f69 --- /dev/null +++ b/DubbedAnimeProvider/src/main/kotlin/com/lagradost/DubbedAnimeProvider.kt @@ -0,0 +1,270 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.Jsoup +import java.util.* + +class DubbedAnimeProvider : MainAPI() { + override var mainUrl = "https://bestdubbedanime.com" + override var name = "DubbedAnime" + override val hasQuickSearch = true + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.Anime, + ) + + data class QueryEpisodeResultRoot( + @JsonProperty("result") + val result: QueryEpisodeResult, + ) + + data class QueryEpisodeResult( + @JsonProperty("anime") val anime: List, + @JsonProperty("error") val error: Boolean, + @JsonProperty("errorMSG") val errorMSG: String?, + ) + + data class EpisodeInfo( + @JsonProperty("serversHTML") val serversHTML: String, + @JsonProperty("title") val title: String, + @JsonProperty("preview_img") val previewImg: String?, + @JsonProperty("wideImg") val wideImg: String?, + @JsonProperty("year") val year: String?, + @JsonProperty("desc") val desc: String?, + + /* + @JsonProperty("rowid") val rowid: String, + @JsonProperty("status") val status: String, + @JsonProperty("skips") val skips: String, + @JsonProperty("totalEp") val totalEp: Long, + @JsonProperty("ep") val ep: String, + @JsonProperty("NextEp") val nextEp: Long, + @JsonProperty("slug") val slug: String, + @JsonProperty("showid") val showid: String, + @JsonProperty("Epviews") val epviews: String, + @JsonProperty("TotalViews") val totalViews: String, + @JsonProperty("tags") val tags: String,*/ + ) + + private suspend fun parseDocumentTrending(url: String): List { + val response = app.get(url).text + val document = Jsoup.parse(response) + return document.select("li > a").mapNotNull { + val href = fixUrl(it.attr("href")) + val title = it.selectFirst("> div > div.cittx")?.text() ?: return@mapNotNull null + val poster = fixUrlNull(it.selectFirst("> div > div.imghddde > img")?.attr("src")) + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + poster, + null, + EnumSet.of(DubStatus.Dubbed), + ) + } + } + + private suspend fun parseDocument( + url: String, + trimEpisode: Boolean = false + ): List { + val response = app.get(url).text + val document = Jsoup.parse(response) + return document.select("a.grid__link").mapNotNull { + val href = fixUrl(it.attr("href")) + val title = it.selectFirst("> div.gridtitlek")?.text() ?: return@mapNotNull null + val poster = + fixUrl(it.selectFirst("> img.grid__img")?.attr("src") ?: return@mapNotNull null) + AnimeSearchResponse( + title, + if (trimEpisode) href.removeRange(href.lastIndexOf('/'), href.length) else href, + this.name, + TvType.Anime, + poster, + null, + EnumSet.of(DubStatus.Dubbed), + ) + } + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val trendingUrl = "$mainUrl/xz/trending.php?_=$unixTimeMS" + val lastEpisodeUrl = "$mainUrl/xz/epgrid.php?p=1&_=$unixTimeMS" + val recentlyAddedUrl = "$mainUrl/xz/gridgrabrecent.php?p=1&_=$unixTimeMS" + //val allUrl = "$mainUrl/xz/gridgrab.php?p=1&limit=12&_=$unixTimeMS" + + val listItems = listOf( + HomePageList("Trending", parseDocumentTrending(trendingUrl)), + HomePageList("Recently Added", parseDocument(recentlyAddedUrl)), + HomePageList("Recent Releases", parseDocument(lastEpisodeUrl, true)), + // HomePageList("All", parseDocument(allUrl)) + ) + + return HomePageResponse(listItems) + } + + + private suspend fun getEpisode(slug: String, isMovie: Boolean): EpisodeInfo { + val url = + mainUrl + (if (isMovie) "/movies/jsonMovie" else "/xz/v3/jsonEpi") + ".php?slug=$slug&_=$unixTime" + val response = app.get(url).text + val mapped = parseJson(response) + return mapped.result.anime.first() + } + + + private fun getIsMovie(href: String): Boolean { + return href.contains("movies/") + } + + private fun getSlug(href: String): String { + return href.replace("$mainUrl/", "") + } + + override suspend fun quickSearch(query: String): List { + val url = "$mainUrl/xz/searchgrid.php?p=1&limit=12&s=$query&_=$unixTime" + val response = app.get(url).text + val document = Jsoup.parse(response) + val items = document.select("div.grid__item > a") + if (items.isEmpty()) return emptyList() + return items.mapNotNull { i -> + val href = fixUrl(i.attr("href")) + val title = i.selectFirst("div.gridtitlek")?.text() ?: return@mapNotNull null + val img = fixUrlNull(i.selectFirst("img.grid__img")?.attr("src")) + + if (getIsMovie(href)) { + MovieSearchResponse( + title, href, this.name, TvType.AnimeMovie, img, null + ) + } else { + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + img, + null, + EnumSet.of(DubStatus.Dubbed), + ) + } + } + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/search/$query" + val response = app.get(url).text + val document = Jsoup.parse(response) + val items = document.select("div.resultinner > a.resulta") + if (items.isEmpty()) return ArrayList() + return items.mapNotNull { i -> + val innerDiv = i.selectFirst("> div.result") + val href = fixUrl(i.attr("href")) + val img = fixUrl(innerDiv?.selectFirst("> div.imgkz > img")?.attr("src") ?: return@mapNotNull null) + val title = innerDiv.selectFirst("> div.titleresults")?.text() ?: return@mapNotNull null + + if (getIsMovie(href)) { + MovieSearchResponse( + title, href, this.name, TvType.AnimeMovie, img, null + ) + } else { + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + img, + null, + EnumSet.of(DubStatus.Dubbed), + ) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val serversHTML = (if (data.startsWith(mainUrl)) { // CLASSIC EPISODE + val slug = getSlug(data) + getEpisode(slug, false).serversHTML + } else data).replace("\\", "") + + val hls = ArrayList("hl=\"(.*?)\"".toRegex().findAll(serversHTML).map { + it.groupValues[1] + }.toList()) + for (hl in hls) { + try { + val sources = app.get("$mainUrl/xz/api/playeri.php?url=$hl&_=$unixTime").text + val find = "src=\"(.*?)\".*?label=\"(.*?)\"".toRegex().find(sources) + if (find != null) { + val quality = find.groupValues[2] + callback.invoke( + ExtractorLink( + this.name, + this.name + " " + quality + if (quality.endsWith('p')) "" else 'p', + fixUrl(find.groupValues[1]), + this.mainUrl, + getQualityFromName(quality) + ) + ) + } + } catch (e: Exception) { + //IDK + } + } + return true + } + + override suspend fun load(url: String): LoadResponse { + if (getIsMovie(url)) { + val realSlug = url.replace("movies/", "") + val episode = getEpisode(realSlug, true) + val poster = episode.previewImg ?: episode.wideImg + return MovieLoadResponse( + episode.title, + realSlug, + this.name, + TvType.AnimeMovie, + episode.serversHTML, + if (poster == null) null else fixUrl(poster), + episode.year?.toIntOrNull(), + episode.desc, + null + ) + } else { + val response = app.get(url).text + val document = Jsoup.parse(response) + val title = document.selectFirst("h4")!!.text() + val descriptHeader = document.selectFirst("div.animeDescript") + val descript = descriptHeader?.selectFirst("> p")?.text() + val year = descriptHeader?.selectFirst("> div.distatsx > div.sroverd") + ?.text() + ?.replace("Released: ", "") + ?.toIntOrNull() + + val episodes = document.select("a.epibloks").map { + val epTitle = it.selectFirst("> div.inwel > span.isgrxx")?.text() + Episode(fixUrl(it.attr("href")), epTitle) + } + + val img = fixUrl(document.select("div.fkimgs > img").attr("src")) + return newAnimeLoadResponse(title, url, TvType.Anime) { + posterUrl = img + this.year = year + addEpisodes(DubStatus.Dubbed, episodes) + plot = descript + } + } + } +} \ No newline at end of file diff --git a/DubbedAnimeProvider/src/main/kotlin/com/lagradost/DubbedAnimeProviderPlugin.kt b/DubbedAnimeProvider/src/main/kotlin/com/lagradost/DubbedAnimeProviderPlugin.kt new file mode 100644 index 0000000..939fe9f --- /dev/null +++ b/DubbedAnimeProvider/src/main/kotlin/com/lagradost/DubbedAnimeProviderPlugin.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 DubbedAnimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(DubbedAnimeProvider()) + } +} \ No newline at end of file diff --git a/GogoanimeProvider/build.gradle.kts b/GogoanimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/GogoanimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ 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 0000000..29aec9d --- /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 0000000..f8037ff --- /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 0000000..34e0fb1 --- /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 diff --git a/GomunimeProvider/build.gradle.kts b/GomunimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/GomunimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/GomunimeProvider/src/main/AndroidManifest.xml b/GomunimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/GomunimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/GomunimeProvider/src/main/kotlin/com/lagradost/GomunimeProvider.kt b/GomunimeProvider/src/main/kotlin/com/lagradost/GomunimeProvider.kt new file mode 100644 index 0000000..30689dc --- /dev/null +++ b/GomunimeProvider/src/main/kotlin/com/lagradost/GomunimeProvider.kt @@ -0,0 +1,232 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +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.loadExtractor +import org.jsoup.Jsoup + +class GomunimeProvider : MainAPI() { + override var mainUrl = "https://185.231.223.76" + override var name = "Gomunime" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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 + } + } + } + + override val mainPage = mainPageOf( + "e" to "Episode Baru", + "c" to "Completed", + "la" to "Live Action", + "t" to "Trending" + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val home = Jsoup.parse( + (app.post( + url = "$mainUrl/wp-admin/admin-ajax.php/wp-admin/admin-ajax.php", + headers = mapOf("Referer" to mainUrl), + data = mapOf( + "action" to "home_ajax", + "fungsi" to request.data, + "pag" to "$page" + ) + ).parsedSafe()?.html ?: throw ErrorLoadingException("Invalid Json reponse")) + ).select("li").mapNotNull { + val title = it.selectFirst("a.name")?.text()?.trim() ?: return@mapNotNull null + val href = getProperAnimeLink(it.selectFirst("a")!!.attr("href")) + val posterUrl = it.selectFirst("img")?.attr("src") + val type = getType(it.selectFirst(".taglist > span")!!.text().trim()) + val epNum = it.select(".tag.ep").text().replace(Regex("[^0-9]"), "").trim() + .toIntOrNull() + newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + addSub(epNum) + } + } + + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("-episode")) { + val href = + "$mainUrl/anime/" + Regex("\\w\\d/(.*)-episode.*").find(uri)?.groupValues?.get(1) + .toString() + when { + href.contains("pokemon") -> href.replace(Regex("-[0-9]+"), "") + else -> href + } + } else { + uri + } + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select(".anime-list > li").map { + val title = it.selectFirst("a.name")!!.text() + val poster = it.selectFirst("img")!!.attr("src") + val tvType = getType(it.selectFirst(".taglist > span")?.text().toString()) + val href = fixUrl(it.selectFirst("a.name")!!.attr("href")) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addDubStatus(dubExist = false, subExist = true) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst(".entry-title")?.text().toString() + val poster = document.selectFirst(".thumbposter > img")?.attr("data-lazy-src") + val tags = document.select(".genxed > a").map { it.text() } + + val year = Regex("\\d, ([0-9]*)").find( + document.select("time[itemprop = datePublished]").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus(document.selectFirst(".spe > span")!!.ownText()) + val description = document.select("div[itemprop = description] > p").text() + val trailer = document.selectFirst("div.embed-responsive noscript iframe")?.attr("src") + val episodes = parseJson>( + Regex("var episodelist = (\\[.*])").find( + document.select(".bixbox.bxcl.epcheck > script").toString().trim() + )?.groupValues?.get(1).toString().replace(Regex("""\\"""), "").trim() + ).map { + val name = + Regex("(Episode\\s?[0-9]+)").find(it.epTitle.toString())?.groupValues?.getOrNull(0) + ?: it.epTitle + val link = it.epLink + Episode(link, name) + }.reversed() + + return newAnimeLoadResponse(title, url, TvType.Anime) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + addTrailer(trailer) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + + val scriptData = document.select("aside.sidebar > script").dataNodes().toString() + val key = scriptData.substringAfter("var a_ray = '").substringBefore("';") + val title = scriptData.substringAfter("var judul_postingan = \"").substringBefore("\";") + + val sources: List> = app.post( + url = "https://path.gomuni.me/app/vapi.php", + data = mapOf("data" to key, "judul" to title, "func" to "mirror") + ).document.select("div.gomunime-server-mirror").map { + Pair( + it.attr("data-vhash"), + it.attr("data-type") + ) + } + + sources.apmap { + safeApiCall { + when { + it.second.contains("frame") -> { + loadExtractor(it.first, data, subtitleCallback, callback) + } + it.second.contains("hls") -> { + app.post( + url = "https://path.gomuni.me/app/vapi.php", + data = mapOf("fid" to it.first, "func" to "hls") + ).text.let { link -> + M3u8Helper.generateM3u8( + this.name, + link, + "$mainUrl/", + headers = mapOf("Origin" to mainUrl) + ).forEach(callback) + } + } + it.second.contains("mp4") -> { + app.post( + url = "https://path.gomuni.me/app/vapi.php", + data = mapOf("data" to it.first, "func" to "blogs") + ).parsed>().map { + callback.invoke( + ExtractorLink( + source = name, + name = "Mobi SD", + url = it.file, + referer = "$mainUrl/", + quality = Qualities.P360.value + ) + ) + } + } + else -> null + } + } + } + + return true + } + + private data class Response( + @JsonProperty("status") val status: Boolean, + @JsonProperty("html") val html: String + ) + + data class MobiSource( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("type") val type: String + ) + + private data class EpisodeElement( + @JsonProperty("data-index") val dataIndex: Long?, + @JsonProperty("ep-num") val epNum: String?, + @JsonProperty("ep-title") val epTitle: String?, + @JsonProperty("ep-link") val epLink: String, + @JsonProperty("ep-date") val epDate: String? + ) + +} \ No newline at end of file diff --git a/GomunimeProvider/src/main/kotlin/com/lagradost/GomunimeProviderPlugin.kt b/GomunimeProvider/src/main/kotlin/com/lagradost/GomunimeProviderPlugin.kt new file mode 100644 index 0000000..3a7ecee --- /dev/null +++ b/GomunimeProvider/src/main/kotlin/com/lagradost/GomunimeProviderPlugin.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 GomunimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(GomunimeProvider()) + } +} \ No newline at end of file diff --git a/JKAnimeProvider/build.gradle.kts b/JKAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/JKAnimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/JKAnimeProvider/src/main/AndroidManifest.xml b/JKAnimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/JKAnimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProvider.kt b/JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProvider.kt new file mode 100644 index 0000000..a3174ab --- /dev/null +++ b/JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProvider.kt @@ -0,0 +1,319 @@ +package com.lagradost + + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.* + + +class JKAnimeProvider : MainAPI() { + companion object { + fun getType(t: String): TvType { + return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA + else if (t.contains("Pelicula")) TvType.AnimeMovie + else TvType.Anime + } + } + + override var mainUrl = "https://jkanime.net" + override var name = "JKAnime" + override var lang = "es" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair( + "$mainUrl/directorio/?filtro=fecha&tipo=TV&estado=1&fecha=none&temporada=none&orden=desc", + "En emisión" + ), + Pair( + "$mainUrl/directorio/?filtro=fecha&tipo=none&estado=none&fecha=none&temporada=none&orden=none", + "Animes" + ), + Pair( + "$mainUrl/directorio/?filtro=fecha&tipo=Movie&estado=none&fecha=none&temporada=none&orden=none", + "Películas" + ), + ) + + val items = ArrayList() + + items.add( + HomePageList( + "Últimos episodios", + app.get(mainUrl).document.select(".listadoanime-home a.bloqq").map { + val title = it.selectFirst("h5")?.text() + val dubstat = if (title!!.contains("Latino") || title.contains("Castellano")) + DubStatus.Dubbed else DubStatus.Subbed + val poster = + it.selectFirst(".anime__sidebar__comment__item__pic img")?.attr("src") ?: "" + val epRegex = Regex("/(\\d+)/|/especial/|/ova/") + val url = it.attr("href").replace(epRegex, "") + val epNum = + it.selectFirst("h6")?.text()?.replace("Episodio ", "")?.toIntOrNull() + newAnimeSearchResponse(title, url) { + this.posterUrl = poster + addDubStatus(dubstat, epNum) + } + }) + ) + urls.apmap { (url, name) -> + val soup = app.get(url).document + val home = soup.select(".g-0").map { + val title = it.selectFirst("h5 a")?.text() + val poster = it.selectFirst("img")?.attr("src") ?: "" + AnimeSearchResponse( + title!!, + fixUrl(it.selectFirst("a")?.attr("href") ?: ""), + this.name, + TvType.Anime, + fixUrl(poster), + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + items.add(HomePageList(name, home)) + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + data class MainSearch( + @JsonProperty("animes") val animes: List, + @JsonProperty("anime_types") val animeTypes: AnimeTypes + ) + + data class Animes( + @JsonProperty("id") val id: String, + @JsonProperty("slug") val slug: String, + @JsonProperty("title") val title: String, + @JsonProperty("image") val image: String, + @JsonProperty("synopsis") val synopsis: String, + @JsonProperty("type") val type: String, + @JsonProperty("status") val status: String, + @JsonProperty("thumbnail") val thumbnail: String + ) + + data class AnimeTypes( + @JsonProperty("TV") val TV: String, + @JsonProperty("OVA") val OVA: String, + @JsonProperty("Movie") val Movie: String, + @JsonProperty("Special") val Special: String, + @JsonProperty("ONA") val ONA: String, + @JsonProperty("Music") val Music: String + ) + + override suspend fun search(query: String): List { + val main = app.get("$mainUrl/ajax/ajax_search/?q=$query").text + val json = parseJson(main) + return json.animes.map { + val title = it.title + val href = "$mainUrl/${it.slug}" + val image = "https://cdn.jkanime.net/assets/images/animes/image/${it.slug}.jpg" + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + image, + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url, timeout = 120).document + val poster = doc.selectFirst(".set-bg")?.attr("data-setbg") + val title = doc.selectFirst(".anime__details__title > h3")?.text() + val type = doc.selectFirst(".anime__details__text")?.text() + val description = doc.selectFirst(".anime__details__text > p")?.text() + val genres = doc.select("div.col-lg-6:nth-child(1) > ul:nth-child(1) > li:nth-child(2) > a") + .map { it.text() } + val status = when (doc.selectFirst("span.enemision")?.text()) { + "En emisión" -> ShowStatus.Ongoing + "Concluido" -> ShowStatus.Completed + else -> null + } + val animeID = doc.selectFirst("div.ml-2")?.attr("data-anime")?.toInt() + val animeeps = "$mainUrl/ajax/last_episode/$animeID/" + val jsoneps = app.get(animeeps).text + val lastepnum = + jsoneps.substringAfter("{\"number\":\"").substringBefore("\",\"title\"").toInt() + val episodes = (1..lastepnum).map { + val link = "${url.removeSuffix("/")}/$it" + Episode(link) + } + + return newAnimeLoadResponse(title!!, url, getType(type!!)) { + posterUrl = poster + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + tags = genres + } + } + + data class Nozomi( + @JsonProperty("file") val file: String? + ) + + private fun streamClean( + name: String, + url: String, + referer: String, + quality: String?, + callback: (ExtractorLink) -> Unit, + m3u8: Boolean + ): Boolean { + callback( + ExtractorLink( + name, + name, + url, + referer, + getQualityFromName(quality), + m3u8 + ) + ) + return true + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("script").apmap { script -> + if (script.data().contains("var video = []")) { + val videos = script.data().replace("\\/", "/") + fetchUrls(videos).map { + it.replace("$mainUrl/jkfembed.php?u=", "https://embedsito.com/v/") + .replace("$mainUrl/jkokru.php?u=", "http://ok.ru/videoembed/") + .replace("$mainUrl/jkvmixdrop.php?u=", "https://mixdrop.co/e/") + .replace("$mainUrl/jk.php?u=", "$mainUrl/") + }.apmap { link -> + loadExtractor(link, data, subtitleCallback, callback) + if (link.contains("um2.php")) { + val doc = app.get(link, referer = data).document + val gsplaykey = doc.select("form input[value]").attr("value") + app.post( + "$mainUrl/gsplay/redirect_post.php", + headers = mapOf( + "Host" to "jkanime.net", + "User-Agent" to USER_AGENT, + "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language" to "en-US,en;q=0.5", + "Referer" to link, + "Content-Type" to "application/x-www-form-urlencoded", + "Origin" to "https://jkanime.net", + "DNT" to "1", + "Connection" to "keep-alive", + "Upgrade-Insecure-Requests" to "1", + "Sec-Fetch-Dest" to "iframe", + "Sec-Fetch-Mode" to "navigate", + "Sec-Fetch-Site" to "same-origin", + "TE" to "trailers", + "Pragma" to "no-cache", + "Cache-Control" to "no-cache", + ), + data = mapOf(Pair("data", gsplaykey)), + allowRedirects = false + ).okhttpResponse.headers.values("location").apmap { loc -> + val postkey = loc.replace("/gsplay/player.html#", "") + val nozomitext = app.post( + "$mainUrl/gsplay/api.php", + headers = mapOf( + "Host" to "jkanime.net", + "User-Agent" to USER_AGENT, + "Accept" to "application/json, text/javascript, */*; q=0.01", + "Accept-Language" to "en-US,en;q=0.5", + "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", + "X-Requested-With" to "XMLHttpRequest", + "Origin" to "https://jkanime.net", + "DNT" to "1", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "same-origin", + ), + data = mapOf(Pair("v", postkey)), + allowRedirects = false + ).text + val json = parseJson(nozomitext) + val nozomiurl = listOf(json.file) + if (nozomiurl.isEmpty()) null else + nozomiurl.forEach { url -> + val nozominame = "Nozomi" + streamClean( + nozominame, + url!!, + "", + null, + callback, + url.contains(".m3u8") + ) + } + } + } + if (link.contains("um.php")) { + val desutext = app.get(link, referer = data).text + val desuRegex = Regex("((https:|http:)//.*\\.m3u8)") + val file = desuRegex.find(desutext)?.value + val namedesu = "Desu" + generateM3u8( + namedesu, + file!!, + mainUrl, + ).forEach { desurl -> + streamClean( + namedesu, + desurl.url, + mainUrl, + desurl.quality.toString(), + callback, + true + ) + } + } + if (link.contains("jkmedia")) { + app.get( + link, + referer = data, + allowRedirects = false + ).okhttpResponse.headers.values("location").apmap { xtremeurl -> + val namex = "Xtreme S" + streamClean( + namex, + xtremeurl, + "", + null, + callback, + xtremeurl.contains(".m3u8") + ) + } + } + } + } + } + return true + } +} \ No newline at end of file diff --git a/JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProviderPlugin.kt b/JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProviderPlugin.kt new file mode 100644 index 0000000..330bc29 --- /dev/null +++ b/JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProviderPlugin.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 JKAnimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(JKAnimeProvider()) + } +} \ No newline at end of file diff --git a/KawaiifuProvider/build.gradle.kts b/KawaiifuProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/KawaiifuProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/KawaiifuProvider/src/main/AndroidManifest.xml b/KawaiifuProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/KawaiifuProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/KawaiifuProvider/src/main/kotlin/com/lagradost/KawaiifuProvider.kt b/KawaiifuProvider/src/main/kotlin/com/lagradost/KawaiifuProvider.kt new file mode 100644 index 0000000..84567f2 --- /dev/null +++ b/KawaiifuProvider/src/main/kotlin/com/lagradost/KawaiifuProvider.kt @@ -0,0 +1,174 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.Jsoup +import java.util.* + +class KawaiifuProvider : MainAPI() { + override var mainUrl = "https://kawaiifu.com" + override var name = "Kawaiifu" + override val hasQuickSearch = false + override val hasMainPage = true + + override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val resp = app.get(mainUrl).text + + val soup = Jsoup.parse(resp) + + items.add(HomePageList("Latest Updates", soup.select(".today-update .item").mapNotNull { + val title = it.selectFirst("img")?.attr("alt") + AnimeSearchResponse( + title ?: return@mapNotNull null, + it.selectFirst("a")?.attr("href") ?: return@mapNotNull null, + this.name, + TvType.Anime, + it.selectFirst("img")?.attr("src"), + it.selectFirst("h4 > a")?.attr("href")?.split("-")?.last()?.toIntOrNull(), + if (title.contains("(DUB)")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of( + DubStatus.Subbed + ), + ) + })) + for (section in soup.select(".section")) { + try { + val title = section.selectFirst(".title")!!.text() + val anime = section.select(".list-film > .item").mapNotNull { ani -> + val animTitle = ani.selectFirst("img")?.attr("alt") + AnimeSearchResponse( + animTitle ?: return@mapNotNull null, + ani.selectFirst("a")?.attr("href") ?: return@mapNotNull null, + this.name, + TvType.Anime, + ani.selectFirst("img")?.attr("src"), + ani.selectFirst(".vl-chil-date")?.text()?.toIntOrNull(), + if (animTitle.contains("(DUB)")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of( + DubStatus.Subbed + ), + ) + } + items.add(HomePageList(title, anime)) + + } catch (e: Exception) { + e.printStackTrace() + } + } + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + + override suspend fun search(query: String): ArrayList { + val link = "$mainUrl/search-movie?keyword=${query}" + val html = app.get(link).text + val soup = Jsoup.parse(html) + + return ArrayList(soup.select(".item").mapNotNull { + val year = it.selectFirst("h4 > a")?.attr("href")?.split("-")?.last()?.toIntOrNull() + val title = it.selectFirst("img")?.attr("alt") ?: return@mapNotNull null + val poster = it.selectFirst("img")?.attr("src") + val uri = it.selectFirst("a")?.attr("href") ?: return@mapNotNull null + AnimeSearchResponse( + title, + uri, + this.name, + TvType.Anime, + poster, + year, + if (title.contains("(DUB)")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed), + ) + }) + } + + override suspend fun load(url: String): LoadResponse { + val html = app.get(url).text + val soup = Jsoup.parse(html) + + val title = soup.selectFirst(".title")!!.text() + val tags = soup.select(".table a[href*=\"/tag/\"]").map { tag -> tag.text() } + val description = soup.select(".sub-desc p") + .filter { it -> it.select("strong").isEmpty() && it.select("iframe").isEmpty() } + .joinToString("\n") { it.text() } + val year = url.split("/").filter { it.contains("-") }[0].split("-")[1].toIntOrNull() + + val episodesLink = soup.selectFirst("a[href*=\".html-episode\"]")?.attr("href") + ?: throw ErrorLoadingException("Error getting episode list") + val episodes = Jsoup.parse( + app.get(episodesLink).text + ).selectFirst(".list-ep")?.select("li")?.map { + Episode( + it.selectFirst("a")!!.attr("href"), + if (it.text().trim().toIntOrNull() != null) "Episode ${ + it.text().trim() + }" else it.text().trim() + ) + } + val poster = soup.selectFirst("a.thumb > img")?.attr("src") + + return newAnimeLoadResponse(title, url, TvType.Anime) { + this.year = year + posterUrl = poster + addEpisodes(DubStatus.Subbed, episodes) + plot = description + this.tags = tags + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val htmlSource = app.get(data).text + val soupa = Jsoup.parse(htmlSource) + + val episodeNum = + if (data.contains("ep=")) data.split("ep=")[1].split("&")[0].toIntOrNull() else null + + val servers = soupa.select(".list-server").map { + val serverName = it.selectFirst(".server-name")!!.text() + val episodes = it.select(".list-ep > li > a") + .map { episode -> Pair(episode.attr("href"), episode.text()) } + val episode = if (episodeNum == null) episodes[0] else episodes.mapNotNull { ep -> + if ((if (ep.first.contains("ep=")) ep.first.split("ep=")[1].split("&")[0].toIntOrNull() else null) == episodeNum) { + ep + } else null + }[0] + Pair(serverName, episode) + }.map { + if (it.second.first == data) { + val sources = soupa.select("video > source") + .map { source -> Pair(source.attr("src"), source.attr("data-quality")) } + Triple(it.first, sources, it.second.second) + } else { + val html = app.get(it.second.first).text + val soup = Jsoup.parse(html) + + val sources = soup.select("video > source") + .map { source -> Pair(source.attr("src"), source.attr("data-quality")) } + Triple(it.first, sources, it.second.second) + } + } + + servers.forEach { + it.second.forEach { source -> + callback( + ExtractorLink( + "Kawaiifu", + it.first, + source.first, + "", + getQualityFromName(source.second), + source.first.contains(".m3u") + ) + ) + } + } + return true + } +} diff --git a/KawaiifuProvider/src/main/kotlin/com/lagradost/KawaiifuProviderPlugin.kt b/KawaiifuProvider/src/main/kotlin/com/lagradost/KawaiifuProviderPlugin.kt new file mode 100644 index 0000000..a04c63d --- /dev/null +++ b/KawaiifuProvider/src/main/kotlin/com/lagradost/KawaiifuProviderPlugin.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 KawaiifuProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(KawaiifuProvider()) + } +} \ No newline at end of file diff --git a/KimCartoonProvider/build.gradle.kts b/KimCartoonProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/KimCartoonProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/KimCartoonProvider/src/main/AndroidManifest.xml b/KimCartoonProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/KimCartoonProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/KimCartoonProvider/src/main/kotlin/com/lagradost/KimCartoonProvider.kt b/KimCartoonProvider/src/main/kotlin/com/lagradost/KimCartoonProvider.kt new file mode 100644 index 0000000..fcc2c5e --- /dev/null +++ b/KimCartoonProvider/src/main/kotlin/com/lagradost/KimCartoonProvider.kt @@ -0,0 +1,152 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor + +class KimCartoonProvider : MainAPI() { + + override var mainUrl = "https://kimcartoon.li" + override var name = "Kim Cartoon" + override val hasQuickSearch = true + override val hasMainPage = true + + override val supportedTypes = setOf(TvType.Cartoon) + + private fun fixUrl(url: String): String { + return if (url.startsWith("/")) mainUrl + url else url + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val doc = app.get(mainUrl).document.select("#container") + val response = mutableListOf( + HomePageList( + "Latest Update", + doc.select("div.bigBarContainer div.items > div > a").map { + AnimeSearchResponse( + it.select(".item-title").let { div -> + //Because it doesn't contain Title separately + div.text().replace(div.select("span").text(), "") + }, + mainUrl + it.attr("href"), + mainUrl, + TvType.Cartoon, + fixUrl(it.select("img").let { img -> + img.attr("src").let { src -> + src.ifEmpty { img.attr("srctemp") } + } + }) + ) + } + ) + ) + val list = mapOf( + "Top Day" to "tab-top-day", + "Top Week" to "tab-top-week", + "Top Month" to "tab-top-month", + "New Cartoons" to "tab-newest-series" + ) + response.addAll(list.map { item -> + HomePageList( + item.key, + doc.select("#${item.value} > div").map { + AnimeSearchResponse( + it.select("span.title").text(), + mainUrl + it.select("a")[0].attr("href"), + mainUrl, + TvType.Cartoon, + fixUrl(it.select("a > img").attr("src")) + ) + } + ) + }) + return HomePageResponse(response) + } + + override suspend fun search(query: String): List { + return app.post( + "$mainUrl/Search/Cartoon", + data = mapOf("keyword" to query) + ).document + .select("#leftside > div.bigBarContainer div.list-cartoon > div.item > a") + .map { + AnimeSearchResponse( + it.select("span").text(), + mainUrl + it.attr("href"), + mainUrl, + TvType.Cartoon, + fixUrl(it.select("img").attr("src")) + ) + } + } + + override suspend fun quickSearch(query: String): List { + return app.post( + "$mainUrl/Ajax/SearchSuggest", + data = mapOf("keyword" to query) + ).document.select("a").map { + AnimeSearchResponse( + it.text(), + it.attr("href"), + mainUrl, + TvType.Cartoon, + ) + } + } + + + private fun getStatus(from: String?): ShowStatus? { + return when { + from?.contains("Completed") == true -> ShowStatus.Completed + from?.contains("Ongoing") == true -> ShowStatus.Ongoing + else -> null + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url).document.select("#leftside") + val info = doc.select("div.barContent") + val name = info.select("a.bigChar").text() + val eps = doc.select("table.listing > tbody > tr a").reversed().map { + Episode( + fixUrl(it.attr("href")), + it.text().replace(name, "").trim() + ) + } + val infoText = info.text() + fun getData(after: String, before: String): String? { + return if (infoText.contains(after)) + infoText + .substringAfter("$after:") + .substringBefore(before) + .trim() + else null + } + + return newTvSeriesLoadResponse(name, url, TvType.Cartoon, eps) { + posterUrl = fixUrl(info.select("div > img").attr("src")) + showStatus = getStatus(getData("Status", "Views")) + plot = getData("Summary", "Tags:") + tags = getData("Genres", "Date aired")?.split(",") + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val servers = + app.get(data).document.select("#selectServer > option").map { fixUrl(it.attr("value")) } + servers.apmap { + app.get(it).document.select("#my_video_1").attr("src").let { iframe -> + if (iframe.isNotEmpty()) { + loadExtractor(iframe, "$mainUrl/", subtitleCallback, callback) + } + //There are other servers, but they require some work to do + } + } + return true + } +} \ No newline at end of file diff --git a/KimCartoonProvider/src/main/kotlin/com/lagradost/KimCartoonProviderPlugin.kt b/KimCartoonProvider/src/main/kotlin/com/lagradost/KimCartoonProviderPlugin.kt new file mode 100644 index 0000000..9cbee8d --- /dev/null +++ b/KimCartoonProvider/src/main/kotlin/com/lagradost/KimCartoonProviderPlugin.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 KimCartoonProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(KimCartoonProvider()) + } +} \ No newline at end of file diff --git a/KuramanimeProvider/build.gradle.kts b/KuramanimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/KuramanimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/KuramanimeProvider/src/main/AndroidManifest.xml b/KuramanimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/KuramanimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/KuramanimeProvider/src/main/kotlin/com/lagradost/KuramanimeProvider.kt b/KuramanimeProvider/src/main/kotlin/com/lagradost/KuramanimeProvider.kt new file mode 100644 index 0000000..48da3b3 --- /dev/null +++ b/KuramanimeProvider/src/main/kotlin/com/lagradost/KuramanimeProvider.kt @@ -0,0 +1,176 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.utils.ExtractorLink +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class KuramanimeProvider : MainAPI() { + override var mainUrl = "https://kuramanime.com" + override var name = "Kuramanime" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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) { + "Selesai Tayang" -> ShowStatus.Completed + "Sedang Tayang" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/anime/ongoing?order_by=updated&page=" to "Sedang Tayang", + "$mainUrl/anime/finished?order_by=updated&page=" to "Selesai Tayang", + "$mainUrl/properties/season/summer-2022?order_by=most_viewed&page=" to "Dilihat Terbanyak Musim Ini", + "$mainUrl/anime/movie?order_by=updated&page=" to "Film Layar Lebar", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + + val home = document.select("div.col-lg-4.col-md-6.col-sm-6").mapNotNull { + it.toSearchResult() + } + + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/episode")) { + Regex("(.*)/episode/.+").find(uri)?.groupValues?.get(1).toString() + "/" + } else { + uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = getProperAnimeLink(fixUrl(this.selectFirst("a")!!.attr("href"))) + val title = this.selectFirst("h5 a")?.text() ?: return null + val posterUrl = fixUrl(this.select("div.product__item__pic.set-bg").attr("data-setbg")) + val episode = Regex("([0-9*])\\s?/").find( + this.select("div.ep span").text() + )?.groupValues?.getOrNull(1)?.toIntOrNull() + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(episode) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/anime?search=$query&order_by=oldest" + val document = app.get(link).document + + return document.select(".product__item").mapNotNull { + val title = it.selectFirst("div.product__item__text > h5")!!.text().trim() + val poster = it.selectFirst("a > div")!!.attr("data-setbg") + val tvType = + getType(it.selectFirst(".product__item__text > ul > li")!!.text().toString()) + val href = fixUrl(it.selectFirst("a")!!.attr("href")) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addDubStatus(dubExist = false, subExist = true) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst(".anime__details__title > h3")!!.text().trim() + val poster = document.selectFirst(".anime__details__pic")?.attr("data-setbg") + val tags = + document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(1)") + .text().trim().replace("Genre: ", "").split(", ") + + val year = Regex("[^0-9]").replace( + document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(5)") + .text().trim().replace("Musim: ", ""), "" + ).toIntOrNull() + val status = getStatus( + document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(3)") + .text().trim().replace("Status: ", "") + ) + val description = document.select(".anime__details__text > p").text().trim() + + val episodes = + Jsoup.parse(document.select("#episodeLists").attr("data-content")).select("a").map { + val name = it.text().trim() + val link = it.attr("href") + Episode(link, name) + } + + val recommendations = document.select("div#randomList > a").mapNotNull { + val epHref = it.attr("href") + val epTitle = it.select("h5.sidebar-title-h5.px-2.py-2").text() + val epPoster = it.select(".product__sidebar__view__item.set-bg").attr("data-setbg") + + newAnimeSearchResponse(epTitle, epHref, TvType.Anime) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + return newAnimeLoadResponse(title, url, TvType.Anime) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val servers = app.get(data).document + servers.select("video#player > source").map { + suspendSafeApiCall { + val url = it.attr("src") + val quality = it.attr("size").toInt() + callback.invoke( + ExtractorLink( + name, + name, + url, + referer = "$mainUrl/", + quality = quality + ) + ) + } + } + + return true + } + +} \ No newline at end of file diff --git a/KuramanimeProvider/src/main/kotlin/com/lagradost/KuramanimeProviderPlugin.kt b/KuramanimeProvider/src/main/kotlin/com/lagradost/KuramanimeProviderPlugin.kt new file mode 100644 index 0000000..9776cf6 --- /dev/null +++ b/KuramanimeProvider/src/main/kotlin/com/lagradost/KuramanimeProviderPlugin.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 KuramanimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(KuramanimeProvider()) + } +} \ No newline at end of file diff --git a/KuronimeProvider/build.gradle.kts b/KuronimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/KuronimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/KuronimeProvider/src/main/AndroidManifest.xml b/KuronimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/KuronimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/KuronimeProvider/src/main/kotlin/com/lagradost/KuronimeProvider.kt b/KuronimeProvider/src/main/kotlin/com/lagradost/KuronimeProvider.kt new file mode 100644 index 0000000..5f68ec6 --- /dev/null +++ b/KuronimeProvider/src/main/kotlin/com/lagradost/KuronimeProvider.kt @@ -0,0 +1,197 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +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.Element + +class KuronimeProvider : MainAPI() { + override var mainUrl = "https://45.12.2.2" + override var name = "Kuronime" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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 + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "New Episodes", + "$mainUrl/popular-anime/page/" to "Popular Anime", + "$mainUrl/movies/page/" to "Movies", + "$mainUrl/genres/donghua/page/" to "Donghua", + "$mainUrl/live-action/page/" to "Live Action", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("article").map { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> Regex("nonton-(.+)-episode").find( + title + )?.groupValues?.get(1).toString() + (title.contains("-movie")) -> Regex("nonton-(.+)-movie").find(title)?.groupValues?.get( + 1 + ).toString() + else -> title + } + + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString()) + val title = this.select(".bsuxtt, .tt > h4").text().trim() + val posterUrl = fixUrlNull( + this.selectFirst("div.view,div.bt")?.nextElementSibling()?.select("img") + ?.attr("data-src") + ) + val epNum = this.select(".ep").text().replace(Regex("[^0-9]"), "").trim().toIntOrNull() + val tvType = getType(this.selectFirst(".bt > span")?.text().toString()) + return newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select("article.bs").map { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst(".entry-title")?.text().toString().trim() + val poster = document.selectFirst("div.l[itemprop=image] > img")?.attr("data-src") + val tags = document.select(".infodetail > ul > li:nth-child(2) > a").map { it.text() } + val type = getType( + document.selectFirst(".infodetail > ul > li:nth-child(7)")?.ownText()?.trim().toString() + ) + val trailer = document.selectFirst("div.tply iframe")?.attr("data-lazy-src") + val year = Regex("\\d, ([0-9]*)").find( + document.select(".infodetail > ul > li:nth-child(5)").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.selectFirst(".infodetail > ul > li:nth-child(3)")!!.ownText() + .replace(Regex("\\W"), "") + ) + val description = document.select("span.const > p").text() + + val episodes = document.select("div.bixbox.bxcl > ul > li").map { + val name = it.selectFirst("a")?.text()?.trim() + val episode = + it.selectFirst("a")?.text()?.trim()?.replace("Episode", "")?.trim()?.toIntOrNull() + val link = it.selectFirst("a")!!.attr("href") + Episode(link, name = name, episode = episode) + }.reversed() + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + addTrailer(trailer) + this.tags = tags + } + } + + private suspend fun invokeKuroSource( + url: String, + sourceCallback: (ExtractorLink) -> Unit + ) { + val doc = app.get(url, referer = "${mainUrl}/").document + + doc.select("script").map { script -> + if (script.data().contains("function jalankan_jwp() {")) { + val data = script.data() + val doma = data.substringAfter("var doma = \"").substringBefore("\";") + val token = data.substringAfter("var token = \"").substringBefore("\";") + val pat = data.substringAfter("var pat = \"").substringBefore("\";") + val link = "$doma$token$pat/index.m3u8" + val quality = + Regex("\\d{3,4}p").find(doc.select("title").text())?.groupValues?.get(0) + + sourceCallback.invoke( + ExtractorLink( + this.name, + this.name, + link, + referer = "https://animeku.org/", + quality = getQualityFromName(quality), + headers = mapOf("Origin" to "https://animeku.org"), + isM3u8 = true + ) + ) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + val sources = document.select(".mobius > .mirror > option").mapNotNull { + fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("data-src")) + } + + sources.apmap { + safeApiCall { + when { + it.startsWith("https://animeku.org") -> invokeKuroSource(it, callback) + else -> loadExtractor(it, mainUrl, subtitleCallback, callback) + } + } + } + return true + } + +} \ No newline at end of file diff --git a/KuronimeProvider/src/main/kotlin/com/lagradost/KuronimeProviderPlugin.kt b/KuronimeProvider/src/main/kotlin/com/lagradost/KuronimeProviderPlugin.kt new file mode 100644 index 0000000..91aad99 --- /dev/null +++ b/KuronimeProvider/src/main/kotlin/com/lagradost/KuronimeProviderPlugin.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 KuronimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(KuronimeProvider()) + } +} \ No newline at end of file diff --git a/MonoschinosProvider/build.gradle.kts b/MonoschinosProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/MonoschinosProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/MonoschinosProvider/src/main/AndroidManifest.xml b/MonoschinosProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/MonoschinosProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/MonoschinosProvider/src/main/kotlin/com/lagradost/MonoschinosProvider.kt b/MonoschinosProvider/src/main/kotlin/com/lagradost/MonoschinosProvider.kt new file mode 100644 index 0000000..ba6f7ab --- /dev/null +++ b/MonoschinosProvider/src/main/kotlin/com/lagradost/MonoschinosProvider.kt @@ -0,0 +1,155 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.* + + +class MonoschinosProvider : MainAPI() { + companion object { + fun getType(t: String): TvType { + return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA + else if (t.contains("Pelicula")) TvType.AnimeMovie + else TvType.Anime + } + + fun getDubStatus(title: String): DubStatus { + return if (title.contains("Latino") || title.contains("Castellano")) + DubStatus.Dubbed + else DubStatus.Subbed + } + } + + override var mainUrl = "https://monoschinos2.com" + override var name = "Monoschinos" + override var lang = "es" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.OVA, + TvType.Anime, + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("$mainUrl/emision", "En emisión"), + Pair( + "$mainUrl/animes?categoria=pelicula&genero=false&fecha=false&letra=false", + "Peliculas" + ), + Pair("$mainUrl/animes", "Animes"), + ) + + val items = ArrayList() + + items.add( + HomePageList( + "Capítulos actualizados", + app.get(mainUrl, timeout = 120).document.select(".col-6").map { + val title = it.selectFirst("p.animetitles")?.text() ?: it.selectFirst(".animetitles")?.text() ?: "" + val poster = it.selectFirst(".animeimghv")!!.attr("data-src") + val epRegex = Regex("episodio-(\\d+)") + val url = it.selectFirst("a")?.attr("href")!!.replace("ver/", "anime/") + .replace(epRegex, "sub-espanol") + val epNum = (it.selectFirst(".positioning h5")?.text() ?: it.selectFirst("div.positioning p")?.text())?.toIntOrNull() + newAnimeSearchResponse(title, url) { + this.posterUrl = fixUrl(poster) + addDubStatus(getDubStatus(title), epNum) + } + }) + ) + + for (i in urls) { + try { + val home = app.get(i.first, timeout = 120).document.select(".col-6").map { + val title = it.selectFirst(".seristitles")!!.text() + val poster = it.selectFirst("img.animemainimg")!!.attr("src") + newAnimeSearchResponse(title, fixUrl(it.selectFirst("a")!!.attr("href"))) { + this.posterUrl = fixUrl(poster) + addDubStatus(getDubStatus(title)) + } + } + + items.add(HomePageList(i.second, home)) + } catch (e: Exception) { + e.printStackTrace() + } + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): ArrayList { + val search = + app.get("$mainUrl/buscar?q=$query", timeout = 120).document.select(".col-6").map { + val title = it.selectFirst(".seristitles")!!.text() + val href = fixUrl(it.selectFirst("a")!!.attr("href")) + val image = it.selectFirst("img.animemainimg")!!.attr("src") + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + fixUrl(image), + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + return ArrayList(search) + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url, timeout = 120).document + val poster = doc.selectFirst(".chapterpic img")!!.attr("src") + val title = doc.selectFirst(".chapterdetails h1")!!.text() + val type = doc.selectFirst("div.chapterdetls2")!!.text() + val description = doc.selectFirst("p.textComplete")!!.text().replace("Ver menos", "") + val genres = doc.select(".breadcrumb-item a").map { it.text() } + val status = when (doc.selectFirst("button.btn1")?.text()) { + "Estreno" -> ShowStatus.Ongoing + "Finalizado" -> ShowStatus.Completed + else -> null + } + val episodes = doc.select("div.col-item").map { + val name = it.selectFirst("p.animetitles")!!.text() + val link = it.selectFirst("a")!!.attr("href") + val epThumb = it.selectFirst(".animeimghv")!!.attr("data-src") + Episode(link, name, posterUrl = epThumb) + } + return newAnimeLoadResponse(title, url, getType(type)) { + posterUrl = poster + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + tags = genres + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("div.playother p").forEach { + val encodedurl = it.select("p").attr("data-player") + val urlDecoded = base64Decode(encodedurl) + val url = (urlDecoded).replace("https://monoschinos2.com/reproductor?url=", "") + if (url.startsWith("https://www.fembed.com")) { + val extractor = FEmbed() + extractor.getUrl(url).forEach { link -> + callback.invoke(link) + } + } else { + loadExtractor(url, mainUrl, subtitleCallback, callback) + } + } + return true + } +} \ No newline at end of file diff --git a/MonoschinosProvider/src/main/kotlin/com/lagradost/MonoschinosProviderPlugin.kt b/MonoschinosProvider/src/main/kotlin/com/lagradost/MonoschinosProviderPlugin.kt new file mode 100644 index 0000000..f3b0b35 --- /dev/null +++ b/MonoschinosProvider/src/main/kotlin/com/lagradost/MonoschinosProviderPlugin.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 MonoschinosProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(MonoschinosProvider()) + } +} \ No newline at end of file diff --git a/MonoschinosProvider/src/main/kotlin/com/lagradost/XStreamCdn.kt b/MonoschinosProvider/src/main/kotlin/com/lagradost/XStreamCdn.kt new file mode 100644 index 0000000..866aee0 --- /dev/null +++ b/MonoschinosProvider/src/main/kotlin/com/lagradost/XStreamCdn.kt @@ -0,0 +1,67 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName + +class FEmbed: XStreamCdn() { + override val name: String = "FEmbed" + override val mainUrl: String = "https://www.fembed.com" +} + +open class XStreamCdn : ExtractorApi() { + override val name: String = "XStreamCdn" + override val mainUrl: String = "https://embedsito.com" + override val requiresReferer = false + open var domainUrl: String = "embedsito.com" + + private data class ResponseData( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + //val type: String // Mp4 + ) + + private data class ResponseJson( + @JsonProperty("success") val success: Boolean, + @JsonProperty("data") val data: List? + ) + + override fun getExtractorUrl(id: String): String { + return "$domainUrl/api/source/$id" + } + + override suspend fun getUrl(url: String, referer: String?): List { + val headers = mapOf( + "Referer" to url, + "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0", + ) + val id = url.trimEnd('/').split("/").last() + val newUrl = "https://${domainUrl}/api/source/${id}" + val extractedLinksList: MutableList = mutableListOf() + with(app.post(newUrl, headers = headers)) { + if (this.code != 200) return listOf() + val text = this.text + if (text.isEmpty()) return listOf() + if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf() + AppUtils.parseJson(text)?.let { + if (it.success && it.data != null) { + it.data.forEach { data -> + extractedLinksList.add( + ExtractorLink( + name, + name = name, + data.file, + url, + getQualityFromName(data.label), + ) + ) + } + } + } + } + return extractedLinksList + } +} \ No newline at end of file diff --git a/MundoDonghuaProvider/build.gradle.kts b/MundoDonghuaProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/MundoDonghuaProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/MundoDonghuaProvider/src/main/AndroidManifest.xml b/MundoDonghuaProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/MundoDonghuaProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/MundoDonghuaProvider/src/main/kotlin/com/lagradost/MundoDonghuaProvider.kt b/MundoDonghuaProvider/src/main/kotlin/com/lagradost/MundoDonghuaProvider.kt new file mode 100644 index 0000000..8381b2f --- /dev/null +++ b/MundoDonghuaProvider/src/main/kotlin/com/lagradost/MundoDonghuaProvider.kt @@ -0,0 +1,217 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +import com.lagradost.cloudstream3.utils.getAndUnpack +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.* + + +class MundoDonghuaProvider : MainAPI() { + + override var mainUrl = "https://www.mundodonghua.com" + override var name = "MundoDonghua" + override var lang = "es" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Anime, + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("$mainUrl/lista-donghuas", "Donghuas"), + ) + + val items = ArrayList() + items.add( + HomePageList( + "Últimos episodios", + app.get(mainUrl, timeout = 120).document.select("div.row .col-xs-4").map { + val title = it.selectFirst("h5")?.text() ?: "" + val poster = it.selectFirst(".fit-1 img")?.attr("src") + val epRegex = Regex("(\\/(\\d+)\$)") + val url = it.selectFirst("a")?.attr("href")?.replace(epRegex,"")?.replace("/ver/","/donghua/") + val epnumRegex = Regex("((\\d+)$)") + val epNum = epnumRegex.find(title)?.value?.toIntOrNull() + val dubstat = if (title.contains("Latino") || title.contains("Castellano")) DubStatus.Dubbed else DubStatus.Subbed + newAnimeSearchResponse(title.replace(Regex("Episodio|(\\d+)"),"").trim(), fixUrl(url ?: "")) { + this.posterUrl = fixUrl(poster ?: "") + addDubStatus(dubstat, epNum) + } + }) + ) + + urls.apmap { (url, name) -> + val home = app.get(url, timeout = 120).document.select(".col-xs-4").map { + val title = it.selectFirst(".fs-14")?.text() ?: "" + val poster = it.selectFirst(".fit-1 img")?.attr("src") ?: "" + AnimeSearchResponse( + title, + fixUrl(it.selectFirst("a")?.attr("href") ?: ""), + this.name, + TvType.Anime, + fixUrl(poster), + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + + items.add(HomePageList(name, home)) + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/busquedas/$query", timeout = 120).document.select(".col-xs-4").map { + val title = it.selectFirst(".fs-14")?.text() ?: "" + val href = fixUrl(it.selectFirst("a")?.attr("href") ?: "") + val image = it.selectFirst(".fit-1 img")?.attr("src") + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + fixUrl(image ?: ""), + null, + if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url, timeout = 120).document + val poster = doc.selectFirst("head meta[property=og:image]")?.attr("content") ?: "" + val title = doc.selectFirst(".ls-title-serie")?.text() ?: "" + val description = doc.selectFirst("p.text-justify.fc-dark")?.text() ?: "" + val genres = doc.select("span.label.label-primary.f-bold").map { it.text() } + val status = when (doc.selectFirst("div.col-md-6.col-xs-6.align-center.bg-white.pt-10.pr-15.pb-0.pl-15 p span.badge.bg-default")?.text()) { + "En Emisión" -> ShowStatus.Ongoing + "Finalizada" -> ShowStatus.Completed + else -> null + } + val episodes = doc.select("ul.donghua-list a").map { + val name = it.selectFirst(".fs-16")?.text() + val link = it.attr("href") + Episode(fixUrl(link), name) + }.reversed() + val typeinfo = doc.select("div.row div.col-md-6.pl-15 p.fc-dark").text() + val tvType = if (typeinfo.contains(Regex("Tipo.*Pel.cula"))) TvType.AnimeMovie else TvType.Anime + return newAnimeLoadResponse(title, url, tvType) { + posterUrl = poster + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + tags = genres + } + } + data class Protea ( + @JsonProperty("source") val source: List, + @JsonProperty("poster") val poster: String? + ) + + data class Source ( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("default") val default: String? + ) + + private fun cleanStream( + name: String, + url: String, + qualityString: String?, + callback: (ExtractorLink) -> Unit, + isM3U8: Boolean + ): Boolean { + callback( + ExtractorLink( + name, + name, + url, + "", + getQualityFromName(qualityString), + isM3U8 + ) + ) + return true + } + + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("script").apmap { script -> + if (script.data().contains("eval(function(p,a,c,k,e")) { + val packedRegex = Regex("eval\\(function\\(p,a,c,k,e,.*\\)\\)") + packedRegex.findAll(script.data()).map { + it.value + }.toList().apmap { + val unpack = getAndUnpack(it).replace("diasfem","embedsito") + fetchUrls(unpack).apmap { url -> + loadExtractor(url, data, subtitleCallback, callback) + } + if (unpack.contains("protea_tab")) { + val protearegex = Regex("(protea_tab.*slug.*,type)") + val slug = protearegex.findAll(unpack).map { + it.value.replace(Regex("(protea_tab.*slug\":\")"),"").replace("\"},type","") + }.first() + val requestlink = "$mainUrl/api_donghua.php?slug=$slug" + val response = app.get(requestlink, headers = + mapOf("Host" to "www.mundodonghua.com", + "User-Agent" to USER_AGENT, + "Accept" to "*/*", + "Accept-Language" to "en-US,en;q=0.5", + "Referer" to data, + "X-Requested-With" to "XMLHttpRequest", + "DNT" to "1", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "no-cors", + "Sec-Fetch-Site" to "same-origin", + "TE" to "trailers", + "Pragma" to "no-cache", + "Cache-Control" to "no-cache",) + ).text.removePrefix("[").removeSuffix("]") + val json = parseJson(response) + json.source.forEach { source -> + val protename = "Protea" + cleanStream(protename, fixUrl(source.file), source.label, callback, false) + } + } + if (unpack.contains("asura_player")) { + val asuraRegex = Regex("(asura_player.*type)") + asuraRegex.findAll(unpack).map { + it.value + }.toList().apmap { protea -> + val asuraname = "Asura" + val file = protea.substringAfter("{file:\"").substringBefore("\"") + generateM3u8( + asuraname, + file, + "" + ).forEach { + cleanStream(asuraname, it.url, it.quality.toString(), callback, true) + } + } + } + } + } + } + return true + } +} \ No newline at end of file diff --git a/MundoDonghuaProvider/src/main/kotlin/com/lagradost/MundoDonghuaProviderPlugin.kt b/MundoDonghuaProvider/src/main/kotlin/com/lagradost/MundoDonghuaProviderPlugin.kt new file mode 100644 index 0000000..7b2574f --- /dev/null +++ b/MundoDonghuaProvider/src/main/kotlin/com/lagradost/MundoDonghuaProviderPlugin.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 MundoDonghuaProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(MundoDonghuaProvider()) + } +} \ No newline at end of file diff --git a/NeonimeProvider/build.gradle.kts b/NeonimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/NeonimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/NeonimeProvider/src/main/AndroidManifest.xml b/NeonimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/NeonimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProvider.kt b/NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProvider.kt new file mode 100644 index 0000000..b8c5683 --- /dev/null +++ b/NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProvider.kt @@ -0,0 +1,178 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element +import java.util.* + +class NeonimeProvider : MainAPI() { + override var mainUrl = "https://neonime.watch" + override var name = "Neonime" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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) { + "Ended" -> ShowStatus.Completed + "OnGoing" -> ShowStatus.Ongoing + "Ongoing" -> ShowStatus.Ongoing + "In Production" -> ShowStatus.Ongoing + "Returning Series" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/episode/page/" to "Episode Terbaru", + "$mainUrl/tvshows/page/" to "Anime Terbaru", + "$mainUrl/movies/page/" to "Movie", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("tbody tr,div.item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return when { + uri.contains("/episode") -> { + val title = uri.substringAfter("$mainUrl/episode/").let { tt -> + val fixTitle = Regex("(.*)-\\d{1,2}x\\d+").find(tt)?.groupValues?.getOrNull(1).toString() + when { + !tt.contains("-season") && !tt.contains(Regex("-1x\\d+")) && !tt.contains("one-piece") -> "$fixTitle-season-${Regex("-(\\d{1,2})x\\d+").find(tt)?.groupValues?.getOrNull(1).toString()}" + tt.contains("-special") -> fixTitle.replace(Regex("-x\\d+"), "") + !fixTitle.contains("-subtitle-indonesia") -> "$fixTitle-subtitle-indonesia" + else -> fixTitle + } + } + +// title = when { +// title.contains("youkoso-jitsuryoku") && !title.contains("-season") -> title.replace("-e-", "-e-tv-") +// else -> title +// } + + "$mainUrl/tvshows/$title" + } + else -> uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("td.bb a")?.ownText() ?: this.selectFirst("h2")?.text() ?: return null + val href = getProperAnimeLink(fixUrl(this.select("a").attr("href"))) + val posterUrl = fixUrl(this.select("img").attr("data-src")) + val epNum = this.selectFirst("td.bb span")?.text()?.let { eps -> + Regex("Episode\\s?([0-9]+)").find(eps)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select("div.item.episode-home").mapNotNull { + val title = it.selectFirst("div.judul-anime > span")!!.text() + val poster = it.select("img").attr("data-src").toString().trim() + val episodes = it.selectFirst("div.fixyear > h2.text-center")!! + .text().replace(Regex("[^0-9]"), "").trim().toIntOrNull() + val tvType = getType(it.selectFirst("span.calidad2.episode")?.text().toString()) + val href = getProperAnimeLink(fixUrl(it.selectFirst("a")!!.attr("href"))) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addSub(episodes) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + if (url.contains("movie") || url.contains("live-action")) { + val mTitle = document.selectFirst(".sbox > .data > h1[itemprop = name]")?.text().toString().trim() + val mTrailer = document.selectFirst("div.youtube_id iframe")?.attr("data-wpfc-original-src")?.substringAfterLast("html#")?.let{ "https://www.youtube.com/embed/$it"} + + return newMovieLoadResponse(name = mTitle, url = url, type = TvType.Movie, dataUrl = url) { + posterUrl = document.selectFirst(".sbox > .imagen > .fix > img[itemprop = image]")?.attr("data-src") + year = document.selectFirst("a[href*=release-year]")!!.text().toIntOrNull() + plot = document.select("div[itemprop = description]").text().trim() + rating = document.select("span[itemprop = ratingValue]").text().toIntOrNull() + tags = document.select("p.meta_dd > a").map { it.text() } + addTrailer(mTrailer) + } + } + else { + val title = document.select("h1[itemprop = name]").text().trim() + val trailer = document.selectFirst("div.youtube_id_tv iframe")?.attr("data-wpfc-original-src")?.substringAfterLast("html#")?.let{ "https://www.youtube.com/embed/$it"} + + val episodes = document.select("ul.episodios > li").mapNotNull { + val header = it.selectFirst(".episodiotitle > a")?.ownText().toString() + val name = Regex("(Episode\\s?[0-9]+)").find(header)?.groupValues?.getOrNull(0) ?: header + val link = fixUrl(it.selectFirst(".episodiotitle > a")!!.attr("href")) + Episode(link, name) + }.reversed() + + return newAnimeLoadResponse(title, url, TvType.Anime) { + engName = title + posterUrl = document.selectFirst(".imagen > img")?.attr("data-src") + year = document.select("#info a[href*=\"-year/\"]").text().toIntOrNull() + addEpisodes(DubStatus.Subbed, episodes) + showStatus = getStatus(document.select("div.metadatac > span").last()!!.text().trim()) + plot = document.select("div[itemprop = description] > p").text().trim() + tags = document.select("#info a[href*=\"-genre/\"]").map { it.text() } + addTrailer(trailer) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val source = if(data.contains("movie") || data.contains("live-action")) { + app.get(data).document.select("#player2-1 > div[id*=div]").mapNotNull { + fixUrl(it.select("iframe").attr("data-src")) + } + } else { + app.get(data).document.select(".player2 > .embed2 > div[id*=player]").mapNotNull { + fixUrl(it.select("iframe").attr("data-src")) + } + } + + source.apmap { + loadExtractor(it, data, subtitleCallback, callback) + } + + return true + } + +} \ No newline at end of file diff --git a/NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProviderPlugin.kt b/NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProviderPlugin.kt new file mode 100644 index 0000000..e99ad13 --- /dev/null +++ b/NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProviderPlugin.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 NeonimeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(NeonimeProvider()) + } +} \ No newline at end of file diff --git a/NineAnimeProvider/build.gradle.kts b/NineAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/NineAnimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/NineAnimeProvider/src/main/AndroidManifest.xml b/NineAnimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/NineAnimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/NineAnimeProvider/src/main/kotlin/com/lagradost/NineAnimeProvider.kt b/NineAnimeProvider/src/main/kotlin/com/lagradost/NineAnimeProvider.kt new file mode 100644 index 0000000..b8dc644 --- /dev/null +++ b/NineAnimeProvider/src/main/kotlin/com/lagradost/NineAnimeProvider.kt @@ -0,0 +1,357 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +class NineAnimeProvider : MainAPI() { + override var mainUrl = "https://9anime.id" + override var name = "9Anime" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf(TvType.Anime) + override val hasQuickSearch = true + + // taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/NineAnime.kt + // GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md + companion object { + private const val nineAnimeKey = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + private const val cipherKey = "kMXzgyNzT3k5dYab" + + fun encodeVrf(text: String, mainKey: String): String { + return encode( + encrypt( + cipher(mainKey, encode(text)), + nineAnimeKey + )//.replace("""=+$""".toRegex(), "") + ) + } + + fun decodeVrf(text: String, mainKey: String): String { + return decode(cipher(mainKey, decrypt(text, nineAnimeKey))) + } + + fun encrypt(input: String, key: String): String { + if (input.any { it.code > 255 }) throw Exception("illegal characters!") + var output = "" + for (i in input.indices step 3) { + val a = intArrayOf(-1, -1, -1, -1) + a[0] = input[i].code shr 2 + a[1] = (3 and input[i].code) shl 4 + if (input.length > i + 1) { + a[1] = a[1] or (input[i + 1].code shr 4) + a[2] = (15 and input[i + 1].code) shl 2 + } + if (input.length > i + 2) { + a[2] = a[2] or (input[i + 2].code shr 6) + a[3] = 63 and input[i + 2].code + } + for (n in a) { + if (n == -1) output += "=" + else { + if (n in 0..63) output += key[n] + } + } + } + return output + } + + fun cipher(key: String, text: String): String { + val arr = IntArray(256) { it } + + var u = 0 + var r: Int + arr.indices.forEach { + u = (u + arr[it] + key[it % key.length].code) % 256 + r = arr[it] + arr[it] = arr[u] + arr[u] = r + } + u = 0 + var c = 0 + + return text.indices.map { j -> + c = (c + 1) % 256 + u = (u + arr[c]) % 256 + r = arr[c] + arr[c] = arr[u] + arr[u] = r + (text[j].code xor arr[(arr[c] + arr[u]) % 256]).toChar() + }.joinToString("") + } + + @Suppress("SameParameterValue") + private fun decrypt(input: String, key: String): String { + val t = if (input.replace("""[\t\n\f\r]""".toRegex(), "").length % 4 == 0) { + input.replace("""==?$""".toRegex(), "") + } else input + if (t.length % 4 == 1 || t.contains("""[^+/0-9A-Za-z]""".toRegex())) throw Exception("bad input") + var i: Int + var r = "" + var e = 0 + var u = 0 + for (o in t.indices) { + e = e shl 6 + i = key.indexOf(t[o]) + e = e or i + u += 6 + if (24 == u) { + r += ((16711680 and e) shr 16).toChar() + r += ((65280 and e) shr 8).toChar() + r += (255 and e).toChar() + e = 0 + u = 0 + } + } + return if (12 == u) { + e = e shr 4 + r + e.toChar() + } else { + if (18 == u) { + e = e shr 2 + r += ((65280 and e) shr 8).toChar() + r += (255 and e).toChar() + } + r + } + } + + fun encode(input: String): String = + java.net.URLEncoder.encode(input, "utf-8").replace("+", "%20") + + private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8") + } + + override val mainPage = mainPageOf( + "$mainUrl/ajax/home/widget/trending?page=" to "Trending", + "$mainUrl/ajax/home/widget/updated-all?page=" to "All", + "$mainUrl/ajax/home/widget/updated-sub?page=" to "Recently Updated (SUB)", + "$mainUrl/ajax/home/widget/updated-dub?page=" to "Recently Updated (DUB)", + "$mainUrl/ajax/home/widget/updated-china?page=" to "Recently Updated (Chinese)", + "$mainUrl/ajax/home/widget/random?page=" to "Random", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val url = request.data + page + val home = Jsoup.parse( + app.get( + url + ).parsed().html + ).select("div.item").mapNotNull { element -> + val title = element.selectFirst(".info > .name") ?: return@mapNotNull null + val link = title.attr("href") + val poster = element.selectFirst(".poster > a > img")?.attr("src") + val meta = element.selectFirst(".poster > a > .meta > .inner > .left") + val subbedEpisodes = meta?.selectFirst(".sub")?.text()?.toIntOrNull() + val dubbedEpisodes = meta?.selectFirst(".dub")?.text()?.toIntOrNull() + + newAnimeSearchResponse(title.text() ?: return@mapNotNull null, link) { + this.posterUrl = poster + addDubStatus( + dubbedEpisodes != null, + subbedEpisodes != null, + dubbedEpisodes, + subbedEpisodes + ) + } + } + + return newHomePageResponse(request.name, home) + } + + data class Response( + @JsonProperty("result") val html: String + ) + + data class QuickSearchResponse( + //@JsonProperty("status") val status: Int? = null, + @JsonProperty("result") val result: QuickSearchResult? = null, + //@JsonProperty("message") val message: String? = null, + //@JsonProperty("messages") val messages: ArrayList = arrayListOf() + ) + + data class QuickSearchResult( + @JsonProperty("html") val html: String? = null, + //@JsonProperty("linkMore") val linkMore: String? = null + ) + + override suspend fun quickSearch(query: String): List? { + val vrf = encodeVrf(query, cipherKey) + val url = + "$mainUrl/ajax/anime/search?keyword=$query&vrf=$vrf" + val response = app.get(url).parsedSafe() + val document = Jsoup.parse(response?.result?.html ?: return null) + return document.select(".items > a").mapNotNull { element -> + val link = fixUrl(element?.attr("href") ?: return@mapNotNull null) + val title = element.selectFirst(".info > .name")?.text() ?: return@mapNotNull null + newAnimeSearchResponse(title, link) { + posterUrl = element.selectFirst(".poster > span > img")?.attr("src") + } + } + } + + override suspend fun search(query: String): List { + val vrf = encodeVrf(query, cipherKey) + //?language%5B%5D=${if (selectDub) "dubbed" else "subbed"}& + val url = + "$mainUrl/filter?keyword=${encode(query)}&vrf=${vrf}&page=1" + return app.get(url).document.select("#list-items div.ani.poster.tip > a").mapNotNull { + val link = fixUrl(it.attr("href") ?: return@mapNotNull null) + val img = it.select("img") + val title = img.attr("alt") + newAnimeSearchResponse(title, link) { + posterUrl = img.attr("src") + } + } + } + + override suspend fun load(url: String): LoadResponse { + val validUrl = url.replace("https://9anime.to", mainUrl) + val doc = app.get(validUrl).document + + val meta = doc.selectFirst("#w-info") ?: throw ErrorLoadingException("Could not find info") + val ratingElement = meta.selectFirst(".brating > #w-rating") + val id = ratingElement?.attr("data-id") ?: throw ErrorLoadingException("Could not find id") + val binfo = + meta.selectFirst(".binfo") ?: throw ErrorLoadingException("Could not find binfo") + val info = binfo.selectFirst(".info") ?: throw ErrorLoadingException("Could not find info") + + val title = (info.selectFirst(".title") ?: info.selectFirst(".d-title"))?.text() + ?: throw ErrorLoadingException("Could not find title") + + val vrf = encodeVrf(id, cipherKey) + val episodeListUrl = "$mainUrl/ajax/episode/list/$id?vrf=$vrf" + val body = + app.get(episodeListUrl).parsedSafe()?.html + ?: throw ErrorLoadingException("Could not parse json with cipherKey=$cipherKey id=$id url=\n$episodeListUrl") + + val subEpisodes = ArrayList() + val dubEpisodes = ArrayList() + + //TODO RECOMMENDATIONS + + Jsoup.parse(body).body().select(".episodes > ul > li > a").mapNotNull { element -> + val ids = element.attr("data-ids").split(",", limit = 2) + + val epNum = element.attr("data-num") + .toIntOrNull() // might fuck up on 7.5 ect might use data-slug instead + val epTitle = element.selectFirst("span.d-title")?.text() + //val filler = element.hasClass("filler") + ids.getOrNull(1)?.let { dub -> + dubEpisodes.add( + Episode( + "$mainUrl/ajax/server/list/$dub?vrf=${encodeVrf(dub, cipherKey)}", + epTitle, + episode = epNum + ) + ) + } + ids.getOrNull(0)?.let { sub -> + subEpisodes.add( + Episode( + "$mainUrl/ajax/server/list/$sub?vrf=${encodeVrf(sub, cipherKey)}", + epTitle, + episode = epNum + ) + ) + } + } + + return newAnimeLoadResponse(title, url, TvType.Anime) { + addEpisodes(DubStatus.Dubbed, dubEpisodes) + addEpisodes(DubStatus.Subbed, subEpisodes) + + plot = info.selectFirst(".synopsis > .shorting > .content")?.text() + posterUrl = binfo.selectFirst(".poster > span > img")?.attr("src") + rating = ratingElement.attr("data-score").toFloat().times(1000f).toInt() + + info.select(".bmeta > .meta > div").forEach { element -> + when (element.ownText()) { + "Genre: " -> { + tags = element.select("span > a").mapNotNull { it?.text() } + } + "Duration: " -> { + duration = getDurationFromString(element.selectFirst("span")?.text()) + } + "Type: " -> { + type = when (element.selectFirst("span > a")?.text()) { + "ONA" -> TvType.OVA + else -> { + type + } + } + } + "Status: " -> { + showStatus = when (element.selectFirst("span")?.text()) { + "Releasing" -> ShowStatus.Ongoing + "Completed" -> ShowStatus.Completed + else -> { + showStatus + } + } + } + else -> {} + } + } + } + } + + data class Result( + @JsonProperty("url") + val url: String? = null + ) + + data class Links( + @JsonProperty("result") + val result: Result? = null + ) + + //TODO 9anime outro into {"status":200,"result":{"url":"","skip_data":{"intro_begin":67,"intro_end":154,"outro_begin":1337,"outro_end":1415,"count":3}},"message":"","messages":[]} + private suspend fun getEpisodeLinks(id: String): Links? { + return app.get("$mainUrl/ajax/server/$id?vrf=${encodeVrf(id, cipherKey)}").parsedSafe() + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val body = app.get(data).parsed().html + val document = Jsoup.parse(body) + + document.select("li").apmap { + try { + val name = it.text() + val encodedStreamUrl = + getEpisodeLinks(it.attr("data-link-id"))?.result?.url ?: return@apmap + val url = decodeVrf(encodedStreamUrl, cipherKey) + if (!loadExtractor(url, mainUrl, subtitleCallback, callback)) { + callback( + ExtractorLink( + this.name, + name, + url, + mainUrl, + Qualities.Unknown.value, + url.contains(".m3u8") + ) + ) + } + } catch (e: Exception) { + logError(e) + } + } + + return true + } +} diff --git a/NineAnimeProvider/src/main/kotlin/com/lagradost/NineAnimeProviderPlugin.kt b/NineAnimeProvider/src/main/kotlin/com/lagradost/NineAnimeProviderPlugin.kt new file mode 100644 index 0000000..3a21e3e --- /dev/null +++ b/NineAnimeProvider/src/main/kotlin/com/lagradost/NineAnimeProviderPlugin.kt @@ -0,0 +1,27 @@ +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class NineAnimeProviderPlugin : Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(NineAnimeProvider()) + registerMainAPI(WcoProvider()) + registerExtractorAPI(Mcloud()) + registerExtractorAPI(Vidstreamz()) + registerExtractorAPI(Vizcloud()) + registerExtractorAPI(Vizcloud2()) + registerExtractorAPI(VizcloudOnline()) + registerExtractorAPI(VizcloudXyz()) + registerExtractorAPI(VizcloudLive()) + registerExtractorAPI(VizcloudInfo()) + registerExtractorAPI(MwvnVizcloudInfo()) + registerExtractorAPI(VizcloudDigital()) + registerExtractorAPI(VizcloudCloud()) + registerExtractorAPI(VizcloudSite()) + registerExtractorAPI(WcoStream()) + } +} \ No newline at end of file diff --git a/NineAnimeProvider/src/main/kotlin/com/lagradost/WcoProvider.kt b/NineAnimeProvider/src/main/kotlin/com/lagradost/WcoProvider.kt new file mode 100644 index 0000000..9aeafb0 --- /dev/null +++ b/NineAnimeProvider/src/main/kotlin/com/lagradost/WcoProvider.kt @@ -0,0 +1,238 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import org.json.JSONObject +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import java.util.* + + +class WcoProvider : 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 + } + } + + override var mainUrl = "https://wcostream.cc" + override var name = "WCO Stream" + override val hasQuickSearch = true + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.Anime, + TvType.OVA + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("$mainUrl/ajax/list/recently_updated?type=tv", "Recently Updated Anime"), + Pair("$mainUrl/ajax/list/recently_updated?type=movie", "Recently Updated Movies"), + Pair("$mainUrl/ajax/list/recently_added?type=tv", "Recently Added Anime"), + Pair("$mainUrl/ajax/list/recently_added?type=movie", "Recently Added Movies"), + ) + + val items = ArrayList() + for (i in urls) { + try { + val response = JSONObject( + app.get( + i.first, + ).text + ).getString("html") // I won't make a dataclass for this shit + val document = Jsoup.parse(response) + val results = document.select("div.flw-item").map { + val filmPoster = it.selectFirst("> div.film-poster") + val filmDetail = it.selectFirst("> div.film-detail") + val nameHeader = filmDetail!!.selectFirst("> h3.film-name > a") + val title = nameHeader!!.text().replace(" (Dub)", "") + val href = + nameHeader.attr("href").replace("/watch/", "/anime/") + .replace(Regex("-episode-.*"), "/") + val isDub = + filmPoster!!.selectFirst("> div.film-poster-quality")?.text() + ?.contains("DUB") + ?: false + val poster = filmPoster.selectFirst("> img")!!.attr("data-src") + val set: EnumSet = + EnumSet.of(if (isDub) DubStatus.Dubbed else DubStatus.Subbed) + AnimeSearchResponse(title, href, this.name, TvType.Anime, poster, null, set) + } + items.add(HomePageList(i.second, results)) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + + private fun fixAnimeLink(url: String): String { + val regex = "watch/([a-zA-Z\\-0-9]*)-episode".toRegex() + val (aniId) = regex.find(url)!!.destructured + return "$mainUrl/anime/$aniId" + } + + private fun parseSearchPage(soup: Document): List { + val items = soup.select(".film_list-wrap > .flw-item") + if (items.isEmpty()) return ArrayList() + return items.map { i -> + val href = fixAnimeLink(i.selectFirst("a")!!.attr("href")) + val img = fixUrl(i.selectFirst("img")!!.attr("data-src")) + val title = i.selectFirst("img")!!.attr("title") + val isDub = !i.select(".pick.film-poster-quality").isEmpty() + val year = + i.selectFirst(".film-detail.film-detail-fix > div > span:nth-child(1)")!!.text() + .toIntOrNull() + val type = + i.selectFirst(".film-detail.film-detail-fix > div > span:nth-child(3)")!!.text() + + if (getType(type) == TvType.AnimeMovie) { + MovieSearchResponse( + title, href, this.name, TvType.AnimeMovie, img, year + ) + } else { + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + img, + year, + EnumSet.of(if (isDub) DubStatus.Dubbed else DubStatus.Subbed), + ) + } + } + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/search" + val response = + app.get(url, params = mapOf("keyword" to query)) + var document = Jsoup.parse(response.text) + val returnValue = parseSearchPage(document).toMutableList() + + while (!document.select(".pagination").isEmpty()) { + val link = document.select("a.page-link[rel=\"next\"]") + if (!link.isEmpty() && returnValue.size < 40) { + val extraResponse = app.get(fixUrl(link[0].attr("href"))).text + document = Jsoup.parse(extraResponse) + returnValue.addAll(parseSearchPage(document)) + } else { + break + } + } + return returnValue.distinctBy { it.url } + } + + override suspend fun quickSearch(query: String): List { + val response = JSONObject( + app.post( + "https://wcostream.cc/ajax/search", + data = mapOf("keyword" to query) + ).text + ).getString("html") // I won't make a dataclass for this shit + val document = Jsoup.parse(response) + + return document.select("a.nav-item").mapNotNull { + val title = it.selectFirst("img")?.attr("title") ?: return@mapNotNull null + val img = it?.selectFirst("img")?.attr("src") ?: return@mapNotNull null + val href = it?.attr("href") ?: return@mapNotNull null + val isDub = title.contains("(Dub)") + val filmInfo = it.selectFirst(".film-infor") + val year = filmInfo?.select("span")?.get(0)?.text()?.toIntOrNull() + val type = filmInfo?.select("span")?.get(1)?.text().toString() + if (getType(type) == TvType.AnimeMovie) { + MovieSearchResponse( + title, href, this.name, TvType.AnimeMovie, img, year + ) + } else { + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + img, + year, + EnumSet.of(if (isDub) DubStatus.Dubbed else DubStatus.Subbed), + ) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val response = app.get(url, timeout = 120).text + val document = Jsoup.parse(response) + + val japaneseTitle = + document.selectFirst("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(1)") + ?.text()?.trim()?.replace("Other names:", "")?.trim() + + val canonicalTitle = document.selectFirst("meta[name=\"title\"]") + ?.attr("content")?.split("| W")?.get(0).toString() + + val isDubbed = canonicalTitle.contains("Dub") + val episodeNodes = document.select(".tab-content .nav-item > a") + + val episodes = ArrayList(episodeNodes?.map { + Episode(it.attr("href")) + } ?: ArrayList()) + + val statusElem = + document.selectFirst("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(2)") + val status = when (statusElem?.text()?.replace("Status:", "")?.trim()) { + "Ongoing" -> ShowStatus.Ongoing + "Completed" -> ShowStatus.Completed + else -> null + } + val yearText = + document.selectFirst("div.elements div.row > div:nth-child(2) > div.row-line:nth-child(4)") + ?.text() + val year = yearText?.replace("Date release:", "")?.trim()?.split("-")?.get(0)?.toIntOrNull() + + val poster = document.selectFirst(".film-poster-img")?.attr("src") + val type = document.selectFirst("span.item.mr-1 > a")?.text()?.trim() + + val synopsis = document.selectFirst(".description > p")?.text()?.trim() + val genre = + document.select("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(5) > a") + .map { it?.text()?.trim().toString() } + + return newAnimeLoadResponse(canonicalTitle, url, getType(type ?: "")) { + japName = japaneseTitle + engName = canonicalTitle + posterUrl = poster + this.year = year + addEpisodes(if (isDubbed) DubStatus.Dubbed else DubStatus.Subbed, episodes) + showStatus = status + plot = synopsis + tags = genre + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val response = app.get(data).text + val servers = Jsoup.parse(response).select("#servers-list > ul > li").map { + mapOf( + "link" to it?.selectFirst("a")?.attr("data-embed"), + "title" to it?.selectFirst("span")?.text()?.trim() + ) + } + + for (server in servers) { + WcoStream().getSafeUrl(server["link"].toString(), null, subtitleCallback, callback) + Mcloud().getSafeUrl(server["link"].toString(), null, subtitleCallback, callback) + } + return true + } +} diff --git a/NineAnimeProvider/src/main/kotlin/com/lagradost/WcoStream.kt b/NineAnimeProvider/src/main/kotlin/com/lagradost/WcoStream.kt new file mode 100644 index 0000000..c3cdfbf --- /dev/null +++ b/NineAnimeProvider/src/main/kotlin/com/lagradost/WcoStream.kt @@ -0,0 +1,133 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.NineAnimeProvider.Companion.cipher +import com.lagradost.NineAnimeProvider.Companion.encrypt +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +class Vidstreamz : WcoStream() { + override var mainUrl = "https://vidstreamz.online" +} + +open class Mcloud : WcoStream() { + override var name = "Mcloud" + override var mainUrl = "https://mcloud.to" + override val requiresReferer = true +} + +class Vizcloud : WcoStream() { + override var mainUrl = "https://vizcloud2.ru" +} + +class Vizcloud2 : WcoStream() { + override var mainUrl = "https://vizcloud2.online" +} + +class VizcloudOnline : WcoStream() { + override var mainUrl = "https://vizcloud.online" +} + +class VizcloudXyz : WcoStream() { + override var mainUrl = "https://vizcloud.xyz" +} + +class VizcloudLive : WcoStream() { + override var mainUrl = "https://vizcloud.live" +} + +class VizcloudInfo : WcoStream() { + override var mainUrl = "https://vizcloud.info" +} + +class MwvnVizcloudInfo : WcoStream() { + override var mainUrl = "https://mwvn.vizcloud.info" +} + +class VizcloudDigital : WcoStream() { + override var mainUrl = "https://vizcloud.digital" +} + +class VizcloudCloud : WcoStream() { + override var mainUrl = "https://vizcloud.cloud" +} + +class VizcloudSite : WcoStream() { + override var mainUrl = "https://vizcloud.site" +} + +open class WcoStream : ExtractorApi() { + override var name = "VidStream" // Cause works for animekisa and wco + override var mainUrl = "https://vidstream.pro" + override val requiresReferer = false + private val regex = Regex("(.+?/)e(?:mbed)?/([a-zA-Z0-9]+)") + + companion object { + // taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/extractors/VizCloud.kt + // GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md + private var lastChecked = 0L + private const val jsonLink = + "https://raw.githubusercontent.com/chenkaslowankiya/BruhFlow/main/keys.json" + private var cipherKey: VizCloudKey? = null + suspend fun getKey(): VizCloudKey { + cipherKey = + if (cipherKey != null && (lastChecked - System.currentTimeMillis()) < 1000 * 60 * 30) cipherKey!! + else { + lastChecked = System.currentTimeMillis() + app.get(jsonLink).parsed() + } + return cipherKey!! + } + + data class VizCloudKey( + @JsonProperty("cipherKey") val cipherKey: String, + @JsonProperty("mainKey") val mainKey: String, + @JsonProperty("encryptKey") val encryptKey: String, + @JsonProperty("dashTable") val dashTable: String + ) + + private const val baseTable = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=/_" + + private fun dashify(id: String, dashTable: String): String { + val table = dashTable.split(" ") + return id.mapIndexedNotNull { i, c -> + table.getOrNull((baseTable.indexOf(c) * 16) + (i % 16)) + }.joinToString("-") + } + } + + //private val key = "LCbu3iYC7ln24K7P" // key credits @Modder4869 + override suspend fun getUrl(url: String, referer: String?): List { + val group = regex.find(url)?.groupValues!! + + val host = group[1] + val viz = getKey() + val id = encrypt( + cipher( + viz.cipherKey, + encrypt(group[2], viz.encryptKey).also { println(it) } + ).also { println(it) }, + viz.encryptKey + ).also { println(it) } + + val link = + "${host}mediainfo/${dashify(id, viz.dashTable)}?key=${viz.mainKey}" // + val response = app.get(link, referer = referer) + + data class Sources(@JsonProperty("file") val file: String) + data class Media(@JsonProperty("sources") val sources: List) + data class Data(@JsonProperty("media") val media: Media) + data class Response(@JsonProperty("data") val data: Data) + + + if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server") + return response.parsed().data.media.sources.map { + ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8")) + } + + } +} diff --git a/NontonAnimeIDProvider/build.gradle.kts b/NontonAnimeIDProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/NontonAnimeIDProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/NontonAnimeIDProvider/src/main/AndroidManifest.xml b/NontonAnimeIDProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/NontonAnimeIDProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/NontonAnimeIDProvider/src/main/kotlin/com/lagradost/NontonAnimeIDProvider.kt b/NontonAnimeIDProvider/src/main/kotlin/com/lagradost/NontonAnimeIDProvider.kt new file mode 100644 index 0000000..bc75ac2 --- /dev/null +++ b/NontonAnimeIDProvider/src/main/kotlin/com/lagradost/NontonAnimeIDProvider.kt @@ -0,0 +1,257 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class NontonAnimeIDProvider : MainAPI() { + override var mainUrl = "https://75.119.159.228" + override var name = "NontonAnimeID" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + 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") -> TvType.Anime + t.contains("Movie") -> 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 suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val document = app.get(mainUrl).document + + val homePageList = ArrayList() + + document.select("section#postbaru").forEach { block -> + val header = block.selectFirst("h2")!!.text().trim() + val animes = block.select("article.animeseries").map { + it.toSearchResult() + } + if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes)) + } + + document.select("aside#sidebar_right > div:nth-child(4)").forEach { block -> + val header = block.selectFirst("h3")!!.ownText().trim() + val animes = block.select("li.fullwdth").map { + it.toSearchResultPopular() + } + if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes)) + } + + return HomePageResponse(homePageList) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + val fixTitle = Regex("(.*)-episode.*").find(title)?.groupValues?.getOrNull(1).toString() + title = when { + title.contains("utawarerumono-season-3") -> fixTitle.replace( + "season-3", + "futari-no-hakuoro" + ) + title.contains("kingdom-season-4") -> fixTitle.replace("season-4", "4th-season") + title.contains("maou-sama-season-2") -> fixTitle.replace("season-2", "2") + title.contains("overlord-season-4") -> fixTitle.replace("season-4", "iv") + title.contains("kyoushitsu-e-season-2") -> fixTitle.replace( + "kyoushitsu-e-season-2", + "kyoushitsu-e-tv-2nd-season" + ) + title.contains("season-2") -> fixTitle.replace("season-2", "2nd-season") + title.contains("season-3") -> fixTitle.replace("season-3", "3rd-season") + title.contains("movie") -> title.substringBefore("-movie") + else -> fixTitle + } + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrl(this.selectFirst("a")!!.attr("href"))) + val title = this.selectFirst("h3.title")!!.text() + val posterUrl = fixUrl(this.select("img").attr("data-src")) + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addDubStatus(dubExist = false, subExist = true) + } + + } + + private fun Element.toSearchResultPopular(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrl(this.selectFirst("a")!!.attr("href"))) + val title = this.select("h4").text().trim() + val posterUrl = fixUrl(this.select("img").attr("data-src")) + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addDubStatus(dubExist = false, subExist = true) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select(".result > ul > li").mapNotNull { + val title = it.selectFirst("h2")!!.text().trim() + val poster = it.selectFirst("img")!!.attr("src") + val tvType = getType( + it.selectFirst(".boxinfores > span.typeseries")!!.text().toString() + ) + val href = fixUrl(it.selectFirst("a")!!.attr("href")) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addDubStatus(dubExist = false, subExist = true) + } + } + } + + private data class EpResponse( + @JsonProperty("posts") val posts: String?, + @JsonProperty("max_page") val max_page: Int?, + @JsonProperty("found_posts") val found_posts: Int?, + @JsonProperty("content") val content: String + ) + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h1.entry-title.cs")!!.text().trim() + val poster = document.selectFirst(".poster > img")?.attr("data-src") + val tags = document.select(".tagline > a").map { it.text() } + + val year = Regex("\\d, ([0-9]*)").find( + document.select(".bottomtitle > span:nth-child(5)").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.select("span.statusseries").text().trim() + ) + val type = getType(document.select("span.typeseries").text().trim()) + val rating = document.select("span.nilaiseries").text().trim().toIntOrNull() + val description = document.select(".entry-content.seriesdesc > p").text().trim() + val trailer = document.selectFirst("iframe#traileryt")?.attr("data-src") + + val episodes = if (document.select("button.buttfilter").isNotEmpty()) { + val id = document.select("input[name=series_id]").attr("value") + val numEp = + document.selectFirst(".latestepisode > a")?.text()?.replace(Regex("[^0-9]"), "") + .toString() + Jsoup.parse( + app.post( + url = "$mainUrl/wp-admin/admin-ajax.php", + data = mapOf( + "misha_number_of_results" to numEp, + "misha_order_by" to "date-DESC", + "action" to "mishafilter", + "series_id" to id + ) + ).parsed().content + ).select("li").map { + val name = Regex("(Episode\\s?[0-9]+)").find( + it.selectFirst("a")?.text().toString() + )?.groupValues?.getOrNull(0) ?: it.selectFirst("a")?.text() + val link = fixUrl(it.selectFirst("a")!!.attr("href")) + Episode(link, name) + }.reversed() + } else { + document.select("ul.misha_posts_wrap2 > li").map { + val name = Regex("(Episode\\s?[0-9]+)").find( + it.selectFirst("a")?.text().toString() + )?.groupValues?.getOrNull(0) ?: it.selectFirst("a")?.text() + val link = it.select("a").attr("href") + Episode(link, name) + }.reversed() + } + + + val recommendations = document.select(".result > li").mapNotNull { + val epHref = it.selectFirst("a")!!.attr("href") + val epTitle = it.selectFirst("h3")!!.text() + val epPoster = it.select(".top > img").attr("data-src") + + newAnimeSearchResponse(epTitle, epHref, TvType.Anime) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + this.rating = rating + plot = description + addTrailer(trailer) + this.tags = tags + this.recommendations = recommendations + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val sources = ArrayList() + + document.select(".container1 > ul > li:not(.boxtab)").apmap { + val dataPost = it.attr("data-post") + val dataNume = it.attr("data-nume") + val dataType = it.attr("data-type") + + val iframe = app.post( + url = "$mainUrl/wp-admin/admin-ajax.php", + data = mapOf( + "action" to "player_ajax", + "post" to dataPost, + "nume" to dataNume, + "type" to dataType + ), + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document.select("iframe").attr("src") + + sources.add(fixUrl(iframe)) + } + + sources.apmap { + loadExtractor(it, "$mainUrl/", subtitleCallback, callback) + } + + return true + } +} diff --git a/NontonAnimeIDProvider/src/main/kotlin/com/lagradost/NontonAnimeIDProviderPlugin.kt b/NontonAnimeIDProvider/src/main/kotlin/com/lagradost/NontonAnimeIDProviderPlugin.kt new file mode 100644 index 0000000..996603b --- /dev/null +++ b/NontonAnimeIDProvider/src/main/kotlin/com/lagradost/NontonAnimeIDProviderPlugin.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 NontonAnimeIDProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(NontonAnimeIDProvider()) + } +} \ No newline at end of file diff --git a/OploverzProvider/build.gradle.kts b/OploverzProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/OploverzProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/OploverzProvider/src/main/AndroidManifest.xml b/OploverzProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/OploverzProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/OploverzProvider/src/main/kotlin/com/lagradost/OploverzProvider.kt b/OploverzProvider/src/main/kotlin/com/lagradost/OploverzProvider.kt new file mode 100644 index 0000000..d739d09 --- /dev/null +++ b/OploverzProvider/src/main/kotlin/com/lagradost/OploverzProvider.kt @@ -0,0 +1,203 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.* +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.util.ArrayList + + +class OploverzProvider : MainAPI() { + override var mainUrl = "https://65.108.132.145" + override var name = "Oploverz" + override val hasMainPage = true + override var lang = "id" + 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") -> TvType.Anime + t.contains("Movie") -> TvType.AnimeMovie + else -> TvType.OVA + } + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "&status=&type=&order=update" to "Episode Terbaru", + "&status=&type=&order=latest" to "Anime Terbaru", + "&sub=&order=popular" to "Popular Anime", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get("$mainUrl/anime/?page=$page${request.data}").document + val home = document.select("article[itemscope=itemscope]").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-ova")) -> Regex("(.+)-episode").find( + title + )?.groupValues?.get(1).toString() + (title.contains("-ova")) -> Regex("(.+)-ova").find(title)?.groupValues?.get(1) + .toString() + (title.contains("-movie")) -> Regex("(.+)-subtitle").find(title)?.groupValues?.get(1) + .toString() + else -> Regex("(.+)-subtitle").find(title)?.groupValues?.get(1).toString() + .replace(Regex("-\\d+"), "") + } + + when { + title.contains("overlord") -> { + title = title.replace("s", "season-") + } + title.contains("kaguya-sama") -> { + title = title.replace("s3", "ultra-romantic") + } + } + + "$mainUrl/anime/$title" + } + + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = getProperAnimeLink(this.selectFirst("a.tip")!!.attr("href")) + val title = this.selectFirst("h2[itemprop=headline]")?.text()?.trim() ?: return null + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val type = getType(this.selectFirst(".eggtype, .typez")?.text()?.trim().toString()) + + return newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + } + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select("article[itemscope=itemscope]").map { + val title = it.selectFirst(".tt")?.ownText()?.trim().toString() + val poster = fixUrlNull(it.selectFirst("img")?.attr("src")) + val tvType = getType(it.selectFirst(".typez")?.text().toString()) + val href = fixUrl(it.selectFirst("a.tip")!!.attr("href")) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addDubStatus(dubExist = false, subExist = true) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h1.entry-title")!!.text().trim() + val poster = document.select(".thumb > img").attr("src") + val tags = document.select(".genxed > a").map { it.text() } + + val year = Regex("\\d, ([0-9]*)").find( + document.selectFirst(".info-content > .spe > span > time")!!.text().trim() + )?.groupValues?.get(1).toString().toIntOrNull() + val status = getStatus( + document.select(".info-content > .spe > span:nth-child(1)") + .text().trim().replace("Status: ", "") + ) + val typeCheck = + when (document.select(".info-content > .spe > span:nth-child(5), .info-content > .spe > span") + .text().trim()) { + "OVA" -> "OVA" + "Movie" -> "Movie" + else -> "TV" + } + val type = getType(typeCheck) + val description = document.select(".entry-content > p").text().trim() + val trailer = document.selectFirst("a.trailerbutton")?.attr("href") + + val episodes = document.select(".eplister > ul > li").map { + val header = it.select(".epl-title").text() + val name = + Regex("(Episode\\s?[0-9]+)").find(header)?.groupValues?.getOrNull(0) ?: header + val link = fixUrl(it.select("a").attr("href")) + Episode(link, name) + }.reversed() + + val recommendations = + document.select(".listupd > article[itemscope=itemscope]").mapNotNull { rec -> + val epTitle = rec.selectFirst(".tt")!!.ownText().trim() + val epPoster = rec.selectFirst("img")!!.attr("src") + val epType = getType(rec.selectFirst(".typez")?.text().toString()) + val epHref = fixUrl(rec.selectFirst("a.tip")!!.attr("href")) + + newAnimeSearchResponse(epTitle, epHref, epType) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + addTrailer(trailer) + } + + } + + data class Source( + @JsonProperty("play_url") val play_url: String, + @JsonProperty("format_id") val format_id: Int + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + val sources = document.select(".mobius > .mirror > option").mapNotNull { + fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("src")) + } + + sources.apmap { + loadExtractor(it, data, subtitleCallback, callback) + } + + return true + } + +} \ No newline at end of file diff --git a/OploverzProvider/src/main/kotlin/com/lagradost/OploverzProviderPlugin.kt b/OploverzProvider/src/main/kotlin/com/lagradost/OploverzProviderPlugin.kt new file mode 100644 index 0000000..ff5fc69 --- /dev/null +++ b/OploverzProvider/src/main/kotlin/com/lagradost/OploverzProviderPlugin.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 OploverzProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(OploverzProvider()) + } +} \ No newline at end of file diff --git a/OtakudesuProvider/build.gradle.kts b/OtakudesuProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/OtakudesuProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/OtakudesuProvider/src/main/AndroidManifest.xml b/OtakudesuProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/OtakudesuProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/OtakudesuProvider/src/main/kotlin/com/lagradost/OtakudesuProvider.kt b/OtakudesuProvider/src/main/kotlin/com/lagradost/OtakudesuProvider.kt new file mode 100644 index 0000000..3de0ce0 --- /dev/null +++ b/OtakudesuProvider/src/main/kotlin/com/lagradost/OtakudesuProvider.kt @@ -0,0 +1,204 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.util.ArrayList + +class OtakudesuProvider : MainAPI() { + override var mainUrl = "https://otakudesu.watch" + override var name = "Otakudesu" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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 + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/ongoing-anime/page/" to "Anime Ongoing", + "$mainUrl/complete-anime/page/" to "Anime Completed" + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("div.venz > ul > li").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("h2.jdlflm")?.text()?.trim() ?: return null + val href = this.selectFirst("a")!!.attr("href") + val posterUrl = this.select("div.thumbz > img").attr("src").toString() + val epNum = this.selectFirst("div.epz")?.ownText()?.replace(Regex("[^0-9]"), "")?.trim() + ?.toIntOrNull() + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query&post_type=anime" + val document = app.get(link).document + + return document.select("ul.chivsrc > li").map { + val title = it.selectFirst("h2 > a")!!.ownText().trim() + val href = it.selectFirst("h2 > a")!!.attr("href") + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + } + } + } + + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("div.infozingle > p:nth-child(1) > span")?.ownText() + ?.replace(":", "")?.trim().toString() + val poster = document.selectFirst("div.fotoanime > img")?.attr("src") + val tags = document.select("div.infozingle > p:nth-child(11) > span > a").map { it.text() } + val type = getType( + document.selectFirst("div.infozingle > p:nth-child(5) > span")?.ownText() + ?.replace(":", "")?.trim().toString() + ) + val year = Regex("\\d, ([0-9]*)").find( + document.select("div.infozingle > p:nth-child(9) > span").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.selectFirst("div.infozingle > p:nth-child(6) > span")!!.ownText() + .replace(":", "") + .trim() + ) + val description = document.select("div.sinopc > p").text() + + val episodes = document.select("div.episodelist")[1].select("ul > li").mapNotNull { + val name = Regex("(Episode\\s?[0-9]+)").find( + it.selectFirst("a")?.text().toString() + )?.groupValues?.getOrNull(0) ?: it.selectFirst("a")?.text() + val link = fixUrl(it.selectFirst("a")!!.attr("href")) + Episode(link, name) + }.reversed() + + val recommendations = + document.select("div.isi-recommend-anime-series > div.isi-konten").map { + val recName = it.selectFirst("span.judul-anime > a")!!.text() + val recHref = it.selectFirst("a")!!.attr("href") + val recPosterUrl = it.selectFirst("a > img")?.attr("src").toString() + newAnimeSearchResponse(recName, recHref, TvType.Anime) { + this.posterUrl = recPosterUrl + } + } + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + } + } + + + data class ResponseSources( + @JsonProperty("id") val id: String, + @JsonProperty("i") val i: String, + @JsonProperty("q") val q: String, + ) + + data class ResponseData( + @JsonProperty("data") val data: String + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val scriptData = document.select("script").last()?.data() + val token = scriptData?.substringAfter("{action:\"")?.substringBefore("\"}").toString() + + val nonce = app.post("$mainUrl/wp-admin/admin-ajax.php", data = mapOf("action" to token)) + .parsed().data + val action = scriptData?.substringAfter(",action:\"")?.substringBefore("\"}").toString() + + val mirrorData = document.select("div.mirrorstream > ul > li").mapNotNull { + base64Decode(it.select("a").attr("data-content")) + }.toString() + + tryParseJson>(mirrorData)?.apmap { res -> + val id = res.id + val i = res.i + val q = res.q + + var sources = Jsoup.parse( + base64Decode( + app.post( + "${mainUrl}/wp-admin/admin-ajax.php", data = mapOf( + "id" to id, + "i" to i, + "q" to q, + "nonce" to nonce, + "action" to action + ) + ).parsed().data + ) + ).select("iframe").attr("src") + + if (sources.startsWith("https://desustream.me")) { + if (!sources.contains("/arcg/") && !sources.contains("/odchan/") && !sources.contains( + "/desudrive/" + ) + ) { + sources = app.get(sources).document.select("iframe").attr("src") + } + if (sources.startsWith("https://yourupload.com")) { + sources = sources.replace("//", "//www.") + } + } + + loadExtractor(sources, data, subtitleCallback, callback) + + } + + return true + } + +} \ No newline at end of file diff --git a/OtakudesuProvider/src/main/kotlin/com/lagradost/OtakudesuProviderPlugin.kt b/OtakudesuProvider/src/main/kotlin/com/lagradost/OtakudesuProviderPlugin.kt new file mode 100644 index 0000000..4d4cd76 --- /dev/null +++ b/OtakudesuProvider/src/main/kotlin/com/lagradost/OtakudesuProviderPlugin.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 OtakudesuProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(OtakudesuProvider()) + } +} \ No newline at end of file diff --git a/SflixProvider/build.gradle.kts b/SflixProvider/build.gradle.kts index 76988f8..f79f267 100644 --- a/SflixProvider/build.gradle.kts +++ b/SflixProvider/build.gradle.kts @@ -1,11 +1,11 @@ // use an integer for version numbers -version = 1 +version = 2 cloudstream { // All of these properties are optional, you can safely remove them - description = "Also includes Dopebox, Solarmovie and 2embed" + description = "Also includes Dopebox, Solarmovie, Zoro and 2embed" // authors = listOf("Cloudburst") /** diff --git a/SflixProvider/src/main/kotlin/com/lagradost/SflixProviderPlugin.kt b/SflixProvider/src/main/kotlin/com/lagradost/SflixProviderPlugin.kt index d977cb4..4d5cc38 100644 --- a/SflixProvider/src/main/kotlin/com/lagradost/SflixProviderPlugin.kt +++ b/SflixProvider/src/main/kotlin/com/lagradost/SflixProviderPlugin.kt @@ -12,5 +12,6 @@ class SflixProviderPlugin : Plugin() { registerMainAPI(SolarmovieProvider()) registerMainAPI(TwoEmbedProvider()) registerMainAPI(DopeboxProvider()) + registerMainAPI(ZoroProvider()) } } \ No newline at end of file diff --git a/SflixProvider/src/main/kotlin/com/lagradost/ZoroProvider.kt b/SflixProvider/src/main/kotlin/com/lagradost/ZoroProvider.kt new file mode 100644 index 0000000..7ff14a6 --- /dev/null +++ b/SflixProvider/src/main/kotlin/com/lagradost/ZoroProvider.kt @@ -0,0 +1,371 @@ +package com.lagradost + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.SflixProvider.Companion.extractRabbitStream +import com.lagradost.SflixProvider.Companion.runSflixExtractorVerifierJob +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import com.lagradost.nicehttp.Requests.Companion.await +import okhttp3.Interceptor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.net.URI + +private const val OPTIONS = "OPTIONS" + +class ZoroProvider : MainAPI() { + override var mainUrl = "https://zoro.to" + override var name = "Zoro" + override val hasQuickSearch = false + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val usesWebView = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + 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) { + "Finished Airing" -> ShowStatus.Completed + "Currently Airing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + val epRegex = Regex("Ep (\\d+)/") + private fun Element.toSearchResult(): SearchResponse? { + val href = fixUrl(this.select("a").attr("href")) + val title = this.select("h3.film-name").text() + val dubSub = this.select(".film-poster > .tick.ltr").text() + //val episodes = this.selectFirst(".film-poster > .tick-eps")?.text()?.toIntOrNull() + + val dubExist = dubSub.contains("dub", ignoreCase = true) + val subExist = dubSub.contains("sub", ignoreCase = true) + val episodes = + this.selectFirst(".film-poster > .tick.rtl > .tick-eps")?.text()?.let { eps -> + //println("REGEX:::: $eps") + // current episode / max episode + //Regex("Ep (\\d+)/(\\d+)") + epRegex.find(eps)?.groupValues?.get(1)?.toIntOrNull() + } + if (href.contains("/news/") || title.trim().equals("News", ignoreCase = true)) return null + val posterUrl = fixUrl(this.select("img").attr("data-src")) + val type = getType(this.select("div.fd-infor > span.fdi-item").text()) + + return newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + addDubStatus(dubExist, subExist, episodes, episodes) + } + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val html = app.get("$mainUrl/home").text + val document = Jsoup.parse(html) + + val homePageList = ArrayList() + + document.select("div.anif-block").forEach { block -> + val header = block.select("div.anif-block-header").text().trim() + val animes = block.select("li").mapNotNull { + it.toSearchResult() + } + if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes)) + } + + document.select("section.block_area.block_area_home").forEach { block -> + val header = block.select("h2.cat-heading").text().trim() + val animes = block.select("div.flw-item").mapNotNull { + it.toSearchResult() + } + if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes)) + } + + return HomePageResponse(homePageList) + } + + private data class Response( + @JsonProperty("status") val status: Boolean, + @JsonProperty("html") val html: String + ) + +// override suspend fun quickSearch(query: String): List { +// val url = "$mainUrl/ajax/search/suggest?keyword=${query}" +// val html = mapper.readValue(khttp.get(url).text).html +// val document = Jsoup.parse(html) +// +// return document.select("a.nav-item").map { +// val title = it.selectFirst(".film-name")?.text().toString() +// val href = fixUrl(it.attr("href")) +// val year = it.selectFirst(".film-infor > span")?.text()?.split(",")?.get(1)?.trim()?.toIntOrNull() +// val image = it.select("img").attr("data-src") +// +// AnimeSearchResponse( +// title, +// href, +// this.name, +// TvType.TvSeries, +// image, +// year, +// null, +// EnumSet.of(DubStatus.Subbed), +// null, +// null +// ) +// +// } +// } + + override suspend fun search(query: String): List { + val link = "$mainUrl/search?keyword=$query" + val html = app.get(link).text + val document = Jsoup.parse(html) + + return document.select(".flw-item").map { + val title = it.selectFirst(".film-detail > .film-name > a")?.attr("title").toString() + val filmPoster = it.selectFirst(".film-poster") + val poster = filmPoster!!.selectFirst("img")?.attr("data-src") + + val episodes = filmPoster.selectFirst("div.rtl > div.tick-eps")?.text()?.let { eps -> + // current episode / max episode + val epRegex = Regex("Ep (\\d+)/")//Regex("Ep (\\d+)/(\\d+)") + epRegex.find(eps)?.groupValues?.get(1)?.toIntOrNull() + } + val dubsub = filmPoster.selectFirst("div.ltr")?.text() + val dubExist = dubsub?.contains("DUB") ?: false + val subExist = dubsub?.contains("SUB") ?: false || dubsub?.contains("RAW") ?: false + + val tvType = + getType(it.selectFirst(".film-detail > .fd-infor > .fdi-item")?.text().toString()) + val href = fixUrl(it.selectFirst(".film-name a")!!.attr("href")) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addDubStatus(dubExist, subExist, episodes, episodes) + } + } + } + + private fun Element?.getActor(): Actor? { + val image = + fixUrlNull(this?.selectFirst(".pi-avatar > img")?.attr("data-src")) ?: return null + val name = this?.selectFirst(".pi-detail > .pi-name")?.text() ?: return null + return Actor(name = name, image = image) + } + + data class ZoroSyncData( + @JsonProperty("mal_id") val malId: String?, + @JsonProperty("anilist_id") val aniListId: String?, + ) + + override suspend fun load(url: String): LoadResponse { + val html = app.get(url).text + val document = Jsoup.parse(html) + + val syncData = tryParseJson(document.selectFirst("#syncData")?.data()) + + val title = document.selectFirst(".anisc-detail > .film-name")?.text().toString() + val poster = document.selectFirst(".anisc-poster img")?.attr("src") + val tags = document.select(".anisc-info a[href*=\"/genre/\"]").map { it.text() } + + var year: Int? = null + var japaneseTitle: String? = null + var status: ShowStatus? = null + + for (info in document.select(".anisc-info > .item.item-title")) { + val text = info?.text().toString() + when { + (year != null && japaneseTitle != null && status != null) -> break + text.contains("Premiered") && year == null -> + year = + info.selectFirst(".name")?.text().toString().split(" ").last().toIntOrNull() + + text.contains("Japanese") && japaneseTitle == null -> + japaneseTitle = info.selectFirst(".name")?.text().toString() + + text.contains("Status") && status == null -> + status = getStatus(info.selectFirst(".name")?.text().toString()) + } + } + + val description = document.selectFirst(".film-description.m-hide > .text")?.text() + val animeId = URI(url).path.split("-").last() + + val episodes = Jsoup.parse( + parseJson( + app.get( + "$mainUrl/ajax/v2/episode/list/$animeId" + ).text + ).html + ).select(".ss-list > a[href].ssl-item.ep-item").map { + newEpisode(it.attr("href")) { + this.name = it?.attr("title") + this.episode = it.selectFirst(".ssli-order")?.text()?.toIntOrNull() + } + } + + val actors = document.select("div.block-actors-content > div.bac-list-wrap > div.bac-item") + .mapNotNull { head -> + val subItems = head.select(".per-info") ?: return@mapNotNull null + if (subItems.isEmpty()) return@mapNotNull null + var role: ActorRole? = null + val mainActor = subItems.first()?.let { + role = when (it.selectFirst(".pi-detail > .pi-cast")?.text()?.trim()) { + "Supporting" -> ActorRole.Supporting + "Main" -> ActorRole.Main + else -> null + } + it.getActor() + } ?: return@mapNotNull null + val voiceActor = if (subItems.size >= 2) subItems[1]?.getActor() else null + ActorData(actor = mainActor, role = role, voiceActor = voiceActor) + } + + val recommendations = + document.select("#main-content > section > .tab-content > div > .film_list-wrap > .flw-item") + .mapNotNull { head -> + val filmPoster = head?.selectFirst(".film-poster") + val epPoster = filmPoster?.selectFirst("img")?.attr("data-src") + val a = head?.selectFirst(".film-detail > .film-name > a") + val epHref = a?.attr("href") + val epTitle = a?.attr("title") + if (epHref == null || epTitle == null || epPoster == null) { + null + } else { + AnimeSearchResponse( + epTitle, + fixUrl(epHref), + this.name, + TvType.Anime, + epPoster, + dubStatus = null + ) + } + } + + return newAnimeLoadResponse(title, url, TvType.Anime) { + japName = japaneseTitle + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + this.actors = actors + addMalId(syncData?.malId?.toIntOrNull()) + addAniListId(syncData?.aniListId?.toIntOrNull()) + } + } + + private data class RapidCloudResponse( + @JsonProperty("link") val link: String + ) + + override suspend fun extractorVerifierJob(extractorData: String?) { + Log.d(this.name, "Starting ${this.name} job!") + runSflixExtractorVerifierJob(this, extractorData, "https://rapid-cloud.ru/") + } + + /** Url hashcode to sid */ + var sid: HashMap = hashMapOf() + + /** + * Makes an identical Options request before .ts request + * Adds an SID header to the .ts request. + * */ + override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor { + // Needs to be object instead of lambda to make it compile correctly + return object : Interceptor { + override fun intercept(chain: Interceptor.Chain): okhttp3.Response { + val request = chain.request() + if (request.url.toString().endsWith(".ts") + && request.method != OPTIONS + // No option requests on VidCloud + && !request.url.toString().contains("betterstream") + ) { + val newRequest = + chain.request() + .newBuilder().apply { + sid[extractorLink.url.hashCode()]?.let { sid -> + addHeader("SID", sid) + } + } + .build() + val options = request.newBuilder().method(OPTIONS, request.body).build() + ioSafe { app.baseClient.newCall(options).await() } + + return chain.proceed(newRequest) + } else { + return chain.proceed(chain.request()) + } + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val servers: List> = Jsoup.parse( + app.get("$mainUrl/ajax/v2/episode/servers?episodeId=" + data.split("=")[1]) + .parsed().html + ).select(".server-item[data-type][data-id]").map { + Pair( + if (it.attr("data-type") == "sub") DubStatus.Subbed else DubStatus.Dubbed, + it.attr("data-id") + ) + } + + val extractorData = + "https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling" + + // Prevent duplicates + servers.distinctBy { it.second }.apmap { + val link = + "$mainUrl/ajax/v2/episode/sources?id=${it.second}" + val extractorLink = app.get( + link, + ).parsed().link + val hasLoadedExtractorLink = + loadExtractor(extractorLink, "https://rapid-cloud.ru/", subtitleCallback, callback) + + if (!hasLoadedExtractorLink) { + extractRabbitStream( + extractorLink, + subtitleCallback, + // Blacklist VidCloud for now + { videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) }, + true, + extractorData + ) { sourceName -> + sourceName + " - ${it.first}" + } + } + } + + return true + } +} diff --git a/TenshiProvider/build.gradle.kts b/TenshiProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/TenshiProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/TenshiProvider/src/main/AndroidManifest.xml b/TenshiProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/TenshiProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TenshiProvider/src/main/kotlin/com/lagradost/TenshiProvider.kt b/TenshiProvider/src/main/kotlin/com/lagradost/TenshiProvider.kt new file mode 100644 index 0000000..1fcf6a3 --- /dev/null +++ b/TenshiProvider/src/main/kotlin/com/lagradost/TenshiProvider.kt @@ -0,0 +1,352 @@ +package com.lagradost + +import android.annotation.SuppressLint +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.network.DdosGuardKiller +import com.lagradost.cloudstream3.network.getHeaders +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.nodes.Document +import java.net.URI +import java.text.SimpleDateFormat +import java.util.* + +class TenshiProvider : MainAPI() { + companion object { + //var token: String? = null + //var cookie: Map = mapOf() + + 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 + } + } + + override var mainUrl = "https://tenshi.moe" + override var name = "Tenshi.moe" + override val hasQuickSearch = false + override val hasMainPage = true + override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA) + private var ddosGuardKiller = DdosGuardKiller(true) + + /*private fun loadToken(): Boolean { + return try { + val response = get(mainUrl) + cookie = response.cookies + val document = Jsoup.parse(response.text) + token = document.selectFirst("""meta[name="csrf-token"]""").attr("content") + token != null + } catch (e: Exception) { + false + } + }*/ + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document + for (section in soup.select("#content > section")) { + try { + if (section.attr("id") == "toplist-tabs") { + for (top in section.select(".tab-content > [role=\"tabpanel\"]")) { + val title = "Top - " + top.attr("id").split("-")[1].replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.UK + ) else it.toString() + } + val anime = top.select("li > a").map { + AnimeSearchResponse( + it.selectFirst(".thumb-title")!!.text(), + fixUrl(it.attr("href")), + this.name, + TvType.Anime, + it.selectFirst("img")!!.attr("src"), + null, + EnumSet.of(DubStatus.Subbed), + ) + } + items.add(HomePageList(title, anime)) + } + } else { + val title = section.selectFirst("h2")!!.text() + val anime = section.select("li > a").map { + AnimeSearchResponse( + it.selectFirst(".thumb-title")?.text() ?: "", + fixUrl(it.attr("href")), + this.name, + TvType.Anime, + it.selectFirst("img")!!.attr("src"), + null, + EnumSet.of(DubStatus.Subbed), + ) + } + items.add(HomePageList(title, anime)) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + private fun getIsMovie(type: String, id: Boolean = false): Boolean { + if (!id) return type == "Movie" + + val movies = listOf("rrso24fa", "e4hqvtym", "bl5jdbqn", "u4vtznut", "37t6h2r4", "cq4azcrj") + val aniId = type.replace("$mainUrl/anime/", "") + return movies.contains(aniId) + } + + private fun parseSearchPage(soup: Document): List { + val items = soup.select("ul.thumb > li > a") + return items.map { + val href = fixUrl(it.attr("href")) + val img = fixUrl(it.selectFirst("img")!!.attr("src")) + val title = it.attr("title") + if (getIsMovie(href, true)) { + MovieSearchResponse( + title, href, this.name, TvType.Movie, img, null + ) + } else { + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + img, + null, + EnumSet.of(DubStatus.Subbed), + ) + } + } + } + + @SuppressLint("SimpleDateFormat") + private fun dateParser(dateString: String?): Date? { + if (dateString == null) return null + try { + val format = SimpleDateFormat("dd 'of' MMM',' yyyy") + val data = format.parse( + dateString.replace("th ", " ").replace("st ", " ").replace("nd ", " ") + .replace("rd ", " ") + ) ?: return null + return data + } catch (e: Exception) { + return null + } + } + +// data class TenshiSearchResponse( +// @JsonProperty("url") var url : String, +// @JsonProperty("title") var title : String, +// @JsonProperty("cover") var cover : String, +// @JsonProperty("genre") var genre : String, +// @JsonProperty("year") var year : Int, +// @JsonProperty("type") var type : String, +// @JsonProperty("eps") var eps : String, +// @JsonProperty("cen") var cen : String +// ) + +// override suspend fun quickSearch(query: String): ArrayList? { +// if (!autoLoadToken()) return quickSearch(query) +// val url = "$mainUrl/anime/search" +// val response = khttp.post( +// url, +// data=mapOf("q" to query), +// headers=mapOf("x-csrf-token" to token, "x-requested-with" to "XMLHttpRequest"), +// cookies = cookie +// +// ) +// +// val items = mapper.readValue>(response.text) +// +// if (items.isEmpty()) return ArrayList() +// +// val returnValue = ArrayList() +// for (i in items) { +// val href = fixUrl(i.url) +// val title = i.title +// val img = fixUrl(i.cover) +// val year = i.year +// +// returnValue.add( +// if (getIsMovie(i.type)) { +// MovieSearchResponse( +// title, href, getSlug(href), this.name, TvType.Movie, img, year +// ) +// } else { +// AnimeSearchResponse( +// title, href, getSlug(href), this.name, +// TvType.Anime, img, year, null, +// EnumSet.of(DubStatus.Subbed), +// null, null +// ) +// } +// ) +// } +// return returnValue +// } + + override suspend fun search(query: String): List { + val url = "$mainUrl/anime" + var document = app.get( + url, + params = mapOf("q" to query), + cookies = mapOf("loop-view" to "thumb"), + interceptor = ddosGuardKiller + ).document + + val returnValue = parseSearchPage(document).toMutableList() + + while (!document.select("""a.page-link[rel="next"]""").isEmpty()) { + val link = document.selectFirst("""a.page-link[rel="next"]""")?.attr("href") + if (!link.isNullOrBlank()) { + document = app.get( + link, + cookies = mapOf("loop-view" to "thumb"), + interceptor = ddosGuardKiller + ).document + returnValue.addAll(parseSearchPage(document)) + } else { + break + } + } + + return returnValue + } + + override suspend fun load(url: String): LoadResponse { + var document = app.get( + url, + cookies = mapOf("loop-view" to "thumb"), + interceptor = ddosGuardKiller + ).document + + val canonicalTitle = document.selectFirst("header.entry-header > h1.mb-3")!!.text().trim() + val episodeNodes = document.select("li[class*=\"episode\"] > a").toMutableList() + val totalEpisodePages = if (document.select(".pagination").size > 0) + document.select(".pagination .page-item a.page-link:not([rel])").last()!!.text() + .toIntOrNull() + else 1 + + if (totalEpisodePages != null && totalEpisodePages > 1) { + for (pageNum in 2..totalEpisodePages) { + document = app.get( + "$url?page=$pageNum", + cookies = mapOf("loop-view" to "thumb"), + interceptor = ddosGuardKiller + ).document + episodeNodes.addAll(document.select("li[class*=\"episode\"] > a")) + } + } + + val episodes = ArrayList(episodeNodes.map { + val title = it.selectFirst(".episode-title")?.text()?.trim() + newEpisode(it.attr("href")) { + this.name = if (title == "No Title") null else title + this.posterUrl = it.selectFirst("img")?.attr("src") + addDate(dateParser(it?.selectFirst(".episode-date")?.text()?.trim())) + this.description = it.attr("data-content").trim() + } + }) + + val similarAnime = document.select("ul.anime-loop > li > a").mapNotNull { element -> + val href = element.attr("href") ?: return@mapNotNull null + val title = + element.selectFirst("> .overlay > .thumb-title")?.text() ?: return@mapNotNull null + val img = element.selectFirst("> img")?.attr("src") + AnimeSearchResponse(title, href, this.name, TvType.Anime, img) + } + + val type = document.selectFirst("a[href*=\"$mainUrl/type/\"]")?.text()?.trim() + + return newAnimeLoadResponse(canonicalTitle, url, getType(type ?: "")) { + recommendations = similarAnime + posterUrl = document.selectFirst("img.cover-image")?.attr("src") + plot = document.selectFirst(".entry-description > .card-body")?.text()?.trim() + tags = + document.select("li.genre.meta-data > span.value") + .map { it?.text()?.trim().toString() } + + synonyms = + document.select("li.synonym.meta-data > div.info-box > span.value") + .map { it?.text()?.trim().toString() } + + engName = + document.selectFirst("span.value > span[title=\"English\"]")?.parent()?.text() + ?.trim() + japName = + document.selectFirst("span.value > span[title=\"Japanese\"]")?.parent()?.text() + ?.trim() + + val pattern = Regex("(\\d{4})") + val yearText = document.selectFirst("li.release-date .value")!!.text() + year = pattern.find(yearText)?.groupValues?.get(1)?.toIntOrNull() + + addEpisodes(DubStatus.Subbed, episodes) + + showStatus = when (document.selectFirst("li.status > .value")?.text()?.trim()) { + "Ongoing" -> ShowStatus.Ongoing + "Completed" -> ShowStatus.Completed + else -> null + } + } + } + + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val soup = app.get(data, interceptor = ddosGuardKiller).document + + data class Quality( + @JsonProperty("src") val src: String, + @JsonProperty("size") val size: Int + ) + + for (source in soup.select("""[aria-labelledby="mirror-dropdown"] > li > a.dropdown-item""")) { + val release = source.text().replace("/", "").trim() + val sourceHTML = app.get( + "https://tenshi.moe/embed?v=${source.attr("href").split("v=")[1].split("&")[0]}", + headers = mapOf("Referer" to data), interceptor = ddosGuardKiller + ).text + + val match = Regex("""sources: (\[(?:.|\s)+?type: ['"]video/.*?['"](?:.|\s)+?])""").find( + sourceHTML + ) + if (match != null) { + val qualities = parseJson>( + match.destructured.component1() + .replace("'", "\"") + .replace(Regex("""(\w+): """), "\"\$1\": ") + .replace(Regex("""\s+"""), "") + .replace(",}", "}") + .replace(",]", "]") + ) + qualities.forEach { + callback.invoke( + ExtractorLink( + this.name, + "${this.name} $release", + fixUrl(it.src), + this.mainUrl, + getQualityFromName("${it.size}"), + headers = getHeaders(emptyMap(), + ddosGuardKiller.savedCookiesMap[URI(this.mainUrl).host] + ?: emptyMap() + ).toMap() + ) + ) + } + } + } + + return true + } +} diff --git a/TenshiProvider/src/main/kotlin/com/lagradost/TenshiProviderPlugin.kt b/TenshiProvider/src/main/kotlin/com/lagradost/TenshiProviderPlugin.kt new file mode 100644 index 0000000..5961ef9 --- /dev/null +++ b/TenshiProvider/src/main/kotlin/com/lagradost/TenshiProviderPlugin.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 TenshiProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(TenshiProvider()) + } +} \ No newline at end of file diff --git a/TocanimeProvider/build.gradle.kts b/TocanimeProvider/build.gradle.kts new file mode 100644 index 0000000..e7d2158 --- /dev/null +++ b/TocanimeProvider/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // 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 + + // Set to true to get an 18+ symbol next to the plugin + adult = false // will be false if unspecified +} \ No newline at end of file diff --git a/TocanimeProvider/src/main/AndroidManifest.xml b/TocanimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/TocanimeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TocanimeProvider/src/main/kotlin/com/lagradost/TocanimeProvider.kt b/TocanimeProvider/src/main/kotlin/com/lagradost/TocanimeProvider.kt new file mode 100644 index 0000000..bc67cba --- /dev/null +++ b/TocanimeProvider/src/main/kotlin/com/lagradost/TocanimeProvider.kt @@ -0,0 +1,178 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.jsoup.nodes.Element +import java.util.* + +class TocanimeProvider : MainAPI() { + override var mainUrl = "https://tocanime.co" + override var name = "Tocanime" + override val hasMainPage = true + override var lang = "vi" + 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("OVA") || t.contains("Special") -> TvType.OVA + t.contains("Movie") -> TvType.AnimeMovie + else -> TvType.Anime + } + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Đã hoàn thành" -> ShowStatus.Completed + "Chưa hoàn thành" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val document = app.get(mainUrl).document + + val homePageList = ArrayList() + + document.select("div#playlists > div").forEach { block -> + val header = block.selectFirst("h2")?.text()?.trim() ?: "" + val items = block.select("div.col-lg-3.col-md-4.col-6").map { + it.toSearchResult() + } + if (items.isNotEmpty()) homePageList.add(HomePageList(header, items)) + } + + return HomePageResponse(homePageList) + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val title = this.selectFirst("h3 a")?.text()?.trim() ?: "" + val href = fixUrl(this.selectFirst("h3 a")!!.attr("href")) + val posterUrl = fixUrlNull(this.selectFirst("div.card-item-img")?.attr("data-original")) + val epNum = this.selectFirst("div.card-item-badget.rtl")?.text()?.let { eps -> + val num = eps.filter { it.isDigit() }.toIntOrNull() + if(eps.contains("Preview")) { + num?.minus(1) + } else { + num + } + } + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/content/search?t=kw&q=$query").document + + return document.select("div.col-lg-3.col-md-4.col-6").map { + it.toSearchResult() + } + + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst("h1.title")?.text() ?: return null + val type = + if (document.select("div.me-list.scroller a").size == 1) TvType.AnimeMovie else TvType.Anime + val episodes = document.select("div.me-list.scroller a").mapNotNull { + Episode(fixUrl(it.attr("href")), it.text()) + }.reversed() + val trailer = + document.selectFirst("div#trailer script")?.data()?.substringAfter("