mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Added AsianLoadProvider.kt and made vidstream a template
This commit is contained in:
parent
94f0f30580
commit
068f0f6e0d
12 changed files with 401 additions and 288 deletions
|
@ -43,7 +43,8 @@ object APIHolder {
|
|||
AllMoviesForYouProvider(),
|
||||
AsiaFlixProvider(),
|
||||
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…
Reference in a new issue