diff --git a/AllAnimeProvider/build.gradle.kts b/AllAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..c5ef5f0 --- /dev/null +++ b/AllAnimeProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=allanime.site&sz=%size%" +} \ 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/AllMoviesForYouProvider/build.gradle.kts b/AllMoviesForYouProvider/build.gradle.kts new file mode 100644 index 0000000..ca25253 --- /dev/null +++ b/AllMoviesForYouProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=allmoviesforyou.net&sz=%size%" +} \ No newline at end of file diff --git a/AllMoviesForYouProvider/src/main/AndroidManifest.xml b/AllMoviesForYouProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AllMoviesForYouProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AllMoviesForYouProvider/src/main/kotlin/com/lagradost/AllMoviesForYouProvider.kt b/AllMoviesForYouProvider/src/main/kotlin/com/lagradost/AllMoviesForYouProvider.kt new file mode 100644 index 0000000..5cda4d2 --- /dev/null +++ b/AllMoviesForYouProvider/src/main/kotlin/com/lagradost/AllMoviesForYouProvider.kt @@ -0,0 +1,206 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +class AllMoviesForYouProvider : MainAPI() { + companion object { + fun getType(t: String): TvType { + return when { + t.contains("series") -> TvType.TvSeries + t.contains("movies") -> TvType.Movie + else -> TvType.Movie + } + } + } + + // Fetching movies will not work if this link is outdated. + override var mainUrl = "https://allmoviesforyou.net" + override var name = "AllMoviesForYou" + override val hasMainPage = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val soup = app.get(mainUrl).document + val urls = listOf( + Pair("Movies", "section[data-id=movies] article.TPost.B"), + Pair("TV Series", "section[data-id=series] article.TPost.B"), + ) + for ((name, element) in urls) { + try { + val home = soup.select(element).map { + val title = it.selectFirst("h2.title")!!.text() + val link = it.selectFirst("a")!!.attr("href") + TvSeriesSearchResponse( + title, + link, + this.name, + TvType.Movie, + fixUrl(it.selectFirst("figure img")!!.attr("data-src")), + null, + null, + ) + } + + items.add(HomePageList(name, home)) + } catch (e: Exception) { + logError(e) + } + } + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/?s=$query" + val document = app.get(url).document + + val items = document.select("ul.MovieList > li > article > a") + return items.map { item -> + val href = item.attr("href") + val title = item.selectFirst("> h2.Title")!!.text() + val img = fixUrl(item.selectFirst("> div.Image > figure > img")!!.attr("data-src")) + val type = getType(href) + if (type == TvType.Movie) { + MovieSearchResponse(title, href, this.name, type, img, null) + } else { + TvSeriesSearchResponse( + title, + href, + this.name, + type, + img, + null, + null + ) + } + } + } + +// private fun getLink(document: Document): List? { +// val list = ArrayList() +// Regex("iframe src=\"(.*?)\"").find(document.html())?.groupValues?.get(1)?.let { +// list.add(it) +// } +// document.select("div.OptionBx")?.forEach { element -> +// val baseElement = element.selectFirst("> a.Button") +// val elementText = element.selectFirst("> p.AAIco-dns")?.text() +// if (elementText == "Streamhub" || elementText == "Dood") { +// baseElement?.attr("href")?.let { href -> +// list.add(href) +// } +// } +// } +// +// return if (list.isEmpty()) null else list +// } + + override suspend fun load(url: String): LoadResponse { + val type = getType(url) + + val document = app.get(url).document + + val title = document.selectFirst("h1.Title")!!.text() + val descipt = document.selectFirst("div.Description > p")!!.text() + val rating = + document.selectFirst("div.Vote > div.post-ratings > span")?.text()?.toRatingInt() + val year = document.selectFirst("span.Date")?.text() + val duration = document.selectFirst("span.Time")!!.text() + val backgroundPoster = + fixUrlNull(document.selectFirst("div.Image > figure > img")?.attr("data-src")) + + if (type == TvType.TvSeries) { + val list = ArrayList>() + + document.select("main > section.SeasonBx > div > div.Title > a").forEach { element -> + val season = element.selectFirst("> span")?.text()?.toIntOrNull() + val href = element.attr("href") + if (season != null && season > 0 && !href.isNullOrBlank()) { + list.add(Pair(season, fixUrl(href))) + } + } + if (list.isEmpty()) throw ErrorLoadingException("No Seasons Found") + + val episodeList = ArrayList() + + for (season in list) { + val seasonResponse = app.get(season.second).text + val seasonDocument = Jsoup.parse(seasonResponse) + val episodes = seasonDocument.select("table > tbody > tr") + if (episodes.isNotEmpty()) { + episodes.forEach { episode -> + val epNum = episode.selectFirst("> td > span.Num")?.text()?.toIntOrNull() + val poster = episode.selectFirst("> td.MvTbImg > a > img")?.attr("data-src") + val aName = episode.selectFirst("> td.MvTbTtl > a") + val name = aName!!.text() + val href = aName.attr("href") + val date = episode.selectFirst("> td.MvTbTtl > span")?.text() + + episodeList.add( + newEpisode(href) { + this.name = name + this.season = season.first + this.episode = epNum + this.posterUrl = fixUrlNull(poster) + addDate(date) + } + ) + } + } + } + return TvSeriesLoadResponse( + title, + url, + this.name, + type, + episodeList, + backgroundPoster, + year?.toIntOrNull(), + descipt, + null, + rating + ) + } else { + return newMovieLoadResponse( + title, + url, + type, + fixUrl(url) + ) { + posterUrl = backgroundPoster + this.year = year?.toIntOrNull() + this.plot = descipt + this.rating = rating + addDuration(duration) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val doc = app.get(data).document + val iframe = doc.select("body iframe").map { fixUrl(it.attr("src")) } + iframe.apmap { id -> + if (id.contains("trembed")) { + val soup = app.get(id).document + soup.select("body iframe").map { + val link = fixUrl(it.attr("src").replace("streamhub.to/d/", "streamhub.to/e/")) + loadExtractor(link, data, subtitleCallback, callback) + } + } else loadExtractor(id, data, subtitleCallback, callback) + } + return true + } +} diff --git a/AllMoviesForYouProvider/src/main/kotlin/com/lagradost/AllMoviesForYouProviderPlugin.kt b/AllMoviesForYouProvider/src/main/kotlin/com/lagradost/AllMoviesForYouProviderPlugin.kt new file mode 100644 index 0000000..bc59d5f --- /dev/null +++ b/AllMoviesForYouProvider/src/main/kotlin/com/lagradost/AllMoviesForYouProviderPlugin.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 AllMoviesForYouProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AllMoviesForYouProvider()) + } +} \ No newline at end of file diff --git a/AniflixProvider/build.gradle.kts b/AniflixProvider/build.gradle.kts new file mode 100644 index 0000000..71e0ff6 --- /dev/null +++ b/AniflixProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Anime", + "AnimeMovie", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=aniflix.pro&sz=%size%" +} \ 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..6d39094 --- /dev/null +++ b/AnimeFlickProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=animeflick.net&sz=%size%" +} \ 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/AnimePaheProvider/build.gradle.kts b/AnimePaheProvider/build.gradle.kts new file mode 100644 index 0000000..c74b698 --- /dev/null +++ b/AnimePaheProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 0 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + +} \ 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/AnimekisaProvider/build.gradle.kts b/AnimekisaProvider/build.gradle.kts new file mode 100644 index 0000000..29a10ce --- /dev/null +++ b/AnimekisaProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 0 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=animekisa.in&sz=%size%" +} \ 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/AsiaFlixProvider/build.gradle.kts b/AsiaFlixProvider/build.gradle.kts new file mode 100644 index 0000000..6043385 --- /dev/null +++ b/AsiaFlixProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=asiaflix.app&sz=%size%" +} \ No newline at end of file diff --git a/AsiaFlixProvider/src/main/AndroidManifest.xml b/AsiaFlixProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/AsiaFlixProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AsiaFlixProvider/src/main/kotlin/com/lagradost/AsiaFlixProvider.kt b/AsiaFlixProvider/src/main/kotlin/com/lagradost/AsiaFlixProvider.kt new file mode 100644 index 0000000..cc8d3ad --- /dev/null +++ b/AsiaFlixProvider/src/main/kotlin/com/lagradost/AsiaFlixProvider.kt @@ -0,0 +1,198 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.* +//import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider.Companion.getStatus +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + +class AsiaFlixProvider : 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 + } + } + } + + override var mainUrl = "https://asiaflix.app" + override var name = "AsiaFlix" + override val hasQuickSearch = false + override val hasMainPage = true + override val hasChromecastSupport = false + override val supportedTypes = setOf(TvType.AsianDrama) + + private val apiUrl = "https://api.asiaflix.app/api/v2" + + data class DashBoardObject( + @JsonProperty("sectionName") val sectionName: String, + @JsonProperty("type") val type: String?, + @JsonProperty("data") val data: List? + ) + + data class Episodes( + @JsonProperty("_id") val _id: String, + @JsonProperty("epUrl") val epUrl: String?, + @JsonProperty("number") val number: Int?, + @JsonProperty("type") val type: String?, + @JsonProperty("extracted") val extracted: String?, + @JsonProperty("videoUrl") val videoUrl: String? + ) + + + data class Data( + @JsonProperty("_id") val _id: String, + @JsonProperty("name") val name: String, + @JsonProperty("altNames") val altNames: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("tvStatus") val tvStatus: String?, + @JsonProperty("genre") val genre: String?, + @JsonProperty("releaseYear") val releaseYear: Int?, + @JsonProperty("createdAt") val createdAt: Long?, + @JsonProperty("episodes") val episodes: List?, + @JsonProperty("views") val views: Int? + ) + + + data class DramaPage( + @JsonProperty("_id") val _id: String, + @JsonProperty("name") val name: String, + @JsonProperty("altNames") val altNames: String?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("language") val language: String?, + @JsonProperty("dramaUrl") val dramaUrl: String?, + @JsonProperty("published") val published: Boolean?, + @JsonProperty("tvStatus") val tvStatus: String?, + @JsonProperty("firstAirDate") val firstAirDate: String?, + @JsonProperty("genre") val genre: String?, + @JsonProperty("releaseYear") val releaseYear: Int?, + @JsonProperty("createdAt") val createdAt: Long?, + @JsonProperty("modifiedAt") val modifiedAt: Long?, + @JsonProperty("episodes") val episodes: List, + @JsonProperty("__v") val __v: Int?, + @JsonProperty("cdnImage") val cdnImage: String?, + @JsonProperty("views") val views: Int? + ) + + private fun Data.toSearchResponse(): TvSeriesSearchResponse { + return TvSeriesSearchResponse( + name, + _id, + this@AsiaFlixProvider.name, + TvType.AsianDrama, + image, + releaseYear, + episodes?.size, + ) + } + + private fun Episodes.toEpisode(): Episode? { + if (videoUrl != null && videoUrl.contains("watch/null") || number == null) return null + return videoUrl?.let { + Episode( + it, + null, + number, + ) + } + } + + private fun DramaPage.toLoadResponse(): TvSeriesLoadResponse { + return TvSeriesLoadResponse( + name, + "$mainUrl$dramaUrl/$_id".replace("drama-detail", "show-details"), + this@AsiaFlixProvider.name, + TvType.AsianDrama, + episodes.mapNotNull { it.toEpisode() }.sortedBy { it.episode }, + image, + releaseYear, + synopsis, + getStatus(tvStatus ?: ""), + null, + genre?.split(",")?.map { it.trim() } + ) + } + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val headers = mapOf("X-Requested-By" to "asiaflix-web") + val response = app.get("$apiUrl/dashboard", headers = headers).text + + val customMapper = + mapper.copy().configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) + // Hack, because it can either be object or a list + val cleanedResponse = Regex(""""data":(\{.*?),\{"sectionName"""").replace(response) { + """"data":null},{"sectionName"""" + } + + val dashBoard = customMapper.readValue?>(cleanedResponse) + + val listItems = dashBoard?.mapNotNull { + it.data?.map { data -> + data.toSearchResponse() + }?.let { searchResponse -> + HomePageList(it.sectionName, searchResponse) + } + } + return HomePageResponse(listItems ?: listOf()) + } + + data class Link( + @JsonProperty("url") val url: String?, + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + if (isCasting) return false + val headers = mapOf("X-Requested-By" to "asiaflix-web") + app.get( + "$apiUrl/utility/get-stream-links?url=$data", + headers = headers + ).text.toKotlinObject().url?.let { +// val fixedUrl = "https://api.asiaflix.app/api/v2/utility/cors-proxy/playlist/${URLEncoder.encode(it, StandardCharsets.UTF_8.toString())}" + callback.invoke( + ExtractorLink( + name, + name, + it, + "https://asianload1.com/", + /** <------ This provider should be added instead */ + getQualityFromName(it), + URI(it).path.endsWith(".m3u8") + ) + ) + } + return true + } + + override suspend fun search(query: String): List? { + val headers = mapOf("X-Requested-By" to "asiaflix-web") + val url = "$apiUrl/drama/search?q=$query" + val response = app.get(url, headers = headers).text + return mapper.readValue?>(response)?.map { it.toSearchResponse() } + } + + override suspend fun load(url: String): LoadResponse { + val headers = mapOf("X-Requested-By" to "asiaflix-web") + val requestUrl = "$apiUrl/drama?id=${url.split("/").lastOrNull()}" + val response = app.get(requestUrl, headers = headers).text + val dramaPage = response.toKotlinObject() + return dramaPage.toLoadResponse() + } +} diff --git a/AsiaFlixProvider/src/main/kotlin/com/lagradost/AsiaFlixProviderPlugin.kt b/AsiaFlixProvider/src/main/kotlin/com/lagradost/AsiaFlixProviderPlugin.kt new file mode 100644 index 0000000..f6656e4 --- /dev/null +++ b/AsiaFlixProvider/src/main/kotlin/com/lagradost/AsiaFlixProviderPlugin.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 AsiaFlixProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(AsiaFlixProvider()) + } +} \ No newline at end of file diff --git a/BflixProvider/build.gradle.kts b/BflixProvider/build.gradle.kts new file mode 100644 index 0000000..95f3120 --- /dev/null +++ b/BflixProvider/build.gradle.kts @@ -0,0 +1,23 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 0 // will be 3 if unspecified + + + iconUrl = "https://www.google.com/s2/favicons?domain=bflix.ru&sz=%size%" +} \ No newline at end of file diff --git a/BflixProvider/src/main/AndroidManifest.xml b/BflixProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/BflixProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/BflixProvider/src/main/kotlin/com/lagradost/BflixProvider.kt b/BflixProvider/src/main/kotlin/com/lagradost/BflixProvider.kt new file mode 100644 index 0000000..98dc3bb --- /dev/null +++ b/BflixProvider/src/main/kotlin/com/lagradost/BflixProvider.kt @@ -0,0 +1,300 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.NineAnimeApi.decodeVrf +import com.lagradost.NineAnimeApi.encode +import com.lagradost.NineAnimeApi.encodeVrf +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +open class BflixProvider : MainAPI() { + override var mainUrl = "https://bflix.ru" + override var name = "Bflix" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + //override val uniqueId: Int by lazy { "BflixProvider".hashCode() } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val soup = app.get("$mainUrl/home").document + val testa = listOf( + Pair("Movies", "div.tab-content[data-name=movies] div.filmlist div.item"), + Pair("Shows", "div.tab-content[data-name=shows] div.filmlist div.item"), + Pair("Trending", "div.tab-content[data-name=trending] div.filmlist div.item"), + Pair( + "Latest Movies", + "div.container section.bl:contains(Latest Movies) div.filmlist div.item" + ), + Pair( + "Latest TV-Series", + "div.container section.bl:contains(Latest TV-Series) div.filmlist div.item" + ), + ) + for ((name, element) in testa) try { + val test = soup.select(element).map { + val title = it.selectFirst("h3 a")!!.text() + val link = fixUrl(it.selectFirst("a")!!.attr("href")) + val qualityInfo = it.selectFirst("div.quality")!!.text() + val quality = getQualityFromString(qualityInfo) + TvSeriesSearchResponse( + title, + link, + this.name, + if (link.contains("/movie/")) TvType.Movie else TvType.TvSeries, + it.selectFirst("a.poster img")!!.attr("src"), + null, + null, + quality = quality + ) + } + items.add(HomePageList(name, test)) + } catch (e: Exception) { + e.printStackTrace() + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + override suspend fun search(query: String): List? { + val encodedquery = encodeVrf(query, mainKey) + val url = "$mainUrl/search?keyword=$query&vrf=$encodedquery" + val html = app.get(url).text + val document = Jsoup.parse(html) + + return document.select(".filmlist div.item").map { + val title = it.selectFirst("h3 a")!!.text() + val href = fixUrl(it.selectFirst("a")!!.attr("href")) + val image = it.selectFirst("a.poster img")!!.attr("src") + val isMovie = href.contains("/movie/") + val qualityInfo = it.selectFirst("div.quality")!!.text() + val quality = getQualityFromString(qualityInfo) + + if (isMovie) { + MovieSearchResponse( + title, + href, + this.name, + TvType.Movie, + image, + null, + quality = quality + ) + } else { + TvSeriesSearchResponse( + title, + href, + this.name, + TvType.TvSeries, + image, + null, + null, + quality = quality + ) + } + } + } + + data class Response( + @JsonProperty("html") val html: String + ) + + companion object { + val mainKey = "OrAimkpzm6phmN3j" + } + + override suspend fun load(url: String): LoadResponse? { + val soup = app.get(url).document + val movieid = soup.selectFirst("div#watch")!!.attr("data-id") + val movieidencoded = encodeVrf(movieid, mainKey) + val title = soup.selectFirst("div.info h1")!!.text() + val description = soup.selectFirst(".info .desc")?.text()?.trim() + val poster: String? = try { + soup.selectFirst("img.poster")!!.attr("src") + } catch (e: Exception) { + soup.selectFirst(".info .poster img")!!.attr("src") + } + + val tags = soup.select("div.info .meta div:contains(Genre) a").map { it.text() } + val vrfUrl = "$mainUrl/ajax/film/servers?id=$movieid&vrf=$movieidencoded" + println("VRF___ $vrfUrl") + val episodes = Jsoup.parse( + app.get( + vrfUrl + ).parsed().html + ).select("div.episode").map { + val a = it.selectFirst("a") + val href = fixUrl(a!!.attr("href")) + val extraData = a.attr("data-kname").let { str -> + str.split("-").mapNotNull { subStr -> subStr.toIntOrNull() } + } + val isValid = extraData.size == 2 + val episode = if (isValid) extraData.getOrNull(1) else null + val season = if (isValid) extraData.getOrNull(0) else null + + val eptitle = it.selectFirst(".episode a span.name")!!.text() + val secondtitle = it.selectFirst(".episode a span")!!.text() + .replace(Regex("(Episode (\\d+):|Episode (\\d+)-|Episode (\\d+))"), "") ?: "" + Episode( + href, + secondtitle + eptitle, + season, + episode, + ) + } + val tvType = + if (url.contains("/movie/") && episodes.size == 1) TvType.Movie else TvType.TvSeries + val recommendations = + soup.select("div.bl-2 section.bl div.content div.filmlist div.item") + .mapNotNull { element -> + val recTitle = element.select("h3 a").text() ?: return@mapNotNull null + val image = element.select("a.poster img")?.attr("src") + val recUrl = fixUrl(element.select("a").attr("href")) + MovieSearchResponse( + recTitle, + recUrl, + this.name, + if (recUrl.contains("/movie/")) TvType.Movie else TvType.TvSeries, + image, + year = null + ) + } + val rating = soup.selectFirst(".info span.imdb")?.text()?.toRatingInt() + val durationdoc = soup.selectFirst("div.info div.meta").toString() + val durationregex = Regex("((\\d+) min)") + val yearegex = Regex("(\\d+)") + val duration = if (durationdoc.contains("na min")) null + else durationregex.find(durationdoc)?.destructured?.component1()?.replace(" min", "") + ?.toIntOrNull() + val year = if (mainUrl == "https://bflix.ru") { + yearegex.find(durationdoc)?.destructured?.component1() + ?.replace(Regex("|"), "") + } else null + return when (tvType) { + TvType.TvSeries -> { + TvSeriesLoadResponse( + title, + url, + this.name, + tvType, + episodes, + poster, + year?.toIntOrNull(), + description, + null, + rating, + tags, + recommendations = recommendations, + duration = duration, + ) + } + TvType.Movie -> { + MovieLoadResponse( + title, + url, + this.name, + tvType, + url, + poster, + year?.toIntOrNull(), + description, + rating, + tags, + recommendations = recommendations, + duration = duration + ) + } + else -> null + } + } + + + data class Subtitles( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("kind") val kind: String + ) + + data class Links( + @JsonProperty("url") val url: String + ) + + data class Servers( + @JsonProperty("28") val mcloud: String?, + @JsonProperty("35") val mp4upload: String?, + @JsonProperty("40") val streamtape: String?, + @JsonProperty("41") val vidstream: String?, + @JsonProperty("43") val videovard: String? + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val soup = app.get(data).document + + val movieid = encode(soup.selectFirst("div#watch")?.attr("data-id") ?: return false) + val movieidencoded = encodeVrf(movieid, mainKey) + Jsoup.parse( + parseJson( + app.get( + "$mainUrl/ajax/film/servers?id=$movieid&vrf=$movieidencoded" + ).text + ).html + ) + .select("html body #episodes").map { + val cleandata = data.replace(mainUrl, "") + val a = it.select("a").map { + it.attr("data-kname") + } + val tvType = + if (data.contains("movie/") && a.size == 1) TvType.Movie else TvType.TvSeries + val servers = if (tvType == TvType.Movie) it.select(".episode a").attr("data-ep") + else + it.select(".episode a[href=$cleandata]").attr("data-ep") + ?: it.select(".episode a[href=${cleandata.replace("/1-full", "")}]") + .attr("data-ep") + val jsonservers = parseJson(servers) ?: return@map + listOfNotNull( + jsonservers.vidstream, + jsonservers.mcloud, + jsonservers.mp4upload, + jsonservers.streamtape, + jsonservers.videovard, + ).mapNotNull { + val epserver = app.get("$mainUrl/ajax/episode/info?id=$it").text + (if (epserver.contains("url")) { + parseJson(epserver) + } else null)?.url?.let { + decodeVrf(it, mainKey) + } + }.apmap { url -> + loadExtractor( + url, data, subtitleCallback, callback + ) + } + //Apparently any server works, I haven't found any diference + val sublink = + app.get("$mainUrl/ajax/episode/subtitles/${jsonservers.mcloud}").text + val jsonsub = parseJson>(sublink) + jsonsub.forEach { subtitle -> + subtitleCallback( + SubtitleFile(subtitle.label, subtitle.file) + ) + } + } + + return true + } +} diff --git a/BflixProvider/src/main/kotlin/com/lagradost/BflixProviderPlugin.kt b/BflixProvider/src/main/kotlin/com/lagradost/BflixProviderPlugin.kt new file mode 100644 index 0000000..8379bea --- /dev/null +++ b/BflixProvider/src/main/kotlin/com/lagradost/BflixProviderPlugin.kt @@ -0,0 +1,16 @@ + +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class BflixProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(BflixProvider()) + registerMainAPI(FmoviesToProvider()) + registerMainAPI(SflixProProvider()) + } +} \ No newline at end of file diff --git a/BflixProvider/src/main/kotlin/com/lagradost/FmoviesToProvider.kt b/BflixProvider/src/main/kotlin/com/lagradost/FmoviesToProvider.kt new file mode 100644 index 0000000..75eda7f --- /dev/null +++ b/BflixProvider/src/main/kotlin/com/lagradost/FmoviesToProvider.kt @@ -0,0 +1,6 @@ +package com.lagradost + +class FmoviesToProvider : BflixProvider() { + override var mainUrl = "https://fmovies.to" + override var name = "Fmovies.to" +} \ No newline at end of file diff --git a/BflixProvider/src/main/kotlin/com/lagradost/NineAnimeApi.kt b/BflixProvider/src/main/kotlin/com/lagradost/NineAnimeApi.kt new file mode 100644 index 0000000..c40dd67 --- /dev/null +++ b/BflixProvider/src/main/kotlin/com/lagradost/NineAnimeApi.kt @@ -0,0 +1,112 @@ +package com.lagradost + +// 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 +object NineAnimeApi { + 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") +} \ No newline at end of file diff --git a/BflixProvider/src/main/kotlin/com/lagradost/SflixProProvider.kt b/BflixProvider/src/main/kotlin/com/lagradost/SflixProProvider.kt new file mode 100644 index 0000000..3ae7f81 --- /dev/null +++ b/BflixProvider/src/main/kotlin/com/lagradost/SflixProProvider.kt @@ -0,0 +1,6 @@ +package com.lagradost + +class SflixProProvider : BflixProvider() { + override var mainUrl = "https://sflix.pro" + override var name = "Sflix.pro" +} \ No newline at end of file diff --git a/Crunchyroll/build.gradle.kts b/Crunchyroll/build.gradle.kts new file mode 100644 index 0000000..f650b1c --- /dev/null +++ b/Crunchyroll/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 3 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + description = "The Crunchyroll provider allows you to watch all the shows that are on Crunchyroll." + authors = listOf("Sir Aguacata (KillerDogeEmpire)") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf("AnimeMovie", "Anime", "OVA") + iconUrl = "https://www.google.com/s2/favicons?domain=crunchyroll.com&sz=%size%" +} \ No newline at end of file diff --git a/Crunchyroll/src/main/AndroidManifest.xml b/Crunchyroll/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/Crunchyroll/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt b/Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt new file mode 100644 index 0000000..ff11972 --- /dev/null +++ b/Crunchyroll/src/main/kotlin/com/lagradost/CookieInterceptor.kt @@ -0,0 +1,46 @@ +package com.lagradost + +import com.lagradost.nicehttp.Requests +import okhttp3.* +import okhttp3.internal.parseCookie + +/** + * An HTTP session manager. + * + * This class simply keeps cookies across requests. No security about which site should use which cookies. + * + */ + +class CustomSession( + client: OkHttpClient +) : Requests() { + var cookies = mutableMapOf() + + init { + this.baseClient = client + .newBuilder() + .addInterceptor { + val time = System.currentTimeMillis() + val request = it.request() + request.headers.forEach { header -> + if (header.first.equals("cookie", ignoreCase = true)) { + val cookie = parseCookie(time, request.url, header.second) ?: return@forEach + cookies += cookie.name to cookie + } + } + it.proceed(request) + } + .cookieJar(CustomCookieJar()) + .build() + } + + inner class CustomCookieJar : CookieJar { + override fun loadForRequest(url: HttpUrl): List { + return this@CustomSession.cookies.values.toList() + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + this@CustomSession.cookies += cookies.map { it.name to it } + } + } +} \ No newline at end of file diff --git a/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt new file mode 100644 index 0000000..dc3a2af --- /dev/null +++ b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProvider.kt @@ -0,0 +1,497 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.capitalize +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.nicehttp.NiceResponse +import kotlinx.coroutines.delay +import org.jsoup.Jsoup +import java.util.* + +private fun String.toAscii() = this.map { it.code }.joinToString() + +class KrunchyGeoBypasser { + companion object { + const val BYPASS_SERVER = "https://cr-unblocker.us.to/start_session" + val headers = mapOf( + "accept" to "*/*", +// "Accept-Encoding" to "gzip, deflate", + "connection" to "keep-alive", +// "Referer" to "https://google.com/", + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36".toAscii() + ) + var sessionId: String? = null + + // val interceptor = CookieInterceptor() + val session = CustomSession(app.baseClient) + } + + data class KrunchySession( + @JsonProperty("data") var data: DataInfo? = DataInfo(), + @JsonProperty("error") var error: Boolean? = null, + @JsonProperty("code") var code: String? = null + ) + + data class DataInfo( + @JsonProperty("session_id") var sessionId: String? = null, + @JsonProperty("country_code") var countryCode: String? = null, + ) + + private suspend fun getSessionId(): Boolean { + return try { + val response = app.get(BYPASS_SERVER, params = mapOf("version" to "1.1")).text + val json = parseJson(response) + sessionId = json.data?.sessionId + true + } catch (e: Exception) { + sessionId = null + false + } + } + + private suspend fun autoLoadSession(): Boolean { + if (sessionId != null) return true + getSessionId() + // Do not spam the api! + delay(3000) + return autoLoadSession() + } + + suspend fun geoBypassRequest(url: String): NiceResponse { + autoLoadSession() + return session.get(url, headers = headers, cookies = mapOf("session_id" to sessionId!!)) + } +} + +class KrunchyProvider : MainAPI() { + companion object { + val crUnblock = KrunchyGeoBypasser() + val episodeNumRegex = Regex("""Episode (\d+)""") + } + + // Do not make https! It will fail! + override var mainUrl = "http://www.crunchyroll.com" + override var name: String = "Crunchyroll" + override var lang = "en" + override val hasQuickSearch = false + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.AnimeMovie, + TvType.Anime, + TvType.OVA + ) + + override val mainPage = mainPageOf( + "$mainUrl/videos/anime/popular/ajax_page?pg=" to "Popular", + "$mainUrl/videos/anime/simulcasts/ajax_page" to "Simulcasts" + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + println("GETMAINPAGE ") + val categoryData = request.data + + val paginated = categoryData.endsWith("=") + val pagedLink = if (paginated) categoryData + page else categoryData + val items = mutableListOf() + + // Only fetch page at first-time load of homepage + if (page <= 1 && request.name == "Popular") { + val doc = Jsoup.parse(crUnblock.geoBypassRequest(mainUrl).text) + val featured = doc.select(".js-featured-show-list > li").mapNotNull { anime -> + val url = + fixUrlNull(anime?.selectFirst("a")?.attr("href")) ?: return@mapNotNull null + val imgEl = anime.selectFirst("img") + val name = imgEl?.attr("alt") ?: "" + val posterUrl = imgEl?.attr("src")?.replace("small", "full") + AnimeSearchResponse( + name = name, + url = url, + apiName = this.name, + type = TvType.Anime, + posterUrl = posterUrl, + dubStatus = EnumSet.of(DubStatus.Subbed) + ) + } + val recent = + doc.select("div.welcome-countdown-day:contains(Now Showing) li").mapNotNull { + val link = + fixUrlNull(it.selectFirst("a")?.attr("href")) ?: return@mapNotNull null + val name = it.selectFirst("span.welcome-countdown-name")?.text() ?: "" + val img = it.selectFirst("img")?.attr("src")?.replace("medium", "full") + val dubstat = if (name.contains("Dub)", true)) EnumSet.of(DubStatus.Dubbed) else + EnumSet.of(DubStatus.Subbed) + val details = it.selectFirst("span.welcome-countdown-details")?.text() + val epnum = + if (details.isNullOrBlank()) null else episodeNumRegex.find(details)?.value?.replace( + "Episode ", + "" + ) ?: "0" + val episodesMap = mutableMapOf() + episodesMap[DubStatus.Subbed] = epnum?.toIntOrNull() ?: 0 + episodesMap[DubStatus.Dubbed] = epnum?.toIntOrNull() ?: 0 + AnimeSearchResponse( + name = "★ $name ★", + url = link.replace(Regex("(\\/episode.*)"), ""), + apiName = this.name, + type = TvType.Anime, + posterUrl = fixUrlNull(img), + dubStatus = dubstat, + episodes = episodesMap + ) + } + if (recent.isNotEmpty()) { + items.add( + HomePageList( + name = "Now Showing", + list = recent, + ) + ) + } + if (featured.isNotEmpty()) { + items.add(HomePageList("Featured", featured)) + } + } + + if (paginated || !paginated && page <= 1) { + crUnblock.geoBypassRequest(pagedLink).let { respText -> + val soup = Jsoup.parse(respText.text) + + val episodes = soup.select("li").mapNotNull { + val innerA = it.selectFirst("a") ?: return@mapNotNull null + val urlEps = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + AnimeSearchResponse( + name = innerA.attr("title"), + url = urlEps, + apiName = this.name, + type = TvType.Anime, + posterUrl = it.selectFirst("img")?.attr("src"), + dubStatus = EnumSet.of(DubStatus.Subbed) + ) + } + if (episodes.isNotEmpty()) { + items.add( + HomePageList( + name = request.name, + list = episodes, + ) + ) + } + } + } + + if (items.isNotEmpty()) { + return newHomePageResponse(items) + } + throw ErrorLoadingException() + } + + // Maybe fuzzy match in the future + private fun getCloseMatches(sequence: String, items: Collection): List { + val a = sequence.trim().lowercase() + + return items.mapNotNull { item -> + val b = item.trim().lowercase() + if (b.contains(a)) + item + else if (a.contains(b)) + item + else null + } + } + + private data class CrunchyAnimeData( + @JsonProperty("name") val name: String, + @JsonProperty("img") var img: String, + @JsonProperty("link") var link: String + ) + + private data class CrunchyJson( + @JsonProperty("data") val data: List, + ) + + + override suspend fun search(query: String): ArrayList { + val json = + crUnblock.geoBypassRequest("http://www.crunchyroll.com/ajax/?req=RpcApiSearch_GetSearchCandidates").text.split( + "*/" + )[0].replace("\\/", "/") + val data = parseJson( + json.split("\n").mapNotNull { if (!it.startsWith("/")) it else null }.joinToString("\n") + ).data + + val results = getCloseMatches(query, data.map { it.name }) + if (results.isEmpty()) return ArrayList() + val searchResutls = ArrayList() + + var count = 0 + for (anime in data) { + if (count == results.size) { + break + } + if (anime.name == results[count]) { + val dubstat = + if (anime.name.contains("Dub)", true)) EnumSet.of(DubStatus.Dubbed) else + EnumSet.of(DubStatus.Subbed) + anime.link = fixUrl(anime.link) + anime.img = anime.img.replace("small", "full") + searchResutls.add( + AnimeSearchResponse( + name = anime.name, + url = anime.link, + apiName = this.name, + type = TvType.Anime, + posterUrl = anime.img, + dubStatus = dubstat, + ) + ) + ++count + } + } + + return searchResutls + } + + override suspend fun load(url: String): LoadResponse { + val soup = Jsoup.parse(crUnblock.geoBypassRequest(url).text) + val title = soup.selectFirst("#showview-content-header .ellipsis")?.text()?.trim() + val posterU = soup.selectFirst(".poster")?.attr("src") + + val p = soup.selectFirst(".description") + var description = p?.selectFirst(".more")?.text()?.trim() + if (description.isNullOrBlank()) { + description = p?.selectFirst("span")?.text()?.trim() + } + + val genres = soup.select(".large-margin-bottom > ul:nth-child(2) li:nth-child(2) a") + .map { it.text().capitalize() } + val year = genres.filter { it.toIntOrNull() != null }.map { it.toInt() }.sortedBy { it } + .getOrNull(0) + + val subEpisodes = mutableListOf() + val dubEpisodes = mutableListOf() + val premiumSubEpisodes = mutableListOf() + val premiumDubEpisodes = mutableListOf() + soup.select(".season").forEach { + val seasonName = it.selectFirst("a.season-dropdown")?.text()?.trim() + it.select(".episode").forEach { ep -> + val epTitle = ep.selectFirst(".short-desc")?.text() + + val epNum = episodeNumRegex.find( + ep.selectFirst("span.ellipsis")?.text().toString() + )?.destructured?.component1() + var poster = ep.selectFirst("img.landscape")?.attr("data-thumbnailurl") + val poster2 = ep.selectFirst("img")?.attr("src") + if (poster.isNullOrBlank()) { + poster = poster2 + } + + var epDesc = + (if (epNum == null) "" else "Episode $epNum") + (if (!seasonName.isNullOrEmpty()) " - $seasonName" else "") + val isPremium = poster?.contains("widestar", ignoreCase = true) ?: false + if (isPremium) { + epDesc = "★ $epDesc ★" + } + + val epi = Episode( + fixUrl(ep.attr("href")), + "$epTitle", + posterUrl = poster?.replace("widestar", "full")?.replace("wide", "full"), + description = epDesc + ) + if (isPremium && seasonName != null && (seasonName.contains("Dub") || seasonName.contains( + "Russian" + ) || seasonName.contains("Spanish")) + ) { + premiumDubEpisodes.add(epi) + } else if (isPremium) { + premiumSubEpisodes.add(epi) + } else if (seasonName != null && (seasonName.contains("Dub"))) { + dubEpisodes.add(epi) + } else { + subEpisodes.add(epi) + } + } + } + val recommendations = + soup.select(".other-series > ul li")?.mapNotNull { element -> + val recTitle = + element.select("span.ellipsis[dir=auto]").text() ?: return@mapNotNull null + val image = element.select("img")?.attr("src") + val recUrl = fixUrl(element.select("a").attr("href")) + AnimeSearchResponse( + recTitle, + fixUrl(recUrl), + this.name, + TvType.Anime, + fixUrl(image!!), + dubStatus = + if (recTitle.contains("(DUB)") || recTitle.contains("Dub")) EnumSet.of( + DubStatus.Dubbed + ) else EnumSet.of(DubStatus.Subbed), + ) + } + return newAnimeLoadResponse(title.toString(), url, TvType.Anime) { + this.posterUrl = posterU + this.engName = title + if (subEpisodes.isNotEmpty()) addEpisodes(DubStatus.Subbed, subEpisodes.reversed()) + if (dubEpisodes.isNotEmpty()) addEpisodes(DubStatus.Dubbed, dubEpisodes.reversed()) + // TODO add arbitrary seasons + + //if (premiumDubEpisodes.isNotEmpty()) addEpisodes( + // DubStatus.PremiumDub, + // premiumDubEpisodes.reversed() + // ) + // if (premiumSubEpisodes.isNotEmpty()) addEpisodes( + // DubStatus.PremiumSub, + // premiumSubEpisodes.reversed() + // ) + this.plot = description + this.tags = genres + this.year = year + this.recommendations = recommendations + } + } + + data class Subtitles( + @JsonProperty("language") val language: String, + @JsonProperty("url") val url: String, + @JsonProperty("title") val title: String?, + @JsonProperty("format") val format: String? + ) + + data class Streams( + @JsonProperty("format") val format: String?, + @JsonProperty("audio_lang") val audioLang: String?, + @JsonProperty("hardsub_lang") val hardsubLang: String?, + @JsonProperty("url") val url: String, + @JsonProperty("resolution") val resolution: String?, + @JsonProperty("title") var title: String? + ) { + fun title(): String { + return when { + this.hardsubLang == "enUS" && this.audioLang == "jaJP" -> "Hardsub (English)" + this.hardsubLang == "esLA" && this.audioLang == "jaJP" -> "Hardsub (Latino)" + this.hardsubLang == "esES" && this.audioLang == "jaJP" -> "Hardsub (Español España)" + this.audioLang == "esLA" -> "Latino" + this.audioLang == "esES" -> "Español España" + this.audioLang == "enUS" -> "English (US)" + else -> "RAW" + } + } + } + + data class KrunchyVideo( + @JsonProperty("streams") val streams: List, + @JsonProperty("subtitles") val subtitles: List, + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val contentRegex = Regex("""vilos\.config\.media = (\{.+\})""") + val response = crUnblock.geoBypassRequest(data) + + val hlsHelper = M3u8Helper() + + val dat = contentRegex.find(response.text)?.destructured?.component1() + + if (!dat.isNullOrEmpty()) { + val json = parseJson(dat) + val streams = ArrayList() + + for (stream in json.streams) { + if ( + listOf( + "adaptive_hls", "adaptive_dash", + "multitrack_adaptive_hls_v2", + "vo_adaptive_dash", "vo_adaptive_hls", + "trailer_hls", + ).contains(stream.format) + ) { + if (stream.format!!.contains("adaptive") && listOf( + "jaJP", + "esLA", + "esES", + "enUS" + ) + .contains(stream.audioLang) && (listOf( + "esLA", + "esES", + "enUS", + null + ).contains(stream.hardsubLang)) +// && URI(stream.url).path.endsWith(".m3u") + ) { + stream.title = stream.title() + streams.add(stream) + } + // Premium eps + else if (stream.format == "trailer_hls" && listOf( + "jaJP", + "esLA", + "esES", + "enUS" + ).contains(stream.audioLang) && + (listOf("esLA", "esES", "enUS", null).contains(stream.hardsubLang)) + ) { + stream.title = stream.title() + streams.add(stream) + } + } + } + + streams.apmap { stream -> + if (stream.url.contains("m3u8") && stream.format!!.contains("adaptive")) { + hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(stream.url, null), false) + .forEach { + callback( + ExtractorLink( + "Crunchyroll", + "Crunchy - ${stream.title}", + it.streamUrl, + "", + getQualityFromName(it.quality.toString()), + true + ) + ) + } + } else if (stream.format == "trailer_hls") { + val premiumStream = stream.url + .replace("\\/", "/") + .replace(Regex("\\/clipFrom.*?index.m3u8"), "").replace("'_,'", "'_'") + .replace(stream.url.split("/")[2], "fy.v.vrv.co") + callback( + ExtractorLink( + this.name, + "Crunchy - ${stream.title} ★", + premiumStream, + "", + Qualities.Unknown.value, + false + ) + ) + } else null + } + json.subtitles.forEach { + val langclean = it.language.replace("esLA", "Spanish") + .replace("enUS", "English") + .replace("esES", "Spanish (Spain)") + subtitleCallback( + SubtitleFile(langclean, it.url) + ) + } + + return true + } + return false + } +} \ No newline at end of file diff --git a/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt new file mode 100644 index 0000000..26997ee --- /dev/null +++ b/Crunchyroll/src/main/kotlin/com/lagradost/CrunchyrollProviderPlugin.kt @@ -0,0 +1,14 @@ + +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class CrunchyrollProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(KrunchyProvider()) + } +} \ No newline at end of file diff --git a/DubbedAnimeProvider/build.gradle.kts b/DubbedAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..7e68732 --- /dev/null +++ b/DubbedAnimeProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + + tvTypes = listOf( + "AnimeMovie", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=bestdubbedanime.com&sz=%size%" +} \ 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/EjaTv/build.gradle.kts b/EjaTv/build.gradle.kts new file mode 100644 index 0000000..d6a8605 --- /dev/null +++ b/EjaTv/build.gradle.kts @@ -0,0 +1,22 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf("Live") + iconUrl = "https://www.google.com/s2/favicons?domain=eja.tv&sz=%size%" +} \ No newline at end of file diff --git a/EjaTv/src/main/AndroidManifest.xml b/EjaTv/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/EjaTv/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/EjaTv/src/main/kotlin/com/lagradost/EjaTv.kt b/EjaTv/src/main/kotlin/com/lagradost/EjaTv.kt new file mode 100644 index 0000000..d017ef5 --- /dev/null +++ b/EjaTv/src/main/kotlin/com/lagradost/EjaTv.kt @@ -0,0 +1,120 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.jsoup.nodes.Element + +class EjaTv : MainAPI() { + override var mainUrl = "https://eja.tv" + override var name = "Eja.tv" + + // Universal language? + override var lang = "en" + override val hasDownloadSupport = false + + override val hasMainPage = true + override val supportedTypes = setOf( + TvType.Live + ) + + private fun Element.toSearchResponse(): LiveSearchResponse? { + val link = this.select("div.alternative a").last() ?: return null + val href = fixUrl(link.attr("href")) + val img = this.selectFirst("div.thumb img") + val lang = this.selectFirst(".card-title > a")?.attr("href")?.removePrefix("?country=") + ?.replace("int", "eu") //international -> European Union 🇪🇺 + return LiveSearchResponse( + // Kinda hack way to get the title + img?.attr("alt")?.replaceFirst("Watch ", "") ?: return null, + href, + this@EjaTv.name, + TvType.Live, + fixUrl(img.attr("src")), + lang = lang + ) + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + // Maybe this based on app language or as setting? + val language = "English" + val dataMap = mapOf( + "News" to mapOf("language" to language, "category" to "News"), + "Sports" to mapOf("language" to language, "category" to "Sports"), + "Entertainment" to mapOf("language" to language, "category" to "Entertainment") + ) + return HomePageResponse(dataMap.apmap { (title, data) -> + val document = app.post(mainUrl, data = data).document + val shows = document.select("div.card-body").mapNotNull { + it.toSearchResponse() + } + HomePageList( + title, + shows, + isHorizontalImages = true + ) + }) + } + + override suspend fun search(query: String): List { + return app.post( + mainUrl, data = mapOf("search" to query) + ).document.select("div.card-body").mapNotNull { + it.toSearchResponse() + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url).document + val sections = + doc.select("li.list-group-item.d-flex.justify-content-between.align-items-center") + + val link = fixUrl(sections.last()!!.select("a").attr("href")) + + val title = doc.select("h5.text-center").text() + val poster = fixUrl(doc.select("p.text-center img").attr("src")) + + val summary = sections.subList(0, 3).joinToString("
") { + val innerText = it.ownText().trim() + val outerText = it.select("a").text().trim() + "$innerText: $outerText" + } + + return LiveStreamLoadResponse( + title, + url, + this.name, + LoadData(link, title).toJson(), + poster, + plot = summary + ) + } + + data class LoadData( + val url: String, + val title: String + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val loadData = parseJson(data) + + callback.invoke( + ExtractorLink( + this.name, + loadData.title, + loadData.url, + "", + Qualities.Unknown.value, + isM3u8 = true + ) + ) + return true + } +} \ No newline at end of file diff --git a/EjaTv/src/main/kotlin/com/lagradost/EjaTvPlugin.kt b/EjaTv/src/main/kotlin/com/lagradost/EjaTvPlugin.kt new file mode 100644 index 0000000..644a44d --- /dev/null +++ b/EjaTv/src/main/kotlin/com/lagradost/EjaTvPlugin.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 EjaTvPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(EjaTv()) + } +} \ No newline at end of file diff --git a/GogoanimeProvider/build.gradle.kts b/GogoanimeProvider/build.gradle.kts new file mode 100644 index 0000000..a21f6ba --- /dev/null +++ b/GogoanimeProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=gogoanime.lu&sz=%size%" +} \ No newline at end of file diff --git a/GogoanimeProvider/src/main/AndroidManifest.xml b/GogoanimeProvider/src/main/AndroidManifest.xml new file mode 100644 index 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/HDMProvider/build.gradle.kts b/HDMProvider/build.gradle.kts new file mode 100644 index 0000000..75c5595 --- /dev/null +++ b/HDMProvider/build.gradle.kts @@ -0,0 +1,24 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Movie", + ) + + } \ No newline at end of file diff --git a/HDMProvider/src/main/AndroidManifest.xml b/HDMProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/HDMProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/HDMProvider/src/main/kotlin/com/lagradost/HDMProvider.kt b/HDMProvider/src/main/kotlin/com/lagradost/HDMProvider.kt new file mode 100644 index 0000000..f9378ca --- /dev/null +++ b/HDMProvider/src/main/kotlin/com/lagradost/HDMProvider.kt @@ -0,0 +1,116 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.jsoup.Jsoup + +class HDMProvider : MainAPI() { + override var name = "HD Movies" + override var mainUrl = "https://hdm.to" + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.Movie, + ) + + 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.col-md-2 > article > a") + if (items.isEmpty()) return emptyList() + + return items.map { i -> + val href = i.attr("href") + val data = i.selectFirst("> div.item")!! + val img = data.selectFirst("> img")!!.attr("src") + val name = data.selectFirst("> div.movie-details")!!.text() + MovieSearchResponse(name, href, this.name, TvType.Movie, img, null) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + if (data == "") return false + val slug = Regex(".*/(.*?)\\.mp4").find(data)?.groupValues?.get(1) ?: return false + val response = app.get(data).text + val key = Regex("playlist\\.m3u8(.*?)\"").find(response)?.groupValues?.get(1) ?: return false + callback.invoke( + ExtractorLink( + this.name, + this.name, + "https://hls.1o.to/vod/$slug/playlist.m3u8$key", + "", + Qualities.P720.value, + true + ) + ) + return true + } + + override suspend fun load(url: String): LoadResponse? { + val response = app.get(url).text + val document = Jsoup.parse(response) + val title = document.selectFirst("h2.movieTitle")?.text() ?: throw ErrorLoadingException("No Data Found") + val poster = document.selectFirst("div.post-thumbnail > img")!!.attr("src") + val descript = document.selectFirst("div.synopsis > p")!!.text() + val year = document.select("div.movieInfoAll > div.row > div.col-md-6").getOrNull(1)?.selectFirst("> p > a")?.text() + ?.toIntOrNull() + val data = "src/player/\\?v=(.*?)\"".toRegex().find(response)?.groupValues?.get(1) ?: return null + + return MovieLoadResponse( + title, url, this.name, TvType.Movie, + "$mainUrl/src/player/?v=$data", poster, year, descript, null + ) + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val html = app.get(mainUrl, timeout = 25).text + val document = Jsoup.parse(html) + val all = ArrayList() + + val mainbody = document.getElementsByTag("body") + ?.select("div.homeContentOuter > section > div.container > div") + // Fetch row title + val inner = mainbody?.select("div.col-md-2.col-sm-2.mrgb") + val title = mainbody?.select("div > div")?.firstOrNull()?.select("div.title.titleBar")?.text() ?: "Unnamed Row" + // Fetch list of items and map + if (inner != null) { + val elements: List = inner.map { + + val aa = it.select("a").firstOrNull() + val item = aa?.select("div.item") + val href = aa?.attr("href") + val link = when (href != null) { + true -> fixUrl(href) + false -> "" + } + val name = item?.select("div.movie-details")?.text() ?: "" + var image = item?.select("img")?.get(1)?.attr("src") ?: "" + val year = null + + MovieSearchResponse( + name, + link, + this.name, + TvType.Movie, + image, + year, + null, + ) + } + + all.add( + HomePageList( + title, elements + ) + ) + } + return HomePageResponse(all) + } +} \ No newline at end of file diff --git a/HDMProvider/src/main/kotlin/com/lagradost/HDMProviderPlugin.kt b/HDMProvider/src/main/kotlin/com/lagradost/HDMProviderPlugin.kt new file mode 100644 index 0000000..f2eaf2f --- /dev/null +++ b/HDMProvider/src/main/kotlin/com/lagradost/HDMProviderPlugin.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 HDMProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(HDMProvider()) + } +} \ No newline at end of file diff --git a/IHaveNoTvProvider/build.gradle.kts b/IHaveNoTvProvider/build.gradle.kts new file mode 100644 index 0000000..dcdb951 --- /dev/null +++ b/IHaveNoTvProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + "Documentary", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=ihavenotv.com&sz=%size%" +} \ No newline at end of file diff --git a/IHaveNoTvProvider/src/main/AndroidManifest.xml b/IHaveNoTvProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/IHaveNoTvProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/IHaveNoTvProvider/src/main/kotlin/com/lagradost/IHaveNoTvProvider.kt b/IHaveNoTvProvider/src/main/kotlin/com/lagradost/IHaveNoTvProvider.kt new file mode 100644 index 0000000..8f73130 --- /dev/null +++ b/IHaveNoTvProvider/src/main/kotlin/com/lagradost/IHaveNoTvProvider.kt @@ -0,0 +1,222 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import java.net.URLEncoder + +class IHaveNoTvProvider : MainAPI() { + override var mainUrl = "https://ihavenotv.com" + override var name = "I Have No TV" + override val hasQuickSearch = false + override val hasMainPage = true + + override val supportedTypes = setOf(TvType.Documentary) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + // Uhh, I am too lazy to scrape the "latest documentaries" and "recommended documentaries", + // so I am just scraping 3 random categories + val allCategories = listOf( + "astronomy", + "brain", + "creativity", + "design", + "economics", + "environment", + "health", + "history", + "lifehack", + "math", + "music", + "nature", + "people", + "physics", + "science", + "technology", + "travel" + ) + + val categories = allCategories.asSequence().shuffled().take(3) + .toList() // randomly get 3 categories, because there are too many + + val items = ArrayList() + + categories.forEach { cat -> + val link = "$mainUrl/category/$cat" + val html = app.get(link).text + val soup = Jsoup.parse(html) + + val searchResults: MutableMap = mutableMapOf() + soup.select(".episodesDiv .episode").forEach { res -> + val poster = res.selectFirst("img")?.attr("src") + val aTag = if (res.html().contains("/series/")) { + res.selectFirst(".episodeMeta > a") + } else { + res.selectFirst("a[href][title]") + } + val year = Regex("""•?\s+(\d{4})\s+•""").find( + res.selectFirst(".episodeMeta")!!.text() + )?.destructured?.component1()?.toIntOrNull() + + val title = aTag!!.attr("title") + val href = fixUrl(aTag.attr("href")) + searchResults[href] = TvSeriesSearchResponse( + title, + href, + this.name, + TvType.Documentary,//if (href.contains("/series/")) TvType.TvSeries else TvType.Movie, + poster, + year, + null + ) + } + items.add( + HomePageList( + capitalizeString(cat), + ArrayList(searchResults.values).subList(0, 5) + ) + ) // just 5 results per category, app crashes when they are too many + } + + return HomePageResponse(items) + } + + override suspend fun search(query: String): ArrayList { + val url = """$mainUrl/search/${URLEncoder.encode(query, "UTF-8")}""" + val response = app.get(url).text + val soup = Jsoup.parse(response) + + val searchResults: MutableMap = mutableMapOf() + + soup.select(".episodesDiv .episode").forEach { res -> + val poster = res.selectFirst("img")?.attr("src") + val aTag = if (res.html().contains("/series/")) { + res.selectFirst(".episodeMeta > a") + } else { + res.selectFirst("a[href][title]") + } + val year = + Regex("""•?\s+(\d{4})\s+•""").find( + res.selectFirst(".episodeMeta")!!.text() + )?.destructured?.component1() + ?.toIntOrNull() + + val title = aTag!!.attr("title") + val href = fixUrl(aTag.attr("href")) + searchResults[href] = TvSeriesSearchResponse( + title, + href, + this.name, + TvType.Documentary, //if (href.contains("/series/")) TvType.TvSeries else TvType.Movie, + poster, + year, + null + ) + } + + return ArrayList(searchResults.values) + } + + override suspend fun load(url: String): LoadResponse { + val isSeries = url.contains("/series/") + val html = app.get(url).text + val soup = Jsoup.parse(html) + + val container = soup.selectFirst(".container-fluid h1")?.parent() + val title = if (isSeries) { + container?.selectFirst("h1")?.text()?.split("•")?.firstOrNull().toString() + } else soup.selectFirst(".videoDetails")!!.selectFirst("strong")?.text().toString() + val description = if (isSeries) { + container?.selectFirst("p")?.text() + } else { + soup.selectFirst(".videoDetails > p")?.text() + } + + var year: Int? = null + val categories: MutableSet = mutableSetOf() + + val episodes = if (isSeries) { + container?.select(".episode")?.map { ep -> + val thumb = ep.selectFirst("img")!!.attr("src") + + val epLink = fixUrl(ep.selectFirst("a[title]")!!.attr("href")) + val (season, epNum) = if (ep.selectFirst(".episodeMeta > strong") != null && + ep.selectFirst(".episodeMeta > strong")!!.html().contains("S") + ) { + val split = ep.selectFirst(".episodeMeta > strong")?.text()?.split("E") + Pair( + split?.firstOrNull()?.replace("S", "")?.toIntOrNull(), + split?.get(1)?.toIntOrNull() + ) + } else Pair(null, null) + + year = Regex("""•?\s+(\d{4})\s+•""").find( + ep.selectFirst(".episodeMeta")!!.text() + )?.destructured?.component1()?.toIntOrNull() + + categories.addAll( + ep.select(".episodeMeta > a[href*=\"/category/\"]").map { it.text().trim() }) + + newEpisode(epLink) { + this.name = ep.selectFirst("a[title]")!!.attr("title") + this.season = season + this.episode = epNum + this.posterUrl = thumb + this.description = ep.selectFirst(".episodeSynopsis")?.text() + } + } + } else { + listOf(MovieLoadResponse( + title, + url, + this.name, + TvType.Movie, + url, + soup.selectFirst("[rel=\"image_src\"]")!!.attr("href"), + Regex("""•?\s+(\d{4})\s+•""").find( + soup.selectFirst(".videoDetails")!!.text() + )?.destructured?.component1()?.toIntOrNull(), + description, + null, + soup.selectFirst(".videoDetails")!!.select("a[href*=\"/category/\"]") + .map { it.text().trim() } + )) + } + + val poster = episodes?.firstOrNull().let { + if (isSeries && it != null) (it as Episode).posterUrl + else null + } + + return if (isSeries) TvSeriesLoadResponse( + title, + url, + this.name, + TvType.TvSeries, + episodes!!.map { it as Episode }, + poster, + year, + description, + null, + null, + categories.toList() + ) else (episodes?.first() as MovieLoadResponse) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val html = app.get(data).text + val soup = Jsoup.parse(html) + + val iframe = soup.selectFirst("#videoWrap iframe") + if (iframe != null) { + loadExtractor(iframe.attr("src"), null, subtitleCallback, callback) + } + return true + } +} diff --git a/IHaveNoTvProvider/src/main/kotlin/com/lagradost/IHaveNoTvProviderPlugin.kt b/IHaveNoTvProvider/src/main/kotlin/com/lagradost/IHaveNoTvProviderPlugin.kt new file mode 100644 index 0000000..b888b7e --- /dev/null +++ b/IHaveNoTvProvider/src/main/kotlin/com/lagradost/IHaveNoTvProviderPlugin.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 IHaveNoTvProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(IHaveNoTvProvider()) + } +} \ No newline at end of file diff --git a/KawaiifuProvider/build.gradle.kts b/KawaiifuProvider/build.gradle.kts new file mode 100644 index 0000000..948c706 --- /dev/null +++ b/KawaiifuProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=kawaiifu.com&sz=%size%" +} \ 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..3e69fb3 --- /dev/null +++ b/KimCartoonProvider/build.gradle.kts @@ -0,0 +1,25 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Cartoon", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=kimcartoon.li&sz=%size%" +} \ 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/KisskhProvider/build.gradle.kts b/KisskhProvider/build.gradle.kts new file mode 100644 index 0000000..83fa73d --- /dev/null +++ b/KisskhProvider/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "TvSeries", + "Anime", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=kisskh.me&sz=%size%" +} \ No newline at end of file diff --git a/KisskhProvider/src/main/AndroidManifest.xml b/KisskhProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/KisskhProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/KisskhProvider/src/main/kotlin/com/lagradost/KisskhProvider.kt b/KisskhProvider/src/main/kotlin/com/lagradost/KisskhProvider.kt new file mode 100644 index 0000000..940bb19 --- /dev/null +++ b/KisskhProvider/src/main/kotlin/com/lagradost/KisskhProvider.kt @@ -0,0 +1,208 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.ArrayList + +class KisskhProvider : MainAPI() { + override var mainUrl = "https://kisskh.me" + override var name = "Kisskh" + override val hasMainPage = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.AsianDrama, + TvType.Anime + ) + + override val mainPage = mainPageOf( + "&type=2&sub=0&country=2&status=0&order=1" to "Movie Popular", + "&type=2&sub=0&country=2&status=0&order=2" to "Movie Last Update", + "&type=1&sub=0&country=2&status=0&order=1" to "TVSeries Popular", + "&type=1&sub=0&country=2&status=0&order=2" to "TVSeries Last Update", + "&type=3&sub=0&country=0&status=0&order=1" to "Anime Popular", + "&type=3&sub=0&country=0&status=0&order=2" to "Anime Last Update", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val home = app.get("$mainUrl/api/DramaList/List?page=$page${request.data}") + .parsedSafe()?.data + ?.mapNotNull { media -> + media.toSearchResponse() + } ?: throw ErrorLoadingException("Invalid Json reponse") + return newHomePageResponse(request.name, home) + } + + private fun Media.toSearchResponse(): SearchResponse? { + + return newAnimeSearchResponse( + title ?: return null, + "$title/$id", + TvType.TvSeries, + ) { + this.posterUrl = thumbnail + addSub(episodesCount) + } + } + + override suspend fun search(query: String): List { + val searchResponse = + app.get("$mainUrl/api/DramaList/Search?q=$query&type=0", referer = "$mainUrl/").text + return tryParseJson>(searchResponse)?.mapNotNull { media -> + media.toSearchResponse() + } ?: throw ErrorLoadingException("Invalid Json reponse") + } + + private fun getTitle(str: String): String { + return str.replace(Regex("[^a-zA-Z0-9]"), "-") + } + + override suspend fun load(url: String): LoadResponse? { + val id = url.split("/") + val res = app.get( + "$mainUrl/api/DramaList/Drama/${id.last()}?isq=false", + referer = "$mainUrl/Drama/${ + getTitle(id.first()) + }?id=${id.last()}" + ).parsedSafe() + ?: throw ErrorLoadingException("Invalid Json reponse") + + val episodes = res.episodes?.map { eps -> + Episode( + data = Data(res.title, eps.number, res.id, eps.id).toJson(), + episode = eps.number + ) + } ?: throw ErrorLoadingException("No Episode") + + return newTvSeriesLoadResponse( + res.title ?: return null, + url, + if (res.type == "Movie" || episodes.size == 1) TvType.Movie else TvType.TvSeries, + episodes + ) { + this.posterUrl = res.thumbnail + this.year = res.releaseDate?.split("-")?.first()?.toIntOrNull() + this.plot = res.description + this.tags = listOf("${res.country}", "${res.status}", "${res.type}") + this.showStatus = when (res.status) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> null + } + } + + } + + private fun getLanguage(str: String): String { + return when (str) { + "Indonesia" -> "Indonesian" + else -> str + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val loadData = parseJson(data) + + app.get( + "$mainUrl/api/DramaList/Episode/${loadData.epsId}.png?err=false&ts=&time=", + referer = "$mainUrl/Drama/${getTitle("${loadData.title}")}/Episode-${loadData.eps}?id=${loadData.id}&ep=${loadData.epsId}&page=0&pageSize=100" + ).parsedSafe()?.let { source -> + listOf(source.video, source.thirdParty).apmap { link -> + safeApiCall { + if (link?.contains(".m3u8") == true) { + M3u8Helper.generateM3u8( + this.name, + link, + referer = "$mainUrl/", + headers = mapOf("Origin" to mainUrl) + ).forEach(callback) + } else { + loadExtractor( + link?.substringBefore("=http") ?: return@safeApiCall, + "$mainUrl/", + subtitleCallback, + callback + ) + } + } + } + } + + // parsedSafe doesn't work in > + app.get("$mainUrl/api/Sub/${loadData.epsId}").text.let { res -> + tryParseJson>(res)?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + getLanguage(sub.label ?: return@map), + sub.src ?: return@map + ) + ) + } + } + + return true + + } + + data class Data( + val title: String?, + val eps: Int?, + val id: Int?, + val epsId: Int?, + ) + + data class Sources( + @JsonProperty("Video") val video: String?, + @JsonProperty("ThirdParty") val thirdParty: String?, + ) + + data class Subtitle( + @JsonProperty("src") val src: String?, + @JsonProperty("label") val label: String?, + ) + + data class Responses( + @JsonProperty("data") val data: ArrayList? = arrayListOf(), + ) + + data class Media( + @JsonProperty("episodesCount") val episodesCount: Int?, + @JsonProperty("thumbnail") val thumbnail: String?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: String?, + ) + + data class Episodes( + @JsonProperty("id") val id: Int?, + @JsonProperty("number") val number: Int?, + @JsonProperty("sub") val sub: Int?, + ) + + data class MediaDetail( + @JsonProperty("description") val description: String?, + @JsonProperty("releaseDate") val releaseDate: String?, + @JsonProperty("status") val status: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("country") val country: String?, + @JsonProperty("episodes") val episodes: ArrayList? = arrayListOf(), + @JsonProperty("thumbnail") val thumbnail: String?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: String?, + ) + +} diff --git a/KisskhProvider/src/main/kotlin/com/lagradost/KisskhProviderPlugin.kt b/KisskhProvider/src/main/kotlin/com/lagradost/KisskhProviderPlugin.kt new file mode 100644 index 0000000..4d0b0af --- /dev/null +++ b/KisskhProvider/src/main/kotlin/com/lagradost/KisskhProviderPlugin.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 KisskhProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(KisskhProvider()) + } +} \ No newline at end of file diff --git a/MeloMovieProvider/build.gradle.kts b/MeloMovieProvider/build.gradle.kts new file mode 100644 index 0000000..6766a16 --- /dev/null +++ b/MeloMovieProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=melomovie.com&sz=%size%" +} \ No newline at end of file diff --git a/MeloMovieProvider/src/main/AndroidManifest.xml b/MeloMovieProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/MeloMovieProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/MeloMovieProvider/src/main/kotlin/com/lagradost/MeloMovieProvider.kt b/MeloMovieProvider/src/main/kotlin/com/lagradost/MeloMovieProvider.kt new file mode 100644 index 0000000..5149a93 --- /dev/null +++ b/MeloMovieProvider/src/main/kotlin/com/lagradost/MeloMovieProvider.kt @@ -0,0 +1,195 @@ +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.addImdbUrl +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class MeloMovieProvider : MainAPI() { + override var name = "MeloMovie" + override var mainUrl = "https://melomovie.com" + override val instantLinkLoading = true + override val hasQuickSearch = true + override val hasChromecastSupport = false // MKV FILES CANT BE PLAYED ON A CHROMECAST + + data class MeloMovieSearchResult( + @JsonProperty("id") val id: Int, + @JsonProperty("imdb_code") val imdbId: String, + @JsonProperty("title") val title: String, + @JsonProperty("type") val type: Int, // 1 = MOVIE, 2 = TV-SERIES + @JsonProperty("year") val year: Int?, // 1 = MOVIE, 2 = TV-SERIES + //"mppa" for tags + ) + + data class MeloMovieLink( + @JsonProperty("name") val name: String, + @JsonProperty("link") val link: String + ) + + override suspend fun quickSearch(query: String): List { + return search(query) + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/movie/search/?name=$query" + val returnValue: ArrayList = ArrayList() + val response = app.get(url).text + val mapped = response.let { mapper.readValue>(it) } + if (mapped.isEmpty()) return returnValue + + for (i in mapped) { + val currentUrl = "$mainUrl/movie/${i.id}" + val currentPoster = "$mainUrl/assets/images/poster/${i.imdbId}.jpg" + if (i.type == 2) { // TV-SERIES + returnValue.add( + TvSeriesSearchResponse( + i.title, + currentUrl, + this.name, + TvType.TvSeries, + currentPoster, + i.year, + null + ) + ) + } else if (i.type == 1) { // MOVIE + returnValue.add( + MovieSearchResponse( + i.title, + currentUrl, + this.name, + TvType.Movie, + currentUrl, + i.year + ) + ) + } + } + return returnValue + } + + // http not https, the links are not https! + private fun fixUrl(url: String): String { + if (url.isEmpty()) return "" + + if (url.startsWith("//")) { + return "http:$url" + } + if (!url.startsWith("http")) { + return "http://$url" + } + return url + } + + private fun serializeData(element: Element): List { + val eps = element.select("> tbody > tr") + val parsed = eps.mapNotNull { + try { + val tds = it.select("> td") + val name = tds[if (tds.size == 5) 1 else 0].text() + val url = fixUrl(tds.last()!!.selectFirst("> a")!!.attr("data-lnk").replace(" ", "%20")) + MeloMovieLink(name, url) + } catch (e: Exception) { + MeloMovieLink("", "") + } + }.filter { it.link != "" && it.name != "" } + return parsed + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val links = parseJson>(data) + for (link in links) { + callback.invoke( + ExtractorLink( + this.name, + link.name, + link.link, + "", + getQualityFromName(link.name), + false + ) + ) + } + return true + } + + override suspend fun load(url: String): LoadResponse? { + val response = app.get(url).text + + //backdrop = imgurl + fun findUsingRegex(src: String): String? { + return src.toRegex().find(response)?.groups?.get(1)?.value ?: return null + } + + val imdbUrl = findUsingRegex("var imdb = \"(.*?)\"") + val document = Jsoup.parse(response) + val poster = document.selectFirst("img.img-fluid")!!.attr("src") + val type = findUsingRegex("var posttype = ([0-9]*)")?.toInt() ?: return null + val titleInfo = document.selectFirst("div.movie_detail_title > div > div > h1") + val title = titleInfo!!.ownText() + val year = + titleInfo.selectFirst("> a")?.text()?.replace("(", "")?.replace(")", "")?.toIntOrNull() + val plot = document.selectFirst("div.col-lg-12 > p")!!.text() + + if (type == 1) { // MOVIE + val serialize = document.selectFirst("table.accordion__list") + ?: throw ErrorLoadingException("No links found") + return newMovieLoadResponse( + title, + url, + TvType.Movie, + serializeData(serialize) + ) { + this.posterUrl = poster + this.year = year + this.plot = plot + addImdbUrl(imdbUrl) + } + } else if (type == 2) { + val episodes = ArrayList() + val seasons = document.select("div.accordion__card") + ?: throw ErrorLoadingException("No episodes found") + for (s in seasons) { + val season = + s.selectFirst("> div.card-header > button > span")!!.text() + .replace("Season: ", "").toIntOrNull() + val localEpisodes = s.select("> div.collapse > div > div > div.accordion__card") + for (e in localEpisodes) { + val episode = + e.selectFirst("> div.card-header > button > span")!!.text() + .replace("Episode: ", "").toIntOrNull() + val links = + e.selectFirst("> div.collapse > div > table.accordion__list") ?: continue + val data = serializeData(links) + episodes.add(newEpisode(data) { + this.season = season + this.episode = episode + }) + } + } + episodes.reverse() + return newTvSeriesLoadResponse( + title, + url, + TvType.TvSeries, + episodes + ) { + this.posterUrl = poster + this.year = year + this.plot = plot + addImdbUrl(imdbUrl) + } + } + return null + } +} \ No newline at end of file diff --git a/MeloMovieProvider/src/main/kotlin/com/lagradost/MeloMovieProviderPlugin.kt b/MeloMovieProvider/src/main/kotlin/com/lagradost/MeloMovieProviderPlugin.kt new file mode 100644 index 0000000..56f0bb4 --- /dev/null +++ b/MeloMovieProvider/src/main/kotlin/com/lagradost/MeloMovieProviderPlugin.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 MeloMovieProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(MeloMovieProvider()) + } +} \ No newline at end of file diff --git a/NineAnimeProvider/build.gradle.kts b/NineAnimeProvider/build.gradle.kts new file mode 100644 index 0000000..64e09ef --- /dev/null +++ b/NineAnimeProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=9anime.id&sz=%size%" +} \ 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/SoaptwoDayProvider/build.gradle.kts b/SoaptwoDayProvider/build.gradle.kts new file mode 100644 index 0000000..314d8b7 --- /dev/null +++ b/SoaptwoDayProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=secretlink.xyz&sz=%size%" +} \ No newline at end of file diff --git a/SoaptwoDayProvider/src/main/AndroidManifest.xml b/SoaptwoDayProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/SoaptwoDayProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/SoaptwoDayProvider/src/main/kotlin/com/lagradost/SoaptwoDayProvider.kt b/SoaptwoDayProvider/src/main/kotlin/com/lagradost/SoaptwoDayProvider.kt new file mode 100644 index 0000000..8b90e0b --- /dev/null +++ b/SoaptwoDayProvider/src/main/kotlin/com/lagradost/SoaptwoDayProvider.kt @@ -0,0 +1,263 @@ +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 org.jsoup.Jsoup + +class SoaptwoDayProvider : MainAPI() { + override var mainUrl = "https://secretlink.xyz" //Probably a rip off, but it has no captcha + override var name = "Soap2Day" + override val hasMainPage = true + override val hasChromecastSupport = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + override val mainPage = mainPageOf( + Pair("$mainUrl/movielist?page=", "Movies"), + Pair("$mainUrl/tvlist?page=", "TV Series"), + ) + + override suspend fun getMainPage( + page: Int, + request : MainPageRequest + ): HomePageResponse { + val url = request.data + page + + val soup = app.get(url).document + val home = + soup.select("div.container div.row div.col-sm-12.col-lg-12 div.row div.col-sm-12.col-lg-12 .col-xs-6") + .map { + val title = it.selectFirst("h5 a")!!.text() + val link = it.selectFirst("a")!!.attr("href") + TvSeriesSearchResponse( + title, + link, + this.name, + TvType.TvSeries, + fixUrl(it.selectFirst("img")!!.attr("src")), + null, + null, + ) + } + return newHomePageResponse(request.name, home) + } + + override suspend fun search(query: String): List { + val doc = app.get("$mainUrl/search/keyword/$query").document + return doc.select("div.container div.row div.col-sm-12.col-lg-12 div.row div.col-sm-12.col-lg-12 .col-xs-6") + .map { + val title = it.selectFirst("h5 a")!!.text() + val image = fixUrl(it.selectFirst("img")!!.attr("src")) + val href = fixUrl(it.selectFirst("a")!!.attr("href")) + TvSeriesSearchResponse( + title, + href, + this.name, + TvType.TvSeries, + image, + null, + null + ) + } + } + + override suspend fun load(url: String): LoadResponse? { + val soup = app.get(url).document + val title = soup.selectFirst(".hidden-lg > div:nth-child(1) > h4")?.text() ?: "" + val description = soup.selectFirst("p#wrap")?.text()?.trim() + val poster = + soup.selectFirst(".col-md-5 > div:nth-child(1) > div:nth-child(1) > img")?.attr("src") + val episodes = mutableListOf() + soup.select("div.alert").forEach { + val season = it?.selectFirst("h4")?.text()?.filter { c -> c.isDigit() }?.toIntOrNull() + it?.select("div > div > a")?.forEach { entry -> + val link = fixUrlNull(entry?.attr("href")) ?: return@forEach + val text = entry?.text() ?: "" + val name = text.replace(Regex("(^(\\d+)\\.)"), "") + val epNum = text.substring(0, text.indexOf(".")).toIntOrNull() + episodes.add( + Episode( + name = name, + data = link, + season = season, + episode = epNum + ) + ) + } + } + val otherInfoBody = soup.select("div.col-sm-8 div.panel-body").toString() + //Fetch casts + val casts = otherInfoBody.substringAfter("Stars : ") + .substringBefore("Genre : ").let { + Jsoup.parse(it).select("a") + }.mapNotNull { + val castName = it?.text() ?: return@mapNotNull null + ActorData( + Actor( + name = castName + ) + ) + } + //Fetch year + val year = otherInfoBody.substringAfter("

    Release :

    ") + .substringBefore(" year string: $it") + Jsoup.parse(it).select("p")[1] + }?.text()?.take(4)?.toIntOrNull() + //Fetch genres + val genre = otherInfoBody.substringAfter("

    Genre :

    ") + .substringBefore("

    Release :

    ").let { + //Log.i(this.name, "Result => genre string: $it") + Jsoup.parse(it).select("a") + }.mapNotNull { it?.text()?.trim() ?: return@mapNotNull null } + + return when (val tvType = if (episodes.isEmpty()) TvType.Movie else TvType.TvSeries) { + TvType.TvSeries -> { + TvSeriesLoadResponse( + title, + url, + this.name, + tvType, + episodes.reversed(), + fixUrlNull(poster), + year = year, + description, + actors = casts, + tags = genre + ) + } + TvType.Movie -> { + MovieLoadResponse( + title, + url, + this.name, + tvType, + url, + fixUrlNull(poster), + year = year, + description, + actors = casts, + tags = genre + ) + } + else -> null + } + } + + data class ServerJson( + @JsonProperty("0") val zero: String?, + @JsonProperty("key") val key: Boolean?, + @JsonProperty("val") val stream: String?, + @JsonProperty("val_bak") val streambackup: String?, + @JsonProperty("pos") val pos: Int?, + @JsonProperty("type") val type: String?, + @JsonProperty("subs") val subs: List?, + @JsonProperty("prev_epi_title") val prevEpiTitle: String?, + @JsonProperty("prev_epi_url") val prevEpiUrl: String?, + @JsonProperty("next_epi_title") val nextEpiTitle: String?, + @JsonProperty("next_epi_url") val nextEpiUrl: String? + ) + + data class Subs( + @JsonProperty("id") val id: Int?, + @JsonProperty("movieId") val movieId: Int?, + @JsonProperty("tvId") val tvId: Int?, + @JsonProperty("episodeId") val episodeId: Int?, + @JsonProperty("default") val default: Int?, + @JsonProperty("IsShow") val IsShow: Int?, + @JsonProperty("name") val name: String, + @JsonProperty("path") val path: String?, + @JsonProperty("downlink") val downlink: String?, + @JsonProperty("source_file_name") val sourceFileName: String?, + @JsonProperty("createtime") val createtime: Int? + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val doc = app.get(data).document + val idplayer = doc.selectFirst("#divU")?.text() + val idplayer2 = doc.selectFirst("#divP")?.text() + val movieid = doc.selectFirst("div.row input#hId")!!.attr("value") + val tvType = try { + doc.selectFirst(".col-md-5 > div:nth-child(1) > div:nth-child(1) > img")!!.attr("src") + ?: "" + } catch (e: Exception) { + "" + } + val ajaxlink = + if (tvType.contains("movie")) "$mainUrl/home/index/GetMInfoAjax" else "$mainUrl/home/index/GetEInfoAjax" + listOf( + idplayer, + idplayer2, + ).mapNotNull { playerID -> + val url = app.post( + ajaxlink, + headers = mapOf( + "Host" to "secretlink.xyz", + "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://secretlink.xyz", + "DNT" to "1", + "Connection" to "keep-alive", + "Referer" to data, + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "same-origin", + ), + data = mapOf( + Pair("pass", movieid), + Pair("param", playerID ?: ""), + ) + ).text.replace("\\\"", "\"").replace("\"{", "{").replace("}\"", "}") + .replace("\\\\\\/", "\\/") + val json = parseJson(url) + listOfNotNull( + json.stream, + json.streambackup + ).apmap { stream -> + val cleanstreamurl = stream.replace("\\/", "/").replace("\\\\\\", "") + if (cleanstreamurl.isNotBlank()) { + callback( + ExtractorLink( + "Soap2Day", + "Soap2Day", + cleanstreamurl, + "https://soap2day.ac", + Qualities.Unknown.value, + isM3u8 = false + ) + ) + } + } + json.subs?.forEach { subtitle -> + val sublink = mainUrl + subtitle.path + listOf( + sublink, + subtitle.downlink + ).mapNotNull { subs -> + if (subs != null) { + if (subs.isNotBlank()) { + subtitleCallback( + SubtitleFile(subtitle.name, subs) + ) + } + } + } + } + } + return true + } +} diff --git a/SoaptwoDayProvider/src/main/kotlin/com/lagradost/SoaptwoDayProviderPlugin.kt b/SoaptwoDayProvider/src/main/kotlin/com/lagradost/SoaptwoDayProviderPlugin.kt new file mode 100644 index 0000000..e006f92 --- /dev/null +++ b/SoaptwoDayProvider/src/main/kotlin/com/lagradost/SoaptwoDayProviderPlugin.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 SoaptwoDayProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(SoaptwoDayProvider()) + } +} \ No newline at end of file diff --git a/TenshiProvider/build.gradle.kts b/TenshiProvider/build.gradle.kts new file mode 100644 index 0000000..4eb23b3 --- /dev/null +++ b/TenshiProvider/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "Movie", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=tenshi.moe&sz=%size%" +} \ 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/TheFlixToProvider/build.gradle.kts b/TheFlixToProvider/build.gradle.kts new file mode 100644 index 0000000..0526dcb --- /dev/null +++ b/TheFlixToProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=theflix.to&sz=%size%" +} \ No newline at end of file diff --git a/TheFlixToProvider/src/main/AndroidManifest.xml b/TheFlixToProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/TheFlixToProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TheFlixToProvider/src/main/kotlin/com/lagradost/TheFlixToProvider.kt b/TheFlixToProvider/src/main/kotlin/com/lagradost/TheFlixToProvider.kt new file mode 100644 index 0000000..cbe03b4 --- /dev/null +++ b/TheFlixToProvider/src/main/kotlin/com/lagradost/TheFlixToProvider.kt @@ -0,0 +1,602 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName + +class TheFlixToProvider : MainAPI() { + companion object { + var latestCookies: Map = emptyMap() + } + + override var name = "TheFlix.to" + override var mainUrl = "https://theflix.to" + override val instantLinkLoading = false + override val hasMainPage = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + + + data class HomeJson( + @JsonProperty("props") val props: HomeProps = HomeProps(), + ) + + data class HomeProps( + @JsonProperty("pageProps") val pageProps: PageProps = PageProps(), + ) + + data class PageProps( + @JsonProperty("moviesListTrending") val moviesListTrending: MoviesListTrending = MoviesListTrending(), + @JsonProperty("moviesListNewArrivals") val moviesListNewArrivals: MoviesListNewArrivals = MoviesListNewArrivals(), + @JsonProperty("tvsListTrending") val tvsListTrending: TvsListTrending = TvsListTrending(), + @JsonProperty("tvsListNewEpisodes") val tvsListNewEpisodes: TvsListNewEpisodes = TvsListNewEpisodes(), + ) + + + data class MoviesListTrending( + @JsonProperty("docs") val docs: ArrayList = arrayListOf(), + @JsonProperty("total") val total: Int? = null, + @JsonProperty("page") val page: Int? = null, + @JsonProperty("limit") val limit: Int? = null, + @JsonProperty("pages") val pages: Int? = null, + @JsonProperty("type") val type: String? = null, + ) + + data class MoviesListNewArrivals( + @JsonProperty("docs") val docs: ArrayList = arrayListOf(), + @JsonProperty("total") val total: Int? = null, + @JsonProperty("page") val page: Int? = null, + @JsonProperty("limit") val limit: Int? = null, + @JsonProperty("pages") val pages: Int? = null, + @JsonProperty("type") val type: String? = null, + ) + + data class TvsListTrending( + @JsonProperty("docs") val docs: ArrayList = arrayListOf(), + @JsonProperty("total") val total: Int? = null, + @JsonProperty("page") val page: Int? = null, + @JsonProperty("limit") val limit: Int? = null, + @JsonProperty("pages") val pages: Int? = null, + @JsonProperty("type") val type: String? = null, + ) + + data class TvsListNewEpisodes( + @JsonProperty("docs") val docs: ArrayList = arrayListOf(), + @JsonProperty("total") val total: Int? = null, + @JsonProperty("page") val page: Int? = null, + @JsonProperty("limit") val limit: Int? = null, + @JsonProperty("pages") val pages: Int? = null, + @JsonProperty("type") val type: String? = null, + ) + + data class Docs( + @JsonProperty("name") val name: String = String(), + @JsonProperty("originalLanguage") val originalLanguage: String? = null, + @JsonProperty("popularity") val popularity: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("voteAverage") val voteAverage: Double? = null, + @JsonProperty("voteCount") val voteCount: Int? = null, + @JsonProperty("cast") val cast: String? = null, + @JsonProperty("director") val director: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("posterUrl") val posterUrl: String? = null, + @JsonProperty("releaseDate") val releaseDate: String? = null, + @JsonProperty("createdAt") val createdAt: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("conversionDate") val conversionDate: String? = null, + @JsonProperty("id") val id: Int? = null, + @JsonProperty("available") val available: Boolean? = null, + @JsonProperty("videos" ) val videos : ArrayList? = arrayListOf(), + ) + + + private suspend fun getCookies(): Map { + // val cookieResponse = app.post( + // "https://theflix.to:5679/authorization/session/continue?contentUsageType=Viewing", + // headers = mapOf( + // "Host" to "theflix.to:5679", + // "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0", + // "Accept" to "application/json, text/plain," + // "Accept-Language" to "en-US,en;q=0.5", + // "Content-Type" to "application/json;charset=utf-8", + // "Content-Length" to "35", + // "Origin" to "https://theflix.to", + // "DNT" to "1", + // "Connection" to "keep-alive", + // "Referer" to "https://theflix.to/", + // "Sec-Fetch-Dest" to "empty", + // "Sec-Fetch-Mode" to "cors", + // "Sec-Fetch-Site" to "same-site",)).okhttpResponse.headers.values("Set-Cookie") + + val cookies = app.post( + "$mainUrl:5679/authorization/session/continue?contentUsageType=Viewing", + headers = mapOf( + "Host" to "theflix.to:5679", + "User-Agent" to USER_AGENT, + "Accept" to "application/json, text/plain, */*", + "Accept-Language" to "en-US,en;q=0.5", + "Content-Type" to "application/json;charset=utf-8", + "Content-Length" to "35", + "Origin" to mainUrl, + "DNT" to "1", + "Connection" to "keep-alive", + "Referer" to mainUrl, + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "same-site",) + ).cookies + /* val cookieRegex = Regex("(theflix\\..*?id\\=[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)") + val findcookie = cookieRegex.findAll(cookieResponse.toString()).map { it.value }.toList() + val cookiesstring = findcookie.toString().replace(", ","; ").replace("[","").replace("]","") + val cookiesmap = mapOf("Cookie" to cookiesstring) */ + latestCookies = cookies + return latestCookies + } + + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val items = ArrayList() + val doc = app.get(mainUrl).document + val scriptText = doc.selectFirst("script[type=application/json]")!!.data() + if (scriptText.contains("moviesListTrending")) { + val json = parseJson(scriptText) + val homePageProps = json.props.pageProps + listOf( + Triple( + homePageProps.moviesListNewArrivals.docs, + homePageProps.moviesListNewArrivals.type, + "New Movie arrivals" + ), + Triple( + homePageProps.moviesListTrending.docs, + homePageProps.moviesListTrending.type, + "Trending Movies" + ), + Triple( + homePageProps.tvsListTrending.docs, + homePageProps.tvsListTrending.type, + "Trending TV Series" + ), + Triple( + homePageProps.tvsListNewEpisodes.docs, + homePageProps.tvsListNewEpisodes.type, + "New Episodes" + ) + ).map { (docs, type, homename) -> + val home = docs.map { info -> + val title = info.name + val poster = info.posterUrl + val typeinfo = + if (type?.contains("TV") == true) TvType.TvSeries else TvType.Movie + val link = + if (typeinfo == TvType.Movie) "$mainUrl/movie/${info.id}-${cleanTitle(title)}" + else "$mainUrl/tv-show/${info.id}-${cleanTitle(title).replace("?","")}/season-1/episode-1" + TvSeriesSearchResponse( + title, + link, + this.name, + typeinfo, + poster, + null, + null, + ) + } + items.add(HomePageList(homename, home)) + } + + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + data class SearchJson( + @JsonProperty("props") val props: SearchProps = SearchProps(), + ) + + data class SearchProps( + @JsonProperty("pageProps") val pageProps: SearchPageProps = SearchPageProps(), + ) + + data class SearchPageProps( + @JsonProperty("mainList") val mainList: SearchMainList = SearchMainList(), + ) + + data class SearchMainList( + @JsonProperty("docs") val docs: ArrayList = arrayListOf(), + @JsonProperty("total") val total: Int? = null, + @JsonProperty("page") val page: Int? = null, + @JsonProperty("limit") val limit: Int? = null, + @JsonProperty("pages") val pages: Int? = null, + @JsonProperty("type") val type: String? = null, + ) + + + override suspend fun search(query: String): List { + val search = ArrayList() + val urls = listOf( + "$mainUrl/movies/trending?search=$query", + "$mainUrl/tv-shows/trending?search=$query" + ) + urls.apmap { url -> + val doc = app.get(url).document + val scriptText = doc.selectFirst("script[type=application/json]")!!.data() + if (scriptText.contains("pageProps")) { + val json = parseJson(scriptText) + val searchPageProps = json.props.pageProps.mainList + val pair = listOf(Pair(searchPageProps.docs, searchPageProps.type)) + pair.map { (docs, type) -> + docs.map { info -> + val title = info.name + val poster = info.posterUrl + val typeinfo = + if (type?.contains("TV") == true) TvType.TvSeries else TvType.Movie + val link = if (typeinfo == TvType.Movie) "$mainUrl/movie/${info.id}-${ + cleanTitle(title) + }" + else "$mainUrl/tv-show/${info.id}-${cleanTitle(title)}/season-1/episode-1" + if (typeinfo == TvType.Movie) { + search.add( + MovieSearchResponse( + title, + link, + this.name, + TvType.Movie, + poster, + null + ) + ) + } else { + search.add( + TvSeriesSearchResponse( + title, + link, + this.name, + TvType.TvSeries, + poster, + null, + null + ) + ) + } + } + } + } + } + return search + } + data class LoadMain ( + @JsonProperty("props" ) val props : LoadProps? = LoadProps(), + @JsonProperty("page" ) val page : String? = null, + @JsonProperty("buildId" ) val buildId : String? = null, + @JsonProperty("runtimeConfig" ) val runtimeConfig : RuntimeConfig? = RuntimeConfig(), + @JsonProperty("isFallback" ) val isFallback : Boolean? = null, + @JsonProperty("gssp" ) val gssp : Boolean? = null, + @JsonProperty("customServer" ) val customServer : Boolean? = null, + @JsonProperty("appGip" ) val appGip : Boolean? = null + ) + + data class LoadProps ( + @JsonProperty("pageProps" ) val pageProps : LoadPageProps? = LoadPageProps(), + @JsonProperty("__N_SSP" ) val _NSSP : Boolean? = null + ) + + data class LoadPageProps ( + @JsonProperty("selectedTv" ) val selectedTv : TheFlixMetadata? = TheFlixMetadata(), + @JsonProperty("movie") val movie: TheFlixMetadata? = TheFlixMetadata(), + @JsonProperty("recommendationsList" ) val recommendationsList : RecommendationsList? = RecommendationsList(), + @JsonProperty("basePageSegments" ) val basePageSegments : ArrayList? = arrayListOf() + ) + + data class TheFlixMetadata ( + @JsonProperty("episodeRuntime" ) val episodeRuntime : Int? = null, + @JsonProperty("name" ) val name : String? = null, + @JsonProperty("numberOfSeasons" ) val numberOfSeasons : Int? = null, + @JsonProperty("numberOfEpisodes" ) val numberOfEpisodes : Int? = null, + @JsonProperty("originalLanguage" ) val originalLanguage : String? = null, + @JsonProperty("popularity" ) val popularity : Double? = null, + @JsonProperty("status" ) val status : String? = null, + @JsonProperty("voteAverage" ) val voteAverage : Double? = null, + @JsonProperty("voteCount" ) val voteCount : Int? = null, + @JsonProperty("cast" ) val cast : String? = null, + @JsonProperty("director" ) val director : String? = null, + @JsonProperty("overview" ) val overview : String? = null, + @JsonProperty("posterUrl" ) val posterUrl : String? = null, + @JsonProperty("releaseDate" ) val releaseDate : String? = null, + @JsonProperty("createdAt" ) val createdAt : String? = null, + @JsonProperty("updatedAt" ) val updatedAt : String? = null, + @JsonProperty("id" ) val id : Int? = null, + @JsonProperty("available" ) val available : Boolean? = null, + @JsonProperty("genres" ) val genres : ArrayList? = arrayListOf(), + @JsonProperty("seasons" ) val seasons : ArrayList? = arrayListOf(), + @JsonProperty("videos" ) val videos : ArrayList? = arrayListOf(), + @JsonProperty("runtime" ) val runtime : Int? = null, + ) + data class Seasons( + @JsonProperty("name") val name: String? = null, + @JsonProperty("numberOfEpisodes") val numberOfEpisodes: Int? = null, + @JsonProperty("seasonNumber") val seasonNumber: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("posterUrl") val posterUrl: String? = null, + @JsonProperty("releaseDate") val releaseDate: String? = null, + @JsonProperty("createdAt") val createdAt: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("id") val id: Int? = null, + @JsonProperty("episodes") val episodes: ArrayList? = arrayListOf() + ) + + data class Episodes( + @JsonProperty("episodeNumber") val episodeNumber: Int? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("seasonNumber") val seasonNumber: Int? = null, + @JsonProperty("voteAverage") val voteAverage: Double? = null, + @JsonProperty("voteCount") val voteCount: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("releaseDate") val releaseDate: String? = null, + @JsonProperty("createdAt") val createdAt: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("id") val id: Int? = null, + @JsonProperty("videos") val videos: ArrayList? = arrayListOf() + ) + + + data class Genres ( + @JsonProperty("name" ) val name : String? = null, + @JsonProperty("id" ) val id : Int? = null + ) + + data class RuntimeConfig ( + @JsonProperty("AddThisService" ) val AddThisService : RuntimeConfigData? = RuntimeConfigData(), + @JsonProperty("Application" ) val Application : RuntimeConfigData? = RuntimeConfigData(), + @JsonProperty("GtmService" ) val GtmService : RuntimeConfigData? = RuntimeConfigData(), + @JsonProperty("Services" ) val Services : RuntimeConfigData? = RuntimeConfigData(), + ) + + data class RuntimeConfigData( + @JsonProperty("PublicId" ) val PublicId : String? = null, + @JsonProperty("ContentUsageType" ) val ContentUsageType : String? = null, + @JsonProperty("IsDevelopmentMode" ) val IsDevelopmentMode : Boolean? = null, + @JsonProperty("IsDevelopmentOrProductionMode" ) val IsDevelopmentOrProductionMode : Boolean? = null, + @JsonProperty("IsProductionMode" ) val IsProductionMode : Boolean? = null, + @JsonProperty("IsStagingMode" ) val IsStagingMode : Boolean? = null, + @JsonProperty("IsTestMode" ) val IsTestMode : Boolean? = null, + @JsonProperty("Mode" ) val Mode : String? = null, + @JsonProperty("Name" ) val Name : String? = null, + @JsonProperty("Url" ) val Url : String? = null, + @JsonProperty("UseFilterInfoInUrl" ) val UseFilterInfoInUrl : Boolean? = null, + @JsonProperty("TrackingId" ) val TrackingId : String? = null, + @JsonProperty("Server" ) val Server : Server? = Server(), + @JsonProperty("TmdbServer" ) val TmdbServer : TmdbServer? = TmdbServer(), + ) + + data class TmdbServer ( + @JsonProperty("Url" ) val Url : String? = null + ) + + + data class Server ( + @JsonProperty("Url" ) val Url : String? = null + ) + + data class RecommendationsList ( + @JsonProperty("docs" ) val docs : ArrayList = arrayListOf(), + @JsonProperty("total" ) val total : Int? = null, + @JsonProperty("page" ) val page : Int? = null, + @JsonProperty("limit" ) val limit : Int? = null, + @JsonProperty("pages" ) val pages : Int? = null, + @JsonProperty("type" ) val type : String? = null, + ) + + private fun cleanTitle(title: String): String { + val dotTitle = title.substringBefore("/season") + if (dotTitle.contains(Regex("\\..\\."))) { //For titles containing more than two dots (S.W.A.T.) + return (dotTitle.removeSuffix(".") + .replace(" - ", "-") + .replace(".", "-").replace(" ", "-") + .replace("-&", "") + .replace(Regex("(:|-&)"), "") + .replace("'", "-")).lowercase() + } + return (title + .replace(" - ", "-") + .replace(" ", "-") + .replace("-&", "") + .replace("/", "-") + .replace(Regex("(:|-&|\\.)"), "") + .replace("'", "-")).lowercase() + } + + private suspend fun getLoadMan(url: String): LoadMain { + getCookies() + val og = app.get(url, headers = latestCookies) + val soup = og.document + val script = soup.selectFirst("script[type=application/json]")!!.data() + return parseJson(script) + } + + override suspend fun load(url: String): LoadResponse? { + val tvtype = if (url.contains("movie")) TvType.Movie else TvType.TvSeries + val json = getLoadMan(url) + val episodes = ArrayList() + val isMovie = tvtype == TvType.Movie + val pageMain = json.props?.pageProps + + val metadata: TheFlixMetadata? = if (isMovie) pageMain?.movie else pageMain?.selectedTv + + val available = metadata?.available + + val comingsoon = !available!! + + val movieId = metadata.id + + val movietitle = metadata.name + + val poster = metadata.posterUrl + + val description = metadata.overview + + if (!isMovie) { + metadata.seasons?.map { seasons -> + val seasonPoster = seasons.posterUrl ?: metadata.posterUrl + seasons.episodes?.forEach { epi -> + val episodenu = epi.episodeNumber + val seasonum = epi.seasonNumber + val title = epi.name + val epDesc = epi.overview + val test = epi.videos + val ratinginfo = (epi.voteAverage)?.times(10)?.toInt() + val rating = if (ratinginfo?.equals(0) == true) null else ratinginfo + val eps = Episode( + "$mainUrl/tv-show/$movieId-${cleanTitle(movietitle!!)}/season-$seasonum/episode-$episodenu", + title, + seasonum, + episodenu, + description = epDesc!!, + posterUrl = seasonPoster, + rating = rating, + ) + if (test!!.isNotEmpty()) { + episodes.add(eps) + } else { + //Nothing, will prevent seasons/episodes with no videos to be added + } + } + } + } + val rating = metadata.voteAverage?.toFloat()?.times(1000)?.toInt() + + val tags = metadata.genres?.mapNotNull { it.name } + + val recommendationsitem = pageMain?.recommendationsList?.docs?.map { loadDocs -> + val title = loadDocs.name + val posterrec = loadDocs.posterUrl + val link = if (isMovie) "$mainUrl/movie/${loadDocs.id}-${cleanTitle(title)}" + else "$mainUrl/tv-show/${loadDocs.id}-${cleanTitle(title)}/season-1/episode-1" + MovieSearchResponse( + title, + link, + this.name, + tvtype, + posterrec, + year = null + ) + } + + val year = metadata.releaseDate?.substringBefore("-") + + val runtime = metadata.runtime?.div(60) ?: metadata.episodeRuntime?.div(60) + val cast = metadata.cast?.split(",") + + return when (tvtype) { + TvType.TvSeries -> { + return newTvSeriesLoadResponse(movietitle!!, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year?.toIntOrNull() + this.plot = description + this.duration = runtime + addActors(cast) + this.tags = tags + this.recommendations = recommendationsitem + this.comingSoon = comingsoon + this.rating = rating + } + } + TvType.Movie -> { + newMovieLoadResponse(movietitle!!, url, TvType.Movie, url) { + this.year = year?.toIntOrNull() + this.posterUrl = poster + this.plot = description + this.duration = runtime + addActors(cast) + this.tags = tags + this.recommendations = recommendationsitem + this.comingSoon = comingsoon + this.rating = rating + } + } + else -> null + } + } + + + data class VideoData ( + @JsonProperty("url" ) val url : String? = null, + @JsonProperty("id" ) val id : String? = null, + @JsonProperty("type" ) val type : String? = null, + ) + + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val json = getLoadMan(data) + val authhost = json.runtimeConfig?.Services?.Server?.Url + val isMovie = data.contains("/movie/") + val qualityReg = Regex("(\\d+p)") + if (isMovie){ + json.props?.pageProps?.movie?.videos?.apmap { id -> + val jsonmovie = app.get("$authhost/movies/videos/$id/request-access?contentUsageType=Viewing", + headers = latestCookies).parsedSafe() ?: return@apmap false + val extractedlink = jsonmovie.url + if (!extractedlink.isNullOrEmpty()) { + val quality = qualityReg.find(extractedlink)?.value ?: "" + callback( + ExtractorLink( + name, + name, + extractedlink, + "", + getQualityFromName(quality), + false + ) + ) + } else null + } + } + else + { + val dataRegex = Regex("(season-(\\d+)\\/episode-(\\d+))") + val cleandatainfo = dataRegex.find(data)?.value?.replace(Regex("(season-|episode-)"),"")?.replace("/","x") + val tesatt = cleandatainfo.let { str -> + str?.split("x")?.mapNotNull { subStr -> subStr.toIntOrNull() } + } + val epID = tesatt?.getOrNull(1) + val seasonid = tesatt?.getOrNull(0) + json.props?.pageProps?.selectedTv?.seasons?.map { + it.episodes?.map { + val epsInfo = Triple(it.seasonNumber, it.episodeNumber, it.videos) + if (epsInfo.first == seasonid && epsInfo.second == epID) { + epsInfo.third?.apmap { id -> + val jsonserie = app.get("$authhost/tv/videos/$id/request-access?contentUsageType=Viewing", headers = latestCookies).parsedSafe() ?: return@apmap false + val extractedlink = jsonserie.url + if (!extractedlink.isNullOrEmpty()) { + val quality = qualityReg.find(extractedlink)?.value ?: "" + callback( + ExtractorLink( + name, + name, + extractedlink, + "", + getQualityFromName(quality), + false + ) + ) + } else null + } + } + } + } + } + return true + } +} diff --git a/TheFlixToProvider/src/main/kotlin/com/lagradost/TheFlixToProviderPlugin.kt b/TheFlixToProvider/src/main/kotlin/com/lagradost/TheFlixToProviderPlugin.kt new file mode 100644 index 0000000..6eb5cfa --- /dev/null +++ b/TheFlixToProvider/src/main/kotlin/com/lagradost/TheFlixToProviderPlugin.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 TheFlixToProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(TheFlixToProvider()) + } +} \ No newline at end of file diff --git a/VMoveeProvider/build.gradle.kts b/VMoveeProvider/build.gradle.kts new file mode 100644 index 0000000..d983a47 --- /dev/null +++ b/VMoveeProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + + iconUrl = "https://www.google.com/s2/favicons?domain=www.vmovee.watch&sz=%size%" +} \ No newline at end of file diff --git a/VMoveeProvider/src/main/AndroidManifest.xml b/VMoveeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/VMoveeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/VMoveeProvider/src/main/kotlin/com/lagradost/VMoveeProvider.kt b/VMoveeProvider/src/main/kotlin/com/lagradost/VMoveeProvider.kt new file mode 100644 index 0000000..57f1e7a --- /dev/null +++ b/VMoveeProvider/src/main/kotlin/com/lagradost/VMoveeProvider.kt @@ -0,0 +1,125 @@ +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.getQualityFromName +import org.jsoup.Jsoup + +class VMoveeProvider : MainAPI() { + override var name = "VMovee" + override var mainUrl = "https://www.vmovee.watch" + + override val supportedTypes = setOf(TvType.Movie) + + override suspend fun search(query: String): List { + val url = "$mainUrl/?s=$query" + val response = app.get(url).text + val document = Jsoup.parse(response) + val searchItems = document.select("div.search-page > div.result-item > article") + if (searchItems.size == 0) return ArrayList() + val returnValue = ArrayList() + for (item in searchItems) { + val details = item.selectFirst("> div.details") + val imgHolder = item.selectFirst("> div.image > div.thumbnail > a") + // val href = imgHolder.attr("href") + val poster = imgHolder!!.selectFirst("> img")!!.attr("data-lazy-src") + val isTV = imgHolder.selectFirst("> span")!!.text() == "TV" + if (isTV) continue // no TV support yet + + val titleHolder = details!!.selectFirst("> div.title > a") + val title = titleHolder!!.text() + val href = titleHolder.attr("href") + val meta = details.selectFirst("> div.meta") + val year = meta!!.selectFirst("> span.year")!!.text().toIntOrNull() + // val rating = parseRating(meta.selectFirst("> span.rating").text().replace("IMDb ", "")) + // val descript = details.selectFirst("> div.contenido").text() + returnValue.add( + if (isTV) TvSeriesSearchResponse(title, href, this.name, TvType.TvSeries, poster, year, null) + else MovieSearchResponse(title, href, this.name, TvType.Movie, poster, year) + ) + } + return returnValue + } + + data class LoadLinksAjax( + @JsonProperty("embed_url") + val embedUrl: String, + ) + + data class ReeoovAPIData( + @JsonProperty("file") + val file: String, + @JsonProperty("label") + val label: String, + ) + + data class ReeoovAPI( + @JsonProperty("data") + val data: List, + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val url = "$mainUrl/dashboard/admin-ajax.php" + val post = + app.post( + url, + headers = mapOf("referer" to url), + data = mapOf("action" to "doo_player_ajax", "post" to data, "nume" to "2", "type" to "movie") + ).text + + val ajax = parseJson(post) + var realUrl = ajax.embedUrl + if (realUrl.startsWith("//")) { + realUrl = "https:$realUrl" + } + + val request = app.get(realUrl) + val prefix = "https://reeoov.tube/v/" + if (request.url.startsWith(prefix)) { + val apiUrl = "https://reeoov.tube/api/source/${request.url.removePrefix(prefix)}" + val apiResponse = app.post( + apiUrl, + headers = mapOf("Referer" to request.url), + data = mapOf("r" to "https://www.vmovee.watch/", "d" to "reeoov.tube") + ).text + val apiData = parseJson(apiResponse) + for (d in apiData.data) { + callback.invoke( + ExtractorLink( + this.name, + this.name + " " + d.label, + d.file, + "https://reeoov.tube/", + getQualityFromName(d.label), + false + ) + ) + } + } + + return true + } + + override suspend fun load(url: String): LoadResponse { + val response = app.get(url).text + val document = Jsoup.parse(response) + + val sheader = document.selectFirst("div.sheader") + + val poster = sheader!!.selectFirst("> div.poster > img")!!.attr("data-lazy-src") + val data = sheader.selectFirst("> div.data") + val title = data!!.selectFirst("> h1")!!.text() + val descript = document.selectFirst("div#info > div")!!.text() + val id = document.select("div.starstruck").attr("data-id") + + return MovieLoadResponse(title, url, this.name, TvType.Movie, id, poster, null, descript, null, null) + } +} \ No newline at end of file diff --git a/VMoveeProvider/src/main/kotlin/com/lagradost/VMoveeProviderPlugin.kt b/VMoveeProvider/src/main/kotlin/com/lagradost/VMoveeProviderPlugin.kt new file mode 100644 index 0000000..3e60908 --- /dev/null +++ b/VMoveeProvider/src/main/kotlin/com/lagradost/VMoveeProviderPlugin.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 VMoveeProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(VMoveeProvider()) + } +} \ No newline at end of file diff --git a/VidstreamBundle/build.gradle.kts b/VidstreamBundle/build.gradle.kts new file mode 100644 index 0000000..c5fa328 --- /dev/null +++ b/VidstreamBundle/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + description = "Includes many providers with the same layout as Vidstream" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Anime", + "Movie", + "AnimeMovie", + "TvSeries", + ) + + + } \ No newline at end of file diff --git a/VidstreamBundle/src/main/AndroidManifest.xml b/VidstreamBundle/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/VidstreamBundle/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/AsianEmbedHelper.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/AsianEmbedHelper.kt new file mode 100644 index 0000000..f6b71a1 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/AsianEmbedHelper.kt @@ -0,0 +1,32 @@ +package com.lagradost + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor + +class AsianEmbedHelper { + companion object { + suspend fun getUrls( + url: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + // Fetch links + val doc = app.get(url).document + val links = doc.select("div#list-server-more > ul > li.linkserver") + if (!links.isNullOrEmpty()) { + links.apmap { + val datavid = it.attr("data-video") ?: "" + //Log.i("AsianEmbed", "Result => (datavid) ${datavid}") + if (datavid.isNotBlank()) { + val res = loadExtractor(datavid, url, subtitleCallback, callback) + Log.i("AsianEmbed", "Result => ($res) (datavid) $datavid") + } + } + } + } + } +} \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/AsianLoadProvider.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/AsianLoadProvider.kt new file mode 100644 index 0000000..58bafdf --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/AsianLoadProvider.kt @@ -0,0 +1,25 @@ +package com.lagradost + +import com.lagradost.cloudstream3.TvType + +/** Needs to inherit from MainAPI() to + * make the app know what functions to call + */ +class AsianLoadProvider : VidstreamProviderTemplate() { + override var name = "AsianLoad" + override var mainUrl = "https://asianembed.io" + override val homePageUrlList = listOf( + mainUrl, + "$mainUrl/recently-added-raw", + "$mainUrl/movies", + "$mainUrl/kshow", + "$mainUrl/popular", + "$mainUrl/ongoing-series" + ) + + override val iv = "9262859232435825" + override val secretKey = "93422192433952489752342908585752" + override val secretDecryptKey = secretKey + + override val supportedTypes = setOf(TvType.AsianDrama) +} diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/DramaSeeProvider.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/DramaSeeProvider.kt new file mode 100644 index 0000000..47e13e9 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/DramaSeeProvider.kt @@ -0,0 +1,217 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +//import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider.Companion.extractVidstream +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor + +class DramaSeeProvider : MainAPI() { + override var mainUrl = "https://dramasee.net" + override var name = "DramaSee" + override val hasQuickSearch = false + override val hasMainPage = true + override val hasChromecastSupport = false + override val hasDownloadSupport = true + override val supportedTypes = setOf(TvType.AsianDrama) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val headers = mapOf("X-Requested-By" to mainUrl) + val document = app.get(mainUrl, headers = headers).document + val mainbody = document.getElementsByTag("body") + + return HomePageResponse( + mainbody.select("section.block_area.block_area_home")?.map { main -> + val title = main.select("h2.cat-heading").text() ?: "Main" + val inner = main.select("div.flw-item") ?: return@map null + + HomePageList( + title, + inner.mapNotNull { + val innerBody = it?.selectFirst("a") + // Fetch details + val link = fixUrlNull(innerBody?.attr("href")) ?: return@mapNotNull null + val image = fixUrlNull(it.select("img").attr("data-src")) ?: "" + val name = innerBody?.attr("title") ?: "" + //Log.i(this.name, "Result => (innerBody, image) ${innerBody} / ${image}") + MovieSearchResponse( + name, + link, + this.name, + TvType.AsianDrama, + image, + year = null, + id = null, + ) + }.distinctBy { c -> c.url }) + }?.filterNotNull() ?: listOf() + ) + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/search?q=$query" + val document = app.get(url).document + val posters = document.select("div.film-poster") + + + return posters.mapNotNull { + val innerA = it.select("a") ?: return@mapNotNull null + val link = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + val title = innerA.attr("title") ?: return@mapNotNull null + val year = + Regex(""".*\((\d{4})\)""").find(title)?.groupValues?.getOrNull(1)?.toIntOrNull() + val imgSrc = it.select("img")?.attr("data-src") ?: return@mapNotNull null + val image = fixUrlNull(imgSrc) + + MovieSearchResponse( + name = title, + url = link, + apiName = this.name, + type = TvType.Movie, + posterUrl = image, + year = year + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url).document + val body = doc.getElementsByTag("body") + val inner = body?.select("div.anis-content") + + // Video details + val poster = fixUrlNull(inner?.select("img.film-poster-img")?.attr("src")) ?: "" + //Log.i(this.name, "Result => (imgLinkCode) ${imgLinkCode}") + val title = inner?.select("h2.film-name.dynamic-name")?.text() ?: "" + val year = if (title.length > 5) { + title.substring(title.length - 5) + .trim().trimEnd(')').toIntOrNull() + } else { + null + } + //Log.i(this.name, "Result => (year) ${title.substring(title.length - 5)}") + val descript = body?.firstOrNull()?.select("div.film-description.m-hide")?.text() + val tags = inner?.select("div.item.item-list > a") + ?.mapNotNull { it?.text()?.trim() ?: return@mapNotNull null } + val recs = body.select("div.flw-item")?.mapNotNull { + val a = it.select("a") ?: return@mapNotNull null + val aUrl = fixUrlNull(a.attr("href")) ?: return@mapNotNull null + val aImg = fixUrlNull(it.select("img")?.attr("data-src")) + val aName = a.attr("title") ?: return@mapNotNull null + val aYear = aName.trim().takeLast(5).removeSuffix(")").toIntOrNull() + MovieSearchResponse( + url = aUrl, + name = aName, + type = TvType.Movie, + posterUrl = aImg, + year = aYear, + apiName = this.name + ) + } + + // Episodes Links + val episodeUrl = body.select("a.btn.btn-radius.btn-primary.btn-play").attr("href") + val episodeDoc = app.get(episodeUrl).document + + + val episodeList = episodeDoc.select("div.ss-list.ss-list-min > a").mapNotNull { ep -> + val episodeNumber = ep.attr("data-number").toIntOrNull() + val epLink = fixUrlNull(ep.attr("href")) ?: return@mapNotNull null + +// if (epLink.isNotBlank()) { +// // Fetch video links +// val epVidLinkEl = app.get(epLink, referer = mainUrl).document +// val ajaxUrl = epVidLinkEl.select("div#js-player")?.attr("embed") +// //Log.i(this.name, "Result => (ajaxUrl) ${ajaxUrl}") +// if (!ajaxUrl.isNullOrEmpty()) { +// val innerPage = app.get(fixUrl(ajaxUrl), referer = epLink).document +// val listOfLinks = mutableListOf() +// innerPage.select("div.player.active > main > div")?.forEach { em -> +// val href = fixUrlNull(em.attr("src")) ?: "" +// if (href.isNotBlank()) { +// listOfLinks.add(href) +// } +// } +// +// //Log.i(this.name, "Result => (listOfLinks) ${listOfLinks.toJson()}") +// +// } +// } + Episode( + name = null, + season = null, + episode = episodeNumber, + data = epLink, + posterUrl = null, + date = null + ) + } + + //If there's only 1 episode, consider it a movie. + if (episodeList.size == 1) { + return MovieLoadResponse( + name = title, + url = url, + apiName = this.name, + type = TvType.Movie, + dataUrl = episodeList.first().data, + posterUrl = poster, + year = year, + plot = descript, + recommendations = recs, + tags = tags + ) + } + return TvSeriesLoadResponse( + name = title, + url = url, + apiName = this.name, + type = TvType.AsianDrama, + episodes = episodeList, + posterUrl = poster, + year = year, + plot = descript, + recommendations = recs, + tags = tags + ) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + println("DATATATAT $data") + + val document = app.get(data).document + val iframeUrl = document.select("iframe").attr("src") + val iframe = app.get(iframeUrl) + val iframeDoc = iframe.document + + argamap({ + iframeDoc.select(".list-server-items > .linkserver") + .forEach { element -> + val status = element.attr("data-status") ?: return@forEach + if (status != "1") return@forEach + val extractorData = element.attr("data-video") ?: return@forEach + loadExtractor(extractorData, iframe.url, subtitleCallback, callback) + } + }, { + val iv = "9262859232435825" + val secretKey = "93422192433952489752342908585752" + val secretDecryptKey = "93422192433952489752342908585752" + Vidstream.extractVidstream( + iframe.url, + this.name, + callback, + iv, + secretKey, + secretDecryptKey, + isUsingAdaptiveKeys = false, + isUsingAdaptiveData = true, + iframeDocument = iframeDoc + ) + }) + return true + } +} diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/KdramaHoodProvider.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/KdramaHoodProvider.kt new file mode 100644 index 0000000..c68571c --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/KdramaHoodProvider.kt @@ -0,0 +1,294 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +class KdramaHoodProvider : MainAPI() { + override var mainUrl = "https://kdramahood.com" + override var name = "KDramaHood" + override val hasQuickSearch = false + override val hasMainPage = true + override val hasChromecastSupport = false + override val hasDownloadSupport = true + override val supportedTypes = setOf(TvType.AsianDrama) + + private data class ResponseDatas( + @JsonProperty("label") val label: String, + @JsonProperty("file") val file: String + ) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val doc = app.get("$mainUrl/home2").document + val home = ArrayList() + + // Hardcoded homepage cause of site implementation + // Recently added + val recentlyInner = doc.selectFirst("div.peliculas") + val recentlyAddedTitle = recentlyInner!!.selectFirst("h1")?.text() ?: "Recently Added" + val recentlyAdded = recentlyInner.select("div.item_2.items > div.fit.item").mapNotNull { + val innerA = it.select("div.image > a") ?: return@mapNotNull null + val link = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + val image = fixUrlNull(innerA.select("img").attr("src")) + + val innerData = it.selectFirst("div.data") + val title = innerData!!.selectFirst("h1")?.text() ?: return@mapNotNull null + val year = try { + val yearText = innerData.selectFirst("span.titulo_o") + ?.text()?.takeLast(11)?.trim()?.take(4) ?: "" + //Log.i(this.name, "Result => (yearText) $yearText") + val rex = Regex("\\((\\d+)") + //Log.i(this.name, "Result => (rex value) ${rex.find(yearText)?.value}") + rex.find(yearText)?.value?.toIntOrNull() + } catch (e: Exception) { + null + } + + MovieSearchResponse( + name = title, + url = link, + apiName = this.name, + type = TvType.TvSeries, + posterUrl = image, + year = year + ) + }.distinctBy { it.url } ?: listOf() + home.add(HomePageList(recentlyAddedTitle, recentlyAdded)) + return HomePageResponse(home.filter { it.list.isNotEmpty() }) + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/?s=$query" + val html = app.get(url).document + val document = html.getElementsByTag("body") + .select("div.item_1.items > div.item") ?: return listOf() + + return document.mapNotNull { + if (it == null) { + return@mapNotNull null + } + val innerA = it.selectFirst("div.boxinfo > a") ?: return@mapNotNull null + val link = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + val title = innerA.select("span.tt")?.text() ?: return@mapNotNull null + + val year = it.selectFirst("span.year")?.text()?.toIntOrNull() + val image = fixUrlNull(it.selectFirst("div.image > img")?.attr("src")) + + MovieSearchResponse( + name = title, + url = link, + apiName = this.name, + type = TvType.Movie, + posterUrl = image, + year = year + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url).document + val inner = doc.selectFirst("div.central") + + // Video details + val title = inner?.selectFirst("h1")?.text() ?: "" + val poster = fixUrlNull(doc.selectFirst("meta[property=og:image]")?.attr("content")) ?: "" + //Log.i(this.name, "Result => (poster) ${poster}") + val info = inner!!.selectFirst("div#info") + val descript = inner.selectFirst("div.contenidotv > div > p")?.text() + val year = try { + val startLink = "https://kdramahood.com/drama-release-year/" + var res: Int? = null + info?.select("div.metadatac")?.forEach { + if (res != null) { + return@forEach + } + if (it == null) { + return@forEach + } + val yearLink = it.select("a").attr("href") ?: return@forEach + if (yearLink.startsWith(startLink)) { + res = yearLink.substring(startLink.length).replace("/", "").toIntOrNull() + } + } + res + } catch (e: Exception) { + null + } + + val recs = doc.select("div.sidebartv > div.tvitemrel").mapNotNull { + val a = it?.select("a") ?: return@mapNotNull null + val aUrl = fixUrlNull(a.attr("href")) ?: return@mapNotNull null + val aImg = a.select("img") + val aCover = fixUrlNull(aImg.attr("src")) ?: fixUrlNull(aImg.attr("data-src")) + val aNameYear = a.select("div.datatvrel") ?: return@mapNotNull null + val aName = aNameYear.select("h4").text() ?: aImg.attr("alt") ?: return@mapNotNull null + val aYear = aName.trim().takeLast(5).removeSuffix(")").toIntOrNull() + MovieSearchResponse( + url = aUrl, + name = aName, + type = TvType.Movie, + posterUrl = aCover, + year = aYear, + apiName = this.name + ) + } + + // Episodes Links + val episodeList = inner.select("ul.episodios > li")?.mapNotNull { ep -> + //Log.i(this.name, "Result => (ep) ${ep}") + val listOfLinks = mutableListOf() + val count = ep.select("div.numerando")?.text()?.toIntOrNull() ?: 0 + val innerA = ep.select("div.episodiotitle > a") ?: return@mapNotNull null + //Log.i(this.name, "Result => (innerA) ${innerA}") + val epLink = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + //Log.i(this.name, "Result => (epLink) ${epLink}") + if (epLink.isNotBlank()) { + // Fetch video links + val epVidLinkEl = app.get(epLink, referer = mainUrl).document + val epLinksContent = epVidLinkEl.selectFirst("div.player_nav > script")?.html() + ?.replace("ifr_target.src =", "
    ") + ?.replace("';", "
    ") + //Log.i(this.name, "Result => (epLinksContent) $epLinksContent") + if (!epLinksContent.isNullOrEmpty()) { + //Log.i(this.name, "Result => (epLinksContent) ${Jsoup.parse(epLinksContent)?.select("div")}") + Jsoup.parse(epLinksContent)?.select("div")?.forEach { em -> + val href = em?.html()?.trim()?.removePrefix("'") ?: return@forEach + //Log.i(this.name, "Result => (ep#$count link) $href") + if (href.isNotBlank()) { + listOfLinks.add(fixUrl(href)) + } + } + } + //Fetch default source and subtitles + epVidLinkEl.select("div.embed2")?.forEach { defsrc -> + if (defsrc == null) { + return@forEach + } + val scriptstring = defsrc.toString() + if (scriptstring.contains("sources: [{")) { + "(?<=playerInstance2.setup\\()([\\s\\S]*?)(?=\\);)".toRegex() + .find(scriptstring)?.value?.let { itemjs -> + listOfLinks.add("$mainUrl$itemjs") + } + } + } + } + Episode( + name = null, + season = null, + episode = count, + data = listOfLinks.distinct().toJson(), + posterUrl = poster, + date = null + ) + } + + //If there's only 1 episode, consider it a movie. + if (episodeList?.size == 1) { + return MovieLoadResponse( + name = title, + url = url, + apiName = this.name, + type = TvType.Movie, + dataUrl = episodeList[0].data, + posterUrl = poster, + year = year, + plot = descript, + recommendations = recs + ) + } + return TvSeriesLoadResponse( + name = title, + url = url, + apiName = this.name, + type = TvType.AsianDrama, + episodes = episodeList?.reversed() ?: emptyList(), + posterUrl = poster, + year = year, + plot = descript, + recommendations = recs + ) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + var count = 0 + parseJson>(data).apmap { item -> + if (item.isNotBlank()) { + count++ + if (item.startsWith(mainUrl)) { + val text = item.substring(mainUrl.length) + //Log.i(this.name, "Result => (text) $text") + //Find video files + try { + "(?<=sources: )([\\s\\S]*?)(?<=])".toRegex().find(text)?.value?.let { vid -> + parseJson>(vid).forEach { src -> + //Log.i(this.name, "Result => (src) ${src.toJson()}") + callback( + ExtractorLink( + name = name, + url = src.file, + quality = getQualityFromName(src.label), + referer = mainUrl, + source = name + ) + ) + } + } + } catch (e: Exception) { + logError(e) + } + //Find subtitles + try { + "(?<=tracks: )([\\s\\S]*?)(?<=])".toRegex().find(text)?.value?.let { sub -> + val subtext = sub.replace("file:", "\"file\":") + .replace("label:", "\"label\":") + .replace("kind:", "\"kind\":") + parseJson>(subtext).forEach { src -> + //Log.i(this.name, "Result => (sub) ${src.toJson()}") + subtitleCallback( + SubtitleFile( + lang = src.label, + url = src.file + ) + ) + } + } + } catch (e: Exception) { + logError(e) + } + + } else { + val url = fixUrl(item.trim()) + //Log.i(this.name, "Result => (url) $url") + when { + url.startsWith("https://asianembed.io") -> { + com.lagradost.AsianEmbedHelper.getUrls(url, subtitleCallback, callback) + } + url.startsWith("https://embedsito.com") -> { + val extractor = com.lagradost.XStreamCdn() + extractor.domainUrl = "embedsito.com" + extractor.getUrl(url).forEach { link -> + callback.invoke(link) + } + } + else -> { + loadExtractor(url, mainUrl, subtitleCallback, callback) + } + } + } + } + } + return count > 0 + } +} \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/MultiQuality.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/MultiQuality.kt new file mode 100644 index 0000000..2f77415 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/MultiQuality.kt @@ -0,0 +1,59 @@ +package com.lagradost + +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + +class MultiQuality : ExtractorApi() { + override var name = "MultiQuality" + override var mainUrl = "https://gogo-play.net" + private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") + private val m3u8Regex = Regex(""".*?(\d*).m3u8""") + private val urlRegex = Regex("""(.*?)([^/]+$)""") + override val requiresReferer = false + + override fun getExtractorUrl(id: String): String { + return "$mainUrl/loadserver.php?id=$id" + } + + override suspend fun getUrl(url: String, referer: String?): List { + val extractedLinksList: MutableList = mutableListOf() + with(app.get(url)) { + sourceRegex.findAll(this.text).forEach { sourceMatch -> + val extractedUrl = sourceMatch.groupValues[1] + // Trusting this isn't mp4, may fuck up stuff + if (URI(extractedUrl).path.endsWith(".m3u8")) { + with(app.get(extractedUrl)) { + m3u8Regex.findAll(this.text).forEach { match -> + extractedLinksList.add( + ExtractorLink( + name, + name = name, + urlRegex.find(this.url)!!.groupValues[1] + match.groupValues[0], + url, + getQualityFromName(match.groupValues[1]), + isM3u8 = true + ) + ) + } + + } + } else if (extractedUrl.endsWith(".mp4")) { + extractedLinksList.add( + ExtractorLink( + name, + "$name ${sourceMatch.groupValues[2]}", + extractedUrl, + url.replace(" ", "%20"), + Qualities.Unknown.value, + ) + ) + } + } + return extractedLinksList + } + } +} \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/OpenVidsProvider.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/OpenVidsProvider.kt new file mode 100644 index 0000000..150bfe2 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/OpenVidsProvider.kt @@ -0,0 +1,132 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.metaproviders.TmdbLink +import com.lagradost.cloudstream3.metaproviders.TmdbProvider +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class OpenVidsProvider:TmdbProvider() { + override val apiName = "OpenVids" + override var name = "OpenVids" + override var mainUrl = "https://openvids.io" + override val useMetaLoadResponse = true + override val instantLinkLoading = false + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + data class OpenvidsMain( + @JsonProperty("ok" ) val ok : Boolean? = null, + @JsonProperty("servers" ) val servers : OpenvidServers? = OpenvidServers() + ) + + data class OpenvidServers ( + @JsonProperty("streamsb" ) val streamsb : OpenvidServersData? = OpenvidServersData(), + @JsonProperty("voxzer" ) val voxzer : OpenvidServersData? = OpenvidServersData(), + @JsonProperty("mixdrop" ) val mixdrop : OpenvidServersData? = OpenvidServersData(), + @JsonProperty("doodstream" ) val doodstream : OpenvidServersData? = OpenvidServersData(), + @JsonProperty("voe" ) val voe : OpenvidServersData? = OpenvidServersData(), + @JsonProperty("vidcloud" ) val vidcloud : OpenvidServersData? = OpenvidServersData() + ) + data class OpenvidServersData ( + @JsonProperty("code" ) val code : String? = null, + @JsonProperty("updatedAt" ) val updatedAt : String? = null, + @JsonProperty("encoded" ) val encoded : Boolean? = null + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val mappedData = parseJson(data) + val (id, site) = if (mappedData.imdbID != null) listOf( + mappedData.imdbID, + "imdb" + ) else listOf(mappedData.tmdbID.toString(), "tmdb") + val isMovie = mappedData.episode == null && mappedData.season == null + val embedUrl = if (isMovie) { + if(site == "imdb") "$mainUrl/movie/$id" else + "$mainUrl/tmdb/movie/$id" + } else { + val suffix = "$id-${mappedData.season ?: 1}-${mappedData.episode ?: 1}" + if (site == "imdb") "$mainUrl/episode/$suffix" else + "$mainUrl/tmdb/episode/$suffix" + } + val zonedatetime = ZonedDateTime.now() + val timeformated = DateTimeFormatter.ISO_INSTANT.format(zonedatetime) + val headers = if (isMovie) { + mapOf( + "Host" to "openvids.io", + "User-Agent" to USER_AGENT, + "Accept" to "*/*", + "Accept-Language" to "en-US,en;q=0.5", + "Referer" to embedUrl, + "updatedAt" to timeformated, + "title" to "${mappedData.movieName}", + "year" to "2016", + "DNT" to "1", + "Alt-Used" to "openvids.io", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "same-origin", + ) + } else { + mapOf( + "Host" to "openvids.io", + "User-Agent" to USER_AGENT, + "Accept" to "*/*", + "Accept-Language" to "en-US,en;q=0.5", + "Referer" to embedUrl, + "updatedAt" to timeformated, + "title" to "${mappedData.movieName} - season 1", + "year" to "2021", + "e" to "${mappedData.episode}", + "s" to "${mappedData.season}", + "DNT" to "1", + "Alt-Used" to "openvids.io", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "same-origin", + ) + } + val json = app.get("$mainUrl/api/servers.json?imdb=${mappedData.imdbID}", headers = headers).parsedSafe() + + val listservers = listOf( + "https://streamsb.net/e/" to json?.servers?.streamsb?.code, + "https://player.voxzer.org/view/" to json?.servers?.voxzer?.code, + "https://mixdrop.co/e/" to json?.servers?.mixdrop?.code, + "https://dood.pm/e/" to json?.servers?.doodstream?.code, + "https://voe.sx/e/" to json?.servers?.voe?.code, + "https://membed.net/streaming.php?id=" to json?.servers?.vidcloud?.code + ).mapNotNull { (url, id) -> if(id==null) return@mapNotNull null else "$url$id" } + + if (json?.ok != true) return false + listservers.apmap { links -> + if (links.contains("membed")) { + val membed = VidEmbedProvider() + Vidstream.extractVidstream( + links, + this.name, + callback, + membed.iv, + membed.secretKey, + membed.secretDecryptKey, + membed.isUsingAdaptiveKeys, + membed.isUsingAdaptiveData) + } else + loadExtractor(links, data, subtitleCallback, callback) + } + return true + } + +} \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/VidEmbedProvider.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/VidEmbedProvider.kt new file mode 100644 index 0000000..59bd4a5 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/VidEmbedProvider.kt @@ -0,0 +1,30 @@ +package com.lagradost + +import com.lagradost.cloudstream3.TvType + +/** Needs to inherit from MainAPI() to + * make the app know what functions to call + */ +class VidEmbedProvider : VidstreamProviderTemplate() { + // mainUrl is good to have as a holder for the url to make future changes easier. + override var mainUrl = "https://membed.net" + + // name is for how the provider will be named which is visible in the UI, no real rules for this. + override var name = "VidEmbed" + + override val homePageUrlList: List = listOf( + mainUrl, + "$mainUrl/movies", + "$mainUrl/series", + "$mainUrl/recommended-series", + "$mainUrl/cinema-movies" + ) + + override val iv = "9225679083961858" + override val secretKey = "25742532592138496744665879883281" + override val secretDecryptKey = secretKey + + // This is just extra metadata about what type of movies the provider has. + // Needed for search functionality. + override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie) +} diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/Vidstream.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/Vidstream.kt new file mode 100644 index 0000000..ef2f427 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/Vidstream.kt @@ -0,0 +1,234 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.* +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import java.net.URI +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +/** + * overrideMainUrl is necessary for for other vidstream clones like vidembed.cc + * If they diverge it'd be better to make them separate. + * */ +class Vidstream(val mainUrl: String) { + val name: String = "Vidstream" + + companion object { + 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 + ) + + // https://github.com/saikou-app/saikou/blob/3e756bd8e876ad7a9318b17110526880525a5cd3/app/src/main/java/ani/saikou/anime/source/extractors/GogoCDN.kt#L60 + // No Licence on the function + private fun cryptoHandler( + string: String, + iv: String, + secretKeyString: String, + encrypt: Boolean = true + ): String { + //println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") + val ivParameterSpec = IvParameterSpec(iv.toByteArray()) + val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES") + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) + String(cipher.doFinal(base64DecodeArray(string))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) + base64Encode(cipher.doFinal(string.toByteArray())) + } + } + + /** + * @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX + * @param mainApiName used for ExtractorLink names and source + * @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off + * @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off + * @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off + * @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey() + * @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value + * */ + suspend fun extractVidstream( + iframeUrl: String, + mainApiName: String, + callback: (ExtractorLink) -> Unit, + iv: String?, + secretKey: String?, + secretDecryptKey: String?, + // This could be removed, but i prefer it verbose + isUsingAdaptiveKeys: Boolean, + isUsingAdaptiveData: Boolean, + // If you don't want to re-fetch the document + iframeDocument: Document? = null + ) = safeApiCall { + // https://github.com/saikou-app/saikou/blob/3e756bd8e876ad7a9318b17110526880525a5cd3/app/src/main/java/ani/saikou/anime/source/extractors/GogoCDN.kt + // No Licence on the following code + // Also modified of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/gogoanime/src/eu/kanade/tachiyomi/animeextension/en/gogoanime/extractors/GogoCdnExtractor.kt + // License on the code above https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE + + if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys) + return@safeApiCall + + val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=") + + var document: Document? = iframeDocument + val foundIv = + iv ?: (document ?: app.get(iframeUrl).document.also { document = it }) + .select("""div.wrapper[class*=container]""") + .attr("class").split("-").lastOrNull() ?: return@safeApiCall + val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall + val foundDecryptKey = secretDecryptKey ?: foundKey + + val uri = URI(iframeUrl) + val mainUrl = "https://" + uri.host + + val encryptedId = cryptoHandler(id, foundIv, foundKey) + val encryptRequestData = if (isUsingAdaptiveData) { + // Only fetch the document if necessary + val realDocument = document ?: app.get(iframeUrl).document + val dataEncrypted = + realDocument.select("script[data-name='episode']").attr("data-value") + val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false) + "id=$encryptedId&alias=$id&" + headers.substringAfter("&") + } else { + "id=$encryptedId&alias=$id" + } + + val jsonResponse = + app.get( + "$mainUrl/encrypt-ajax.php?$encryptRequestData", + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ) + val dataencrypted = + jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}") + val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false) + val sources = AppUtils.parseJson(datadecrypted) + + 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) + } + } + } + + + private fun getExtractorUrl(id: String): String { + return "$mainUrl/streaming.php?id=$id" + } + + private fun getDownloadUrl(id: String): String { + return "$mainUrl/download?id=$id" + } + + private val normalApis = arrayListOf(MultiQuality()) + + // https://gogo-stream.com/streaming.php?id=MTE3NDg5 + suspend fun getUrl( + id: String, + isCasting: Boolean = false, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ): Boolean { + val extractorUrl = getExtractorUrl(id) + argamap( + { + normalApis.apmap { api -> + val url = api.getExtractorUrl(id) + api.getSafeUrl( + url, + callback = callback, + subtitleCallback = subtitleCallback + ) + } + }, { + /** Stolen from GogoanimeProvider.kt extractor */ + val link = getDownloadUrl(id) + println("Generated vidstream download link: $link") + val page = app.get(link, referer = extractorUrl) + + val pageDoc = Jsoup.parse(page.text) + val qualityRegex = Regex("(\\d+)P") + + //a[download] + pageDoc.select(".dowload > a")?.apmap { element -> + val href = element.attr("href") ?: return@apmap + val qual = if (element.text() + .contains("HDP") + ) "1080" else qualityRegex.find(element.text())?.destructured?.component1() + .toString() + + if (!loadExtractor(href, link, subtitleCallback, callback)) { + callback.invoke( + ExtractorLink( + this.name, + name = this.name, + href, + page.url, + getQualityFromName(qual), + element.attr("href").contains(".m3u8") + ) + ) + } + } + }, { + with(app.get(extractorUrl)) { + val document = Jsoup.parse(this.text) + val primaryLinks = document.select("ul.list-server-items > li.linkserver") + //val extractedLinksList: MutableList = mutableListOf() + + // All vidstream links passed to extractors + primaryLinks.distinctBy { it.attr("data-video") }.forEach { element -> + val link = element.attr("data-video") + //val name = element.text() + + // Matches vidstream links with extractors + extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api -> + if (link.startsWith(api.mainUrl)) { + api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) + } + } + } + } + } + ) + return true + } +} \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/VidstreamBundlePlugin.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/VidstreamBundlePlugin.kt new file mode 100644 index 0000000..e62651b --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/VidstreamBundlePlugin.kt @@ -0,0 +1,26 @@ +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class VidstreamBundlePlugin : Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerExtractorAPI(MultiQuality()) + registerExtractorAPI(XStreamCdn()) + registerExtractorAPI(LayarKaca()) + registerExtractorAPI(DBfilm()) + registerExtractorAPI(Luxubu()) + registerExtractorAPI(FEmbed()) + registerExtractorAPI(Fplayer()) + registerExtractorAPI(FeHD()) + registerMainAPI(VidEmbedProvider()) + registerMainAPI(OpenVidsProvider()) + registerMainAPI(KdramaHoodProvider()) + registerMainAPI(DramaSeeProvider()) + registerMainAPI(AsianLoadProvider()) + registerMainAPI(WatchAsianProvider()) + } +} \ No newline at end of file diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/VidstreamProviderTemplate.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/VidstreamProviderTemplate.kt new file mode 100644 index 0000000..be1bd1e --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/VidstreamProviderTemplate.kt @@ -0,0 +1,337 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +//import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider.Companion.extractVidstream +//import com.lagradost.Vidstream +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.Jsoup +import java.net.URI + +/** Needs to inherit from MainAPI() to + * make the app know what functions to call + */ +open class VidstreamProviderTemplate : MainAPI() { + open val homePageUrlList = listOf() + open val vidstreamExtractorUrl: String? = null + + /** + * Used to generate encrypted video links. + * Try keys from other providers before cracking + * one yourself. + * */ + // Userscript to get the keys: + + /* + // ==UserScript== + // @name Easy keys + // @namespace Violentmonkey Scripts + // @match https://*/streaming.php* + // @grant none + // @version 1.0 + // @author LagradOst + // @description 4/16/2022, 2:05:31 PM + // ==/UserScript== + + let encrypt = CryptoJS.AES.encrypt; + CryptoJS.AES.encrypt = (message, key, cfg) => { + let realKey = CryptoJS.enc.Utf8.stringify(key); + let realIv = CryptoJS.enc.Utf8.stringify(cfg.iv); + + var result = encrypt(message, key, cfg); + let realResult = CryptoJS.enc.Utf8.stringify(result); + + popup = "Encrypt key: " + realKey + "\n\nIV: " + realIv + "\n\nMessage: " + message + "\n\nResult: " + realResult; + alert(popup); + + return result; + }; + + let decrypt = CryptoJS.AES.decrypt; + CryptoJS.AES.decrypt = (message, key, cfg) => { + let realKey = CryptoJS.enc.Utf8.stringify(key); + let realIv = CryptoJS.enc.Utf8.stringify(cfg.iv); + + let result = decrypt(message, key, cfg); + let realResult = CryptoJS.enc.Utf8.stringify(result); + + popup = "Decrypt key: " + realKey + "\n\nIV: " + realIv + "\n\nMessage: " + message + "\n\nResult: " + realResult; + alert(popup); + + return result; + }; + + */ + */ + + open val iv: String? = null + open val secretKey: String? = null + open val secretDecryptKey: String? = null + + /** Generated the key from IV and ID */ + open val isUsingAdaptiveKeys: Boolean = false + + /** + * Generate data for the encrypt-ajax automatically (only on supported sites) + * See $("script[data-name='episode']")[0].dataset.value + * */ + open val isUsingAdaptiveData: Boolean = false + + +// // mainUrl is good to have as a holder for the url to make future changes easier. +// override val mainUrl: String +// get() = "https://vidembed.cc" +// +// // name is for how the provider will be named which is visible in the UI, no real rules for this. +// override val name: String +// get() = "VidEmbed" + + // hasQuickSearch defines if quickSearch() should be called, this is only when typing the searchbar + // gives results on the site instead of bringing you to another page. + // if hasQuickSearch is true and quickSearch() hasn't been overridden you will get errors. + // VidEmbed actually has quick search on their site, but the function wasn't implemented. + override val hasQuickSearch = false + + // If getMainPage() is functional, used to display the homepage in app, an optional, but highly encouraged endevour. + override val hasMainPage = true + + // Searching returns a SearchResponse, which can be one of the following: AnimeSearchResponse, MovieSearchResponse, TorrentSearchResponse, TvSeriesSearchResponse + // Each of the classes requires some different data, but always has some critical things like name, poster and url. + override suspend fun search(query: String): ArrayList { + // Simply looking at devtools network is enough to spot a request like: + // https://vidembed.cc/search.html?keyword=neverland where neverland is the query, can be written as below. + val link = "$mainUrl/search.html?keyword=$query" + val html = app.get(link).text + val soup = Jsoup.parse(html) + + return ArrayList(soup.select(".listing.items > .video-block").map { li -> + // Selects the href in + val href = fixUrl(li.selectFirst("a")!!.attr("href")) + val poster = li.selectFirst("img")?.attr("src") + + // .text() selects all the text in the element, be careful about doing this while too high up in the html hierarchy + val title = li.selectFirst(".name")!!.text() + // Use get(0) and toIntOrNull() to prevent any possible crashes, [0] or toInt() will error the search on unexpected values. + val year = li.selectFirst(".date")?.text()?.split("-")?.get(0)?.toIntOrNull() + + TvSeriesSearchResponse( + // .trim() removes unwanted spaces in the start and end. + if (!title.contains("Episode")) title else title.split("Episode")[0].trim(), + href, + this.name, + TvType.TvSeries, + poster, year, + // You can't get the episodes from the search bar. + null + ) + }) + } + + + // Load, like the name suggests loads the info page, where all the episodes and data usually is. + // Like search you should return either of: AnimeLoadResponse, MovieLoadResponse, TorrentLoadResponse, TvSeriesLoadResponse. + override suspend fun load(url: String): LoadResponse? { + // Gets the url returned from searching. + val html = app.get(url).text + val soup = Jsoup.parse(html) + + var title = soup.selectFirst("h1,h2,h3")!!.text() + title = if (!title.contains("Episode")) title else title.split("Episode")[0].trim() + + val description = soup.selectFirst(".post-entry")?.text()?.trim() + var poster: String? = null + var year: Int? = null + + val episodes = + soup.select(".listing.items.lists > .video-block").withIndex().map { (_, li) -> + val epTitle = if (li.selectFirst(".name") != null) + if (li.selectFirst(".name")!!.text().contains("Episode")) + "Episode " + li.selectFirst(".name")!!.text().split("Episode")[1].trim() + else + li.selectFirst(".name")!!.text() + else "" + val epThumb = li.selectFirst("img")?.attr("src") + val epDate = li.selectFirst(".meta > .date")!!.text() + + if (poster == null) { + poster = li.selectFirst("img")?.attr("onerror")?.split("=")?.get(1) + ?.replace(Regex("[';]"), "") + } + + val epNum = Regex("""Episode (\d+)""").find(epTitle)?.destructured?.component1() + ?.toIntOrNull() + if (year == null) { + year = epDate.split("-")[0].toIntOrNull() + } + newEpisode(li.selectFirst("a")!!.attr("href")) { + this.episode = epNum + this.posterUrl = epThumb + addDate(epDate) + } + }.reversed() + + // Make sure to get the type right to display the correct UI. + val tvType = + if (episodes.size == 1 && episodes[0].name == title) TvType.Movie else TvType.TvSeries + + return when (tvType) { + TvType.TvSeries -> { + TvSeriesLoadResponse( + title, + url, + this.name, + tvType, + episodes, + poster, + year, + description, + ShowStatus.Ongoing, + null, + null + ) + } + TvType.Movie -> { + MovieLoadResponse( + title, + url, + this.name, + tvType, + episodes[0].data, + poster, + year, + description, + null, + null + ) + } + else -> null + } + } + + // This loads the homepage, which is basically a collection of search results with labels. + // Optional function, but make sure to enable hasMainPage if you program this. + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val urls = homePageUrlList + val homePageList = ArrayList() + // .pmap {} is used to fetch the different pages in parallel + urls.apmap { url -> + val response = app.get(url, timeout = 20).text + val document = Jsoup.parse(response) + document.select("div.main-inner").forEach { inner -> + // Always trim your text unless you want the risk of spaces at the start or end. + val title = inner.select(".widget-title").text().trim() + val elements = inner.select(".video-block").map { + val link = fixUrl(it.select("a").attr("href")) + val image = it.select(".picture > img").attr("src") + val name = + it.select("div.name").text().trim().replace(Regex("""[Ee]pisode \d+"""), "") + val isSeries = (name.contains("Season") || name.contains("Episode")) + + if (isSeries) { + newTvSeriesSearchResponse(name, link) { + posterUrl = image + } + } else { + newMovieSearchResponse(name, link) { + posterUrl = image + } + } + } + + homePageList.add( + HomePageList( + title, elements + ) + ) + } + } + return HomePageResponse(homePageList) + } + + // loadLinks gets the raw .mp4 or .m3u8 urls from the data parameter in the episodes class generated in load() + // See Episode(...) in this provider. + // The data are usually links, but can be any other string to help aid loading the links. + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + // These callbacks are functions you should call when you get a link to a subtitle file or media file. + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + // "?: return" is a very useful statement which returns if the iframe link isn't found. + val iframeLink = + Jsoup.parse(app.get(data).text).selectFirst("iframe")?.attr("src") ?: return false + +// extractVidstream( +// iframeLink, +// this.name, +// callback, +// iv, +// secretKey, +// secretDecryptKey, +// isUsingAdaptiveKeys, +// isUsingAdaptiveData +// ) + // In this case the video player is a vidstream clone and can be handled by the vidstream extractor. + // This case is a both unorthodox and you normally do not call extractors as they detect the url returned and does the rest. + val vidstreamObject = Vidstream(vidstreamExtractorUrl ?: mainUrl) + // https://vidembed.cc/streaming.php?id=MzUwNTY2&... -> MzUwNTY2 + val id = Regex("""id=([^&]*)""").find(iframeLink)?.groupValues?.get(1) + + if (id != null) { + vidstreamObject.getUrl(id, isCasting, subtitleCallback, callback) + } + + val html = app.get(fixUrl(iframeLink)).text + val soup = Jsoup.parse(html) + + val servers = soup.select(".list-server-items > .linkserver").mapNotNull { li -> + if (!li?.attr("data-video").isNullOrEmpty()) { + Pair(li.text(), fixUrl(li.attr("data-video"))) + } else { + null + } + } + servers.apmap { + // When checking strings make sure to make them lowercase and trimmed because edgecases like "beta server " wouldn't work otherwise. + if (it.first.trim().equals("beta server", ignoreCase = true)) { + // Group 1: link, Group 2: Label + // Regex can be used to effectively parse small amounts of json without bothering with writing a json class. + val sourceRegex = + Regex("""sources:[\W\w]*?file:\s*["'](.*?)["'][\W\w]*?label:\s*["'](.*?)["']""") + val trackRegex = + Regex("""tracks:[\W\w]*?file:\s*["'](.*?)["'][\W\w]*?label:\s*["'](.*?)["']""") + + // Having a referer is often required. It's a basic security check most providers have. + // Try to replicate what your browser does. + val serverHtml = app.get(it.second, headers = mapOf("referer" to iframeLink)).text + sourceRegex.findAll(serverHtml).forEach { match -> + callback.invoke( + ExtractorLink( + this.name, + match.groupValues.getOrNull(2)?.let { "${this.name} $it" } ?: this.name, + match.groupValues[1], + it.second, + // Useful function to turn something like "1080p" to an app quality. + getQualityFromName(match.groupValues.getOrNull(2) ?: ""), + // Kinda risky + // isM3u8 makes the player pick the correct extractor for the source. + // If isM3u8 is wrong the player will error on that source. + URI(match.groupValues[1]).path.endsWith(".m3u8"), + ) + ) + } + trackRegex.findAll(serverHtml).forEach { match -> + subtitleCallback.invoke( + SubtitleFile( + match.groupValues.getOrNull(2) ?: "Unknown", + match.groupValues[1] + ) + ) + } + } + } + + return true + } +} diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/WatchAsianProvider.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/WatchAsianProvider.kt new file mode 100644 index 0000000..2225ca0 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/WatchAsianProvider.kt @@ -0,0 +1,250 @@ +package com.lagradost + +import com.lagradost.cloudstream3.* +//import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider.Companion.extractVidstream +//import com.lagradost.cloudstream3.extractors.XStreamCdn +//import com.lagradost.cloudstream3.extractors.helper.AsianEmbedHelper +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor + +class WatchAsianProvider : MainAPI() { + override var mainUrl = "https://watchasian.cx" + override var name = "WatchAsian" + override val hasQuickSearch = false + override val hasMainPage = true + override val hasChromecastSupport = false + override val hasDownloadSupport = true + override val supportedTypes = setOf(TvType.AsianDrama) + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val headers = mapOf("X-Requested-By" to mainUrl) + val doc = app.get(mainUrl, headers = headers).document + val rowPair = mutableListOf>() + doc.select("div.block-tab").forEach { + it?.select("ul.tab > li")?.mapNotNull { row -> + val link = row?.attr("data-tab") ?: return@mapNotNull null + val title = row.text() ?: return@mapNotNull null + Pair(title, link) + }?.let { it1 -> + rowPair.addAll( + it1 + ) + } + } + + return HomePageResponse( + rowPair.mapNotNull { row -> + val main = (doc.select("div.tab-content.${row.second}") + ?: doc.select("div.tab-content.${row.second}.selected")) + ?: return@mapNotNull null + + val title = row.first + val inner = main.select("li") ?: return@mapNotNull null + + HomePageList( + title, + inner.map { + // Get inner div from article + val innerBody = it?.selectFirst("a") + // Fetch details + val link = fixUrlNull(innerBody?.attr("href")) ?: return@map null + val image = + fixUrlNull(innerBody?.select("img")?.attr("data-original")) ?: "" + val name = (innerBody?.selectFirst("h3.title")?.text() ?: innerBody?.text()) + ?: "" + //Log.i(this.name, "Result => (innerBody, image) ${innerBody} / ${image}") + MovieSearchResponse( + name, + link, + this.name, + TvType.TvSeries, + image, + year = null, + id = null, + ) + }.filterNotNull().distinctBy { c -> c.url }) + }.filter { a -> a.list.isNotEmpty() } + ) + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/search?type=movies&keyword=$query" + val document = app.get(url).document.getElementsByTag("body") + .select("div.block.tab-container > div > ul > li") ?: return listOf() + + return document.mapNotNull { + val innerA = it?.selectFirst("a") ?: return@mapNotNull null + val link = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + val title = it.select("h3.title").text() ?: return@mapNotNull null + if (title.isEmpty()) { + return@mapNotNull null + } + val year = null + val imgsrc = innerA.select("img").attr("data-original") ?: return@mapNotNull null + val image = fixUrlNull(imgsrc) + //Log.i(this.name, "Result => (img movie) $title / $link") + MovieSearchResponse( + title, + link, + this.name, + TvType.Movie, + image, + year + ) + }.distinctBy { a -> a.url } + } + + override suspend fun load(url: String): LoadResponse { + val body = app.get(url).document + // Declare vars + val isDramaDetail = url.contains("/drama-detail/") + var poster: String? = null + var title = "" + var descript: String? = null + var year: Int? = null + var tags: List? = null + if (isDramaDetail) { + val main = body.select("div.details") + val inner = main.select("div.info") + // Video details + poster = fixUrlNull(main.select("div.img > img").attr("src")) + //Log.i(this.name, "Result => (imgLinkCode) ${imgLinkCode}") + title = inner.select("h1").firstOrNull()?.text() ?: "" + //Log.i(this.name, "Result => (year) ${title.substring(title.length - 5)}") + descript = inner.text() + + inner.select("p").forEach { p -> + val caption = + p?.selectFirst("span")?.text()?.trim()?.lowercase()?.removeSuffix(":")?.trim() + ?: return@forEach + when (caption) { + "genre" -> { + tags = p.select("a").mapNotNull { it?.text()?.trim() } + } + "released" -> { + year = p.select("a").text().trim()?.toIntOrNull() + } + } + } + } else { + poster = body.select("meta[itemprop=\"image\"]")?.attr("content") ?: "" + title = body.selectFirst("div.block.watch-drama")?.selectFirst("h1") + ?.text() ?: "" + year = null + descript = body.select("meta[name=\"description\"]")?.attr("content") + } + //Fallback year from title + if (year == null) { + year = if (title.length > 5) { + title.replace(")", "").replace("(", "").substring(title.length - 5) + .trim().trimEnd(')').toIntOrNull() + } else { + null + } + } + + // Episodes Links + //Log.i(this.name, "Result => (all eps) ${body.select("ul.list-episode-item-2.all-episode > li")}") + val episodeList = body.select("ul.list-episode-item-2.all-episode > li").mapNotNull { ep -> + //Log.i(this.name, "Result => (epA) ${ep.select("a")}") + val innerA = ep.select("a") ?: return@mapNotNull null + //Log.i(this.name, "Result => (innerA) ${fixUrlNull(innerA.attr("href"))}") + val epLink = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + + val regex = "(?<=episode-).*?(?=.html)".toRegex() + val count = regex.find(epLink, mainUrl.length)?.value?.toIntOrNull() ?: 0 + //Log.i(this.name, "Result => $epLink (regexYear) ${count}") + Episode( + name = null, + season = null, + episode = count, + data = epLink, + posterUrl = poster, + date = null + ) + } + //If there's only 1 episode, consider it a movie. + if (episodeList.size == 1) { + //Clean title + title = title.trim().removeSuffix("Episode 1") + val streamlink = getServerLinks(episodeList[0].data) + //Log.i(this.name, "Result => (streamlink) $streamlink") + return MovieLoadResponse( + name = title, + url = url, + apiName = this.name, + type = TvType.Movie, + dataUrl = streamlink, + posterUrl = poster, + year = year, + plot = descript, + tags = tags + ) + } + return TvSeriesLoadResponse( + name = title, + url = url, + apiName = this.name, + type = TvType.AsianDrama, + episodes = episodeList.reversed(), + posterUrl = poster, + year = year, + plot = descript, + tags = tags + ) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val links = if (data.startsWith(mainUrl)) { + getServerLinks(data) + } else { + data + } + var count = 0 + parseJson>(links).apmap { item -> + count++ + val url = fixUrl(item.trim()) + //Log.i(this.name, "Result => (url) $url") + when { + url.startsWith("https://asianembed.io") || url.startsWith("https://asianload.io") || url.contains("/streaming.php?") -> { + val iv = "9262859232435825" + val secretKey = "93422192433952489752342908585752" + Vidstream.extractVidstream( + url, this.name, callback, iv, secretKey, secretKey, + isUsingAdaptiveKeys = false, + isUsingAdaptiveData = false + ) + AsianEmbedHelper.getUrls(url, subtitleCallback, callback) + } + url.startsWith("https://embedsito.com") -> { + val extractor = XStreamCdn() + extractor.domainUrl = "embedsito.com" + extractor.getSafeUrl( + url, + subtitleCallback = subtitleCallback, + callback = callback, + ) + } + else -> { + loadExtractor(url, mainUrl, subtitleCallback, callback) + } + } + } + return count > 0 + } + + private suspend fun getServerLinks(url: String): String { + val moviedoc = app.get(url, referer = mainUrl).document + return moviedoc.select("div.anime_muti_link > ul > li") + .mapNotNull { + fixUrlNull(it?.attr("data-video")) ?: return@mapNotNull null + }.toJson() + } +} diff --git a/VidstreamBundle/src/main/kotlin/com/lagradost/XStreamCdn.kt b/VidstreamBundle/src/main/kotlin/com/lagradost/XStreamCdn.kt new file mode 100644 index 0000000..f6de868 --- /dev/null +++ b/VidstreamBundle/src/main/kotlin/com/lagradost/XStreamCdn.kt @@ -0,0 +1,93 @@ +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 LayarKaca: XStreamCdn() { + override val name: String = "LayarKaca-xxi" + override val mainUrl: String = "https://layarkacaxxi.icu" +} + +class DBfilm: XStreamCdn() { + override val name: String = "DBfilm" + override val mainUrl: String = "https://dbfilm.bar" +} + +class Luxubu : XStreamCdn(){ + override val name: String = "FE" + override val mainUrl: String = "https://www.luxubu.review" +} + +class FEmbed: XStreamCdn() { + override val name: String = "FEmbed" + override val mainUrl: String = "https://www.fembed.com" +} + +class Fplayer: XStreamCdn() { + override val name: String = "Fplayer" + override val mainUrl: String = "https://fplayer.info" +} + +class FeHD: XStreamCdn() { + override val name: String = "FeHD" + override val mainUrl: String = "https://fembed-hd.com" + override var domainUrl: String = "fembed-hd.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/WatchCartoonOnlineProvider/build.gradle.kts b/WatchCartoonOnlineProvider/build.gradle.kts new file mode 100644 index 0000000..7d23b0a --- /dev/null +++ b/WatchCartoonOnlineProvider/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Cartoon", + "Anime", + "TvSeries", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=www.wcostream.com&sz=%size%" +} \ No newline at end of file diff --git a/WatchCartoonOnlineProvider/src/main/AndroidManifest.xml b/WatchCartoonOnlineProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/WatchCartoonOnlineProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/WatchCartoonOnlineProvider/src/main/kotlin/com/lagradost/WatchCartoonOnlineProvider.kt b/WatchCartoonOnlineProvider/src/main/kotlin/com/lagradost/WatchCartoonOnlineProvider.kt new file mode 100644 index 0000000..edb9086 --- /dev/null +++ b/WatchCartoonOnlineProvider/src/main/kotlin/com/lagradost/WatchCartoonOnlineProvider.kt @@ -0,0 +1,270 @@ +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 org.jsoup.Jsoup +import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable +import java.util.* + + +class WatchCartoonOnlineProvider : MainAPI() { + override var name = "WatchCartoonOnline" + override var mainUrl = "https://www.wcostream.com" + + override val supportedTypes = setOf( + TvType.Cartoon, + TvType.Anime, + TvType.AnimeMovie, + TvType.TvSeries + ) + + override suspend fun search(query: String): List { + val url = "https://www.wcostream.com/search" + + var response = + app.post( + url, + headers = mapOf("Referer" to url), + data = mapOf("catara" to query, "konuara" to "series") + ).text + var document = Jsoup.parse(response) + var items = document.select("div#blog > div.cerceve").toList() + + val returnValue = ArrayList() + + for (item in items) { + val header = item.selectFirst("> div.iccerceve") + val titleHeader = header!!.selectFirst("> div.aramadabaslik > a") + val title = titleHeader!!.text() + val href = fixUrl(titleHeader.attr("href")) + val poster = fixUrl(header.selectFirst("> a > img")!!.attr("src")) + val genreText = item.selectFirst("div.cerceve-tur-ve-genre")!!.ownText() + if (genreText.contains("cartoon")) { + returnValue.add(TvSeriesSearchResponse(title, href, this.name, TvType.Cartoon, poster, null, null)) + } else { + val isDubbed = genreText.contains("dubbed") + val set: EnumSet = + EnumSet.of(if (isDubbed) DubStatus.Dubbed else DubStatus.Subbed) + returnValue.add( + AnimeSearchResponse( + title, + href, + this.name, + TvType.Anime, + poster, + null, + set, + ) + ) + } + } + + // "episodes-search", is used for finding movies, anime episodes should be filtered out + response = + app.post( + url, + headers = mapOf("Referer" to url), + data = mapOf("catara" to query, "konuara" to "episodes") + ).text + document = Jsoup.parse(response) + items = document.select("#catlist-listview2 > ul > li") + .filter { it -> it?.text() != null && !it.text().toString().contains("Episode") } + + for (item in items) { + val titleHeader = item.selectFirst("a") + val title = titleHeader!!.text() + val href = fixUrl(titleHeader.attr("href")) + //val isDubbed = title.contains("dubbed") + //val set: EnumSet = + // EnumSet.of(if (isDubbed) DubStatus.Dubbed else DubStatus.Subbed) + returnValue.add( + TvSeriesSearchResponse( + title, + href, + this.name, + TvType.AnimeMovie, + null, + null, + null, + ) + ) + } + + return returnValue + } + + override suspend fun load(url: String): LoadResponse { + val isMovie = !url.contains("/anime/") + val response = app.get(url).text + val document = Jsoup.parse(response) + + return if (!isMovie) { + val title = document.selectFirst("td.vsbaslik > h2")!!.text() + val poster = fixUrlNull(document.selectFirst("div#cat-img-desc > div > img")?.attr("src")) + val plot = document.selectFirst("div.iltext")!!.text() + val genres = document.select("div#cat-genre > div.wcobtn > a").map { it.text() } + val episodes = document.select("div#catlist-listview > ul > li > a").reversed().map { + val text = it.text() + val match = Regex("Season ([0-9]*) Episode ([0-9]*).*? (.*)").find(text) + val href = it.attr("href") + if (match != null) { + val last = match.groupValues[3] + return@map Episode( + href, + if (last.startsWith("English")) null else last, + match.groupValues[1].toIntOrNull(), + match.groupValues[2].toIntOrNull(), + ) + } + val match2 = Regex("Episode ([0-9]*).*? (.*)").find(text) + if (match2 != null) { + val last = match2.groupValues[2] + return@map Episode( + href, + if (last.startsWith("English")) null else last, + null, + match2.groupValues[1].toIntOrNull(), + ) + } + return@map Episode( + href, + text + ) + } + TvSeriesLoadResponse( + title, + url, + this.name, + TvType.TvSeries, + episodes, + poster, + null, + plot, + null, + null, + tags = genres + ) + } else { + val title = document.selectFirst(".iltext .Apple-style-span")?.text().toString() + val b = document.select(".iltext b") + val description = if (b.isNotEmpty()) { + b.last()!!.html().split("
    ")[0] + } else null + + TvSeriesLoadResponse( + title, + url, + this.name, + TvType.TvSeries, + listOf(Episode(url,title)), + null, + null, + description, + null, + null + ) + } + } + + data class LinkResponse( + // @JsonProperty("cdn") + // val cdn: String, + @JsonProperty("enc") + val enc: String, + @JsonProperty("hd") + val hd: String, + @JsonProperty("server") + val server: String, + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val response = app.get(data).text + /*val embedUrl = fixUrl( + Regex("itemprop=\"embedURL\" content=\"(.*?)\"").find(response.text)?.groupValues?.get(1) ?: return false + )*/ + val start = response.indexOf("itemprop=\"embedURL") + val foundJS = Regex("").find(response, start)?.groupValues?.get(1) + ?.replace("document.write", "var returnValue = ") + + val rhino = Context.enter() + rhino.initStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initStandardObjects() + + 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" + + "};" + + rhino.evaluateString(scope, decodeBase64 + foundJS, "JavaScript", 1, null) + val jsEval = scope.get("returnValue", scope) ?: return false + val src = fixUrl(Regex("src=\"(.*?)\"").find(jsEval as String)?.groupValues?.get(1) ?: return false) + + val embedResponse = app.get( + (src), + headers = mapOf("Referer" to data) + ) + + val getVidLink = fixUrl( + Regex("get\\(\"(.*?)\"").find(embedResponse.text)?.groupValues?.get(1) ?: return false + ) + val linkResponse = app.get( + getVidLink, headers = mapOf( + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-ch-ua-mobile" to "?0", + "sec-fetch-dest" to "empty", + "sec-fetch-mode" to "cors", + "sec-fetch-site" to "same-origin", + "accept" to "*/*", + "x-requested-with" to "XMLHttpRequest", + "referer" to src.replace(" ", "%20"), + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "cookie" to "countrytabs=0" + ) + ) + + val link = parseJson(linkResponse.text) + + val hdLink = "${link.server}/getvid?evid=${link.hd}" + val sdLink = "${link.server}/getvid?evid=${link.enc}" + + if (link.hd.isNotBlank()) + callback.invoke( + ExtractorLink( + this.name, + this.name + " HD", + hdLink, + "", + Qualities.P720.value + ) + ) + + if (link.enc.isNotBlank()) + callback.invoke( + ExtractorLink( + this.name, + this.name + " SD", + sdLink, + "", + Qualities.P480.value + ) + ) + + return true + } +} diff --git a/WatchCartoonOnlineProvider/src/main/kotlin/com/lagradost/WatchCartoonOnlineProviderPlugin.kt b/WatchCartoonOnlineProvider/src/main/kotlin/com/lagradost/WatchCartoonOnlineProviderPlugin.kt new file mode 100644 index 0000000..60d5758 --- /dev/null +++ b/WatchCartoonOnlineProvider/src/main/kotlin/com/lagradost/WatchCartoonOnlineProviderPlugin.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 WatchCartoonOnlineProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(WatchCartoonOnlineProvider()) + } +} \ No newline at end of file diff --git a/WcofunProvider/build.gradle.kts b/WcofunProvider/build.gradle.kts new file mode 100644 index 0000000..d8cdd74 --- /dev/null +++ b/WcofunProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + // authors = listOf("Cloudburst") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=www.wcofun.com&sz=%size%" +} \ No newline at end of file diff --git a/WcofunProvider/src/main/AndroidManifest.xml b/WcofunProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/WcofunProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/WcofunProvider/src/main/kotlin/com/lagradost/WcofunProvider.kt b/WcofunProvider/src/main/kotlin/com/lagradost/WcofunProvider.kt new file mode 100644 index 0000000..b89bdcf --- /dev/null +++ b/WcofunProvider/src/main/kotlin/com/lagradost/WcofunProvider.kt @@ -0,0 +1,172 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class WcofunProvider : MainAPI() { + override var mainUrl = "https://www.wcofun.com" + override var name = "WCO Fun" + override val hasMainPage = true + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(mainUrl).document + + val homePageList = ArrayList() + + document.select("div#sidebar_right,div#sidebar_right2").forEach { block -> + val header = block.previousElementSibling()?.ownText() ?: return@forEach + val animes = block.select("ul.items li").mapNotNull { + it.toSearchResult() + } + 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/") + title = when { + (title.contains(Regex("-season-[0-9]+-episode"))) && title.contains("-dubbed") -> title.substringBefore("-season") + (title.contains(Regex("-season-[0-9]+-episode"))) && title.contains("-subbed") -> title.replace(Regex("-season-[0-9]+-episode-[0-9]+"), "") + title.contains("-subbed") -> title.replace(Regex("-episode-[0-9]+"), "") + title.contains("-dubbed") -> title.substringBefore("-episode") + else -> title + } + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val header = this.selectFirst("div.recent-release-episodes a")?.text() + val title = header?.trim() ?: return null + val href = getProperAnimeLink(this.selectFirst("a")!!.attr("href")) + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val epNum = header.let { eps -> + Regex("Episode\\s?([0-9]+)").find(eps)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + val isDub = header.contains("Dubbed") + val isSub = header.contains("Subbed") + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addDubStatus(isDub, isSub, epNum, epNum) + } + } + + override suspend fun search(query: String): List { + val document = app.post( + "$mainUrl/search", + referer = mainUrl, + data = mapOf("catara" to query, "konuara" to "series") + ).document + + return document.select("div#sidebar_right2 li").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + val title = document.selectFirst("div.h1-tag a")?.text() ?: return null + val eps = document.select("div#sidebar_right3 div.cat-eps") + val type = if (eps.size == 1 || eps.first()?.text() + ?.contains(Regex("Episode\\s?[0-9]+")) != true + ) TvType.AnimeMovie else TvType.Anime + val episodes = eps.map { + val name = it.select("a").text() + val link = it.selectFirst("a")!!.attr("href") + Episode(link, name = name) + }.reversed() + + return newAnimeLoadResponse(title, url, type) { + posterUrl = fixUrlNull(document.selectFirst("img.img5")?.attr("src")) + addEpisodes(DubStatus.Subbed, episodes) + plot = document.select("div#sidebar_cat > p").text() + this.tags = document.select("div#sidebar_cat a").map { it.text() } + } + } + + private suspend fun getIframe(url: String): String? { + val document = app.get(url).document + val scriptData = + document.select("script").find { it.data().contains("= \"\";") }?.data() ?: return null + val subtractionNumber = + Regex("""(?<=\.replace\(/\\D/g,''\)\) - )\d+""").find(scriptData)?.value?.toInt() + ?: return null + val html = Regex("""(?<=\["|, ").+?(?=")""").findAll(scriptData).map { + val number = base64Decode(it.value).replace(Regex("\\D"), "").toInt() + (number - subtractionNumber).toChar() + }.joinToString("") + return Jsoup.parse(html).select("iframe").attr("src").let { fixUrl(it) } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + getIframe(data)?.let { iframe -> + val link = app.get(iframe, referer = data).text.let { + fixUrlNull( + Regex("\"(/inc/embed/getvidlink.php.*)\"").find(it)?.groupValues?.getOrNull( + 1 + ) + ) + } + app.get( + link ?: return@let, + referer = iframe, + headers = mapOf("x-requested-with" to "XMLHttpRequest") + ).parsedSafe()?.let { + listOf( + Pair(it.hd, "HD"), + Pair(it.enc, "SD") + ).map { source -> + suspendSafeApiCall { + callback.invoke( + ExtractorLink( + "${this.name} ${source.second}", + "${this.name} ${source.second}", + "${it.server}/getvid?evid=${source.first}", + mainUrl, + if (source.second == "HD") Qualities.P720.value else Qualities.P480.value + ) + ) + } + } + } + } + + return true + } + + data class Sources( + @JsonProperty("enc") val enc: String?, + @JsonProperty("server") val server: String?, + @JsonProperty("cdn") val cdn: String?, + @JsonProperty("hd") val hd: String?, + ) + + +} \ No newline at end of file diff --git a/WcofunProvider/src/main/kotlin/com/lagradost/WcofunProviderPlugin.kt b/WcofunProvider/src/main/kotlin/com/lagradost/WcofunProviderPlugin.kt new file mode 100644 index 0000000..993b06d --- /dev/null +++ b/WcofunProvider/src/main/kotlin/com/lagradost/WcofunProviderPlugin.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 WcofunProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(WcofunProvider()) + } +} \ No newline at end of file