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