really added sflix
This commit is contained in:
parent
69773a66e4
commit
4e5b31404a
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 2
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
description = "Also includes Dopebox, Solarmovie, Zoro and 2embed"
|
||||
// authors = listOf("Cloudburst")
|
||||
|
||||
/**
|
||||
* Status int as the following:
|
||||
* 0: Down
|
||||
* 1: Ok
|
||||
* 2: Slow
|
||||
* 3: Beta only
|
||||
* */
|
||||
status = 1 // will be 3 if unspecified
|
||||
tvTypes = listOf(
|
||||
"TvSeries",
|
||||
"Movie",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=www.2embed.to&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,6 @@
|
|||
package com.lagradost
|
||||
|
||||
class DopeboxProvider : SflixProvider() {
|
||||
override var mainUrl = "https://dopebox.to"
|
||||
override var name = "Dopebox"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.lagradost
|
||||
|
||||
class HDTodayProvider : SflixProvider() {
|
||||
override var mainUrl = "https://hdtoday.cc"
|
||||
override var name = "HDToday"
|
||||
}
|
|
@ -0,0 +1,768 @@
|
|||
package com.lagradost
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
//import com.lagradost.cloudstream3.animeproviders.ZoroProvider
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
open class SflixProvider : MainAPI() {
|
||||
override var mainUrl = "https://sflix.to"
|
||||
override var name = "Sflix.to"
|
||||
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val usesWebView = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
override val vpnStatus = VPNStatus.None
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val html = app.get("$mainUrl/home").text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
val all = ArrayList<HomePageList>()
|
||||
|
||||
val map = mapOf(
|
||||
"Trending Movies" to "div#trending-movies",
|
||||
"Trending TV Shows" to "div#trending-tv",
|
||||
)
|
||||
map.forEach {
|
||||
all.add(HomePageList(
|
||||
it.key,
|
||||
document.select(it.value).select("div.flw-item").map { element ->
|
||||
element.toSearchResult()
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
document.select("section.block_area.block_area_home.section-id-02").forEach {
|
||||
val title = it.select("h2.cat-heading").text().trim()
|
||||
val elements = it.select("div.flw-item").map { element ->
|
||||
element.toSearchResult()
|
||||
}
|
||||
all.add(HomePageList(title, elements))
|
||||
}
|
||||
|
||||
return HomePageResponse(all)
|
||||
}
|
||||
|
||||
private data class TmdbProviderSearchFilter(
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("tmdbYear") val tmdbYear: Int?,
|
||||
@JsonProperty("tmdbPlot") val tmdbPlot: String?,
|
||||
@JsonProperty("duration") val duration: Int?,
|
||||
@JsonProperty("type") val type: TvType?,
|
||||
)
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val parsedFilter = tryParseJson<TmdbProviderSearchFilter>(query)
|
||||
val searchedTitle = parsedFilter?.title ?: throw ErrorLoadingException()
|
||||
val url = "$mainUrl/search/${searchedTitle.replace(" ", "-")}"
|
||||
val html = app.get(url).text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
val output = document.select("div.flw-item").mapNotNull {
|
||||
val title = it.select("h2.film-name").text()
|
||||
val href = fixUrl(it.select("a").attr("href"))
|
||||
val year = it.selectFirst("span.fdi-item:not(:has(i)):not(:has(strong))")?.text()?.toIntOrNull()
|
||||
if (year != parsedFilter.tmdbYear) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val image = it.select("img").attr("data-src")
|
||||
val isMovie = href.contains("/movie/")
|
||||
|
||||
val metaInfo = it.select("div.fd-infor > span.fdi-item")
|
||||
// val rating = metaInfo[0].text()
|
||||
val quality = getQualityFromString(metaInfo.getOrNull(1)?.text())
|
||||
|
||||
if (isMovie) {
|
||||
MovieSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
image,
|
||||
year,
|
||||
quality = quality
|
||||
)
|
||||
} else {
|
||||
if (!isMovie) {
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
image,
|
||||
year,
|
||||
null,
|
||||
quality = quality
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val document = app.get(url).document
|
||||
|
||||
val details = document.select("div.detail_page-watch")
|
||||
val img = details.select("img.film-poster-img")
|
||||
val posterUrl = img.attr("src")
|
||||
val title = img.attr("title") ?: throw ErrorLoadingException("No Title")
|
||||
|
||||
/*
|
||||
val year = Regex("""[Rr]eleased:\s*(\d{4})""").find(
|
||||
document.select("div.elements").text()
|
||||
)?.groupValues?.get(1)?.toIntOrNull()
|
||||
val duration = Regex("""[Dd]uration:\s*(\d*)""").find(
|
||||
document.select("div.elements").text()
|
||||
)?.groupValues?.get(1)?.trim()?.plus(" min")*/
|
||||
var duration = document.selectFirst(".fs-item > .duration")?.text()?.trim()
|
||||
var year: Int? = null
|
||||
var tags: List<String>? = null
|
||||
var cast: List<String>? = null
|
||||
val youtubeTrailer = document.selectFirst("iframe#iframe-trailer")?.attr("data-src")
|
||||
val rating = document.selectFirst(".fs-item > .imdb")?.text()?.trim()
|
||||
?.removePrefix("IMDB:")?.toRatingInt()
|
||||
|
||||
document.select("div.elements > .row > div > .row-line").forEach { element ->
|
||||
val type = element?.select(".type")?.text() ?: return@forEach
|
||||
when {
|
||||
type.contains("Released") -> {
|
||||
year = Regex("\\d+").find(
|
||||
element.ownText() ?: return@forEach
|
||||
)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||
}
|
||||
type.contains("Genre") -> {
|
||||
tags = element.select("a").mapNotNull { it.text() }
|
||||
}
|
||||
type.contains("Cast") -> {
|
||||
cast = element.select("a").mapNotNull { it.text() }
|
||||
}
|
||||
type.contains("Duration") -> {
|
||||
duration = duration ?: element.ownText().trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
val plot = details.select("div.description").text().replace("Overview:", "").trim()
|
||||
|
||||
val isMovie = url.contains("/movie/")
|
||||
|
||||
// https://sflix.to/movie/free-never-say-never-again-hd-18317 -> 18317
|
||||
val idRegex = Regex(""".*-(\d+)""")
|
||||
val dataId = details.attr("data-id")
|
||||
val id = if (dataId.isNullOrEmpty())
|
||||
idRegex.find(url)?.groupValues?.get(1)
|
||||
?: throw ErrorLoadingException("Unable to get id from '$url'")
|
||||
else dataId
|
||||
|
||||
val recommendations =
|
||||
document.select("div.film_list-wrap > div.flw-item").mapNotNull { element ->
|
||||
val titleHeader =
|
||||
element.select("div.film-detail > .film-name > a") ?: return@mapNotNull null
|
||||
val recUrl = fixUrlNull(titleHeader.attr("href")) ?: return@mapNotNull null
|
||||
val recTitle = titleHeader.text() ?: return@mapNotNull null
|
||||
val poster = element.select("div.film-poster > img").attr("data-src")
|
||||
MovieSearchResponse(
|
||||
recTitle,
|
||||
recUrl,
|
||||
this.name,
|
||||
if (recUrl.contains("/movie/")) TvType.Movie else TvType.TvSeries,
|
||||
poster,
|
||||
year = null
|
||||
)
|
||||
}
|
||||
|
||||
if (isMovie) {
|
||||
// Movies
|
||||
val episodesUrl = "$mainUrl/ajax/movie/episodes/$id"
|
||||
val episodes = app.get(episodesUrl).text
|
||||
|
||||
// Supported streams, they're identical
|
||||
val sourceIds = Jsoup.parse(episodes).select("a").mapNotNull { element ->
|
||||
var sourceId = element.attr("data-id")
|
||||
if (sourceId.isNullOrEmpty())
|
||||
sourceId = element.attr("data-linkid")
|
||||
|
||||
if (element.select("span").text().trim().isValidServer()) {
|
||||
if (sourceId.isNullOrEmpty()) {
|
||||
fixUrlNull(element.attr("href"))
|
||||
} else {
|
||||
"$url.$sourceId".replace("/movie/", "/watch-movie/")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val comingSoon = sourceIds.isEmpty()
|
||||
|
||||
return newMovieLoadResponse(title, url, TvType.Movie, sourceIds) {
|
||||
this.year = year
|
||||
this.posterUrl = posterUrl
|
||||
this.plot = plot
|
||||
addDuration(duration)
|
||||
addActors(cast)
|
||||
this.tags = tags
|
||||
this.recommendations = recommendations
|
||||
this.comingSoon = comingSoon
|
||||
addTrailer(youtubeTrailer)
|
||||
this.rating = rating
|
||||
}
|
||||
} else {
|
||||
val seasonsDocument = app.get("$mainUrl/ajax/v2/tv/seasons/$id").document
|
||||
val episodes = arrayListOf<Episode>()
|
||||
var seasonItems = seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a")
|
||||
if (seasonItems.isNullOrEmpty())
|
||||
seasonItems = seasonsDocument.select("div.dropdown-menu > a.dropdown-item")
|
||||
seasonItems.apmapIndexed { season, element ->
|
||||
val seasonId = element.attr("data-id")
|
||||
if (seasonId.isNullOrBlank()) return@apmapIndexed
|
||||
|
||||
var episode = 0
|
||||
val seasonEpisodes = app.get("$mainUrl/ajax/v2/season/episodes/$seasonId").document
|
||||
var seasonEpisodesItems =
|
||||
seasonEpisodes.select("div.flw-item.film_single-item.episode-item.eps-item")
|
||||
if (seasonEpisodesItems.isNullOrEmpty()) {
|
||||
seasonEpisodesItems =
|
||||
seasonEpisodes.select("ul > li > a")
|
||||
}
|
||||
seasonEpisodesItems.forEach {
|
||||
val episodeImg = it?.select("img")
|
||||
val episodeTitle = episodeImg?.attr("title") ?: it.ownText()
|
||||
val episodePosterUrl = episodeImg?.attr("src")
|
||||
val episodeData = it.attr("data-id") ?: return@forEach
|
||||
|
||||
episode++
|
||||
|
||||
val episodeNum =
|
||||
(it.select("div.episode-number").text()
|
||||
?: episodeTitle).let { str ->
|
||||
Regex("""\d+""").find(str)?.groupValues?.firstOrNull()
|
||||
?.toIntOrNull()
|
||||
} ?: episode
|
||||
|
||||
episodes.add(
|
||||
newEpisode(Pair(url, episodeData)) {
|
||||
this.posterUrl = fixUrlNull(episodePosterUrl)
|
||||
this.name = episodeTitle?.removePrefix("Episode $episodeNum: ")
|
||||
this.season = season + 1
|
||||
this.episode = episodeNum
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) {
|
||||
this.posterUrl = posterUrl
|
||||
this.year = year
|
||||
this.plot = plot
|
||||
addDuration(duration)
|
||||
addActors(cast)
|
||||
this.tags = tags
|
||||
this.recommendations = recommendations
|
||||
addTrailer(youtubeTrailer)
|
||||
this.rating = rating
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Tracks(
|
||||
@JsonProperty("file") val file: String?,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("kind") val kind: String?
|
||||
)
|
||||
|
||||
data class Sources(
|
||||
@JsonProperty("file") val file: String?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("label") val label: String?
|
||||
)
|
||||
|
||||
data class SourceObject(
|
||||
@JsonProperty("sources") val sources: List<Sources?>?,
|
||||
@JsonProperty("sources_1") val sources1: List<Sources?>?,
|
||||
@JsonProperty("sources_2") val sources2: List<Sources?>?,
|
||||
@JsonProperty("sourcesBackup") val sourcesBackup: List<Sources?>?,
|
||||
@JsonProperty("tracks") val tracks: List<Tracks?>?
|
||||
)
|
||||
|
||||
data class IframeJson(
|
||||
// @JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("link") val link: String? = null,
|
||||
// @JsonProperty("sources") val sources: ArrayList<String> = arrayListOf(),
|
||||
// @JsonProperty("tracks") val tracks: ArrayList<String> = arrayListOf(),
|
||||
// @JsonProperty("title") val title: String? = null
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val urls = (tryParseJson<Pair<String, String>>(data)?.let { (prefix, server) ->
|
||||
val episodesUrl = "$mainUrl/ajax/v2/episode/servers/$server"
|
||||
|
||||
// Supported streams, they're identical
|
||||
app.get(episodesUrl).document.select("a").mapNotNull { element ->
|
||||
val id = element?.attr("data-id") ?: return@mapNotNull null
|
||||
if (element.select("span").text().trim().isValidServer()) {
|
||||
"$prefix.$id".replace("/tv/", "/watch-tv/")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} ?: tryParseJson<List<String>>(data))?.distinct()
|
||||
|
||||
urls?.apmap { url ->
|
||||
suspendSafeApiCall {
|
||||
// Possible without token
|
||||
|
||||
// val response = app.get(url)
|
||||
// val key =
|
||||
// response.document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||
// .attr("src").substringAfter("render=")
|
||||
// val token = getCaptchaToken(mainUrl, key) ?: return@suspendSafeApiCall
|
||||
|
||||
val serverId = url.substringAfterLast(".")
|
||||
val iframeLink =
|
||||
app.get("${this.mainUrl}/ajax/get_link/$serverId").parsed<IframeJson>().link
|
||||
?: return@suspendSafeApiCall
|
||||
|
||||
// Some smarter ws11 or w10 selection might be required in the future.
|
||||
val extractorData =
|
||||
"https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling"
|
||||
|
||||
if (iframeLink.contains("streamlare", ignoreCase = true)) {
|
||||
loadExtractor(iframeLink, null, subtitleCallback, callback)
|
||||
} else {
|
||||
extractRabbitStream(iframeLink, subtitleCallback, callback, false) { it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !urls.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override suspend fun extractorVerifierJob(extractorData: String?) {
|
||||
runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
|
||||
}
|
||||
|
||||
private fun Element.toSearchResult(): SearchResponse {
|
||||
val inner = this.selectFirst("div.film-poster")
|
||||
val img = inner!!.select("img")
|
||||
val title = img.attr("title")
|
||||
val posterUrl = img.attr("data-src") ?: img.attr("src")
|
||||
val href = fixUrl(inner.select("a").attr("href"))
|
||||
val isMovie = href.contains("/movie/")
|
||||
val otherInfo =
|
||||
this.selectFirst("div.film-detail > div.fd-infor")?.select("span")?.toList() ?: listOf()
|
||||
//var rating: Int? = null
|
||||
var year: Int? = null
|
||||
var quality: SearchQuality? = null
|
||||
when (otherInfo.size) {
|
||||
1 -> {
|
||||
year = otherInfo[0]?.text()?.trim()?.toIntOrNull()
|
||||
}
|
||||
2 -> {
|
||||
year = otherInfo[0]?.text()?.trim()?.toIntOrNull()
|
||||
}
|
||||
3 -> {
|
||||
//rating = otherInfo[0]?.text()?.toRatingInt()
|
||||
quality = getQualityFromString(otherInfo[1]?.text())
|
||||
year = otherInfo[2]?.text()?.trim()?.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
return if (isMovie) {
|
||||
MovieSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this@SflixProvider.name,
|
||||
TvType.Movie,
|
||||
posterUrl = posterUrl,
|
||||
year = year,
|
||||
quality = quality,
|
||||
)
|
||||
} else {
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this@SflixProvider.name,
|
||||
TvType.Movie,
|
||||
posterUrl,
|
||||
year = year,
|
||||
episodes = null,
|
||||
quality = quality,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
data class PollingData(
|
||||
@JsonProperty("sid") val sid: String? = null,
|
||||
@JsonProperty("upgrades") val upgrades: ArrayList<String> = arrayListOf(),
|
||||
@JsonProperty("pingInterval") val pingInterval: Int? = null,
|
||||
@JsonProperty("pingTimeout") val pingTimeout: Int? = null
|
||||
)
|
||||
|
||||
/*
|
||||
# python code to figure out the time offset based on code if necessary
|
||||
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
|
||||
code = "Nxa_-bM"
|
||||
total = 0
|
||||
for i, char in enumerate(code[::-1]):
|
||||
index = chars.index(char)
|
||||
value = index * 64**i
|
||||
total += value
|
||||
print(f"total {total}")
|
||||
*/
|
||||
private fun generateTimeStamp(): String {
|
||||
val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
|
||||
var code = ""
|
||||
var time = unixTimeMS
|
||||
while (time > 0) {
|
||||
code += chars[(time % (chars.length)).toInt()]
|
||||
time /= chars.length
|
||||
}
|
||||
return code.reversed()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a session
|
||||
* 1 Get request.
|
||||
* */
|
||||
private suspend fun negotiateNewSid(baseUrl: String): PollingData? {
|
||||
// Tries multiple times
|
||||
for (i in 1..5) {
|
||||
val jsonText =
|
||||
app.get("$baseUrl&t=${generateTimeStamp()}").text.replaceBefore("{", "")
|
||||
// println("Negotiated sid $jsonText")
|
||||
parseJson<PollingData?>(jsonText)?.let { return it }
|
||||
delay(1000L * i)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new session if the request fails
|
||||
* @return the data and if it is new.
|
||||
* */
|
||||
private suspend fun getUpdatedData(
|
||||
response: NiceResponse,
|
||||
data: PollingData,
|
||||
baseUrl: String
|
||||
): Pair<PollingData, Boolean> {
|
||||
if (!response.okhttpResponse.isSuccessful) {
|
||||
return negotiateNewSid(baseUrl)?.let {
|
||||
it to true
|
||||
} ?: data to false
|
||||
}
|
||||
return data to false
|
||||
}
|
||||
|
||||
|
||||
private suspend fun initPolling(
|
||||
extractorData: String,
|
||||
referer: String
|
||||
): Pair<PollingData?, String?> {
|
||||
val headers = mapOf(
|
||||
"Referer" to referer // "https://rabbitstream.net/"
|
||||
)
|
||||
|
||||
val data = negotiateNewSid(extractorData) ?: return null to null
|
||||
app.post(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
||||
requestBody = "40".toRequestBody(),
|
||||
headers = headers
|
||||
)
|
||||
|
||||
// This makes the second get request work, and re-connect work.
|
||||
val reconnectSid =
|
||||
parseJson<PollingData>(
|
||||
app.get(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
||||
headers = headers
|
||||
)
|
||||
// .also { println("First get ${it.text}") }
|
||||
.text.replaceBefore("{", "")
|
||||
).sid
|
||||
|
||||
// This response is used in the post requests. Same contents in all it seems.
|
||||
val authInt =
|
||||
app.get(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
||||
timeout = 60,
|
||||
headers = headers
|
||||
).text
|
||||
//.also { println("Second get ${it}") }
|
||||
// Dunno if it's actually generated like this, just guessing.
|
||||
.toIntOrNull()?.plus(1) ?: 3
|
||||
|
||||
return data to reconnectSid
|
||||
}
|
||||
|
||||
suspend fun runSflixExtractorVerifierJob(
|
||||
api: MainAPI,
|
||||
extractorData: String?,
|
||||
referer: String
|
||||
) {
|
||||
if (extractorData == null) return
|
||||
val headers = mapOf(
|
||||
"Referer" to referer // "https://rabbitstream.net/"
|
||||
)
|
||||
|
||||
lateinit var data: PollingData
|
||||
var reconnectSid = ""
|
||||
|
||||
initPolling(extractorData, referer)
|
||||
.also {
|
||||
data = it.first ?: throw RuntimeException("Data Null")
|
||||
reconnectSid = it.second ?: throw RuntimeException("ReconnectSid Null")
|
||||
}
|
||||
|
||||
// Prevents them from fucking us over with doing a while(true){} loop
|
||||
val interval = maxOf(data.pingInterval?.toLong()?.plus(2000) ?: return, 10000L)
|
||||
var reconnect = false
|
||||
var newAuth = false
|
||||
|
||||
|
||||
while (true) {
|
||||
val authData =
|
||||
when {
|
||||
newAuth -> "40"
|
||||
reconnect -> """42["_reconnect", "$reconnectSid"]"""
|
||||
else -> "3"
|
||||
}
|
||||
|
||||
val url = "${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}"
|
||||
|
||||
getUpdatedData(
|
||||
app.post(url, json = authData, headers = headers),
|
||||
data,
|
||||
extractorData
|
||||
).also {
|
||||
newAuth = it.second
|
||||
data = it.first
|
||||
}
|
||||
|
||||
//.also { println("Sflix post job ${it.text}") }
|
||||
Log.d(api.name, "Running ${api.name} job $url")
|
||||
|
||||
val time = measureTimeMillis {
|
||||
// This acts as a timeout
|
||||
val getResponse = app.get(
|
||||
url,
|
||||
timeout = interval / 1000,
|
||||
headers = headers
|
||||
)
|
||||
// .also { println("Sflix get job ${it.text}") }
|
||||
reconnect = getResponse.text.contains("sid")
|
||||
}
|
||||
// Always waits even if the get response is instant, to prevent a while true loop.
|
||||
if (time < interval - 4000)
|
||||
delay(4000)
|
||||
}
|
||||
}
|
||||
|
||||
// Only scrape servers with these names
|
||||
fun String?.isValidServer(): Boolean {
|
||||
val list = listOf("upcloud", "vidcloud", "streamlare")
|
||||
return list.contains(this?.lowercase(Locale.ROOT))
|
||||
}
|
||||
|
||||
// For re-use in Zoro
|
||||
private suspend fun Sources.toExtractorLink(
|
||||
caller: MainAPI,
|
||||
name: String,
|
||||
extractorData: String? = null,
|
||||
): List<ExtractorLink>? {
|
||||
return this.file?.let { file ->
|
||||
//println("FILE::: $file")
|
||||
val isM3u8 = URI(this.file).path.endsWith(".m3u8") || this.type.equals(
|
||||
"hls",
|
||||
ignoreCase = true
|
||||
)
|
||||
return if (isM3u8) {
|
||||
suspendSafeApiCall {
|
||||
M3u8Helper().m3u8Generation(
|
||||
M3u8Helper.M3u8Stream(
|
||||
this.file,
|
||||
null,
|
||||
mapOf("Referer" to "https://mzzcloud.life/")
|
||||
), false
|
||||
)
|
||||
.map { stream ->
|
||||
ExtractorLink(
|
||||
caller.name,
|
||||
"${caller.name} $name",
|
||||
stream.streamUrl,
|
||||
caller.mainUrl,
|
||||
getQualityFromName(stream.quality?.toString()),
|
||||
true,
|
||||
extractorData = extractorData
|
||||
)
|
||||
}
|
||||
} ?: listOf(
|
||||
// Fallback if m3u8 extractor fails
|
||||
ExtractorLink(
|
||||
caller.name,
|
||||
"${caller.name} $name",
|
||||
this.file,
|
||||
caller.mainUrl,
|
||||
getQualityFromName(this.label),
|
||||
isM3u8,
|
||||
extractorData = extractorData
|
||||
)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
ExtractorLink(
|
||||
caller.name,
|
||||
caller.name,
|
||||
file,
|
||||
caller.mainUrl,
|
||||
getQualityFromName(this.label),
|
||||
false,
|
||||
extractorData = extractorData
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Tracks.toSubtitleFile(): SubtitleFile? {
|
||||
return this.file?.let {
|
||||
SubtitleFile(
|
||||
this.label ?: "Unknown",
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun MainAPI.extractRabbitStream(
|
||||
url: String,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
useSidAuthentication: Boolean,
|
||||
/** Used for extractorLink name, input: Source name */
|
||||
extractorData: String? = null,
|
||||
nameTransformer: (String) -> String,
|
||||
) = suspendSafeApiCall {
|
||||
// https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> https://rapid-cloud.ru/embed-6
|
||||
val mainIframeUrl =
|
||||
url.substringBeforeLast("/")
|
||||
val mainIframeId = url.substringAfterLast("/")
|
||||
.substringBefore("?") // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> dcPOVRE57YOT
|
||||
val iframe = app.get(url, referer = mainUrl)
|
||||
val iframeKey =
|
||||
iframe.document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||
.attr("src").substringAfter("render=")
|
||||
val iframeToken = getCaptchaToken(url, iframeKey)
|
||||
val number =
|
||||
Regex("""recaptchaNumber = '(.*?)'""").find(iframe.text)?.groupValues?.get(1)
|
||||
|
||||
var sid: String? = null
|
||||
if (useSidAuthentication && extractorData != null) {
|
||||
negotiateNewSid(extractorData)?.also { pollingData ->
|
||||
app.post(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}",
|
||||
requestBody = "40".toRequestBody(),
|
||||
timeout = 60
|
||||
)
|
||||
val text = app.get(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}",
|
||||
timeout = 60
|
||||
).text.replaceBefore("{", "")
|
||||
|
||||
sid = parseJson<PollingData>(text).sid
|
||||
ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}") }
|
||||
}
|
||||
}
|
||||
|
||||
val mapped = app.get(
|
||||
"${
|
||||
mainIframeUrl.replace(
|
||||
"/embed",
|
||||
"/ajax/embed"
|
||||
)
|
||||
}/getSources?id=$mainIframeId&_token=$iframeToken&_number=$number${sid?.let { "$&sId=$it" } ?: ""}",
|
||||
referer = mainUrl,
|
||||
headers = mapOf(
|
||||
"X-Requested-With" to "XMLHttpRequest",
|
||||
"Accept" to "*/*",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
// "Cache-Control" to "no-cache",
|
||||
"Connection" to "keep-alive",
|
||||
// "Sec-Fetch-Dest" to "empty",
|
||||
// "Sec-Fetch-Mode" to "no-cors",
|
||||
// "Sec-Fetch-Site" to "cross-site",
|
||||
// "Pragma" to "no-cache",
|
||||
// "Cache-Control" to "no-cache",
|
||||
"TE" to "trailers"
|
||||
)
|
||||
).parsed<SourceObject>()
|
||||
|
||||
mapped.tracks?.forEach { track ->
|
||||
track?.toSubtitleFile()?.let { subtitleFile ->
|
||||
subtitleCallback.invoke(subtitleFile)
|
||||
}
|
||||
}
|
||||
|
||||
val list = listOf(
|
||||
mapped.sources to "source 1",
|
||||
mapped.sources1 to "source 2",
|
||||
mapped.sources2 to "source 3",
|
||||
mapped.sourcesBackup to "source backup"
|
||||
)
|
||||
list.forEach { subList ->
|
||||
subList.first?.forEach { source ->
|
||||
source?.toExtractorLink(
|
||||
this,
|
||||
nameTransformer(subList.second),
|
||||
extractorData,
|
||||
)
|
||||
?.forEach {
|
||||
// Sets Zoro SID used for video loading
|
||||
// (this as? ZoroProvider)?.sid?.set(it.url.hashCode(), sid)
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class SflixProviderPlugin : Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(SflixProvider())
|
||||
registerMainAPI(SolarmovieProvider())
|
||||
registerMainAPI(TwoEmbedProvider())
|
||||
registerMainAPI(DopeboxProvider())
|
||||
registerMainAPI(ZoroProvider())
|
||||
registerMainAPI(HDTodayProvider())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.lagradost
|
||||
|
||||
class SolarmovieProvider : SflixProvider() {
|
||||
override var mainUrl = "https://solarmovie.pe"
|
||||
override var name = "Solarmovie"
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package com.lagradost
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.SflixProvider.Companion.extractRabbitStream
|
||||
import com.lagradost.SflixProvider.Companion.runSflixExtractorVerifierJob
|
||||
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.metaproviders.TmdbLink
|
||||
import com.lagradost.cloudstream3.metaproviders.TmdbProvider
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
||||
class TwoEmbedProvider : TmdbProvider() {
|
||||
override val apiName = "2Embed"
|
||||
override var name = "2Embed"
|
||||
override var mainUrl = "https://www.2embed.to"
|
||||
override val useMetaLoadResponse = true
|
||||
override val instantLinkLoading = false
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
|
||||
data class EmbedJson (
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("link") val link: String,
|
||||
@JsonProperty("sources") val sources: List<String?>,
|
||||
@JsonProperty("tracks") val tracks: List<String>?
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val mappedData = parseJson<TmdbLink>(data)
|
||||
val (id, site) = if (mappedData.imdbID != null) listOf(
|
||||
mappedData.imdbID,
|
||||
"imdb"
|
||||
) else listOf(mappedData.tmdbID.toString(), "tmdb")
|
||||
val isMovie = mappedData.episode == null && mappedData.season == null
|
||||
val embedUrl = if (isMovie) {
|
||||
"$mainUrl/embed/$site/movie?id=$id"
|
||||
} else {
|
||||
val suffix = "$id&s=${mappedData.season ?: 1}&e=${mappedData.episode ?: 1}"
|
||||
"$mainUrl/embed/$site/tv?id=$suffix"
|
||||
}
|
||||
|
||||
val document = app.get(embedUrl).document
|
||||
val captchaKey =
|
||||
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||
.attr("src").substringAfter("render=")
|
||||
|
||||
val servers = document.select(".dropdown-menu a[data-id]").map { it.attr("data-id") }
|
||||
servers.apmap { serverID ->
|
||||
val token = getCaptchaToken(embedUrl, captchaKey)
|
||||
val ajax = app.get("$mainUrl/ajax/embed/play?id=$serverID&_token=$token", referer = embedUrl).text
|
||||
val mappedservers = parseJson<EmbedJson>(ajax)
|
||||
val iframeLink = mappedservers.link
|
||||
if (iframeLink.contains("rabbitstream")) {
|
||||
extractRabbitStream(iframeLink, subtitleCallback, callback, false) { it }
|
||||
} else {
|
||||
loadExtractor(iframeLink, embedUrl, subtitleCallback, callback)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun extractorVerifierJob(extractorData: String?) {
|
||||
Log.d(this.name, "Starting ${this.name} job!")
|
||||
runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
package com.lagradost
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.SflixProvider.Companion.extractRabbitStream
|
||||
import com.lagradost.SflixProvider.Companion.runSflixExtractorVerifierJob
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import com.lagradost.nicehttp.Requests.Companion.await
|
||||
import okhttp3.Interceptor
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.net.URI
|
||||
|
||||
private const val OPTIONS = "OPTIONS"
|
||||
|
||||
class ZoroProvider : MainAPI() {
|
||||
override var mainUrl = "https://zoro.to"
|
||||
override var name = "Zoro"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val usesWebView = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Anime,
|
||||
TvType.AnimeMovie,
|
||||
TvType.OVA
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun getType(t: String): TvType {
|
||||
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||
else TvType.Anime
|
||||
}
|
||||
|
||||
fun getStatus(t: String): ShowStatus {
|
||||
return when (t) {
|
||||
"Finished Airing" -> ShowStatus.Completed
|
||||
"Currently Airing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val epRegex = Regex("Ep (\\d+)/")
|
||||
private fun Element.toSearchResult(): SearchResponse? {
|
||||
val href = fixUrl(this.select("a").attr("href"))
|
||||
val title = this.select("h3.film-name").text()
|
||||
val dubSub = this.select(".film-poster > .tick.ltr").text()
|
||||
//val episodes = this.selectFirst(".film-poster > .tick-eps")?.text()?.toIntOrNull()
|
||||
|
||||
val dubExist = dubSub.contains("dub", ignoreCase = true)
|
||||
val subExist = dubSub.contains("sub", ignoreCase = true)
|
||||
val episodes =
|
||||
this.selectFirst(".film-poster > .tick.rtl > .tick-eps")?.text()?.let { eps ->
|
||||
//println("REGEX:::: $eps")
|
||||
// current episode / max episode
|
||||
//Regex("Ep (\\d+)/(\\d+)")
|
||||
epRegex.find(eps)?.groupValues?.get(1)?.toIntOrNull()
|
||||
}
|
||||
if (href.contains("/news/") || title.trim().equals("News", ignoreCase = true)) return null
|
||||
val posterUrl = fixUrl(this.select("img").attr("data-src"))
|
||||
val type = getType(this.select("div.fd-infor > span.fdi-item").text())
|
||||
|
||||
return newAnimeSearchResponse(title, href, type) {
|
||||
this.posterUrl = posterUrl
|
||||
addDubStatus(dubExist, subExist, episodes, episodes)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val html = app.get("$mainUrl/home").text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
val homePageList = ArrayList<HomePageList>()
|
||||
|
||||
document.select("div.anif-block").forEach { block ->
|
||||
val header = block.select("div.anif-block-header").text().trim()
|
||||
val animes = block.select("li").mapNotNull {
|
||||
it.toSearchResult()
|
||||
}
|
||||
if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes))
|
||||
}
|
||||
|
||||
document.select("section.block_area.block_area_home").forEach { block ->
|
||||
val header = block.select("h2.cat-heading").text().trim()
|
||||
val animes = block.select("div.flw-item").mapNotNull {
|
||||
it.toSearchResult()
|
||||
}
|
||||
if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes))
|
||||
}
|
||||
|
||||
return HomePageResponse(homePageList)
|
||||
}
|
||||
|
||||
private data class Response(
|
||||
@JsonProperty("status") val status: Boolean,
|
||||
@JsonProperty("html") val html: String
|
||||
)
|
||||
|
||||
// override suspend fun quickSearch(query: String): List<SearchResponse> {
|
||||
// val url = "$mainUrl/ajax/search/suggest?keyword=${query}"
|
||||
// val html = mapper.readValue<Response>(khttp.get(url).text).html
|
||||
// val document = Jsoup.parse(html)
|
||||
//
|
||||
// return document.select("a.nav-item").map {
|
||||
// val title = it.selectFirst(".film-name")?.text().toString()
|
||||
// val href = fixUrl(it.attr("href"))
|
||||
// val year = it.selectFirst(".film-infor > span")?.text()?.split(",")?.get(1)?.trim()?.toIntOrNull()
|
||||
// val image = it.select("img").attr("data-src")
|
||||
//
|
||||
// AnimeSearchResponse(
|
||||
// title,
|
||||
// href,
|
||||
// this.name,
|
||||
// TvType.TvSeries,
|
||||
// image,
|
||||
// year,
|
||||
// null,
|
||||
// EnumSet.of(DubStatus.Subbed),
|
||||
// null,
|
||||
// null
|
||||
// )
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val link = "$mainUrl/search?keyword=$query"
|
||||
val html = app.get(link).text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
return document.select(".flw-item").map {
|
||||
val title = it.selectFirst(".film-detail > .film-name > a")?.attr("title").toString()
|
||||
val filmPoster = it.selectFirst(".film-poster")
|
||||
val poster = filmPoster!!.selectFirst("img")?.attr("data-src")
|
||||
|
||||
val episodes = filmPoster.selectFirst("div.rtl > div.tick-eps")?.text()?.let { eps ->
|
||||
// current episode / max episode
|
||||
val epRegex = Regex("Ep (\\d+)/")//Regex("Ep (\\d+)/(\\d+)")
|
||||
epRegex.find(eps)?.groupValues?.get(1)?.toIntOrNull()
|
||||
}
|
||||
val dubsub = filmPoster.selectFirst("div.ltr")?.text()
|
||||
val dubExist = dubsub?.contains("DUB") ?: false
|
||||
val subExist = dubsub?.contains("SUB") ?: false || dubsub?.contains("RAW") ?: false
|
||||
|
||||
val tvType =
|
||||
getType(it.selectFirst(".film-detail > .fd-infor > .fdi-item")?.text().toString())
|
||||
val href = fixUrl(it.selectFirst(".film-name a")!!.attr("href"))
|
||||
|
||||
newAnimeSearchResponse(title, href, tvType) {
|
||||
this.posterUrl = poster
|
||||
addDubStatus(dubExist, subExist, episodes, episodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element?.getActor(): Actor? {
|
||||
val image =
|
||||
fixUrlNull(this?.selectFirst(".pi-avatar > img")?.attr("data-src")) ?: return null
|
||||
val name = this?.selectFirst(".pi-detail > .pi-name")?.text() ?: return null
|
||||
return Actor(name = name, image = image)
|
||||
}
|
||||
|
||||
data class ZoroSyncData(
|
||||
@JsonProperty("mal_id") val malId: String?,
|
||||
@JsonProperty("anilist_id") val aniListId: String?,
|
||||
)
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val html = app.get(url).text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
val syncData = tryParseJson<ZoroSyncData>(document.selectFirst("#syncData")?.data())
|
||||
|
||||
val title = document.selectFirst(".anisc-detail > .film-name")?.text().toString()
|
||||
val poster = document.selectFirst(".anisc-poster img")?.attr("src")
|
||||
val tags = document.select(".anisc-info a[href*=\"/genre/\"]").map { it.text() }
|
||||
|
||||
var year: Int? = null
|
||||
var japaneseTitle: String? = null
|
||||
var status: ShowStatus? = null
|
||||
|
||||
for (info in document.select(".anisc-info > .item.item-title")) {
|
||||
val text = info?.text().toString()
|
||||
when {
|
||||
(year != null && japaneseTitle != null && status != null) -> break
|
||||
text.contains("Premiered") && year == null ->
|
||||
year =
|
||||
info.selectFirst(".name")?.text().toString().split(" ").last().toIntOrNull()
|
||||
|
||||
text.contains("Japanese") && japaneseTitle == null ->
|
||||
japaneseTitle = info.selectFirst(".name")?.text().toString()
|
||||
|
||||
text.contains("Status") && status == null ->
|
||||
status = getStatus(info.selectFirst(".name")?.text().toString())
|
||||
}
|
||||
}
|
||||
|
||||
val description = document.selectFirst(".film-description.m-hide > .text")?.text()
|
||||
val animeId = URI(url).path.split("-").last()
|
||||
|
||||
val episodes = Jsoup.parse(
|
||||
parseJson<Response>(
|
||||
app.get(
|
||||
"$mainUrl/ajax/v2/episode/list/$animeId"
|
||||
).text
|
||||
).html
|
||||
).select(".ss-list > a[href].ssl-item.ep-item").map {
|
||||
newEpisode(it.attr("href")) {
|
||||
this.name = it?.attr("title")
|
||||
this.episode = it.selectFirst(".ssli-order")?.text()?.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
val actors = document.select("div.block-actors-content > div.bac-list-wrap > div.bac-item")
|
||||
.mapNotNull { head ->
|
||||
val subItems = head.select(".per-info") ?: return@mapNotNull null
|
||||
if (subItems.isEmpty()) return@mapNotNull null
|
||||
var role: ActorRole? = null
|
||||
val mainActor = subItems.first()?.let {
|
||||
role = when (it.selectFirst(".pi-detail > .pi-cast")?.text()?.trim()) {
|
||||
"Supporting" -> ActorRole.Supporting
|
||||
"Main" -> ActorRole.Main
|
||||
else -> null
|
||||
}
|
||||
it.getActor()
|
||||
} ?: return@mapNotNull null
|
||||
val voiceActor = if (subItems.size >= 2) subItems[1]?.getActor() else null
|
||||
ActorData(actor = mainActor, role = role, voiceActor = voiceActor)
|
||||
}
|
||||
|
||||
val recommendations =
|
||||
document.select("#main-content > section > .tab-content > div > .film_list-wrap > .flw-item")
|
||||
.mapNotNull { head ->
|
||||
val filmPoster = head?.selectFirst(".film-poster")
|
||||
val epPoster = filmPoster?.selectFirst("img")?.attr("data-src")
|
||||
val a = head?.selectFirst(".film-detail > .film-name > a")
|
||||
val epHref = a?.attr("href")
|
||||
val epTitle = a?.attr("title")
|
||||
if (epHref == null || epTitle == null || epPoster == null) {
|
||||
null
|
||||
} else {
|
||||
AnimeSearchResponse(
|
||||
epTitle,
|
||||
fixUrl(epHref),
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
epPoster,
|
||||
dubStatus = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||
japName = japaneseTitle
|
||||
engName = title
|
||||
posterUrl = poster
|
||||
this.year = year
|
||||
addEpisodes(DubStatus.Subbed, episodes)
|
||||
showStatus = status
|
||||
plot = description
|
||||
this.tags = tags
|
||||
this.recommendations = recommendations
|
||||
this.actors = actors
|
||||
addMalId(syncData?.malId?.toIntOrNull())
|
||||
addAniListId(syncData?.aniListId?.toIntOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
private data class RapidCloudResponse(
|
||||
@JsonProperty("link") val link: String
|
||||
)
|
||||
|
||||
override suspend fun extractorVerifierJob(extractorData: String?) {
|
||||
Log.d(this.name, "Starting ${this.name} job!")
|
||||
runSflixExtractorVerifierJob(this, extractorData, "https://rapid-cloud.ru/")
|
||||
}
|
||||
|
||||
/** Url hashcode to sid */
|
||||
var sid: HashMap<Int, String?> = hashMapOf()
|
||||
|
||||
/**
|
||||
* Makes an identical Options request before .ts request
|
||||
* Adds an SID header to the .ts request.
|
||||
* */
|
||||
override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor {
|
||||
// Needs to be object instead of lambda to make it compile correctly
|
||||
return object : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
|
||||
val request = chain.request()
|
||||
if (request.url.toString().endsWith(".ts")
|
||||
&& request.method != OPTIONS
|
||||
// No option requests on VidCloud
|
||||
&& !request.url.toString().contains("betterstream")
|
||||
) {
|
||||
val newRequest =
|
||||
chain.request()
|
||||
.newBuilder().apply {
|
||||
sid[extractorLink.url.hashCode()]?.let { sid ->
|
||||
addHeader("SID", sid)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val options = request.newBuilder().method(OPTIONS, request.body).build()
|
||||
ioSafe { app.baseClient.newCall(options).await() }
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
} else {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
|
||||
val servers: List<Pair<DubStatus, String>> = Jsoup.parse(
|
||||
app.get("$mainUrl/ajax/v2/episode/servers?episodeId=" + data.split("=")[1])
|
||||
.parsed<Response>().html
|
||||
).select(".server-item[data-type][data-id]").map {
|
||||
Pair(
|
||||
if (it.attr("data-type") == "sub") DubStatus.Subbed else DubStatus.Dubbed,
|
||||
it.attr("data-id")
|
||||
)
|
||||
}
|
||||
|
||||
val extractorData =
|
||||
"https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling"
|
||||
|
||||
// Prevent duplicates
|
||||
servers.distinctBy { it.second }.apmap {
|
||||
val link =
|
||||
"$mainUrl/ajax/v2/episode/sources?id=${it.second}"
|
||||
val extractorLink = app.get(
|
||||
link,
|
||||
).parsed<RapidCloudResponse>().link
|
||||
val hasLoadedExtractorLink =
|
||||
loadExtractor(extractorLink, "https://rapid-cloud.ru/", subtitleCallback, callback)
|
||||
|
||||
if (!hasLoadedExtractorLink) {
|
||||
extractRabbitStream(
|
||||
extractorLink,
|
||||
subtitleCallback,
|
||||
// Blacklist VidCloud for now
|
||||
{ videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) },
|
||||
true,
|
||||
extractorData
|
||||
) { sourceName ->
|
||||
sourceName + " - ${it.first}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue