mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Merge pull request #119 from Blatzar/master
Added AsianLoadProvider.kt and made vidstream a template
This commit is contained in:
		
						commit
						54139ab99c
					
				
					 12 changed files with 401 additions and 288 deletions
				
			
		|  | @ -43,7 +43,8 @@ object APIHolder { | |||
|         AllMoviesForYouProvider(), | ||||
| 
 | ||||
|         VidEmbedProvider(), | ||||
|         VfFilmProvider() | ||||
|         VfFilmProvider(), | ||||
|         AsianLoadProvider(), | ||||
|     ) | ||||
| 
 | ||||
|     val restrictedApis = arrayListOf( | ||||
|  |  | |||
|  | @ -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<ExtractorLink> { | ||||
|         val extractedLinksList: MutableList<ExtractorLink> = 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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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<ExtractorLink> { | ||||
|         val extractedLinksList: MutableList<ExtractorLink> = 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 | ||||
|                                 ) | ||||
|                             ) | ||||
|  |  | |||
|  | @ -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") | ||||
|                         ) | ||||
|                     ) | ||||
|                 } | ||||
|  |  | |||
|  | @ -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") | ||||
| 
 | ||||
|  |  | |||
|  | @ -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") | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|  |  | |||
|  | @ -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<TvType> | ||||
|         get() = setOf(TvType.TvSeries, TvType.Movie) | ||||
| } | ||||
|  | @ -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<String> = 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<TvType> | ||||
|         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<SearchResponse> { | ||||
|         // 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 <a href="..."> | ||||
|             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<HomePageList>() | ||||
|         // .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) | ||||
| } | ||||
|  |  | |||
|  | @ -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<String>() | ||||
|     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<SearchResponse> { | ||||
|         // 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 <a href="..."> | ||||
|             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<HomePageList>() | ||||
|         // .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 | ||||
|     } | ||||
| } | ||||
|  | @ -407,7 +407,7 @@ class HomeFragment : Fragment() { | |||
|         reloadStored() | ||||
|         val apiName = context?.getKey<String>(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) | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -82,6 +82,8 @@ val extractorApis: Array<ExtractorApi> = arrayOf( | |||
|     DoodToExtractor(), | ||||
|     DoodSoExtractor(), | ||||
|     DoodLaExtractor(), | ||||
| 
 | ||||
|     AsianLoad() | ||||
| ) | ||||
| 
 | ||||
| fun getExtractorApiFromName(name: String): ExtractorApi { | ||||
|  |  | |||
|  | @ -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<DownloadedFileInfo>( | ||||
|                     KEY_DOWNLOAD_INFO, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue