diff --git a/README.md b/README.md index 0f4733de..4f63460c 100644 --- a/README.md +++ b/README.md @@ -75,4 +75,5 @@ It merely scrapes 3rd-party websites that are publicly accessable via any regula - [pinoymoviepedia.ru](https://pinoymoviepedia.ru) - [pinoy-hd.xyz](https://www.pinoy-hd.xyz) - [asiaflix.app](https://asiaflix.app) +- [pelisplus.icu](https://pelisplus.icu) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 580cbb04..bc9e21ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -27,6 +27,7 @@ object APIHolder { private const val defProvider = 0 val apis = arrayListOf( + PelisplusProvider(), GogoanimeProvider(), AllAnimeProvider(), //ShiroProvider(), // v2 fucked me @@ -622,4 +623,4 @@ fun MainAPI.newTvSeriesLoadResponse( val builder = TvSeriesLoadResponse(name = name, url = url, apiName = this.name, type = type, episodes = episodes) builder.initializer() return builder -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 454d2dbe..4317a5a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -14,6 +14,11 @@ class DoodSoExtractor : DoodLaExtractor() { override val mainUrl = "https://dood.so" } +class DoodWsExtractor : DoodLaExtractor() { + override val mainUrl = "https://dood.ws" +} + + open class DoodLaExtractor : ExtractorApi() { override val name = "DoodStream" override val mainUrl = "https://dood.la" @@ -49,4 +54,4 @@ open class DoodLaExtractor : ExtractorApi() { return null } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/FeHD.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/FeHD.kt new file mode 100644 index 00000000..d042620d --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/FeHD.kt @@ -0,0 +1,13 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.network.Session +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.mapper + +class FeHD: XStreamCdn() { + override val name: String = "FeHD" + override val mainUrl: String = "https://fembed-hd.com" +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fplayer.kt new file mode 100644 index 00000000..c7ee9cbf --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fplayer.kt @@ -0,0 +1,13 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.network.Session +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.mapper + +class Fplayer: XStreamCdn() { + override val name: String = "Fplayer" + override val mainUrl: String = "https://fplayer.info" +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt new file mode 100644 index 00000000..f8ccc6c7 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt @@ -0,0 +1,98 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.pmap +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.extractorApis +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +/** + * overrideMainUrl is necessary for for other vidstream clones like vidembed.cc + * If they diverge it'd be better to make them separate. + * */ +class Pelisplus(val mainUrl: String) { + val name: String = "Vidstream" + + private fun getExtractorUrl(id: String): String { + return "$mainUrl/play?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 + fun getUrl(id: String, isCasting: Boolean = false, callback: (ExtractorLink) -> Unit): Boolean { + try { + normalApis.pmap { api -> + val url = api.getExtractorUrl(id) + val source = api.getSafeUrl(url) + source?.forEach { callback.invoke(it) } + } + val extractorUrl = getExtractorUrl(id) + + /** Stolen from GogoanimeProvider.kt extractor */ + normalSafeApiCall { + 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")?.pmap { element -> + val href = element.attr("href") ?: return@pmap + val qual = if (element.text() + .contains("HDP") + ) "1080" else qualityRegex.find(element.text())?.destructured?.component1().toString() + + if (!loadExtractor(href, link, callback)) { + callback.invoke( + ExtractorLink( + this.name, + if (qual == "null") this.name else "${this.name} - " + qual + "p", + 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 }.pmap { api -> + if (link.startsWith(api.mainUrl)) { + val extractedLinks = api.getSafeUrl(link, extractorUrl) + if (extractedLinks?.isNotEmpty() == true) { + extractedLinks.forEach { + callback.invoke(it) + } + } + } + } + } + return true + } + } catch (e: Exception) { + return false + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt index df4211b7..92068c80 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt @@ -9,6 +9,10 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getPostForm import org.jsoup.Jsoup +class SBPlay1 : SBPlay() { + override val mainUrl = "https://sbplay1.com" +} + class SBPlay2 : SBPlay() { override val mainUrl = "https://sbplay2.com" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt index 6602e27c..2e052ad9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.getQualityFromName -class WatchSB : ExtractorApi() { +open class WatchSB : ExtractorApi() { override val name: String get() = "WatchSB" override val mainUrl: String @@ -22,7 +22,7 @@ class WatchSB : ExtractorApi() { ) ) - val extractedLinksList = M3u8Helper().m3u8Generation( + return M3u8Helper().m3u8Generation( M3u8Helper.M3u8Stream( response.url, headers = response.headers.toMap() @@ -39,7 +39,5 @@ class WatchSB : ExtractorApi() { true ) } - - return extractedLinksList } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/PelisplusProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/PelisplusProvider.kt new file mode 100644 index 00000000..45000a81 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/PelisplusProvider.kt @@ -0,0 +1,29 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.lagradost.cloudstream3.TvType + +/** Needs to inherit from MainAPI() to + * make the app know what functions to call + */ +class PelisplusProvider : PelisplusProviderTemplate() { + // mainUrl is good to have as a holder for the url to make future changes easier. + override val mainUrl: String + get() = "https://pelisplus.icu" + + // 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() = "Pelisplus" + + override val homePageUrlList: List = listOf( + mainUrl, + "$mainUrl/movies", + "$mainUrl/series", + "$mainUrl/new-season", + "$mainUrl/popular" + ) + + // This is just extra metadata about what type of movies the provider has. + // Needed for search functionality. + override val supportedTypes: Set + get() = setOf(TvType.TvSeries, TvType.Movie) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/PelisplusProviderTemplate.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/PelisplusProviderTemplate.kt new file mode 100644 index 00000000..6ef672e7 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/PelisplusProviderTemplate.kt @@ -0,0 +1,274 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.Pelisplus +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 PelisplusProviderTemplate : MainAPI() { + override val lang = "es" + open val homePageUrlList = listOf() + open val pelisplusExtractorUrl: String? = null + +// // 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 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 = fixUrl(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 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 + + val episodes = soup.select(".listing.items.lists > .video-block").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 = fixUrl(li.selectFirst("img").attr("src")) + val epDate = li.selectFirst(".meta > .date").text() + + if (poster == null) { + poster = li.selectFirst("img")?.attr("onerror")?.replace("//img", "https://img")?.split("=")?.get(1) + ?.replace(Regex("[';]"), "") + } + + val epNum = Regex("""Episode (\d+)""").find(epTitle)?.destructured?.component1()?.toIntOrNull() + + TvSeriesEpisode( + epTitle, + null, + epNum, + fixUrl(li.selectFirst("a").attr("href")), + epThumb, + epDate + ) + }.reversed() + + val year = episodes.first().date?.split("-")?.get(0)?.toIntOrNull() + + // 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 fun getMainPage(): HomePageResponse { + val urls = homePageUrlList + val homePageList = ArrayList() + // .pmap {} is used to fetch the different pages in parallel + urls.pmap { 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").replace("//img", "https://img") + val name = it.select("div.name").text().trim().replace(Regex("""[Ee]pisode \d+"""), "") + val isSeries = (name.contains("Season") || name.contains("Episode")) + + if (isSeries) { + TvSeriesSearchResponse( + name, + link, + this.name, + TvType.TvSeries, + image, + null, + null, + ) + } else { + MovieSearchResponse( + name, + link, + this.name, + TvType.Movie, + image, + null, + null, + ) + } + } + + 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 TvSeriesEpisode(...) in this provider. + // The data are usually links, but can be any other string to help aid loading the links. + override 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(".tab-video")?.attr("data-video") ?: return false + + // 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 = Pelisplus(pelisplusExtractorUrl ?: 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, 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.forEach { + // 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/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5e06721f..cb3a5f69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -81,6 +81,8 @@ val extractorApis: Array = arrayOf( Streamhub(), FEmbed(), + FeHD(), + Fplayer(), WatchSB(), Uqload(), Evoload(), @@ -91,10 +93,12 @@ val extractorApis: Array = arrayOf( DoodToExtractor(), DoodSoExtractor(), DoodLaExtractor(), + DoodWsExtractor(), AsianLoad(), SBPlay(), + SBPlay1(), SBPlay2(), )