From 068f0f6e0d6219e556107c48927355ce5ded63d3 Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sun, 3 Oct 2021 22:23:06 +0200 Subject: [PATCH] Added AsianLoadProvider.kt and made vidstream a template --- .../com/lagradost/cloudstream3/MainAPI.kt | 3 +- .../cloudstream3/extractors/AsianLoad.kt | 60 ++++ .../cloudstream3/extractors/MultiQuality.kt | 18 +- .../cloudstream3/extractors/Streamhub.kt | 3 +- .../cloudstream3/extractors/Vidstream.kt | 7 +- .../movieproviders/AsiaFlixProvider.kt | 3 +- .../movieproviders/AsianLoadProvider.kt | 26 ++ .../movieproviders/VidEmbedProvider.kt | 276 +---------------- .../VidstreamProviderTemplate.kt | 286 ++++++++++++++++++ .../cloudstream3/ui/home/HomeFragment.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 2 + .../utils/VideoDownloadManager.kt | 3 +- 12 files changed, 401 insertions(+), 288 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsianLoadProvider.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidstreamProviderTemplate.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 01717a85..5a533b11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -43,7 +43,8 @@ object APIHolder { AllMoviesForYouProvider(), AsiaFlixProvider(), VidEmbedProvider(), - VfFilmProvider() + VfFilmProvider(), + AsianLoadProvider(), ) val restrictedApis = arrayListOf( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt new file mode 100644 index 00000000..7ad7105c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.network.get +import com.lagradost.cloudstream3.network.text +import com.lagradost.cloudstream3.network.url +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 AsianLoad : ExtractorApi() { + override val name: String + get() = "AsianLoad" + override val mainUrl: String + get() = "https://asianload1.com" + override val requiresReferer: Boolean + get() = true + + private val urlRegex = Regex("""(.*?)([^/]+$)""") + private val m3u8UrlRegex = Regex("""RESOLUTION=\d*x(\d*).*\n(.*\.m3u8)""") + private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""") + override fun getUrl(url: String, referer: String?): List { + val extractedLinksList: MutableList = mutableListOf() + with(get(url, referer = referer)) { + 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(get(extractedUrl, referer = this.url)) { + m3u8UrlRegex.findAll(this.text).forEach { match -> + extractedLinksList.add( + ExtractorLink( + name, + "$name ${match.groupValues[1]}p", + urlRegex.find(this.url)!!.groupValues[1] + match.groupValues[2], + 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/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt index e8045592..84c9ade6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt @@ -6,11 +6,13 @@ import com.lagradost.cloudstream3.network.url 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 val name: String = "MultiQuality" override val mainUrl: String = "https://gogo-play.net" - private val sourceRegex = Regex("""file:\s*'(.*?)',label:\s*'(.*?)'""") + private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") private val m3u8Regex = Regex(""".*?(\d*).m3u8""") private val urlRegex = Regex("""(.*?)([^/]+$)""") override val requiresReferer = false @@ -19,23 +21,13 @@ class MultiQuality : ExtractorApi() { return "$mainUrl/loadserver.php?id=$id" } - private fun getQuality(string: String): Int { - return when (string) { - "360" -> Qualities.P480.value - "480" -> Qualities.P480.value - "720" -> Qualities.P720.value - "1080" -> Qualities.P1080.value - else -> Qualities.Unknown.value - } - } - override fun getUrl(url: String, referer: String?): List { val extractedLinksList: MutableList = mutableListOf() with(get(url)) { sourceRegex.findAll(this.text).forEach { sourceMatch -> val extractedUrl = sourceMatch.groupValues[1] // Trusting this isn't mp4, may fuck up stuff - if (extractedUrl.endsWith(".m3u8")) { + if (URI(extractedUrl).path.endsWith(".m3u8")) { with(get(extractedUrl)) { m3u8Regex.findAll(this.text).forEach { match -> extractedLinksList.add( @@ -44,7 +36,7 @@ class MultiQuality : ExtractorApi() { "$name ${match.groupValues[1]}p", urlRegex.find(this.url)!!.groupValues[1] + match.groupValues[0], url, - getQuality(match.groupValues[1]), + getQualityFromName(match.groupValues[1]), isM3u8 = true ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt index fba18ed3..30a4b20e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt @@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.JsUnpacker import com.lagradost.cloudstream3.utils.Qualities +import java.net.URI class Streamhub : ExtractorApi() { override val mainUrl: String @@ -31,7 +32,7 @@ class Streamhub : ExtractorApi() { link, referer ?: "", Qualities.Unknown.value, - link.endsWith(".m3u8") + URI(link).path.endsWith(".m3u8") ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt index 327b84e6..24e3864f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt @@ -14,9 +14,8 @@ 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 Vidstream(overrideMainUrl: String? = null) { +class Vidstream(val mainUrl: String) { val name: String = "Vidstream" - private val mainUrl: String = overrideMainUrl ?: "https://gogo-stream.com" private fun getExtractorUrl(id: String): String { return "$mainUrl/streaming.php?id=$id" @@ -41,7 +40,9 @@ class Vidstream(overrideMainUrl: String? = null) { /** Stolen from GogoanimeProvider.kt extractor */ normalSafeApiCall { val link = getDownloadUrl(id) - val page = get(link, headers = mapOf("Referer" to extractorUrl)) + println("Generated vidstream download link: $link") + val page = get(link, referer = extractorUrl) + val pageDoc = Jsoup.parse(page.text) val qualityRegex = Regex("(\\d+)P") diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsiaFlixProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsiaFlixProvider.kt index 821f808d..ab7eb7e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsiaFlixProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsiaFlixProvider.kt @@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.network.text import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI class AsiaFlixProvider : MainAPI() { override val mainUrl: String @@ -162,7 +163,7 @@ class AsiaFlixProvider : MainAPI() { it, "https://asianload1.com/", /** <------ This provider should be added instead */ getQualityFromName(it), - it.endsWith(".m3u8") + URI(it).path.endsWith(".m3u8") ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsianLoadProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsianLoadProvider.kt new file mode 100644 index 00000000..69a70601 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AsianLoadProvider.kt @@ -0,0 +1,26 @@ +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 AsianLoadProvider : VidstreamProviderTemplate() { + override val name: String + get() = "AsianLoad" + + override val mainUrl: String + get() = "https://asianload.cc" + + override val homePageUrlList = listOf( + mainUrl, + "$mainUrl/recently-added-raw", + "$mainUrl/movies", + "$mainUrl/kshow", + "$mainUrl/popular", + "$mainUrl/ongoing-series" + ) + + override val supportedTypes: Set + get() = setOf(TvType.TvSeries, TvType.Movie) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidEmbedProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidEmbedProvider.kt index 532e58db..c0fb873f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidEmbedProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidEmbedProvider.kt @@ -13,7 +13,7 @@ import kotlin.collections.ArrayList /** Needs to inherit from MainAPI() to * make the app know what functions to call */ -class VidEmbedProvider : MainAPI() { +class VidEmbedProvider : VidstreamProviderTemplate() { // mainUrl is good to have as a holder for the url to make future changes easier. override val mainUrl: String get() = "https://vidembed.cc" @@ -22,274 +22,16 @@ class VidEmbedProvider : MainAPI() { 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: Boolean - get() = false - - // If getMainPage() is functional, used to display the homepage in app, an optional, but highly encouraged endevour. - override val hasMainPage: Boolean - get() = true - - // Sometimes on sites the urls can be something like "/movie.html" which translates to "*full site url*/movie.html" in the browser - private fun fixUrl(url: String): String { - return if (url.startsWith("//")) { - "https:$url" - } else if (url.startsWith("/")) { - "$mainUrl$url" - } else { - url - } - } + override val homePageUrlList: List = listOf( + mainUrl, + "$mainUrl/movies", + "$mainUrl/series", + "$mainUrl/recommended-series", + "$mainUrl/cinema-movies" + ) // This is just extra metadata about what type of movies the provider has. // Needed for search functionality. override val supportedTypes: Set - get() = setOf(TvType.Anime, TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) - - // 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 = 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 fun load(url: String): LoadResponse? { - // Gets the url returned from searching. - val html = 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").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() - - 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 = listOf( - mainUrl, - "$mainUrl/movies", - "$mainUrl/series", - "$mainUrl/recommended-series", - "$mainUrl/cinema-movies" - ) - val homePageList = ArrayList() - // .pmap {} is used to fetch the different pages in parallel - urls.pmap { url -> - val response = get(url, timeout = 20).text - val document = Jsoup.parse(response) - document.select("div.main-inner")?.forEach { - // Always trim your text unless you want the risk of spaces at the start or end. - val title = it.select(".widget-title").text().trim() - val elements = it.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() - 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(get(data).text).selectFirst("iframe")?.attr("src") ?: 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 = Vidstream("https://vidembed.cc") - // 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 = 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.toLowerCase(Locale.ROOT).trim() == "beta server") { - // 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 = 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. - match.groupValues[1].endsWith(".m3u8"), - ) - ) - } - trackRegex.findAll(serverHtml).forEach { match -> - subtitleCallback.invoke( - SubtitleFile( - match.groupValues.getOrNull(2) ?: "Unknown", - match.groupValues[1] - ) - ) - } - } - } - - return true - } + get() = setOf(TvType.TvSeries, TvType.Movie) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidstreamProviderTemplate.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidstreamProviderTemplate.kt new file mode 100644 index 00000000..5135e37c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VidstreamProviderTemplate.kt @@ -0,0 +1,286 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.Vidstream +import com.lagradost.cloudstream3.network.get +import com.lagradost.cloudstream3.network.text +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 + +// // 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: Boolean + get() = false + + // If getMainPage() is functional, used to display the homepage in app, an optional, but highly encouraged endevour. + override val hasMainPage: Boolean + get() = true + + // Sometimes on sites the urls can be something like "/movie.html" which translates to "*full site url*/movie.html" in the browser + private fun fixUrl(url: String): String { + return if (url.startsWith("//")) { + "https:$url" + } else if (url.startsWith("/")) { + "$mainUrl$url" + } else { + url + } + } + + // 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 = 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 fun load(url: String): LoadResponse? { + // Gets the url returned from searching. + val html = 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").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() + + 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 = get(url, timeout = 20).text + val document = Jsoup.parse(response) + document.select("div.main-inner")?.forEach { + // Always trim your text unless you want the risk of spaces at the start or end. + val title = it.select(".widget-title").text().trim() + val elements = it.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) { + 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(get(data).text).selectFirst("iframe")?.attr("src") ?: 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 = 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, callback) + } + + val html = 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 = 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/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index aea6d9dc..9a347d2b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -407,7 +407,7 @@ class HomeFragment : Fragment() { reloadStored() val apiName = context?.getKey(HOMEPAGE_API) if(homeViewModel.apiName.value != apiName) { - println("COUGHT HOME : " + homeViewModel.apiName.value + " AT " + apiName) + println("Caught home: " + homeViewModel.apiName.value + " at " + apiName) homeViewModel.loadAndCancel(apiName) } } 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 35298300..8d67bb01 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -82,6 +82,8 @@ val extractorApis: Array = arrayOf( DoodToExtractor(), DoodSoExtractor(), DoodLaExtractor(), + + AsianLoad() ) fun getExtractorApiFromName(name: String): ExtractorApi { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index fa5468e6..453da090 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import java.io.* import java.lang.Thread.sleep +import java.net.URI import java.net.URL import java.net.URLConnection import java.util.* @@ -1110,7 +1111,7 @@ object VideoDownloadManager { ): Int { val name = sanitizeFilename(ep.name ?: "${context.getString(R.string.episode)} ${ep.episode}") - if (link.isM3u8 || link.url.endsWith(".m3u8")) { + if (link.isM3u8 || URI(link.url).path.endsWith(".m3u8")) { val startIndex = if (tryResume) { context.getKey( KEY_DOWNLOAD_INFO,