remove all english providers
This commit is contained in:
parent
50a0175ac3
commit
440d8ef576
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=allanime.site&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,404 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import org.jsoup.Jsoup
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
|
||||
|
||||
class AllAnimeProvider : MainAPI() {
|
||||
override var mainUrl = "https://allanime.site"
|
||||
override var name = "AllAnime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
||||
private fun getStatus(t: String): ShowStatus {
|
||||
return when (t) {
|
||||
"Finished" -> ShowStatus.Completed
|
||||
"Releasing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
|
||||
override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie)
|
||||
|
||||
private data class Data(
|
||||
@JsonProperty("shows") val shows: Shows
|
||||
)
|
||||
|
||||
private data class Shows(
|
||||
@JsonProperty("pageInfo") val pageInfo: PageInfo,
|
||||
@JsonProperty("edges") val edges: List<Edges>,
|
||||
@JsonProperty("__typename") val _typename: String
|
||||
)
|
||||
|
||||
private data class Edges(
|
||||
@JsonProperty("_id") val Id: String?,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("englishName") val englishName: String?,
|
||||
@JsonProperty("nativeName") val nativeName: String?,
|
||||
@JsonProperty("thumbnail") val thumbnail: String?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("season") val season: Season?,
|
||||
@JsonProperty("score") val score: Double?,
|
||||
@JsonProperty("airedStart") val airedStart: AiredStart?,
|
||||
@JsonProperty("availableEpisodes") val availableEpisodes: AvailableEpisodes?,
|
||||
@JsonProperty("availableEpisodesDetail") val availableEpisodesDetail: AvailableEpisodesDetail?,
|
||||
@JsonProperty("studios") val studios: List<String>?,
|
||||
@JsonProperty("description") val description: String?,
|
||||
@JsonProperty("status") val status: String?,
|
||||
)
|
||||
|
||||
private data class AvailableEpisodes(
|
||||
@JsonProperty("sub") val sub: Int,
|
||||
@JsonProperty("dub") val dub: Int,
|
||||
@JsonProperty("raw") val raw: Int
|
||||
)
|
||||
|
||||
private data class AiredStart(
|
||||
@JsonProperty("year") val year: Int,
|
||||
@JsonProperty("month") val month: Int,
|
||||
@JsonProperty("date") val date: Int
|
||||
)
|
||||
|
||||
private data class Season(
|
||||
@JsonProperty("quarter") val quarter: String,
|
||||
@JsonProperty("year") val year: Int
|
||||
)
|
||||
|
||||
private data class PageInfo(
|
||||
@JsonProperty("total") val total: Int,
|
||||
@JsonProperty("__typename") val _typename: String
|
||||
)
|
||||
|
||||
private data class AllAnimeQuery(
|
||||
@JsonProperty("data") val data: Data
|
||||
)
|
||||
|
||||
data class RandomMain(
|
||||
@JsonProperty("data") var data: DataRan? = DataRan()
|
||||
)
|
||||
|
||||
data class DataRan(
|
||||
@JsonProperty("queryRandomRecommendation") var queryRandomRecommendation: ArrayList<QueryRandomRecommendation> = arrayListOf()
|
||||
)
|
||||
|
||||
data class QueryRandomRecommendation(
|
||||
@JsonProperty("_id") val Id: String? = null,
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("englishName") val englishName: String? = null,
|
||||
@JsonProperty("nativeName") val nativeName: String? = null,
|
||||
@JsonProperty("thumbnail") val thumbnail: String? = null,
|
||||
@JsonProperty("airedStart") val airedStart: String? = null,
|
||||
@JsonProperty("availableChapters") val availableChapters: String? = null,
|
||||
@JsonProperty("availableEpisodes") val availableEpisodes: String? = null,
|
||||
@JsonProperty("__typename") val _typename: String? = null
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val urls = listOf(
|
||||
// Pair(
|
||||
// "Top Anime",
|
||||
// """$mainUrl/graphql?variables={"type":"anime","size":30,"dateRange":30}&extensions={"persistedQuery":{"version":1,"sha256Hash":"276d52ba09ca48ce2b8beb3affb26d9d673b22f9d1fd4892aaa39524128bc745"}}"""
|
||||
// ),
|
||||
// "countryOrigin":"JP" for Japanese only
|
||||
Pair(
|
||||
"Recently updated",
|
||||
"""$mainUrl/graphql?variables={"search":{"allowAdult":false,"allowUnknown":false},"limit":30,"page":1,"translationType":"dub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"d2670e3e27ee109630991152c8484fce5ff5e280c523378001f9a23dc1839068"}}"""
|
||||
),
|
||||
)
|
||||
|
||||
val random =
|
||||
"""$mainUrl/graphql?variables={"format":"anime"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"21ac672633498a3698e8f6a93ce6c2b3722b29a216dcca93363bf012c360cd54"}}"""
|
||||
val ranlink = app.get(random).text
|
||||
val jsonran = parseJson<RandomMain>(ranlink)
|
||||
val ranhome = jsonran.data?.queryRandomRecommendation?.map {
|
||||
newAnimeSearchResponse(it.name!!, "$mainUrl/anime/${it.Id}", fix = false) {
|
||||
this.posterUrl = it.thumbnail
|
||||
this.otherName = it.nativeName
|
||||
}
|
||||
}
|
||||
|
||||
items.add(HomePageList("Random", ranhome!!))
|
||||
|
||||
urls.apmap { (HomeName, url) ->
|
||||
val test = app.get(url).text
|
||||
val json = parseJson<AllAnimeQuery>(test)
|
||||
val home = ArrayList<SearchResponse>()
|
||||
val results = json.data.shows.edges.filter {
|
||||
// filtering in case there is an anime with 0 episodes available on the site.
|
||||
!(it.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0)
|
||||
}
|
||||
results.map {
|
||||
home.add(
|
||||
newAnimeSearchResponse(it.name, "$mainUrl/anime/${it.Id}", fix = false) {
|
||||
this.posterUrl = it.thumbnail
|
||||
this.year = it.airedStart?.year
|
||||
this.otherName = it.englishName
|
||||
addDub(it.availableEpisodes?.dub)
|
||||
addSub(it.availableEpisodes?.sub)
|
||||
})
|
||||
}
|
||||
items.add(HomePageList(HomeName, home))
|
||||
}
|
||||
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val link =
|
||||
""" $mainUrl/graphql?variables={"search":{"allowAdult":false,"allowUnknown":false,"query":"$query"},"limit":26,"page":1,"translationType":"dub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"d2670e3e27ee109630991152c8484fce5ff5e280c523378001f9a23dc1839068"}}"""
|
||||
var res = app.get(link).text
|
||||
if (res.contains("PERSISTED_QUERY_NOT_FOUND")) {
|
||||
res = app.get(link).text
|
||||
if (res.contains("PERSISTED_QUERY_NOT_FOUND")) return emptyList()
|
||||
}
|
||||
val response = parseJson<AllAnimeQuery>(res)
|
||||
|
||||
val results = response.data.shows.edges.filter {
|
||||
// filtering in case there is an anime with 0 episodes available on the site.
|
||||
!(it.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0)
|
||||
}
|
||||
|
||||
return results.map {
|
||||
newAnimeSearchResponse(it.name, "$mainUrl/anime/${it.Id}", fix = false) {
|
||||
this.posterUrl = it.thumbnail
|
||||
this.year = it.airedStart?.year
|
||||
this.otherName = it.englishName
|
||||
addDub(it.availableEpisodes?.dub)
|
||||
addSub(it.availableEpisodes?.sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class AvailableEpisodesDetail(
|
||||
@JsonProperty("sub") val sub: List<String>,
|
||||
@JsonProperty("dub") val dub: List<String>,
|
||||
@JsonProperty("raw") val raw: List<String>
|
||||
)
|
||||
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val rhino = Context.enter()
|
||||
rhino.initStandardObjects()
|
||||
rhino.optimizationLevel = -1
|
||||
val scope: Scriptable = rhino.initStandardObjects()
|
||||
|
||||
val html = app.get(url).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
val script = soup.select("script").firstOrNull {
|
||||
it.html().contains("window.__NUXT__")
|
||||
} ?: return null
|
||||
|
||||
val js = """
|
||||
const window = {}
|
||||
${script.html()}
|
||||
const returnValue = JSON.stringify(window.__NUXT__.fetch[0].show)
|
||||
""".trimIndent()
|
||||
|
||||
rhino.evaluateString(scope, js, "JavaScript", 1, null)
|
||||
val jsEval = scope.get("returnValue", scope) ?: return null
|
||||
val showData = parseJson<Edges>(jsEval as String)
|
||||
|
||||
val title = showData.name
|
||||
val description = showData.description
|
||||
val poster = showData.thumbnail
|
||||
|
||||
val episodes = showData.availableEpisodes.let {
|
||||
if (it == null) return@let Pair(null, null)
|
||||
Pair(if (it.sub != 0) ((1..it.sub).map { epNum ->
|
||||
Episode(
|
||||
"$mainUrl/anime/${showData.Id}/episodes/sub/$epNum", episode = epNum
|
||||
)
|
||||
}) else null, if (it.dub != 0) ((1..it.dub).map { epNum ->
|
||||
Episode(
|
||||
"$mainUrl/anime/${showData.Id}/episodes/dub/$epNum", episode = epNum
|
||||
)
|
||||
}) else null)
|
||||
}
|
||||
|
||||
val characters = soup.select("div.character > div.card-character-box").mapNotNull {
|
||||
val img = it?.selectFirst("img")?.attr("src") ?: return@mapNotNull null
|
||||
val name = it.selectFirst("div > a")?.ownText() ?: return@mapNotNull null
|
||||
val role = when (it.selectFirst("div > .text-secondary")?.text()?.trim()) {
|
||||
"Main" -> ActorRole.Main
|
||||
"Supporting" -> ActorRole.Supporting
|
||||
"Background" -> ActorRole.Background
|
||||
else -> null
|
||||
}
|
||||
Pair(Actor(name, img), role)
|
||||
}
|
||||
|
||||
// bruh, they use graphql
|
||||
//val recommendations = soup.select("#suggesction > div > div.p > .swipercard")?.mapNotNull {
|
||||
// val recTitle = it?.selectFirst(".showname > a") ?: return@mapNotNull null
|
||||
// val recName = recTitle.text() ?: return@mapNotNull null
|
||||
// val href = fixUrlNull(recTitle.attr("href")) ?: return@mapNotNull null
|
||||
// val img = it.selectFirst(".image > img").attr("src") ?: return@mapNotNull null
|
||||
// AnimeSearchResponse(recName, href, this.name, TvType.Anime, img)
|
||||
//}
|
||||
|
||||
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||
posterUrl = poster
|
||||
year = showData.airedStart?.year
|
||||
|
||||
addEpisodes(DubStatus.Subbed, episodes.first)
|
||||
addEpisodes(DubStatus.Dubbed, episodes.second)
|
||||
addActors(characters)
|
||||
//this.recommendations = recommendations
|
||||
|
||||
showStatus = getStatus(showData.status.toString())
|
||||
|
||||
plot = description?.replace(Regex("""<(.*?)>"""), "")
|
||||
}
|
||||
}
|
||||
|
||||
private val embedBlackList = listOf(
|
||||
"https://mp4upload.com/",
|
||||
"https://streamsb.net/",
|
||||
"https://dood.to/",
|
||||
"https://videobin.co/",
|
||||
"https://ok.ru",
|
||||
"https://streamlare.com",
|
||||
)
|
||||
|
||||
private fun embedIsBlacklisted(url: String): Boolean {
|
||||
embedBlackList.forEach {
|
||||
if (it.javaClass.name == "kotlin.text.Regex") {
|
||||
if ((it as Regex).matches(url)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if (url.contains(it)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun String.sanitize(): String {
|
||||
var out = this
|
||||
listOf(Pair("\\u002F", "/")).forEach {
|
||||
out = out.replace(it.first, it.second)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private data class Links(
|
||||
@JsonProperty("link") val link: String,
|
||||
@JsonProperty("hls") val hls: Boolean?,
|
||||
@JsonProperty("resolutionStr") val resolutionStr: String,
|
||||
@JsonProperty("src") val src: String?
|
||||
)
|
||||
|
||||
private data class AllAnimeVideoApiResponse(
|
||||
@JsonProperty("links") val links: List<Links>
|
||||
)
|
||||
|
||||
private data class ApiEndPoint(
|
||||
@JsonProperty("episodeIframeHead") val episodeIframeHead: String
|
||||
)
|
||||
|
||||
private suspend fun getM3u8Qualities(
|
||||
m3u8Link: String,
|
||||
referer: String,
|
||||
qualityName: String,
|
||||
): List<ExtractorLink> {
|
||||
return M3u8Helper.generateM3u8(
|
||||
this.name,
|
||||
m3u8Link,
|
||||
referer,
|
||||
name = "${this.name} - $qualityName"
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
var apiEndPoint =
|
||||
parseJson<ApiEndPoint>(app.get("$mainUrl/getVersion").text).episodeIframeHead
|
||||
if (apiEndPoint.endsWith("/")) apiEndPoint =
|
||||
apiEndPoint.slice(0 until apiEndPoint.length - 1)
|
||||
|
||||
val html = app.get(data).text
|
||||
|
||||
val sources = Regex("""sourceUrl[:=]"(.+?)"""").findAll(html).toList()
|
||||
.map { URLDecoder.decode(it.destructured.component1().sanitize(), "UTF-8") }
|
||||
sources.apmap {
|
||||
safeApiCall {
|
||||
var link = it.replace(" ", "%20")
|
||||
if (URI(link).isAbsolute || link.startsWith("//")) {
|
||||
if (link.startsWith("//")) link = "https:$it"
|
||||
|
||||
if (Regex("""streaming\.php\?""").matches(link)) {
|
||||
// for now ignore
|
||||
} else if (!embedIsBlacklisted(link)) {
|
||||
if (URI(link).path.contains(".m3u")) {
|
||||
getM3u8Qualities(link, data, URI(link).host).forEach(callback)
|
||||
} else {
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"AllAnime - " + URI(link).host,
|
||||
"",
|
||||
link,
|
||||
data,
|
||||
Qualities.P1080.value,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
link = apiEndPoint + URI(link).path + ".json?" + URI(link).query
|
||||
val response = app.get(link)
|
||||
|
||||
if (response.code < 400) {
|
||||
val links = parseJson<AllAnimeVideoApiResponse>(response.text).links
|
||||
links.forEach { server ->
|
||||
if (server.hls != null && server.hls) {
|
||||
getM3u8Qualities(
|
||||
server.link,
|
||||
"$apiEndPoint/player?uri=" + (if (URI(server.link).host.isNotEmpty()) server.link else apiEndPoint + URI(
|
||||
server.link
|
||||
).path),
|
||||
server.resolutionStr
|
||||
).forEach(callback)
|
||||
} else {
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"AllAnime - " + URI(server.link).host,
|
||||
server.resolutionStr,
|
||||
server.link,
|
||||
"$apiEndPoint/player?uri=" + (if (URI(server.link).host.isNotEmpty()) server.link else apiEndPoint + URI(
|
||||
server.link
|
||||
).path),
|
||||
Qualities.P1080.value,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AllAnimeProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AllAnimeProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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=allmoviesforyou.net&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,206 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class AllMoviesForYouProvider : MainAPI() {
|
||||
companion object {
|
||||
fun getType(t: String): TvType {
|
||||
return when {
|
||||
t.contains("series") -> TvType.TvSeries
|
||||
t.contains("movies") -> TvType.Movie
|
||||
else -> TvType.Movie
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetching movies will not work if this link is outdated.
|
||||
override var mainUrl = "https://allmoviesforyou.net"
|
||||
override var name = "AllMoviesForYou"
|
||||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val soup = app.get(mainUrl).document
|
||||
val urls = listOf(
|
||||
Pair("Movies", "section[data-id=movies] article.TPost.B"),
|
||||
Pair("TV Series", "section[data-id=series] article.TPost.B"),
|
||||
)
|
||||
for ((name, element) in urls) {
|
||||
try {
|
||||
val home = soup.select(element).map {
|
||||
val title = it.selectFirst("h2.title")!!.text()
|
||||
val link = it.selectFirst("a")!!.attr("href")
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
fixUrl(it.selectFirst("figure img")!!.attr("data-src")),
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
items.add(HomePageList(name, home))
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/?s=$query"
|
||||
val document = app.get(url).document
|
||||
|
||||
val items = document.select("ul.MovieList > li > article > a")
|
||||
return items.map { item ->
|
||||
val href = item.attr("href")
|
||||
val title = item.selectFirst("> h2.Title")!!.text()
|
||||
val img = fixUrl(item.selectFirst("> div.Image > figure > img")!!.attr("data-src"))
|
||||
val type = getType(href)
|
||||
if (type == TvType.Movie) {
|
||||
MovieSearchResponse(title, href, this.name, type, img, null)
|
||||
} else {
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
type,
|
||||
img,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private fun getLink(document: Document): List<String>? {
|
||||
// val list = ArrayList<String>()
|
||||
// Regex("iframe src=\"(.*?)\"").find(document.html())?.groupValues?.get(1)?.let {
|
||||
// list.add(it)
|
||||
// }
|
||||
// document.select("div.OptionBx")?.forEach { element ->
|
||||
// val baseElement = element.selectFirst("> a.Button")
|
||||
// val elementText = element.selectFirst("> p.AAIco-dns")?.text()
|
||||
// if (elementText == "Streamhub" || elementText == "Dood") {
|
||||
// baseElement?.attr("href")?.let { href ->
|
||||
// list.add(href)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return if (list.isEmpty()) null else list
|
||||
// }
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val type = getType(url)
|
||||
|
||||
val document = app.get(url).document
|
||||
|
||||
val title = document.selectFirst("h1.Title")!!.text()
|
||||
val descipt = document.selectFirst("div.Description > p")!!.text()
|
||||
val rating =
|
||||
document.selectFirst("div.Vote > div.post-ratings > span")?.text()?.toRatingInt()
|
||||
val year = document.selectFirst("span.Date")?.text()
|
||||
val duration = document.selectFirst("span.Time")!!.text()
|
||||
val backgroundPoster =
|
||||
fixUrlNull(document.selectFirst("div.Image > figure > img")?.attr("data-src"))
|
||||
|
||||
if (type == TvType.TvSeries) {
|
||||
val list = ArrayList<Pair<Int, String>>()
|
||||
|
||||
document.select("main > section.SeasonBx > div > div.Title > a").forEach { element ->
|
||||
val season = element.selectFirst("> span")?.text()?.toIntOrNull()
|
||||
val href = element.attr("href")
|
||||
if (season != null && season > 0 && !href.isNullOrBlank()) {
|
||||
list.add(Pair(season, fixUrl(href)))
|
||||
}
|
||||
}
|
||||
if (list.isEmpty()) throw ErrorLoadingException("No Seasons Found")
|
||||
|
||||
val episodeList = ArrayList<Episode>()
|
||||
|
||||
for (season in list) {
|
||||
val seasonResponse = app.get(season.second).text
|
||||
val seasonDocument = Jsoup.parse(seasonResponse)
|
||||
val episodes = seasonDocument.select("table > tbody > tr")
|
||||
if (episodes.isNotEmpty()) {
|
||||
episodes.forEach { episode ->
|
||||
val epNum = episode.selectFirst("> td > span.Num")?.text()?.toIntOrNull()
|
||||
val poster = episode.selectFirst("> td.MvTbImg > a > img")?.attr("data-src")
|
||||
val aName = episode.selectFirst("> td.MvTbTtl > a")
|
||||
val name = aName!!.text()
|
||||
val href = aName.attr("href")
|
||||
val date = episode.selectFirst("> td.MvTbTtl > span")?.text()
|
||||
|
||||
episodeList.add(
|
||||
newEpisode(href) {
|
||||
this.name = name
|
||||
this.season = season.first
|
||||
this.episode = epNum
|
||||
this.posterUrl = fixUrlNull(poster)
|
||||
addDate(date)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return TvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
type,
|
||||
episodeList,
|
||||
backgroundPoster,
|
||||
year?.toIntOrNull(),
|
||||
descipt,
|
||||
null,
|
||||
rating
|
||||
)
|
||||
} else {
|
||||
return newMovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
type,
|
||||
fixUrl(url)
|
||||
) {
|
||||
posterUrl = backgroundPoster
|
||||
this.year = year?.toIntOrNull()
|
||||
this.plot = descipt
|
||||
this.rating = rating
|
||||
addDuration(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val doc = app.get(data).document
|
||||
val iframe = doc.select("body iframe").map { fixUrl(it.attr("src")) }
|
||||
iframe.apmap { id ->
|
||||
if (id.contains("trembed")) {
|
||||
val soup = app.get(id).document
|
||||
soup.select("body iframe").map {
|
||||
val link = fixUrl(it.attr("src").replace("streamhub.to/d/", "streamhub.to/e/"))
|
||||
loadExtractor(link, data, subtitleCallback, callback)
|
||||
}
|
||||
} else loadExtractor(id, data, subtitleCallback, callback)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AllMoviesForYouProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AllMoviesForYouProvider())
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"Anime",
|
||||
"AnimeMovie",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=aniflix.pro&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,274 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import java.net.URLDecoder
|
||||
|
||||
class AniflixProvider : MainAPI() {
|
||||
override var mainUrl = "https://aniflix.pro"
|
||||
override var name = "Aniflix"
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.OVA,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
companion object {
|
||||
var token: String? = null
|
||||
}
|
||||
|
||||
private suspend fun getToken(): String {
|
||||
return token ?: run {
|
||||
Regex("([^/]*)/_buildManifest\\.js").find(app.get(mainUrl).text)?.groupValues?.getOrNull(
|
||||
1
|
||||
)
|
||||
?.also {
|
||||
token = it
|
||||
}
|
||||
?: throw ErrorLoadingException("No token found")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Anime.toSearchResponse(): SearchResponse? {
|
||||
return newAnimeSearchResponse(
|
||||
title?.english ?: title?.romaji ?: return null,
|
||||
"$mainUrl/anime/${id ?: return null}"
|
||||
) {
|
||||
posterUrl = coverImage?.large ?: coverImage?.medium
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val soup = app.get(mainUrl).document
|
||||
val elements = listOf(
|
||||
Pair("Trending Now", "div:nth-child(3) > div a"),
|
||||
Pair("Popular", "div:nth-child(4) > div a"),
|
||||
Pair("Top Rated", "div:nth-child(5) > div a"),
|
||||
)
|
||||
|
||||
elements.map { (name, element) ->
|
||||
val home = soup.select(element).map {
|
||||
val href = it.attr("href")
|
||||
val title = it.selectFirst("p.mt-2")!!.text()
|
||||
val image = it.selectFirst("img.rounded-md[sizes]")!!.attr("src").replace("/_next/image?url=","")
|
||||
.replace(Regex("\\&.*\$"),"")
|
||||
val realposter = URLDecoder.decode(image, "UTF-8")
|
||||
newAnimeSearchResponse(title, fixUrl(href)) {
|
||||
this.posterUrl = realposter
|
||||
}
|
||||
}
|
||||
items.add(HomePageList(name, home))
|
||||
}
|
||||
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val token = getToken()
|
||||
val url = "$mainUrl/_next/data/$token/search.json?keyword=$query"
|
||||
val response = app.get(url)
|
||||
val searchResponse =
|
||||
response.parsedSafe<Search>()
|
||||
?: throw ErrorLoadingException("No Media")
|
||||
return searchResponse.pageProps?.searchResults?.Page?.media?.mapNotNull { media ->
|
||||
media.toSearchResponse()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val token = getToken()
|
||||
|
||||
val id = Regex("$mainUrl/anime/([0-9]*)").find(url)?.groupValues?.getOrNull(1)
|
||||
?: throw ErrorLoadingException("Error parsing link for id")
|
||||
|
||||
val res = app.get("https://aniflix.pro/_next/data/$token/anime/$id.json?id=$id")
|
||||
.parsedSafe<AnimeResponsePage>()?.pageProps
|
||||
?: throw ErrorLoadingException("Invalid Json reponse")
|
||||
val isMovie = res.anime.format == "MOVIE"
|
||||
return newAnimeLoadResponse(
|
||||
res.anime.title?.english ?: res.anime.title?.romaji
|
||||
?: throw ErrorLoadingException("Invalid title reponse"),
|
||||
url, if (isMovie) TvType.AnimeMovie else TvType.Anime
|
||||
) {
|
||||
recommendations = res.recommended.mapNotNull { it.toSearchResponse() }
|
||||
tags = res.anime.genres
|
||||
posterUrl = res.anime.coverImage?.large ?: res.anime.coverImage?.medium
|
||||
plot = res.anime.description
|
||||
showStatus = when (res.anime.status) {
|
||||
"FINISHED" -> ShowStatus.Completed
|
||||
"RELEASING" -> ShowStatus.Ongoing
|
||||
else -> null
|
||||
}
|
||||
addAniListId(id.toIntOrNull())
|
||||
|
||||
// subbed because they are both subbed and dubbed
|
||||
if (isMovie)
|
||||
addEpisodes(
|
||||
DubStatus.Subbed,
|
||||
listOf(newEpisode("$mainUrl/api/anime/?id=$id&episode=1"))
|
||||
)
|
||||
else
|
||||
addEpisodes(DubStatus.Subbed, res.episodes.episodes?.nodes?.mapIndexed { index, node ->
|
||||
val episodeIndex = node?.number ?: (index + 1)
|
||||
//"$mainUrl/_next/data/$token/watch/$id.json?episode=${node.number ?: return@mapNotNull null}&id=$id"
|
||||
newEpisode("$mainUrl/api/anime?id=$id&episode=${episodeIndex}") {
|
||||
episode = episodeIndex
|
||||
posterUrl = node?.thumbnail?.original?.url
|
||||
name = node?.titles?.canonical
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
return app.get(data).parsed<AniLoadResponse>().let { res ->
|
||||
val dubReferer = res.dub?.Referer ?: ""
|
||||
res.dub?.sources?.forEach { source ->
|
||||
callback(
|
||||
ExtractorLink(
|
||||
name,
|
||||
"${source.label ?: name} (DUB)",
|
||||
source.file ?: return@forEach,
|
||||
dubReferer,
|
||||
getQualityFromName(source.label),
|
||||
source.type == "hls"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val subReferer = res.dub?.Referer ?: ""
|
||||
res.sub?.sources?.forEach { source ->
|
||||
callback(
|
||||
ExtractorLink(
|
||||
name,
|
||||
"${source.label ?: name} (SUB)",
|
||||
source.file ?: return@forEach,
|
||||
subReferer,
|
||||
getQualityFromName(source.label),
|
||||
source.type == "hls"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
!res.dub?.sources.isNullOrEmpty() && !res.sub?.sources.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
data class AniLoadResponse(
|
||||
@JsonProperty("sub") val sub: DubSubSource?,
|
||||
@JsonProperty("dub") val dub: DubSubSource?,
|
||||
@JsonProperty("episodes") val episodes: Int?
|
||||
)
|
||||
|
||||
data class Sources(
|
||||
@JsonProperty("file") val file: String?,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("type") val type: String?
|
||||
)
|
||||
|
||||
data class DubSubSource(
|
||||
@JsonProperty("Referer") var Referer: String?,
|
||||
@JsonProperty("sources") var sources: ArrayList<Sources> = arrayListOf()
|
||||
)
|
||||
|
||||
data class PageProps(
|
||||
@JsonProperty("searchResults") val searchResults: SearchResults?
|
||||
)
|
||||
|
||||
data class SearchResults(
|
||||
@JsonProperty("Page") val Page: Page?
|
||||
)
|
||||
|
||||
data class Page(
|
||||
@JsonProperty("media") val media: ArrayList<Anime> = arrayListOf()
|
||||
)
|
||||
|
||||
data class CoverImage(
|
||||
@JsonProperty("color") val color: String?,
|
||||
@JsonProperty("medium") val medium: String?,
|
||||
@JsonProperty("large") val large: String?,
|
||||
)
|
||||
|
||||
data class Title(
|
||||
@JsonProperty("english") val english: String?,
|
||||
@JsonProperty("romaji") val romaji: String?,
|
||||
)
|
||||
|
||||
data class Search(
|
||||
@JsonProperty("pageProps") val pageProps: PageProps?,
|
||||
@JsonProperty("__N_SSP") val _NSSP: Boolean?
|
||||
)
|
||||
|
||||
data class Anime(
|
||||
@JsonProperty("status") val status: String?,
|
||||
@JsonProperty("id") val id: Int?,
|
||||
@JsonProperty("title") val title: Title?,
|
||||
@JsonProperty("coverImage") val coverImage: CoverImage?,
|
||||
@JsonProperty("format") val format: String?,
|
||||
@JsonProperty("duration") val duration: Int?,
|
||||
@JsonProperty("meanScore") val meanScore: Int?,
|
||||
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: String?,
|
||||
@JsonProperty("bannerImage") val bannerImage: String?,
|
||||
@JsonProperty("description") val description: String?,
|
||||
@JsonProperty("genres") val genres: ArrayList<String>? = null,
|
||||
@JsonProperty("season") val season: String?,
|
||||
@JsonProperty("startDate") val startDate: StartDate?,
|
||||
)
|
||||
|
||||
data class StartDate(
|
||||
@JsonProperty("year") val year: Int?
|
||||
)
|
||||
|
||||
data class AnimeResponsePage(
|
||||
@JsonProperty("pageProps") val pageProps: AnimeResponse?,
|
||||
)
|
||||
|
||||
data class AnimeResponse(
|
||||
@JsonProperty("anime") val anime: Anime,
|
||||
@JsonProperty("recommended") val recommended: ArrayList<Anime>,
|
||||
@JsonProperty("episodes") val episodes: EpisodesParent,
|
||||
)
|
||||
|
||||
data class EpisodesParent(
|
||||
@JsonProperty("id") val id: String?,
|
||||
@JsonProperty("season") val season: String?,
|
||||
@JsonProperty("startDate") val startDate: String?,
|
||||
@JsonProperty("episodeCount") val episodeCount: Int?,
|
||||
@JsonProperty("episodes") val episodes: Episodes?,
|
||||
)
|
||||
|
||||
data class Episodes(
|
||||
@JsonProperty("nodes") val nodes: ArrayList<Nodes?> = arrayListOf()
|
||||
)
|
||||
|
||||
data class Nodes(
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("titles") val titles: Titles?,
|
||||
@JsonProperty("thumbnail") val thumbnail: Thumbnail?,
|
||||
)
|
||||
|
||||
data class Titles(
|
||||
@JsonProperty("canonical") val canonical: String?,
|
||||
)
|
||||
|
||||
data class Original(
|
||||
@JsonProperty("url") val url: String?,
|
||||
)
|
||||
|
||||
data class Thumbnail(
|
||||
@JsonProperty("original") val original: Original?,
|
||||
)
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AniflixProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AniflixProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=animeflick.net&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,119 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.extractorApis
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.*
|
||||
|
||||
class AnimeFlickProvider : MainAPI() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
override var mainUrl = "https://animeflick.net"
|
||||
override var name = "AnimeFlick"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = false
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.Anime,
|
||||
TvType.OVA
|
||||
)
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val link = "https://animeflick.net/search.php?search=$query"
|
||||
val html = app.get(link).text
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
return doc.select(".row.mt-2").mapNotNull {
|
||||
val href = mainUrl + it.selectFirst("a")?.attr("href")
|
||||
val title = it.selectFirst("h5 > a")?.text() ?: return@mapNotNull null
|
||||
val poster = mainUrl + it.selectFirst("img")?.attr("src")?.replace("70x110", "225x320")
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
getType(title),
|
||||
poster,
|
||||
null,
|
||||
EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val html = app.get(url).text
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
val poster = mainUrl + doc.selectFirst("img.rounded")?.attr("src")
|
||||
val title = doc.selectFirst("h2.title")!!.text()
|
||||
|
||||
val yearText = doc.selectFirst(".trending-year")?.text()
|
||||
val year =
|
||||
if (yearText != null) Regex("""(\d{4})""").find(yearText)?.destructured?.component1()
|
||||
?.toIntOrNull() else null
|
||||
val description = doc.selectFirst("p")?.text()
|
||||
|
||||
val genres = doc.select("a[href*=\"genre-\"]").map { it.text() }
|
||||
|
||||
val episodes = doc.select("#collapseOne .block-space > .row > div:nth-child(2)").map {
|
||||
val name = it.selectFirst("a")?.text()
|
||||
val link = mainUrl + it.selectFirst("a")?.attr("href")
|
||||
Episode(link, name)
|
||||
}.reversed()
|
||||
|
||||
return newAnimeLoadResponse(title, url, getType(title)) {
|
||||
posterUrl = poster
|
||||
this.year = year
|
||||
|
||||
addEpisodes(DubStatus.Subbed, episodes)
|
||||
|
||||
plot = description
|
||||
tags = genres
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val html = app.get(data).text
|
||||
|
||||
val episodeRegex = Regex("""(https://.*?\.mp4)""")
|
||||
val links = episodeRegex.findAll(html).map {
|
||||
it.value
|
||||
}.toList()
|
||||
for (link in links) {
|
||||
var alreadyAdded = false
|
||||
for (extractor in extractorApis) {
|
||||
if (link.startsWith(extractor.mainUrl)) {
|
||||
extractor.getSafeUrl(link, data, subtitleCallback, callback)
|
||||
alreadyAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!alreadyAdded) {
|
||||
callback(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
"${this.name} - Auto",
|
||||
link,
|
||||
"",
|
||||
Qualities.P1080.value
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AnimeFlickProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AnimeFlickProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// authors = listOf("Cloudburst")
|
||||
|
||||
/**
|
||||
* Status int as the following:
|
||||
* 0: Down
|
||||
* 1: Ok
|
||||
* 2: Slow
|
||||
* 3: Beta only
|
||||
* */
|
||||
status = 0 // will be 3 if unspecified
|
||||
tvTypes = listOf(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,562 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.JsUnpacker
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import org.jsoup.Jsoup
|
||||
import kotlin.math.pow
|
||||
|
||||
class AnimePaheProvider : MainAPI() {
|
||||
// credit to https://github.com/justfoolingaround/animdl/tree/master/animdl/core/codebase/providers/animepahe
|
||||
companion object {
|
||||
const val MAIN_URL = "https://animepahe.com"
|
||||
|
||||
var cookies: Map<String, String> = mapOf()
|
||||
private 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
|
||||
}
|
||||
|
||||
suspend fun generateSession(): Boolean {
|
||||
if (cookies.isNotEmpty()) return true
|
||||
return try {
|
||||
val response = app.get("$MAIN_URL/")
|
||||
cookies = response.cookies
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val YTSM = Regex("ysmm = '([^']+)")
|
||||
|
||||
val KWIK_PARAMS_RE = Regex("""\("(\w+)",\d+,"(\w+)",(\d+),(\d+),\d+\)""")
|
||||
val KWIK_D_URL = Regex("action=\"([^\"]+)\"")
|
||||
val KWIK_D_TOKEN = Regex("value=\"([^\"]+)\"")
|
||||
val YOUTUBE_VIDEO_LINK =
|
||||
Regex("""(^(?:https?:)?(?://)?(?:www\.)?(?:youtu\.be/|youtube(?:-nocookie)?\.(?:[A-Za-z]{2,4}|[A-Za-z]{2,3}\.[A-Za-z]{2})/)(?:watch|embed/|vi?/)*(?:\?[\w=&]*vi?=)?[^#&?/]{11}.*${'$'})""")
|
||||
}
|
||||
|
||||
override var mainUrl = MAIN_URL
|
||||
override var name = "AnimePahe"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.Anime,
|
||||
TvType.OVA
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
data class Data(
|
||||
@JsonProperty("id") val id: Int,
|
||||
@JsonProperty("anime_id") val animeId: Int,
|
||||
@JsonProperty("anime_title") val animeTitle: String,
|
||||
@JsonProperty("anime_slug") val animeSlug: String,
|
||||
@JsonProperty("episode") val episode: Int,
|
||||
@JsonProperty("snapshot") val snapshot: String,
|
||||
@JsonProperty("created_at") val createdAt: String,
|
||||
@JsonProperty("anime_session") val animeSession: String,
|
||||
)
|
||||
|
||||
data class AnimePaheLatestReleases(
|
||||
@JsonProperty("total") val total: Int,
|
||||
@JsonProperty("data") val data: List<Data>
|
||||
)
|
||||
|
||||
val urls = listOf(
|
||||
Pair("$mainUrl/api?m=airing&page=1", "Latest Releases"),
|
||||
)
|
||||
|
||||
val items = ArrayList<HomePageList>()
|
||||
for (i in urls) {
|
||||
try {
|
||||
val response = app.get(i.first).text
|
||||
val episodes = parseJson<AnimePaheLatestReleases>(response).data.map {
|
||||
newAnimeSearchResponse(
|
||||
it.animeTitle,
|
||||
"https://pahe.win/a/${it.animeId}?slug=${it.animeTitle}",
|
||||
fix = false
|
||||
) {
|
||||
this.posterUrl = it.snapshot
|
||||
addDubStatus(DubStatus.Subbed, it.episode)
|
||||
}
|
||||
}
|
||||
|
||||
items.add(HomePageList(i.second, episodes))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
data class AnimePaheSearchData(
|
||||
@JsonProperty("id") val id: Int,
|
||||
@JsonProperty("slug") val slug: String,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("type") val type: String,
|
||||
@JsonProperty("episodes") val episodes: Int,
|
||||
@JsonProperty("status") val status: String,
|
||||
@JsonProperty("season") val season: String,
|
||||
@JsonProperty("year") val year: Int,
|
||||
@JsonProperty("score") val score: Double,
|
||||
@JsonProperty("poster") val poster: String,
|
||||
@JsonProperty("session") val session: String,
|
||||
@JsonProperty("relevance") val relevance: String
|
||||
)
|
||||
|
||||
data class AnimePaheSearch(
|
||||
@JsonProperty("total") val total: Int,
|
||||
@JsonProperty("data") val data: List<AnimePaheSearchData>
|
||||
)
|
||||
|
||||
private suspend fun getAnimeByIdAndTitle(title: String, animeId: Int): String? {
|
||||
val url = "$mainUrl/api?m=search&l=8&q=$title"
|
||||
val headers = mapOf("referer" to "$mainUrl/")
|
||||
|
||||
val req = app.get(url, headers = headers).text
|
||||
val data = parseJson<AnimePaheSearch>(req)
|
||||
for (anime in data.data) {
|
||||
if (anime.id == animeId) {
|
||||
return "https://animepahe.com/anime/${anime.session}"
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/api?m=search&l=8&q=$query"
|
||||
val headers = mapOf("referer" to "$mainUrl/")
|
||||
|
||||
val req = app.get(url, headers = headers).text
|
||||
val data = parseJson<AnimePaheSearch>(req)
|
||||
|
||||
return data.data.map {
|
||||
newAnimeSearchResponse(
|
||||
it.title,
|
||||
"https://pahe.win/a/${it.id}?slug=${it.title}",
|
||||
fix = false
|
||||
) {
|
||||
this.posterUrl = it.poster
|
||||
addDubStatus(DubStatus.Subbed, it.episodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class AnimeData(
|
||||
@JsonProperty("id") val id: Int,
|
||||
@JsonProperty("anime_id") val animeId: Int,
|
||||
@JsonProperty("episode") val episode: Int,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("snapshot") val snapshot: String,
|
||||
@JsonProperty("session") val session: String,
|
||||
@JsonProperty("filler") val filler: Int,
|
||||
@JsonProperty("created_at") val createdAt: String
|
||||
)
|
||||
|
||||
private data class AnimePaheAnimeData(
|
||||
@JsonProperty("total") val total: Int,
|
||||
@JsonProperty("per_page") val perPage: Int,
|
||||
@JsonProperty("current_page") val currentPage: Int,
|
||||
@JsonProperty("last_page") val lastPage: Int,
|
||||
@JsonProperty("next_page_url") val nextPageUrl: String?,
|
||||
@JsonProperty("prev_page_url") val prevPageUrl: String?,
|
||||
@JsonProperty("from") val from: Int,
|
||||
@JsonProperty("to") val to: Int,
|
||||
@JsonProperty("data") val data: List<AnimeData>
|
||||
)
|
||||
|
||||
private suspend fun generateListOfEpisodes(link: String): ArrayList<Episode> {
|
||||
try {
|
||||
val attrs = link.split('/')
|
||||
val id = attrs[attrs.size - 1].split("?")[0]
|
||||
|
||||
val uri = "$mainUrl/api?m=release&id=$id&sort=episode_asc&page=1"
|
||||
val headers = mapOf("referer" to "$mainUrl/")
|
||||
|
||||
val req = app.get(uri, headers = headers).text
|
||||
val data = parseJson<AnimePaheAnimeData>(req)
|
||||
|
||||
val lastPage = data.lastPage
|
||||
val perPage = data.perPage
|
||||
val total = data.total
|
||||
var ep = 1
|
||||
val episodes = ArrayList<Episode>()
|
||||
|
||||
fun getEpisodeTitle(k: AnimeData): String {
|
||||
return k.title.ifEmpty {
|
||||
"Episode ${k.episode}"
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPage == 1 && perPage > total) {
|
||||
data.data.forEach {
|
||||
episodes.add(
|
||||
newEpisode("$mainUrl/api?m=links&id=${it.animeId}&session=${it.session}&p=kwik!!TRUE!!") {
|
||||
addDate(it.createdAt)
|
||||
this.name = getEpisodeTitle(it)
|
||||
this.posterUrl = it.snapshot
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for (page in 0 until lastPage) {
|
||||
for (i in 0 until perPage) {
|
||||
if (ep <= total) {
|
||||
episodes.add(
|
||||
Episode(
|
||||
"$mainUrl/api?m=release&id=${id}&sort=episode_asc&page=${page + 1}&ep=${ep}!!FALSE!!"
|
||||
)
|
||||
)
|
||||
++ep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return episodes
|
||||
} catch (e: Exception) {
|
||||
return ArrayList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
return suspendSafeApiCall {
|
||||
val regex = Regex("""a/(\d+)\?slug=(.+)""")
|
||||
val (animeId, animeTitle) = regex.find(url)!!.destructured
|
||||
val link = getAnimeByIdAndTitle(animeTitle, animeId.toInt())!!
|
||||
|
||||
val html = app.get(link).text
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
val japTitle = doc.selectFirst("h2.japanese")?.text()
|
||||
val poster = doc.selectFirst(".anime-poster a")?.attr("href")
|
||||
|
||||
val tvType = doc.selectFirst("""a[href*="/anime/type/"]""")?.text()
|
||||
|
||||
val trailer: String? = if (html.contains("https://www.youtube.com/watch")) {
|
||||
YOUTUBE_VIDEO_LINK.find(html)?.destructured?.component1()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val episodes = generateListOfEpisodes(url)
|
||||
val year = Regex("""<strong>Aired:</strong>[^,]*, (\d+)""")
|
||||
.find(html)!!.destructured.component1()
|
||||
.toIntOrNull()
|
||||
val status =
|
||||
when (Regex("""<strong>Status:</strong>[^a]*a href=["']/anime/(.*?)["']""")
|
||||
.find(html)!!.destructured.component1()) {
|
||||
"airing" -> ShowStatus.Ongoing
|
||||
"completed" -> ShowStatus.Completed
|
||||
else -> null
|
||||
}
|
||||
val synopsis = doc.selectFirst(".anime-synopsis")?.text()
|
||||
|
||||
var anilistId: Int? = null
|
||||
var malId: Int? = null
|
||||
|
||||
doc.select(".external-links > a").forEach { aTag ->
|
||||
val split = aTag.attr("href").split("/")
|
||||
|
||||
if (aTag.attr("href").contains("anilist.co")) {
|
||||
anilistId = split[split.size - 1].toIntOrNull()
|
||||
} else if (aTag.attr("href").contains("myanimelist.net")) {
|
||||
malId = split[split.size - 1].toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
newAnimeLoadResponse(animeTitle, url, getType(tvType.toString())) {
|
||||
engName = animeTitle
|
||||
japName = japTitle
|
||||
|
||||
this.posterUrl = poster
|
||||
this.year = year
|
||||
|
||||
addEpisodes(DubStatus.Subbed, episodes)
|
||||
this.showStatus = status
|
||||
plot = synopsis
|
||||
tags = if (!doc.select(".anime-genre > ul a").isEmpty()) {
|
||||
ArrayList(doc.select(".anime-genre > ul a").map { it.text().toString() })
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
addMalId(malId)
|
||||
addAniListId(anilistId)
|
||||
addTrailer(trailer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun isNumber(s: String?): Boolean {
|
||||
return s?.toIntOrNull() != null
|
||||
}
|
||||
|
||||
private fun cookieStrToMap(cookie: String): Map<String, String> {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
for (string in cookie.split("; ")) {
|
||||
val split = string.split("=").toMutableList()
|
||||
val name = split.removeFirst().trim()
|
||||
val value = if (split.size == 0) {
|
||||
"true"
|
||||
} else {
|
||||
split.joinToString("=")
|
||||
}
|
||||
cookies[name] = value
|
||||
}
|
||||
return cookies.toMap()
|
||||
}
|
||||
|
||||
private fun getString(content: String, s1: Int, s2: Int): String {
|
||||
val characterMap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
|
||||
|
||||
val slice2 = characterMap.slice(0 until s2)
|
||||
var acc: Long = 0
|
||||
|
||||
for ((n, i) in content.reversed().withIndex()) {
|
||||
acc += (when (isNumber("$i")) {
|
||||
true -> "$i".toLong()
|
||||
false -> "0".toLong()
|
||||
}) * s1.toDouble().pow(n.toDouble()).toInt()
|
||||
}
|
||||
|
||||
var k = ""
|
||||
|
||||
while (acc > 0) {
|
||||
k = slice2[(acc % s2).toInt()] + k
|
||||
acc = (acc - (acc % s2)) / s2
|
||||
}
|
||||
|
||||
return when (k != "") {
|
||||
true -> k
|
||||
false -> "0"
|
||||
}
|
||||
}
|
||||
|
||||
private fun decrypt(fullString: String, key: String, v1: Int, v2: Int): String {
|
||||
var r = ""
|
||||
var i = 0
|
||||
|
||||
while (i < fullString.length) {
|
||||
var s = ""
|
||||
|
||||
while (fullString[i] != key[v2]) {
|
||||
s += fullString[i]
|
||||
++i
|
||||
}
|
||||
var j = 0
|
||||
|
||||
while (j < key.length) {
|
||||
s = s.replace(key[j].toString(), j.toString())
|
||||
++j
|
||||
}
|
||||
r += (getString(s, v2, 10).toInt() - v1).toChar()
|
||||
++i
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
private fun zipGen(gen: Sequence<Pair<Int, Int>>): ArrayList<Pair<Pair<Int, Int>, Pair<Int, Int>>> {
|
||||
val allItems = gen.toList().toMutableList()
|
||||
val newList = ArrayList<Pair<Pair<Int, Int>, Pair<Int, Int>>>()
|
||||
|
||||
while (allItems.size > 1) {
|
||||
newList.add(Pair(allItems[0], allItems[1]))
|
||||
allItems.removeAt(0)
|
||||
allItems.removeAt(0)
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
private fun decodeAdfly(codedKey: String): String {
|
||||
var r = ""
|
||||
var j = ""
|
||||
|
||||
for ((n, l) in codedKey.withIndex()) {
|
||||
if (n % 2 != 0) {
|
||||
j = l + j
|
||||
} else {
|
||||
r += l
|
||||
}
|
||||
}
|
||||
|
||||
val encodedUri = ((r + j).toCharArray().map { it.toString() }).toMutableList()
|
||||
val numbers = sequence {
|
||||
for ((i, n) in encodedUri.withIndex()) {
|
||||
if (isNumber(n)) {
|
||||
yield(Pair(i, n.toInt()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ((first, second) in zipGen(numbers)) {
|
||||
val xor = first.second.xor(second.second)
|
||||
if (xor < 10) {
|
||||
encodedUri[first.first] = xor.toString()
|
||||
}
|
||||
}
|
||||
var returnValue = String(encodedUri.joinToString("").toByteArray(), Charsets.UTF_8)
|
||||
returnValue = base64Decode(returnValue)
|
||||
return returnValue.slice(16..returnValue.length - 17)
|
||||
}
|
||||
|
||||
private data class VideoQuality(
|
||||
@JsonProperty("id") val id: Int?,
|
||||
@JsonProperty("audio") val audio: String?,
|
||||
@JsonProperty("kwik") val kwik: String?,
|
||||
@JsonProperty("kwik_pahewin") val kwikPahewin: String
|
||||
)
|
||||
|
||||
private data class AnimePaheEpisodeLoadLinks(
|
||||
@JsonProperty("data") val data: List<Map<String, VideoQuality>>
|
||||
)
|
||||
|
||||
private suspend fun bypassAdfly(adflyUri: String): String {
|
||||
if (!generateSession()) {
|
||||
return bypassAdfly(adflyUri)
|
||||
}
|
||||
|
||||
var responseCode = 302
|
||||
var adflyContent: NiceResponse? = null
|
||||
var tries = 0
|
||||
|
||||
while (responseCode != 200 && tries < 20) {
|
||||
adflyContent = app.get(
|
||||
app.get(adflyUri, cookies = cookies, allowRedirects = false).url,
|
||||
cookies = cookies,
|
||||
allowRedirects = false
|
||||
)
|
||||
cookies = cookies + adflyContent.cookies
|
||||
responseCode = adflyContent.code
|
||||
++tries
|
||||
}
|
||||
if (tries > 19) {
|
||||
throw Exception("Failed to bypass adfly.")
|
||||
}
|
||||
return decodeAdfly(YTSM.find(adflyContent?.text.toString())!!.destructured.component1())
|
||||
}
|
||||
|
||||
private suspend fun getStreamUrlFromKwik(url: String?): String? {
|
||||
if (url == null) return null
|
||||
val response =
|
||||
app.get(
|
||||
url,
|
||||
headers = mapOf("referer" to mainUrl),
|
||||
cookies = cookies
|
||||
).text
|
||||
Regex("eval((.|\\n)*?)</script>").find(response)?.groupValues?.get(1)?.let { jsEval ->
|
||||
JsUnpacker("eval$jsEval").unpack()?.let { unPacked ->
|
||||
Regex("source=\'(.*?)\'").find(unPacked)?.groupValues?.get(1)?.let { link ->
|
||||
return link
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getStreamUrlFromKwikAdfly(adflyUri: String): String {
|
||||
val fContent =
|
||||
app.get(
|
||||
bypassAdfly(adflyUri),
|
||||
headers = mapOf("referer" to "https://kwik.cx/"),
|
||||
cookies = cookies
|
||||
)
|
||||
cookies = cookies + fContent.cookies
|
||||
|
||||
val (fullString, key, v1, v2) = KWIK_PARAMS_RE.find(fContent.text)!!.destructured
|
||||
val decrypted = decrypt(fullString, key, v1.toInt(), v2.toInt())
|
||||
val uri = KWIK_D_URL.find(decrypted)!!.destructured.component1()
|
||||
val tok = KWIK_D_TOKEN.find(decrypted)!!.destructured.component1()
|
||||
var content: NiceResponse? = null
|
||||
|
||||
var code = 419
|
||||
var tries = 0
|
||||
|
||||
while (code != 302 && tries < 20) {
|
||||
content = app.post(
|
||||
uri,
|
||||
allowRedirects = false,
|
||||
data = mapOf("_token" to tok),
|
||||
headers = mapOf("referer" to fContent.url),
|
||||
cookies = fContent.cookies
|
||||
)
|
||||
code = content.code
|
||||
++tries
|
||||
}
|
||||
if (tries > 19) {
|
||||
throw Exception("Failed to extract the stream uri from kwik.")
|
||||
}
|
||||
return content?.headers?.values("location").toString()
|
||||
}
|
||||
|
||||
private suspend fun extractVideoLinks(
|
||||
episodeLink: String,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
var link = episodeLink
|
||||
val headers = mapOf("referer" to "$mainUrl/")
|
||||
|
||||
if (link.contains("!!TRUE!!")) {
|
||||
link = link.replace("!!TRUE!!", "")
|
||||
} else {
|
||||
val regex = """&ep=(\d+)!!FALSE!!""".toRegex()
|
||||
val episodeNum = regex.find(link)?.destructured?.component1()?.toIntOrNull()
|
||||
link = link.replace(regex, "")
|
||||
|
||||
val req = app.get(link, headers = headers).text
|
||||
val jsonResponse = parseJson<AnimePaheAnimeData>(req)
|
||||
val ep = ((jsonResponse.data.map {
|
||||
if (it.episode == episodeNum) {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}).filterNotNull())[0]
|
||||
link = "$mainUrl/api?m=links&id=${ep.animeId}&session=${ep.session}&p=kwik"
|
||||
}
|
||||
val req = app.get(link, headers = headers).text
|
||||
val data = mapper.readValue<AnimePaheEpisodeLoadLinks>(req)
|
||||
|
||||
data.data.forEach {
|
||||
it.entries.toList().apmap { quality ->
|
||||
getStreamUrlFromKwik(quality.value.kwik)?.let { link ->
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"KWIK",
|
||||
"KWIK - ${quality.key} [${quality.value.audio ?: "jpn"}]",
|
||||
link,
|
||||
"https://kwik.cx/",
|
||||
getQualityFromName(quality.key),
|
||||
link.contains(".m3u8")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
extractVideoLinks(data, callback)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AnimePaheProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AnimePaheProvider())
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// authors = listOf("Cloudburst")
|
||||
|
||||
/**
|
||||
* Status int as the following:
|
||||
* 0: Down
|
||||
* 1: Ok
|
||||
* 2: Slow
|
||||
* 3: Beta only
|
||||
* */
|
||||
status = 0 // will be 3 if unspecified
|
||||
tvTypes = listOf(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=animekisa.in&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,131 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.*
|
||||
|
||||
|
||||
class AnimekisaProvider : MainAPI() {
|
||||
override var mainUrl = "https://animekisa.in"
|
||||
override var name = "Animekisa"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.OVA,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
data class Response(
|
||||
@JsonProperty("html") val html: String
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val urls = listOf(
|
||||
Pair("$mainUrl/ajax/list/views?type=all", "All animes"),
|
||||
Pair("$mainUrl/ajax/list/views?type=day", "Trending now"),
|
||||
Pair("$mainUrl/ajax/list/views?type=week", "Trending by week"),
|
||||
Pair("$mainUrl/ajax/list/views?type=month", "Trending by month"),
|
||||
)
|
||||
|
||||
val items = urls.mapNotNull {
|
||||
suspendSafeApiCall {
|
||||
val home = Jsoup.parse(
|
||||
parseJson<Response>(
|
||||
app.get(
|
||||
it.first
|
||||
).text
|
||||
).html
|
||||
).select("div.flw-item").mapNotNull secondMap@ {
|
||||
val title = it.selectFirst("h3.title a")?.text() ?: return@secondMap null
|
||||
val link = it.selectFirst("a")?.attr("href") ?: return@secondMap null
|
||||
val poster = it.selectFirst("img.lazyload")?.attr("data-src")
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
poster,
|
||||
null,
|
||||
if (title.contains("(DUB)") || title.contains("(Dub)")) EnumSet.of(
|
||||
DubStatus.Dubbed
|
||||
) else EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
HomePageList(name, home)
|
||||
}
|
||||
}
|
||||
|
||||
if (items.isEmpty()) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
return app.get("$mainUrl/search/?keyword=$query").document.select("div.flw-item")
|
||||
.mapNotNull {
|
||||
val title = it.selectFirst("h3 a")?.text() ?: ""
|
||||
val url = it.selectFirst("a.film-poster-ahref")?.attr("href")
|
||||
?.replace("watch/", "anime/")?.replace(
|
||||
Regex("(-episode-(\\d+)/\$|-episode-(\\d+)\$|-episode-full|-episode-.*-.(/|))"),
|
||||
""
|
||||
) ?: return@mapNotNull null
|
||||
val poster = it.selectFirst(".film-poster img")?.attr("data-src")
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
poster,
|
||||
null,
|
||||
if (title.contains("(DUB)") || title.contains("(Dub)")) EnumSet.of(
|
||||
DubStatus.Dubbed
|
||||
) else EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val doc = app.get(url, timeout = 120).document
|
||||
val poster = doc.selectFirst(".mb-2 img")?.attr("src")
|
||||
?: doc.selectFirst("head meta[property=og:image]")?.attr("content")
|
||||
val title = doc.selectFirst("h1.heading-name a")!!.text()
|
||||
val description = doc.selectFirst("div.description p")?.text()?.trim()
|
||||
val genres = doc.select("div.row-line a").map { it.text() }
|
||||
val test = if (doc.selectFirst("div.dp-i-c-right").toString()
|
||||
.contains("Airing")
|
||||
) ShowStatus.Ongoing else ShowStatus.Completed
|
||||
val episodes = doc.select("div.tab-content ul li.nav-item").mapNotNull {
|
||||
val link = it.selectFirst("a")?.attr("href") ?: return@mapNotNull null
|
||||
Episode(link)
|
||||
}
|
||||
val type = if (doc.selectFirst(".dp-i-stats").toString()
|
||||
.contains("Movies")
|
||||
) TvType.AnimeMovie else TvType.Anime
|
||||
return newAnimeLoadResponse(title, url, type) {
|
||||
posterUrl = poster
|
||||
addEpisodes(DubStatus.Subbed, episodes)
|
||||
showStatus = test
|
||||
plot = description
|
||||
tags = genres
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
app.get(data).document.select("#servers-list ul.nav li a").apmap {
|
||||
val server = it.attr("data-embed")
|
||||
loadExtractor(server, data, subtitleCallback, callback)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AnimekisaProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AnimekisaProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AsianDrama",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=asiaflix.app&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,198 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.*
|
||||
//import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider.Companion.getStatus
|
||||
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() {
|
||||
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) {
|
||||
"Completed" -> ShowStatus.Completed
|
||||
"Ongoing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var mainUrl = "https://asiaflix.app"
|
||||
override var name = "AsiaFlix"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = false
|
||||
override val supportedTypes = setOf(TvType.AsianDrama)
|
||||
|
||||
private val apiUrl = "https://api.asiaflix.app/api/v2"
|
||||
|
||||
data class DashBoardObject(
|
||||
@JsonProperty("sectionName") val sectionName: String,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("data") val data: List<Data>?
|
||||
)
|
||||
|
||||
data class Episodes(
|
||||
@JsonProperty("_id") val _id: String,
|
||||
@JsonProperty("epUrl") val epUrl: String?,
|
||||
@JsonProperty("number") val number: Int?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("extracted") val extracted: String?,
|
||||
@JsonProperty("videoUrl") val videoUrl: String?
|
||||
)
|
||||
|
||||
|
||||
data class Data(
|
||||
@JsonProperty("_id") val _id: String,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("altNames") val altNames: String?,
|
||||
@JsonProperty("image") val image: String?,
|
||||
@JsonProperty("tvStatus") val tvStatus: String?,
|
||||
@JsonProperty("genre") val genre: String?,
|
||||
@JsonProperty("releaseYear") val releaseYear: Int?,
|
||||
@JsonProperty("createdAt") val createdAt: Long?,
|
||||
@JsonProperty("episodes") val episodes: List<Episodes>?,
|
||||
@JsonProperty("views") val views: Int?
|
||||
)
|
||||
|
||||
|
||||
data class DramaPage(
|
||||
@JsonProperty("_id") val _id: String,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("altNames") val altNames: String?,
|
||||
@JsonProperty("synopsis") val synopsis: String?,
|
||||
@JsonProperty("image") val image: String?,
|
||||
@JsonProperty("language") val language: String?,
|
||||
@JsonProperty("dramaUrl") val dramaUrl: String?,
|
||||
@JsonProperty("published") val published: Boolean?,
|
||||
@JsonProperty("tvStatus") val tvStatus: String?,
|
||||
@JsonProperty("firstAirDate") val firstAirDate: String?,
|
||||
@JsonProperty("genre") val genre: String?,
|
||||
@JsonProperty("releaseYear") val releaseYear: Int?,
|
||||
@JsonProperty("createdAt") val createdAt: Long?,
|
||||
@JsonProperty("modifiedAt") val modifiedAt: Long?,
|
||||
@JsonProperty("episodes") val episodes: List<Episodes>,
|
||||
@JsonProperty("__v") val __v: Int?,
|
||||
@JsonProperty("cdnImage") val cdnImage: String?,
|
||||
@JsonProperty("views") val views: Int?
|
||||
)
|
||||
|
||||
private fun Data.toSearchResponse(): TvSeriesSearchResponse {
|
||||
return TvSeriesSearchResponse(
|
||||
name,
|
||||
_id,
|
||||
this@AsiaFlixProvider.name,
|
||||
TvType.AsianDrama,
|
||||
image,
|
||||
releaseYear,
|
||||
episodes?.size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Episodes.toEpisode(): Episode? {
|
||||
if (videoUrl != null && videoUrl.contains("watch/null") || number == null) return null
|
||||
return videoUrl?.let {
|
||||
Episode(
|
||||
it,
|
||||
null,
|
||||
number,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DramaPage.toLoadResponse(): TvSeriesLoadResponse {
|
||||
return TvSeriesLoadResponse(
|
||||
name,
|
||||
"$mainUrl$dramaUrl/$_id".replace("drama-detail", "show-details"),
|
||||
this@AsiaFlixProvider.name,
|
||||
TvType.AsianDrama,
|
||||
episodes.mapNotNull { it.toEpisode() }.sortedBy { it.episode },
|
||||
image,
|
||||
releaseYear,
|
||||
synopsis,
|
||||
getStatus(tvStatus ?: ""),
|
||||
null,
|
||||
genre?.split(",")?.map { it.trim() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
val headers = mapOf("X-Requested-By" to "asiaflix-web")
|
||||
val response = app.get("$apiUrl/dashboard", headers = headers).text
|
||||
|
||||
val customMapper =
|
||||
mapper.copy().configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
|
||||
// Hack, because it can either be object or a list
|
||||
val cleanedResponse = Regex(""""data":(\{.*?),\{"sectionName"""").replace(response) {
|
||||
""""data":null},{"sectionName""""
|
||||
}
|
||||
|
||||
val dashBoard = customMapper.readValue<List<DashBoardObject>?>(cleanedResponse)
|
||||
|
||||
val listItems = dashBoard?.mapNotNull {
|
||||
it.data?.map { data ->
|
||||
data.toSearchResponse()
|
||||
}?.let { searchResponse ->
|
||||
HomePageList(it.sectionName, searchResponse)
|
||||
}
|
||||
}
|
||||
return HomePageResponse(listItems ?: listOf())
|
||||
}
|
||||
|
||||
data class Link(
|
||||
@JsonProperty("url") val url: String?,
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
if (isCasting) return false
|
||||
val headers = mapOf("X-Requested-By" to "asiaflix-web")
|
||||
app.get(
|
||||
"$apiUrl/utility/get-stream-links?url=$data",
|
||||
headers = headers
|
||||
).text.toKotlinObject<Link>().url?.let {
|
||||
// val fixedUrl = "https://api.asiaflix.app/api/v2/utility/cors-proxy/playlist/${URLEncoder.encode(it, StandardCharsets.UTF_8.toString())}"
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
it,
|
||||
"https://asianload1.com/",
|
||||
/** <------ This provider should be added instead */
|
||||
getQualityFromName(it),
|
||||
URI(it).path.endsWith(".m3u8")
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val headers = mapOf("X-Requested-By" to "asiaflix-web")
|
||||
val url = "$apiUrl/drama/search?q=$query"
|
||||
val response = app.get(url, headers = headers).text
|
||||
return mapper.readValue<List<Data>?>(response)?.map { it.toSearchResponse() }
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val headers = mapOf("X-Requested-By" to "asiaflix-web")
|
||||
val requestUrl = "$apiUrl/drama?id=${url.split("/").lastOrNull()}"
|
||||
val response = app.get(requestUrl, headers = headers).text
|
||||
val dramaPage = response.toKotlinObject<DramaPage>()
|
||||
return dramaPage.toLoadResponse()
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class AsiaFlixProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(AsiaFlixProvider())
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// authors = listOf("Cloudburst")
|
||||
|
||||
/**
|
||||
* Status int as the following:
|
||||
* 0: Down
|
||||
* 1: Ok
|
||||
* 2: Slow
|
||||
* 3: Beta only
|
||||
* */
|
||||
status = 1 // will be 3 if unspecified
|
||||
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=bflix.ru&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,300 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.NineAnimeApi.decodeVrf
|
||||
import com.lagradost.NineAnimeApi.encode
|
||||
import com.lagradost.NineAnimeApi.encodeVrf
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
open class BflixProvider : MainAPI() {
|
||||
override var mainUrl = "https://bflix.ru"
|
||||
override var name = "Bflix"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
|
||||
//override val uniqueId: Int by lazy { "BflixProvider".hashCode() }
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val soup = app.get("$mainUrl/home").document
|
||||
val testa = listOf(
|
||||
Pair("Movies", "div.tab-content[data-name=movies] div.filmlist div.item"),
|
||||
Pair("Shows", "div.tab-content[data-name=shows] div.filmlist div.item"),
|
||||
Pair("Trending", "div.tab-content[data-name=trending] div.filmlist div.item"),
|
||||
Pair(
|
||||
"Latest Movies",
|
||||
"div.container section.bl:contains(Latest Movies) div.filmlist div.item"
|
||||
),
|
||||
Pair(
|
||||
"Latest TV-Series",
|
||||
"div.container section.bl:contains(Latest TV-Series) div.filmlist div.item"
|
||||
),
|
||||
)
|
||||
for ((name, element) in testa) try {
|
||||
val test = soup.select(element).map {
|
||||
val title = it.selectFirst("h3 a")!!.text()
|
||||
val link = fixUrl(it.selectFirst("a")!!.attr("href"))
|
||||
val qualityInfo = it.selectFirst("div.quality")!!.text()
|
||||
val quality = getQualityFromString(qualityInfo)
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
if (link.contains("/movie/")) TvType.Movie else TvType.TvSeries,
|
||||
it.selectFirst("a.poster img")!!.attr("src"),
|
||||
null,
|
||||
null,
|
||||
quality = quality
|
||||
)
|
||||
}
|
||||
items.add(HomePageList(name, test))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val encodedquery = encodeVrf(query, mainKey)
|
||||
val url = "$mainUrl/search?keyword=$query&vrf=$encodedquery"
|
||||
val html = app.get(url).text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
return document.select(".filmlist div.item").map {
|
||||
val title = it.selectFirst("h3 a")!!.text()
|
||||
val href = fixUrl(it.selectFirst("a")!!.attr("href"))
|
||||
val image = it.selectFirst("a.poster img")!!.attr("src")
|
||||
val isMovie = href.contains("/movie/")
|
||||
val qualityInfo = it.selectFirst("div.quality")!!.text()
|
||||
val quality = getQualityFromString(qualityInfo)
|
||||
|
||||
if (isMovie) {
|
||||
MovieSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
image,
|
||||
null,
|
||||
quality = quality
|
||||
)
|
||||
} else {
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
image,
|
||||
null,
|
||||
null,
|
||||
quality = quality
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Response(
|
||||
@JsonProperty("html") val html: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
val mainKey = "OrAimkpzm6phmN3j"
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val soup = app.get(url).document
|
||||
val movieid = soup.selectFirst("div#watch")!!.attr("data-id")
|
||||
val movieidencoded = encodeVrf(movieid, mainKey)
|
||||
val title = soup.selectFirst("div.info h1")!!.text()
|
||||
val description = soup.selectFirst(".info .desc")?.text()?.trim()
|
||||
val poster: String? = try {
|
||||
soup.selectFirst("img.poster")!!.attr("src")
|
||||
} catch (e: Exception) {
|
||||
soup.selectFirst(".info .poster img")!!.attr("src")
|
||||
}
|
||||
|
||||
val tags = soup.select("div.info .meta div:contains(Genre) a").map { it.text() }
|
||||
val vrfUrl = "$mainUrl/ajax/film/servers?id=$movieid&vrf=$movieidencoded"
|
||||
println("VRF___ $vrfUrl")
|
||||
val episodes = Jsoup.parse(
|
||||
app.get(
|
||||
vrfUrl
|
||||
).parsed<Response>().html
|
||||
).select("div.episode").map {
|
||||
val a = it.selectFirst("a")
|
||||
val href = fixUrl(a!!.attr("href"))
|
||||
val extraData = a.attr("data-kname").let { str ->
|
||||
str.split("-").mapNotNull { subStr -> subStr.toIntOrNull() }
|
||||
}
|
||||
val isValid = extraData.size == 2
|
||||
val episode = if (isValid) extraData.getOrNull(1) else null
|
||||
val season = if (isValid) extraData.getOrNull(0) else null
|
||||
|
||||
val eptitle = it.selectFirst(".episode a span.name")!!.text()
|
||||
val secondtitle = it.selectFirst(".episode a span")!!.text()
|
||||
.replace(Regex("(Episode (\\d+):|Episode (\\d+)-|Episode (\\d+))"), "") ?: ""
|
||||
Episode(
|
||||
href,
|
||||
secondtitle + eptitle,
|
||||
season,
|
||||
episode,
|
||||
)
|
||||
}
|
||||
val tvType =
|
||||
if (url.contains("/movie/") && episodes.size == 1) TvType.Movie else TvType.TvSeries
|
||||
val recommendations =
|
||||
soup.select("div.bl-2 section.bl div.content div.filmlist div.item")
|
||||
.mapNotNull { element ->
|
||||
val recTitle = element.select("h3 a").text() ?: return@mapNotNull null
|
||||
val image = element.select("a.poster img")?.attr("src")
|
||||
val recUrl = fixUrl(element.select("a").attr("href"))
|
||||
MovieSearchResponse(
|
||||
recTitle,
|
||||
recUrl,
|
||||
this.name,
|
||||
if (recUrl.contains("/movie/")) TvType.Movie else TvType.TvSeries,
|
||||
image,
|
||||
year = null
|
||||
)
|
||||
}
|
||||
val rating = soup.selectFirst(".info span.imdb")?.text()?.toRatingInt()
|
||||
val durationdoc = soup.selectFirst("div.info div.meta").toString()
|
||||
val durationregex = Regex("((\\d+) min)")
|
||||
val yearegex = Regex("<span>(\\d+)</span>")
|
||||
val duration = if (durationdoc.contains("na min")) null
|
||||
else durationregex.find(durationdoc)?.destructured?.component1()?.replace(" min", "")
|
||||
?.toIntOrNull()
|
||||
val year = if (mainUrl == "https://bflix.ru") {
|
||||
yearegex.find(durationdoc)?.destructured?.component1()
|
||||
?.replace(Regex("<span>|</span>"), "")
|
||||
} else null
|
||||
return when (tvType) {
|
||||
TvType.TvSeries -> {
|
||||
TvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
tvType,
|
||||
episodes,
|
||||
poster,
|
||||
year?.toIntOrNull(),
|
||||
description,
|
||||
null,
|
||||
rating,
|
||||
tags,
|
||||
recommendations = recommendations,
|
||||
duration = duration,
|
||||
)
|
||||
}
|
||||
TvType.Movie -> {
|
||||
MovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
tvType,
|
||||
url,
|
||||
poster,
|
||||
year?.toIntOrNull(),
|
||||
description,
|
||||
rating,
|
||||
tags,
|
||||
recommendations = recommendations,
|
||||
duration = duration
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class Subtitles(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String,
|
||||
@JsonProperty("kind") val kind: String
|
||||
)
|
||||
|
||||
data class Links(
|
||||
@JsonProperty("url") val url: String
|
||||
)
|
||||
|
||||
data class Servers(
|
||||
@JsonProperty("28") val mcloud: String?,
|
||||
@JsonProperty("35") val mp4upload: String?,
|
||||
@JsonProperty("40") val streamtape: String?,
|
||||
@JsonProperty("41") val vidstream: String?,
|
||||
@JsonProperty("43") val videovard: String?
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val soup = app.get(data).document
|
||||
|
||||
val movieid = encode(soup.selectFirst("div#watch")?.attr("data-id") ?: return false)
|
||||
val movieidencoded = encodeVrf(movieid, mainKey)
|
||||
Jsoup.parse(
|
||||
parseJson<Response>(
|
||||
app.get(
|
||||
"$mainUrl/ajax/film/servers?id=$movieid&vrf=$movieidencoded"
|
||||
).text
|
||||
).html
|
||||
)
|
||||
.select("html body #episodes").map {
|
||||
val cleandata = data.replace(mainUrl, "")
|
||||
val a = it.select("a").map {
|
||||
it.attr("data-kname")
|
||||
}
|
||||
val tvType =
|
||||
if (data.contains("movie/") && a.size == 1) TvType.Movie else TvType.TvSeries
|
||||
val servers = if (tvType == TvType.Movie) it.select(".episode a").attr("data-ep")
|
||||
else
|
||||
it.select(".episode a[href=$cleandata]").attr("data-ep")
|
||||
?: it.select(".episode a[href=${cleandata.replace("/1-full", "")}]")
|
||||
.attr("data-ep")
|
||||
val jsonservers = parseJson<Servers?>(servers) ?: return@map
|
||||
listOfNotNull(
|
||||
jsonservers.vidstream,
|
||||
jsonservers.mcloud,
|
||||
jsonservers.mp4upload,
|
||||
jsonservers.streamtape,
|
||||
jsonservers.videovard,
|
||||
).mapNotNull {
|
||||
val epserver = app.get("$mainUrl/ajax/episode/info?id=$it").text
|
||||
(if (epserver.contains("url")) {
|
||||
parseJson<Links>(epserver)
|
||||
} else null)?.url?.let {
|
||||
decodeVrf(it, mainKey)
|
||||
}
|
||||
}.apmap { url ->
|
||||
loadExtractor(
|
||||
url, data, subtitleCallback, callback
|
||||
)
|
||||
}
|
||||
//Apparently any server works, I haven't found any diference
|
||||
val sublink =
|
||||
app.get("$mainUrl/ajax/episode/subtitles/${jsonservers.mcloud}").text
|
||||
val jsonsub = parseJson<List<Subtitles>>(sublink)
|
||||
jsonsub.forEach { subtitle ->
|
||||
subtitleCallback(
|
||||
SubtitleFile(subtitle.label, subtitle.file)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class BflixProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(BflixProvider())
|
||||
registerMainAPI(FmoviesToProvider())
|
||||
registerMainAPI(SflixProProvider())
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
class FmoviesToProvider : BflixProvider() {
|
||||
override var mainUrl = "https://fmovies.to"
|
||||
override var name = "Fmovies.to"
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
// taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/NineAnime.kt
|
||||
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
|
||||
object NineAnimeApi {
|
||||
private const val nineAnimeKey =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
private const val cipherKey = "kMXzgyNzT3k5dYab"
|
||||
|
||||
fun encodeVrf(text: String, mainKey: String): String {
|
||||
return encode(
|
||||
encrypt(
|
||||
cipher(mainKey, encode(text)),
|
||||
nineAnimeKey
|
||||
)//.replace("""=+$""".toRegex(), "")
|
||||
)
|
||||
}
|
||||
|
||||
fun decodeVrf(text: String, mainKey: String): String {
|
||||
return decode(cipher(mainKey, decrypt(text, nineAnimeKey)))
|
||||
}
|
||||
|
||||
fun encrypt(input: String, key: String): String {
|
||||
if (input.any { it.code > 255 }) throw Exception("illegal characters!")
|
||||
var output = ""
|
||||
for (i in input.indices step 3) {
|
||||
val a = intArrayOf(-1, -1, -1, -1)
|
||||
a[0] = input[i].code shr 2
|
||||
a[1] = (3 and input[i].code) shl 4
|
||||
if (input.length > i + 1) {
|
||||
a[1] = a[1] or (input[i + 1].code shr 4)
|
||||
a[2] = (15 and input[i + 1].code) shl 2
|
||||
}
|
||||
if (input.length > i + 2) {
|
||||
a[2] = a[2] or (input[i + 2].code shr 6)
|
||||
a[3] = 63 and input[i + 2].code
|
||||
}
|
||||
for (n in a) {
|
||||
if (n == -1) output += "="
|
||||
else {
|
||||
if (n in 0..63) output += key[n]
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
fun cipher(key: String, text: String): String {
|
||||
val arr = IntArray(256) { it }
|
||||
|
||||
var u = 0
|
||||
var r: Int
|
||||
arr.indices.forEach {
|
||||
u = (u + arr[it] + key[it % key.length].code) % 256
|
||||
r = arr[it]
|
||||
arr[it] = arr[u]
|
||||
arr[u] = r
|
||||
}
|
||||
u = 0
|
||||
var c = 0
|
||||
|
||||
return text.indices.map { j ->
|
||||
c = (c + 1) % 256
|
||||
u = (u + arr[c]) % 256
|
||||
r = arr[c]
|
||||
arr[c] = arr[u]
|
||||
arr[u] = r
|
||||
(text[j].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
|
||||
}.joinToString("")
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun decrypt(input: String, key: String): String {
|
||||
val t = if (input.replace("""[\t\n\f\r]""".toRegex(), "").length % 4 == 0) {
|
||||
input.replace("""==?$""".toRegex(), "")
|
||||
} else input
|
||||
if (t.length % 4 == 1 || t.contains("""[^+/0-9A-Za-z]""".toRegex())) throw Exception("bad input")
|
||||
var i: Int
|
||||
var r = ""
|
||||
var e = 0
|
||||
var u = 0
|
||||
for (o in t.indices) {
|
||||
e = e shl 6
|
||||
i = key.indexOf(t[o])
|
||||
e = e or i
|
||||
u += 6
|
||||
if (24 == u) {
|
||||
r += ((16711680 and e) shr 16).toChar()
|
||||
r += ((65280 and e) shr 8).toChar()
|
||||
r += (255 and e).toChar()
|
||||
e = 0
|
||||
u = 0
|
||||
}
|
||||
}
|
||||
return if (12 == u) {
|
||||
e = e shr 4
|
||||
r + e.toChar()
|
||||
} else {
|
||||
if (18 == u) {
|
||||
e = e shr 2
|
||||
r += ((65280 and e) shr 8).toChar()
|
||||
r += (255 and e).toChar()
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
fun encode(input: String): String =
|
||||
java.net.URLEncoder.encode(input, "utf-8").replace("+", "%20")
|
||||
|
||||
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
class SflixProProvider : BflixProvider() {
|
||||
override var mainUrl = "https://sflix.pro"
|
||||
override var name = "Sflix.pro"
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=bestdubbedanime.com&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,270 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.*
|
||||
|
||||
class DubbedAnimeProvider : MainAPI() {
|
||||
override var mainUrl = "https://bestdubbedanime.com"
|
||||
override var name = "DubbedAnime"
|
||||
override val hasQuickSearch = true
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
data class QueryEpisodeResultRoot(
|
||||
@JsonProperty("result")
|
||||
val result: QueryEpisodeResult,
|
||||
)
|
||||
|
||||
data class QueryEpisodeResult(
|
||||
@JsonProperty("anime") val anime: List<EpisodeInfo>,
|
||||
@JsonProperty("error") val error: Boolean,
|
||||
@JsonProperty("errorMSG") val errorMSG: String?,
|
||||
)
|
||||
|
||||
data class EpisodeInfo(
|
||||
@JsonProperty("serversHTML") val serversHTML: String,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("preview_img") val previewImg: String?,
|
||||
@JsonProperty("wideImg") val wideImg: String?,
|
||||
@JsonProperty("year") val year: String?,
|
||||
@JsonProperty("desc") val desc: String?,
|
||||
|
||||
/*
|
||||
@JsonProperty("rowid") val rowid: String,
|
||||
@JsonProperty("status") val status: String,
|
||||
@JsonProperty("skips") val skips: String,
|
||||
@JsonProperty("totalEp") val totalEp: Long,
|
||||
@JsonProperty("ep") val ep: String,
|
||||
@JsonProperty("NextEp") val nextEp: Long,
|
||||
@JsonProperty("slug") val slug: String,
|
||||
@JsonProperty("showid") val showid: String,
|
||||
@JsonProperty("Epviews") val epviews: String,
|
||||
@JsonProperty("TotalViews") val totalViews: String,
|
||||
@JsonProperty("tags") val tags: String,*/
|
||||
)
|
||||
|
||||
private suspend fun parseDocumentTrending(url: String): List<SearchResponse> {
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
return document.select("li > a").mapNotNull {
|
||||
val href = fixUrl(it.attr("href"))
|
||||
val title = it.selectFirst("> div > div.cittx")?.text() ?: return@mapNotNull null
|
||||
val poster = fixUrlNull(it.selectFirst("> div > div.imghddde > img")?.attr("src"))
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
poster,
|
||||
null,
|
||||
EnumSet.of(DubStatus.Dubbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseDocument(
|
||||
url: String,
|
||||
trimEpisode: Boolean = false
|
||||
): List<SearchResponse> {
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
return document.select("a.grid__link").mapNotNull {
|
||||
val href = fixUrl(it.attr("href"))
|
||||
val title = it.selectFirst("> div.gridtitlek")?.text() ?: return@mapNotNull null
|
||||
val poster =
|
||||
fixUrl(it.selectFirst("> img.grid__img")?.attr("src") ?: return@mapNotNull null)
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
if (trimEpisode) href.removeRange(href.lastIndexOf('/'), href.length) else href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
poster,
|
||||
null,
|
||||
EnumSet.of(DubStatus.Dubbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val trendingUrl = "$mainUrl/xz/trending.php?_=$unixTimeMS"
|
||||
val lastEpisodeUrl = "$mainUrl/xz/epgrid.php?p=1&_=$unixTimeMS"
|
||||
val recentlyAddedUrl = "$mainUrl/xz/gridgrabrecent.php?p=1&_=$unixTimeMS"
|
||||
//val allUrl = "$mainUrl/xz/gridgrab.php?p=1&limit=12&_=$unixTimeMS"
|
||||
|
||||
val listItems = listOf(
|
||||
HomePageList("Trending", parseDocumentTrending(trendingUrl)),
|
||||
HomePageList("Recently Added", parseDocument(recentlyAddedUrl)),
|
||||
HomePageList("Recent Releases", parseDocument(lastEpisodeUrl, true)),
|
||||
// HomePageList("All", parseDocument(allUrl))
|
||||
)
|
||||
|
||||
return HomePageResponse(listItems)
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getEpisode(slug: String, isMovie: Boolean): EpisodeInfo {
|
||||
val url =
|
||||
mainUrl + (if (isMovie) "/movies/jsonMovie" else "/xz/v3/jsonEpi") + ".php?slug=$slug&_=$unixTime"
|
||||
val response = app.get(url).text
|
||||
val mapped = parseJson<QueryEpisodeResultRoot>(response)
|
||||
return mapped.result.anime.first()
|
||||
}
|
||||
|
||||
|
||||
private fun getIsMovie(href: String): Boolean {
|
||||
return href.contains("movies/")
|
||||
}
|
||||
|
||||
private fun getSlug(href: String): String {
|
||||
return href.replace("$mainUrl/", "")
|
||||
}
|
||||
|
||||
override suspend fun quickSearch(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/xz/searchgrid.php?p=1&limit=12&s=$query&_=$unixTime"
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
val items = document.select("div.grid__item > a")
|
||||
if (items.isEmpty()) return emptyList()
|
||||
return items.mapNotNull { i ->
|
||||
val href = fixUrl(i.attr("href"))
|
||||
val title = i.selectFirst("div.gridtitlek")?.text() ?: return@mapNotNull null
|
||||
val img = fixUrlNull(i.selectFirst("img.grid__img")?.attr("src"))
|
||||
|
||||
if (getIsMovie(href)) {
|
||||
MovieSearchResponse(
|
||||
title, href, this.name, TvType.AnimeMovie, img, null
|
||||
)
|
||||
} else {
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
img,
|
||||
null,
|
||||
EnumSet.of(DubStatus.Dubbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/search/$query"
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
val items = document.select("div.resultinner > a.resulta")
|
||||
if (items.isEmpty()) return ArrayList()
|
||||
return items.mapNotNull { i ->
|
||||
val innerDiv = i.selectFirst("> div.result")
|
||||
val href = fixUrl(i.attr("href"))
|
||||
val img = fixUrl(innerDiv?.selectFirst("> div.imgkz > img")?.attr("src") ?: return@mapNotNull null)
|
||||
val title = innerDiv.selectFirst("> div.titleresults")?.text() ?: return@mapNotNull null
|
||||
|
||||
if (getIsMovie(href)) {
|
||||
MovieSearchResponse(
|
||||
title, href, this.name, TvType.AnimeMovie, img, null
|
||||
)
|
||||
} else {
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
img,
|
||||
null,
|
||||
EnumSet.of(DubStatus.Dubbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val serversHTML = (if (data.startsWith(mainUrl)) { // CLASSIC EPISODE
|
||||
val slug = getSlug(data)
|
||||
getEpisode(slug, false).serversHTML
|
||||
} else data).replace("\\", "")
|
||||
|
||||
val hls = ArrayList("hl=\"(.*?)\"".toRegex().findAll(serversHTML).map {
|
||||
it.groupValues[1]
|
||||
}.toList())
|
||||
for (hl in hls) {
|
||||
try {
|
||||
val sources = app.get("$mainUrl/xz/api/playeri.php?url=$hl&_=$unixTime").text
|
||||
val find = "src=\"(.*?)\".*?label=\"(.*?)\"".toRegex().find(sources)
|
||||
if (find != null) {
|
||||
val quality = find.groupValues[2]
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name + " " + quality + if (quality.endsWith('p')) "" else 'p',
|
||||
fixUrl(find.groupValues[1]),
|
||||
this.mainUrl,
|
||||
getQualityFromName(quality)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
//IDK
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
if (getIsMovie(url)) {
|
||||
val realSlug = url.replace("movies/", "")
|
||||
val episode = getEpisode(realSlug, true)
|
||||
val poster = episode.previewImg ?: episode.wideImg
|
||||
return MovieLoadResponse(
|
||||
episode.title,
|
||||
realSlug,
|
||||
this.name,
|
||||
TvType.AnimeMovie,
|
||||
episode.serversHTML,
|
||||
if (poster == null) null else fixUrl(poster),
|
||||
episode.year?.toIntOrNull(),
|
||||
episode.desc,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
val title = document.selectFirst("h4")!!.text()
|
||||
val descriptHeader = document.selectFirst("div.animeDescript")
|
||||
val descript = descriptHeader?.selectFirst("> p")?.text()
|
||||
val year = descriptHeader?.selectFirst("> div.distatsx > div.sroverd")
|
||||
?.text()
|
||||
?.replace("Released: ", "")
|
||||
?.toIntOrNull()
|
||||
|
||||
val episodes = document.select("a.epibloks").map {
|
||||
val epTitle = it.selectFirst("> div.inwel > span.isgrxx")?.text()
|
||||
Episode(fixUrl(it.attr("href")), epTitle)
|
||||
}
|
||||
|
||||
val img = fixUrl(document.select("div.fkimgs > img").attr("src"))
|
||||
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||
posterUrl = img
|
||||
this.year = year
|
||||
addEpisodes(DubStatus.Dubbed, episodes)
|
||||
plot = descript
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class DubbedAnimeProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(DubbedAnimeProvider())
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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("Live")
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,120 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class EjaTv : MainAPI() {
|
||||
override var mainUrl = "https://eja.tv"
|
||||
override var name = "Eja.tv"
|
||||
|
||||
// Universal language?
|
||||
override var lang = "en"
|
||||
override val hasDownloadSupport = false
|
||||
|
||||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Live
|
||||
)
|
||||
|
||||
private fun Element.toSearchResponse(): LiveSearchResponse? {
|
||||
val link = this.select("div.alternative a").last() ?: return null
|
||||
val href = fixUrl(link.attr("href"))
|
||||
val img = this.selectFirst("div.thumb img")
|
||||
val lang = this.selectFirst(".card-title > a")?.attr("href")?.removePrefix("?country=")
|
||||
?.replace("int", "eu") //international -> European Union 🇪🇺
|
||||
return LiveSearchResponse(
|
||||
// Kinda hack way to get the title
|
||||
img?.attr("alt")?.replaceFirst("Watch ", "") ?: return null,
|
||||
href,
|
||||
this@EjaTv.name,
|
||||
TvType.Live,
|
||||
fixUrl(img.attr("src")),
|
||||
lang = lang
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
// Maybe this based on app language or as setting?
|
||||
val language = "English"
|
||||
val dataMap = mapOf(
|
||||
"News" to mapOf("language" to language, "category" to "News"),
|
||||
"Sports" to mapOf("language" to language, "category" to "Sports"),
|
||||
"Entertainment" to mapOf("language" to language, "category" to "Entertainment")
|
||||
)
|
||||
return HomePageResponse(dataMap.apmap { (title, data) ->
|
||||
val document = app.post(mainUrl, data = data).document
|
||||
val shows = document.select("div.card-body").mapNotNull {
|
||||
it.toSearchResponse()
|
||||
}
|
||||
HomePageList(
|
||||
title,
|
||||
shows,
|
||||
isHorizontalImages = true
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
return app.post(
|
||||
mainUrl, data = mapOf("search" to query)
|
||||
).document.select("div.card-body").mapNotNull {
|
||||
it.toSearchResponse()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val doc = app.get(url).document
|
||||
val sections =
|
||||
doc.select("li.list-group-item.d-flex.justify-content-between.align-items-center")
|
||||
|
||||
val link = fixUrl(sections.last()!!.select("a").attr("href"))
|
||||
|
||||
val title = doc.select("h5.text-center").text()
|
||||
val poster = fixUrl(doc.select("p.text-center img").attr("src"))
|
||||
|
||||
val summary = sections.subList(0, 3).joinToString("<br>") {
|
||||
val innerText = it.ownText().trim()
|
||||
val outerText = it.select("a").text().trim()
|
||||
"$innerText: $outerText"
|
||||
}
|
||||
|
||||
return LiveStreamLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
LoadData(link, title).toJson(),
|
||||
poster,
|
||||
plot = summary
|
||||
)
|
||||
}
|
||||
|
||||
data class LoadData(
|
||||
val url: String,
|
||||
val title: String
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val loadData = parseJson<LoadData>(data)
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
loadData.title,
|
||||
loadData.url,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class EjaTvPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(EjaTv())
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=gogoanime.lu&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,412 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class GogoanimeProvider : MainAPI() {
|
||||
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) {
|
||||
"Completed" -> ShowStatus.Completed
|
||||
"Ongoing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id base64Decode(show_id) + IV
|
||||
* @return the encryption key
|
||||
* */
|
||||
private fun getKey(id: String): String? {
|
||||
return normalSafeApiCall {
|
||||
id.map {
|
||||
it.code.toString(16)
|
||||
}.joinToString("").substring(0, 32)
|
||||
}
|
||||
}
|
||||
|
||||
val qualityRegex = Regex("(\\d+)P")
|
||||
|
||||
// https://github.com/saikou-app/saikou/blob/3e756bd8e876ad7a9318b17110526880525a5cd3/app/src/main/java/ani/saikou/anime/source/extractors/GogoCDN.kt#L60
|
||||
// No Licence on the function
|
||||
private fun cryptoHandler(
|
||||
string: String,
|
||||
iv: String,
|
||||
secretKeyString: String,
|
||||
encrypt: Boolean = true
|
||||
): String {
|
||||
//println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
val ivParameterSpec = IvParameterSpec(iv.toByteArray())
|
||||
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
return if (!encrypt) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
|
||||
String(cipher.doFinal(base64DecodeArray(string)))
|
||||
} else {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
|
||||
base64Encode(cipher.doFinal(string.toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.decodeHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX
|
||||
* @param mainApiName used for ExtractorLink names and source
|
||||
* @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey()
|
||||
* @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value
|
||||
* */
|
||||
suspend fun extractVidstream(
|
||||
iframeUrl: String,
|
||||
mainApiName: String,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
iv: String?,
|
||||
secretKey: String?,
|
||||
secretDecryptKey: String?,
|
||||
// This could be removed, but i prefer it verbose
|
||||
isUsingAdaptiveKeys: Boolean,
|
||||
isUsingAdaptiveData: Boolean,
|
||||
// If you don't want to re-fetch the document
|
||||
iframeDocument: Document? = null
|
||||
) = safeApiCall {
|
||||
// https://github.com/saikou-app/saikou/blob/3e756bd8e876ad7a9318b17110526880525a5cd3/app/src/main/java/ani/saikou/anime/source/extractors/GogoCDN.kt
|
||||
// No Licence on the following code
|
||||
// Also modified of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/gogoanime/src/eu/kanade/tachiyomi/animeextension/en/gogoanime/extractors/GogoCdnExtractor.kt
|
||||
// License on the code above https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
|
||||
|
||||
if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys)
|
||||
return@safeApiCall
|
||||
|
||||
val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=")
|
||||
|
||||
var document: Document? = iframeDocument
|
||||
val foundIv =
|
||||
iv ?: (document ?: app.get(iframeUrl).document.also { document = it })
|
||||
.select("""div.wrapper[class*=container]""")
|
||||
.attr("class").split("-").lastOrNull() ?: return@safeApiCall
|
||||
val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall
|
||||
val foundDecryptKey = secretDecryptKey ?: foundKey
|
||||
|
||||
val uri = URI(iframeUrl)
|
||||
val mainUrl = "https://" + uri.host
|
||||
|
||||
val encryptedId = cryptoHandler(id, foundIv, foundKey)
|
||||
val encryptRequestData = if (isUsingAdaptiveData) {
|
||||
// Only fetch the document if necessary
|
||||
val realDocument = document ?: app.get(iframeUrl).document
|
||||
val dataEncrypted =
|
||||
realDocument.select("script[data-name='episode']").attr("data-value")
|
||||
val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false)
|
||||
"id=$encryptedId&alias=$id&" + headers.substringAfter("&")
|
||||
} else {
|
||||
"id=$encryptedId&alias=$id"
|
||||
}
|
||||
|
||||
val jsonResponse =
|
||||
app.get(
|
||||
"$mainUrl/encrypt-ajax.php?$encryptRequestData",
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||
)
|
||||
val dataencrypted =
|
||||
jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}")
|
||||
val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false)
|
||||
val sources = AppUtils.parseJson<GogoSources>(datadecrypted)
|
||||
|
||||
fun invokeGogoSource(
|
||||
source: GogoSource,
|
||||
sourceCallback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
sourceCallback.invoke(
|
||||
ExtractorLink(
|
||||
mainApiName,
|
||||
mainApiName,
|
||||
source.file,
|
||||
mainUrl,
|
||||
getQualityFromName(source.label),
|
||||
isM3u8 = source.type == "hls" || source.label?.contains(
|
||||
"auto",
|
||||
ignoreCase = true
|
||||
) == true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
sources.source?.forEach {
|
||||
invokeGogoSource(it, callback)
|
||||
}
|
||||
sources.sourceBk?.forEach {
|
||||
invokeGogoSource(it, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var mainUrl = "https://gogoanime.lu"
|
||||
override var name = "GogoAnime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.Anime,
|
||||
TvType.OVA
|
||||
)
|
||||
|
||||
val headers = mapOf(
|
||||
"authority" to "ajax.gogo-load.com",
|
||||
"sec-ch-ua" to "\"Google Chrome\";v=\"89\", \"Chromium\";v=\"89\", \";Not A Brand\";v=\"99\"",
|
||||
"accept" to "text/html, */*; q=0.01",
|
||||
"dnt" to "1",
|
||||
"sec-ch-ua-mobile" to "?0",
|
||||
"user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
|
||||
"origin" to mainUrl,
|
||||
"sec-fetch-site" to "cross-site",
|
||||
"sec-fetch-mode" to "cors",
|
||||
"sec-fetch-dest" to "empty",
|
||||
"referer" to "$mainUrl/"
|
||||
)
|
||||
val parseRegex =
|
||||
Regex("""<li>\s*\n.*\n.*<a\s*href=["'](.*?-episode-(\d+))["']\s*title=["'](.*?)["']>\n.*?img src="(.*?)"""")
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
Pair("1", "Recent Release - Sub"),
|
||||
Pair("2", "Recent Release - Dub"),
|
||||
Pair("3", "Recent Release - Chinese"),
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(
|
||||
page: Int,
|
||||
request : MainPageRequest
|
||||
): HomePageResponse {
|
||||
val params = mapOf("page" to page.toString(), "type" to request.data)
|
||||
val html = app.get(
|
||||
"https://ajax.gogo-load.com/ajax/page-recent-release.html",
|
||||
headers = headers,
|
||||
params = params
|
||||
)
|
||||
val isSub = listOf(1, 3).contains(request.data.toInt())
|
||||
|
||||
val home = parseRegex.findAll(html.text).map {
|
||||
val (link, epNum, title, poster) = it.destructured
|
||||
newAnimeSearchResponse(title, link) {
|
||||
this.posterUrl = poster
|
||||
addDubStatus(!isSub, epNum.toIntOrNull())
|
||||
}
|
||||
}.toList()
|
||||
|
||||
return newHomePageResponse(request.name, home)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): ArrayList<SearchResponse> {
|
||||
val link = "$mainUrl/search.html?keyword=$query"
|
||||
val html = app.get(link).text
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
val episodes = doc.select(""".last_episodes li""").mapNotNull {
|
||||
AnimeSearchResponse(
|
||||
it.selectFirst(".name")?.text()?.replace(" (Dub)", "") ?: return@mapNotNull null,
|
||||
fixUrl(it.selectFirst(".name > a")?.attr("href") ?: return@mapNotNull null),
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
it.selectFirst("img")?.attr("src"),
|
||||
it.selectFirst(".released")?.text()?.split(":")?.getOrNull(1)?.trim()
|
||||
?.toIntOrNull(),
|
||||
if (it.selectFirst(".name")?.text()
|
||||
?.contains("Dub") == true
|
||||
) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
|
||||
DubStatus.Subbed
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return ArrayList(episodes)
|
||||
}
|
||||
|
||||
private fun getProperAnimeLink(uri: String): String {
|
||||
if (uri.contains("-episode")) {
|
||||
val split = uri.split("/")
|
||||
val slug = split[split.size - 1].split("-episode")[0]
|
||||
return "$mainUrl/category/$slug"
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val link = getProperAnimeLink(url)
|
||||
val episodeloadApi = "https://ajax.gogo-load.com/ajax/load-list-episode"
|
||||
val doc = app.get(link).document
|
||||
|
||||
val animeBody = doc.selectFirst(".anime_info_body_bg")
|
||||
val title = animeBody?.selectFirst("h1")!!.text()
|
||||
val poster = animeBody.selectFirst("img")?.attr("src")
|
||||
var description: String? = null
|
||||
val genre = ArrayList<String>()
|
||||
var year: Int? = null
|
||||
var status: String? = null
|
||||
var nativeName: String? = null
|
||||
var type: String? = null
|
||||
|
||||
animeBody.select("p.type").forEach { pType ->
|
||||
when (pType.selectFirst("span")?.text()?.trim()) {
|
||||
"Plot Summary:" -> {
|
||||
description = pType.text().replace("Plot Summary:", "").trim()
|
||||
}
|
||||
"Genre:" -> {
|
||||
genre.addAll(pType.select("a").map {
|
||||
it.attr("title")
|
||||
})
|
||||
}
|
||||
"Released:" -> {
|
||||
year = pType.text().replace("Released:", "").trim().toIntOrNull()
|
||||
}
|
||||
"Status:" -> {
|
||||
status = pType.text().replace("Status:", "").trim()
|
||||
}
|
||||
"Other name:" -> {
|
||||
nativeName = pType.text().replace("Other name:", "").trim()
|
||||
}
|
||||
"Type:" -> {
|
||||
type = pType.text().replace("type:", "").trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val animeId = doc.selectFirst("#movie_id")!!.attr("value")
|
||||
val params = mapOf("ep_start" to "0", "ep_end" to "2000", "id" to animeId)
|
||||
|
||||
val episodes = app.get(episodeloadApi, params = params).document.select("a").map {
|
||||
Episode(
|
||||
fixUrl(it.attr("href").trim()),
|
||||
"Episode " + it.selectFirst(".name")?.text()?.replace("EP", "")?.trim()
|
||||
)
|
||||
}.reversed()
|
||||
|
||||
return newAnimeLoadResponse(title, link, getType(type.toString())) {
|
||||
japName = nativeName
|
||||
engName = title
|
||||
posterUrl = poster
|
||||
this.year = year
|
||||
addEpisodes(DubStatus.Subbed, episodes) // TODO CHECK
|
||||
plot = description
|
||||
tags = genre
|
||||
|
||||
showStatus = getStatus(status.toString())
|
||||
}
|
||||
}
|
||||
|
||||
data class GogoSources(
|
||||
@JsonProperty("source") val source: List<GogoSource>?,
|
||||
@JsonProperty("sourceBk") val sourceBk: List<GogoSource>?,
|
||||
//val track: List<Any?>,
|
||||
//val advertising: List<Any?>,
|
||||
//val linkiframe: String
|
||||
)
|
||||
|
||||
data class GogoSource(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("default") val default: String? = null
|
||||
)
|
||||
|
||||
private suspend fun extractVideos(
|
||||
uri: String,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val doc = app.get(uri).document
|
||||
|
||||
val iframe = fixUrlNull(doc.selectFirst("div.play-video > iframe")?.attr("src")) ?: return
|
||||
|
||||
argamap(
|
||||
{
|
||||
val link = iframe.replace("streaming.php", "download")
|
||||
val page = app.get(link, headers = mapOf("Referer" to iframe))
|
||||
|
||||
page.document.select(".dowload > a").apmap {
|
||||
if (it.hasAttr("download")) {
|
||||
val qual = if (it.text()
|
||||
.contains("HDP")
|
||||
) "1080" else qualityRegex.find(it.text())?.destructured?.component1()
|
||||
.toString()
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"Gogoanime",
|
||||
"Gogoanime",
|
||||
it.attr("href"),
|
||||
page.url,
|
||||
getQualityFromName(qual),
|
||||
it.attr("href").contains(".m3u8")
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val url = it.attr("href")
|
||||
loadExtractor(url, null, subtitleCallback, callback)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
val streamingResponse = app.get(iframe, headers = mapOf("Referer" to iframe))
|
||||
val streamingDocument = streamingResponse.document
|
||||
argamap({
|
||||
streamingDocument.select(".list-server-items > .linkserver")
|
||||
.forEach { element ->
|
||||
val status = element.attr("data-status") ?: return@forEach
|
||||
if (status != "1") return@forEach
|
||||
val data = element.attr("data-video") ?: return@forEach
|
||||
loadExtractor(data, streamingResponse.url, subtitleCallback, callback)
|
||||
}
|
||||
}, {
|
||||
val iv = "3134003223491201"
|
||||
val secretKey = "37911490979715163134003223491201"
|
||||
val secretDecryptKey = "54674138327930866480207815084989"
|
||||
extractVidstream(
|
||||
iframe,
|
||||
this.name,
|
||||
callback,
|
||||
iv,
|
||||
secretKey,
|
||||
secretDecryptKey,
|
||||
isUsingAdaptiveKeys = false,
|
||||
isUsingAdaptiveData = true
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
extractVideos(data, subtitleCallback, callback)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class GogoanimeProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(GogoanimeProvider())
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"Movie",
|
||||
)
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,116 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class HDMProvider : MainAPI() {
|
||||
override var name = "HD Movies"
|
||||
override var mainUrl = "https://hdm.to"
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
)
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/search/$query"
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
val items = document.select("div.col-md-2 > article > a")
|
||||
if (items.isEmpty()) return emptyList()
|
||||
|
||||
return items.map { i ->
|
||||
val href = i.attr("href")
|
||||
val data = i.selectFirst("> div.item")!!
|
||||
val img = data.selectFirst("> img")!!.attr("src")
|
||||
val name = data.selectFirst("> div.movie-details")!!.text()
|
||||
MovieSearchResponse(name, href, this.name, TvType.Movie, img, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
if (data == "") return false
|
||||
val slug = Regex(".*/(.*?)\\.mp4").find(data)?.groupValues?.get(1) ?: return false
|
||||
val response = app.get(data).text
|
||||
val key = Regex("playlist\\.m3u8(.*?)\"").find(response)?.groupValues?.get(1) ?: return false
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
"https://hls.1o.to/vod/$slug/playlist.m3u8$key",
|
||||
"",
|
||||
Qualities.P720.value,
|
||||
true
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
val title = document.selectFirst("h2.movieTitle")?.text() ?: throw ErrorLoadingException("No Data Found")
|
||||
val poster = document.selectFirst("div.post-thumbnail > img")!!.attr("src")
|
||||
val descript = document.selectFirst("div.synopsis > p")!!.text()
|
||||
val year = document.select("div.movieInfoAll > div.row > div.col-md-6").getOrNull(1)?.selectFirst("> p > a")?.text()
|
||||
?.toIntOrNull()
|
||||
val data = "src/player/\\?v=(.*?)\"".toRegex().find(response)?.groupValues?.get(1) ?: return null
|
||||
|
||||
return MovieLoadResponse(
|
||||
title, url, this.name, TvType.Movie,
|
||||
"$mainUrl/src/player/?v=$data", poster, year, descript, null
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val html = app.get(mainUrl, timeout = 25).text
|
||||
val document = Jsoup.parse(html)
|
||||
val all = ArrayList<HomePageList>()
|
||||
|
||||
val mainbody = document.getElementsByTag("body")
|
||||
?.select("div.homeContentOuter > section > div.container > div")
|
||||
// Fetch row title
|
||||
val inner = mainbody?.select("div.col-md-2.col-sm-2.mrgb")
|
||||
val title = mainbody?.select("div > div")?.firstOrNull()?.select("div.title.titleBar")?.text() ?: "Unnamed Row"
|
||||
// Fetch list of items and map
|
||||
if (inner != null) {
|
||||
val elements: List<SearchResponse> = inner.map {
|
||||
|
||||
val aa = it.select("a").firstOrNull()
|
||||
val item = aa?.select("div.item")
|
||||
val href = aa?.attr("href")
|
||||
val link = when (href != null) {
|
||||
true -> fixUrl(href)
|
||||
false -> ""
|
||||
}
|
||||
val name = item?.select("div.movie-details")?.text() ?: "<No Title>"
|
||||
var image = item?.select("img")?.get(1)?.attr("src") ?: ""
|
||||
val year = null
|
||||
|
||||
MovieSearchResponse(
|
||||
name,
|
||||
link,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
image,
|
||||
year,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
all.add(
|
||||
HomePageList(
|
||||
title, elements
|
||||
)
|
||||
)
|
||||
}
|
||||
return HomePageResponse(all)
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class HDMProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(HDMProvider())
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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",
|
||||
"Documentary",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=ihavenotv.com&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,222 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URLEncoder
|
||||
|
||||
class IHaveNoTvProvider : MainAPI() {
|
||||
override var mainUrl = "https://ihavenotv.com"
|
||||
override var name = "I Have No TV"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(TvType.Documentary)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
// Uhh, I am too lazy to scrape the "latest documentaries" and "recommended documentaries",
|
||||
// so I am just scraping 3 random categories
|
||||
val allCategories = listOf(
|
||||
"astronomy",
|
||||
"brain",
|
||||
"creativity",
|
||||
"design",
|
||||
"economics",
|
||||
"environment",
|
||||
"health",
|
||||
"history",
|
||||
"lifehack",
|
||||
"math",
|
||||
"music",
|
||||
"nature",
|
||||
"people",
|
||||
"physics",
|
||||
"science",
|
||||
"technology",
|
||||
"travel"
|
||||
)
|
||||
|
||||
val categories = allCategories.asSequence().shuffled().take(3)
|
||||
.toList() // randomly get 3 categories, because there are too many
|
||||
|
||||
val items = ArrayList<HomePageList>()
|
||||
|
||||
categories.forEach { cat ->
|
||||
val link = "$mainUrl/category/$cat"
|
||||
val html = app.get(link).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
val searchResults: MutableMap<String, SearchResponse> = mutableMapOf()
|
||||
soup.select(".episodesDiv .episode").forEach { res ->
|
||||
val poster = res.selectFirst("img")?.attr("src")
|
||||
val aTag = if (res.html().contains("/series/")) {
|
||||
res.selectFirst(".episodeMeta > a")
|
||||
} else {
|
||||
res.selectFirst("a[href][title]")
|
||||
}
|
||||
val year = Regex("""•?\s+(\d{4})\s+•""").find(
|
||||
res.selectFirst(".episodeMeta")!!.text()
|
||||
)?.destructured?.component1()?.toIntOrNull()
|
||||
|
||||
val title = aTag!!.attr("title")
|
||||
val href = fixUrl(aTag.attr("href"))
|
||||
searchResults[href] = TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Documentary,//if (href.contains("/series/")) TvType.TvSeries else TvType.Movie,
|
||||
poster,
|
||||
year,
|
||||
null
|
||||
)
|
||||
}
|
||||
items.add(
|
||||
HomePageList(
|
||||
capitalizeString(cat),
|
||||
ArrayList(searchResults.values).subList(0, 5)
|
||||
)
|
||||
) // just 5 results per category, app crashes when they are too many
|
||||
}
|
||||
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): ArrayList<SearchResponse> {
|
||||
val url = """$mainUrl/search/${URLEncoder.encode(query, "UTF-8")}"""
|
||||
val response = app.get(url).text
|
||||
val soup = Jsoup.parse(response)
|
||||
|
||||
val searchResults: MutableMap<String, SearchResponse> = mutableMapOf()
|
||||
|
||||
soup.select(".episodesDiv .episode").forEach { res ->
|
||||
val poster = res.selectFirst("img")?.attr("src")
|
||||
val aTag = if (res.html().contains("/series/")) {
|
||||
res.selectFirst(".episodeMeta > a")
|
||||
} else {
|
||||
res.selectFirst("a[href][title]")
|
||||
}
|
||||
val year =
|
||||
Regex("""•?\s+(\d{4})\s+•""").find(
|
||||
res.selectFirst(".episodeMeta")!!.text()
|
||||
)?.destructured?.component1()
|
||||
?.toIntOrNull()
|
||||
|
||||
val title = aTag!!.attr("title")
|
||||
val href = fixUrl(aTag.attr("href"))
|
||||
searchResults[href] = TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Documentary, //if (href.contains("/series/")) TvType.TvSeries else TvType.Movie,
|
||||
poster,
|
||||
year,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
return ArrayList(searchResults.values)
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val isSeries = url.contains("/series/")
|
||||
val html = app.get(url).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
val container = soup.selectFirst(".container-fluid h1")?.parent()
|
||||
val title = if (isSeries) {
|
||||
container?.selectFirst("h1")?.text()?.split("•")?.firstOrNull().toString()
|
||||
} else soup.selectFirst(".videoDetails")!!.selectFirst("strong")?.text().toString()
|
||||
val description = if (isSeries) {
|
||||
container?.selectFirst("p")?.text()
|
||||
} else {
|
||||
soup.selectFirst(".videoDetails > p")?.text()
|
||||
}
|
||||
|
||||
var year: Int? = null
|
||||
val categories: MutableSet<String> = mutableSetOf()
|
||||
|
||||
val episodes = if (isSeries) {
|
||||
container?.select(".episode")?.map { ep ->
|
||||
val thumb = ep.selectFirst("img")!!.attr("src")
|
||||
|
||||
val epLink = fixUrl(ep.selectFirst("a[title]")!!.attr("href"))
|
||||
val (season, epNum) = if (ep.selectFirst(".episodeMeta > strong") != null &&
|
||||
ep.selectFirst(".episodeMeta > strong")!!.html().contains("S")
|
||||
) {
|
||||
val split = ep.selectFirst(".episodeMeta > strong")?.text()?.split("E")
|
||||
Pair(
|
||||
split?.firstOrNull()?.replace("S", "")?.toIntOrNull(),
|
||||
split?.get(1)?.toIntOrNull()
|
||||
)
|
||||
} else Pair<Int?, Int?>(null, null)
|
||||
|
||||
year = Regex("""•?\s+(\d{4})\s+•""").find(
|
||||
ep.selectFirst(".episodeMeta")!!.text()
|
||||
)?.destructured?.component1()?.toIntOrNull()
|
||||
|
||||
categories.addAll(
|
||||
ep.select(".episodeMeta > a[href*=\"/category/\"]").map { it.text().trim() })
|
||||
|
||||
newEpisode(epLink) {
|
||||
this.name = ep.selectFirst("a[title]")!!.attr("title")
|
||||
this.season = season
|
||||
this.episode = epNum
|
||||
this.posterUrl = thumb
|
||||
this.description = ep.selectFirst(".episodeSynopsis")?.text()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listOf(MovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
url,
|
||||
soup.selectFirst("[rel=\"image_src\"]")!!.attr("href"),
|
||||
Regex("""•?\s+(\d{4})\s+•""").find(
|
||||
soup.selectFirst(".videoDetails")!!.text()
|
||||
)?.destructured?.component1()?.toIntOrNull(),
|
||||
description,
|
||||
null,
|
||||
soup.selectFirst(".videoDetails")!!.select("a[href*=\"/category/\"]")
|
||||
.map { it.text().trim() }
|
||||
))
|
||||
}
|
||||
|
||||
val poster = episodes?.firstOrNull().let {
|
||||
if (isSeries && it != null) (it as Episode).posterUrl
|
||||
else null
|
||||
}
|
||||
|
||||
return if (isSeries) TvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
episodes!!.map { it as Episode },
|
||||
poster,
|
||||
year,
|
||||
description,
|
||||
null,
|
||||
null,
|
||||
categories.toList()
|
||||
) else (episodes?.first() as MovieLoadResponse)
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val html = app.get(data).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
val iframe = soup.selectFirst("#videoWrap iframe")
|
||||
if (iframe != null) {
|
||||
loadExtractor(iframe.attr("src"), null, subtitleCallback, callback)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class IHaveNoTvProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(IHaveNoTvProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AnimeMovie",
|
||||
"Anime",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=kawaiifu.com&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,174 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.*
|
||||
|
||||
class KawaiifuProvider : MainAPI() {
|
||||
override var mainUrl = "https://kawaiifu.com"
|
||||
override var name = "Kawaiifu"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val resp = app.get(mainUrl).text
|
||||
|
||||
val soup = Jsoup.parse(resp)
|
||||
|
||||
items.add(HomePageList("Latest Updates", soup.select(".today-update .item").mapNotNull {
|
||||
val title = it.selectFirst("img")?.attr("alt")
|
||||
AnimeSearchResponse(
|
||||
title ?: return@mapNotNull null,
|
||||
it.selectFirst("a")?.attr("href") ?: return@mapNotNull null,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
it.selectFirst("img")?.attr("src"),
|
||||
it.selectFirst("h4 > a")?.attr("href")?.split("-")?.last()?.toIntOrNull(),
|
||||
if (title.contains("(DUB)")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
|
||||
DubStatus.Subbed
|
||||
),
|
||||
)
|
||||
}))
|
||||
for (section in soup.select(".section")) {
|
||||
try {
|
||||
val title = section.selectFirst(".title")!!.text()
|
||||
val anime = section.select(".list-film > .item").mapNotNull { ani ->
|
||||
val animTitle = ani.selectFirst("img")?.attr("alt")
|
||||
AnimeSearchResponse(
|
||||
animTitle ?: return@mapNotNull null,
|
||||
ani.selectFirst("a")?.attr("href") ?: return@mapNotNull null,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
ani.selectFirst("img")?.attr("src"),
|
||||
ani.selectFirst(".vl-chil-date")?.text()?.toIntOrNull(),
|
||||
if (animTitle.contains("(DUB)")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
|
||||
DubStatus.Subbed
|
||||
),
|
||||
)
|
||||
}
|
||||
items.add(HomePageList(title, anime))
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun search(query: String): ArrayList<SearchResponse> {
|
||||
val link = "$mainUrl/search-movie?keyword=${query}"
|
||||
val html = app.get(link).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
return ArrayList(soup.select(".item").mapNotNull {
|
||||
val year = it.selectFirst("h4 > a")?.attr("href")?.split("-")?.last()?.toIntOrNull()
|
||||
val title = it.selectFirst("img")?.attr("alt") ?: return@mapNotNull null
|
||||
val poster = it.selectFirst("img")?.attr("src")
|
||||
val uri = it.selectFirst("a")?.attr("href") ?: return@mapNotNull null
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
uri,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
poster,
|
||||
year,
|
||||
if (title.contains("(DUB)")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val html = app.get(url).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
val title = soup.selectFirst(".title")!!.text()
|
||||
val tags = soup.select(".table a[href*=\"/tag/\"]").map { tag -> tag.text() }
|
||||
val description = soup.select(".sub-desc p")
|
||||
.filter { it -> it.select("strong").isEmpty() && it.select("iframe").isEmpty() }
|
||||
.joinToString("\n") { it.text() }
|
||||
val year = url.split("/").filter { it.contains("-") }[0].split("-")[1].toIntOrNull()
|
||||
|
||||
val episodesLink = soup.selectFirst("a[href*=\".html-episode\"]")?.attr("href")
|
||||
?: throw ErrorLoadingException("Error getting episode list")
|
||||
val episodes = Jsoup.parse(
|
||||
app.get(episodesLink).text
|
||||
).selectFirst(".list-ep")?.select("li")?.map {
|
||||
Episode(
|
||||
it.selectFirst("a")!!.attr("href"),
|
||||
if (it.text().trim().toIntOrNull() != null) "Episode ${
|
||||
it.text().trim()
|
||||
}" else it.text().trim()
|
||||
)
|
||||
}
|
||||
val poster = soup.selectFirst("a.thumb > img")?.attr("src")
|
||||
|
||||
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||
this.year = year
|
||||
posterUrl = poster
|
||||
addEpisodes(DubStatus.Subbed, episodes)
|
||||
plot = description
|
||||
this.tags = tags
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val htmlSource = app.get(data).text
|
||||
val soupa = Jsoup.parse(htmlSource)
|
||||
|
||||
val episodeNum =
|
||||
if (data.contains("ep=")) data.split("ep=")[1].split("&")[0].toIntOrNull() else null
|
||||
|
||||
val servers = soupa.select(".list-server").map {
|
||||
val serverName = it.selectFirst(".server-name")!!.text()
|
||||
val episodes = it.select(".list-ep > li > a")
|
||||
.map { episode -> Pair(episode.attr("href"), episode.text()) }
|
||||
val episode = if (episodeNum == null) episodes[0] else episodes.mapNotNull { ep ->
|
||||
if ((if (ep.first.contains("ep=")) ep.first.split("ep=")[1].split("&")[0].toIntOrNull() else null) == episodeNum) {
|
||||
ep
|
||||
} else null
|
||||
}[0]
|
||||
Pair(serverName, episode)
|
||||
}.map {
|
||||
if (it.second.first == data) {
|
||||
val sources = soupa.select("video > source")
|
||||
.map { source -> Pair(source.attr("src"), source.attr("data-quality")) }
|
||||
Triple(it.first, sources, it.second.second)
|
||||
} else {
|
||||
val html = app.get(it.second.first).text
|
||||
val soup = Jsoup.parse(html)
|
||||
|
||||
val sources = soup.select("video > source")
|
||||
.map { source -> Pair(source.attr("src"), source.attr("data-quality")) }
|
||||
Triple(it.first, sources, it.second.second)
|
||||
}
|
||||
}
|
||||
|
||||
servers.forEach {
|
||||
it.second.forEach { source ->
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"Kawaiifu",
|
||||
it.first,
|
||||
source.first,
|
||||
"",
|
||||
getQualityFromName(source.second),
|
||||
source.first.contains(".m3u")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class KawaiifuProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(KawaiifuProvider())
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"Cartoon",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=kimcartoon.li&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,152 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
||||
class KimCartoonProvider : MainAPI() {
|
||||
|
||||
override var mainUrl = "https://kimcartoon.li"
|
||||
override var name = "Kim Cartoon"
|
||||
override val hasQuickSearch = true
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(TvType.Cartoon)
|
||||
|
||||
private fun fixUrl(url: String): String {
|
||||
return if (url.startsWith("/")) mainUrl + url else url
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val doc = app.get(mainUrl).document.select("#container")
|
||||
val response = mutableListOf(
|
||||
HomePageList(
|
||||
"Latest Update",
|
||||
doc.select("div.bigBarContainer div.items > div > a").map {
|
||||
AnimeSearchResponse(
|
||||
it.select(".item-title").let { div ->
|
||||
//Because it doesn't contain Title separately
|
||||
div.text().replace(div.select("span").text(), "")
|
||||
},
|
||||
mainUrl + it.attr("href"),
|
||||
mainUrl,
|
||||
TvType.Cartoon,
|
||||
fixUrl(it.select("img").let { img ->
|
||||
img.attr("src").let { src ->
|
||||
src.ifEmpty { img.attr("srctemp") }
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
val list = mapOf(
|
||||
"Top Day" to "tab-top-day",
|
||||
"Top Week" to "tab-top-week",
|
||||
"Top Month" to "tab-top-month",
|
||||
"New Cartoons" to "tab-newest-series"
|
||||
)
|
||||
response.addAll(list.map { item ->
|
||||
HomePageList(
|
||||
item.key,
|
||||
doc.select("#${item.value} > div").map {
|
||||
AnimeSearchResponse(
|
||||
it.select("span.title").text(),
|
||||
mainUrl + it.select("a")[0].attr("href"),
|
||||
mainUrl,
|
||||
TvType.Cartoon,
|
||||
fixUrl(it.select("a > img").attr("src"))
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
return HomePageResponse(response)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
return app.post(
|
||||
"$mainUrl/Search/Cartoon",
|
||||
data = mapOf("keyword" to query)
|
||||
).document
|
||||
.select("#leftside > div.bigBarContainer div.list-cartoon > div.item > a")
|
||||
.map {
|
||||
AnimeSearchResponse(
|
||||
it.select("span").text(),
|
||||
mainUrl + it.attr("href"),
|
||||
mainUrl,
|
||||
TvType.Cartoon,
|
||||
fixUrl(it.select("img").attr("src"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun quickSearch(query: String): List<SearchResponse> {
|
||||
return app.post(
|
||||
"$mainUrl/Ajax/SearchSuggest",
|
||||
data = mapOf("keyword" to query)
|
||||
).document.select("a").map {
|
||||
AnimeSearchResponse(
|
||||
it.text(),
|
||||
it.attr("href"),
|
||||
mainUrl,
|
||||
TvType.Cartoon,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getStatus(from: String?): ShowStatus? {
|
||||
return when {
|
||||
from?.contains("Completed") == true -> ShowStatus.Completed
|
||||
from?.contains("Ongoing") == true -> ShowStatus.Ongoing
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val doc = app.get(url).document.select("#leftside")
|
||||
val info = doc.select("div.barContent")
|
||||
val name = info.select("a.bigChar").text()
|
||||
val eps = doc.select("table.listing > tbody > tr a").reversed().map {
|
||||
Episode(
|
||||
fixUrl(it.attr("href")),
|
||||
it.text().replace(name, "").trim()
|
||||
)
|
||||
}
|
||||
val infoText = info.text()
|
||||
fun getData(after: String, before: String): String? {
|
||||
return if (infoText.contains(after))
|
||||
infoText
|
||||
.substringAfter("$after:")
|
||||
.substringBefore(before)
|
||||
.trim()
|
||||
else null
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(name, url, TvType.Cartoon, eps) {
|
||||
posterUrl = fixUrl(info.select("div > img").attr("src"))
|
||||
showStatus = getStatus(getData("Status", "Views"))
|
||||
plot = getData("Summary", "Tags:")
|
||||
tags = getData("Genres", "Date aired")?.split(",")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val servers =
|
||||
app.get(data).document.select("#selectServer > option").map { fixUrl(it.attr("value")) }
|
||||
servers.apmap {
|
||||
app.get(it).document.select("#my_video_1").attr("src").let { iframe ->
|
||||
if (iframe.isNotEmpty()) {
|
||||
loadExtractor(iframe, "$mainUrl/", subtitleCallback, callback)
|
||||
}
|
||||
//There are other servers, but they require some work to do
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class KimCartoonProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(KimCartoonProvider())
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AsianDrama",
|
||||
"TvSeries",
|
||||
"Anime",
|
||||
"Movie",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=kisskh.me&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,208 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import java.util.ArrayList
|
||||
|
||||
class KisskhProvider : MainAPI() {
|
||||
override var mainUrl = "https://kisskh.me"
|
||||
override var name = "Kisskh"
|
||||
override val hasMainPage = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AsianDrama,
|
||||
TvType.Anime
|
||||
)
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"&type=2&sub=0&country=2&status=0&order=1" to "Movie Popular",
|
||||
"&type=2&sub=0&country=2&status=0&order=2" to "Movie Last Update",
|
||||
"&type=1&sub=0&country=2&status=0&order=1" to "TVSeries Popular",
|
||||
"&type=1&sub=0&country=2&status=0&order=2" to "TVSeries Last Update",
|
||||
"&type=3&sub=0&country=0&status=0&order=1" to "Anime Popular",
|
||||
"&type=3&sub=0&country=0&status=0&order=2" to "Anime Last Update",
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(
|
||||
page: Int,
|
||||
request: MainPageRequest
|
||||
): HomePageResponse {
|
||||
val home = app.get("$mainUrl/api/DramaList/List?page=$page${request.data}")
|
||||
.parsedSafe<Responses>()?.data
|
||||
?.mapNotNull { media ->
|
||||
media.toSearchResponse()
|
||||
} ?: throw ErrorLoadingException("Invalid Json reponse")
|
||||
return newHomePageResponse(request.name, home)
|
||||
}
|
||||
|
||||
private fun Media.toSearchResponse(): SearchResponse? {
|
||||
|
||||
return newAnimeSearchResponse(
|
||||
title ?: return null,
|
||||
"$title/$id",
|
||||
TvType.TvSeries,
|
||||
) {
|
||||
this.posterUrl = thumbnail
|
||||
addSub(episodesCount)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val searchResponse =
|
||||
app.get("$mainUrl/api/DramaList/Search?q=$query&type=0", referer = "$mainUrl/").text
|
||||
return tryParseJson<ArrayList<Media>>(searchResponse)?.mapNotNull { media ->
|
||||
media.toSearchResponse()
|
||||
} ?: throw ErrorLoadingException("Invalid Json reponse")
|
||||
}
|
||||
|
||||
private fun getTitle(str: String): String {
|
||||
return str.replace(Regex("[^a-zA-Z0-9]"), "-")
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val id = url.split("/")
|
||||
val res = app.get(
|
||||
"$mainUrl/api/DramaList/Drama/${id.last()}?isq=false",
|
||||
referer = "$mainUrl/Drama/${
|
||||
getTitle(id.first())
|
||||
}?id=${id.last()}"
|
||||
).parsedSafe<MediaDetail>()
|
||||
?: throw ErrorLoadingException("Invalid Json reponse")
|
||||
|
||||
val episodes = res.episodes?.map { eps ->
|
||||
Episode(
|
||||
data = Data(res.title, eps.number, res.id, eps.id).toJson(),
|
||||
episode = eps.number
|
||||
)
|
||||
} ?: throw ErrorLoadingException("No Episode")
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
res.title ?: return null,
|
||||
url,
|
||||
if (res.type == "Movie" || episodes.size == 1) TvType.Movie else TvType.TvSeries,
|
||||
episodes
|
||||
) {
|
||||
this.posterUrl = res.thumbnail
|
||||
this.year = res.releaseDate?.split("-")?.first()?.toIntOrNull()
|
||||
this.plot = res.description
|
||||
this.tags = listOf("${res.country}", "${res.status}", "${res.type}")
|
||||
this.showStatus = when (res.status) {
|
||||
"Completed" -> ShowStatus.Completed
|
||||
"Ongoing" -> ShowStatus.Ongoing
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getLanguage(str: String): String {
|
||||
return when (str) {
|
||||
"Indonesia" -> "Indonesian"
|
||||
else -> str
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
|
||||
val loadData = parseJson<Data>(data)
|
||||
|
||||
app.get(
|
||||
"$mainUrl/api/DramaList/Episode/${loadData.epsId}.png?err=false&ts=&time=",
|
||||
referer = "$mainUrl/Drama/${getTitle("${loadData.title}")}/Episode-${loadData.eps}?id=${loadData.id}&ep=${loadData.epsId}&page=0&pageSize=100"
|
||||
).parsedSafe<Sources>()?.let { source ->
|
||||
listOf(source.video, source.thirdParty).apmap { link ->
|
||||
safeApiCall {
|
||||
if (link?.contains(".m3u8") == true) {
|
||||
M3u8Helper.generateM3u8(
|
||||
this.name,
|
||||
link,
|
||||
referer = "$mainUrl/",
|
||||
headers = mapOf("Origin" to mainUrl)
|
||||
).forEach(callback)
|
||||
} else {
|
||||
loadExtractor(
|
||||
link?.substringBefore("=http") ?: return@safeApiCall,
|
||||
"$mainUrl/",
|
||||
subtitleCallback,
|
||||
callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parsedSafe doesn't work in <List<Object>>
|
||||
app.get("$mainUrl/api/Sub/${loadData.epsId}").text.let { res ->
|
||||
tryParseJson<List<Subtitle>>(res)?.map { sub ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
getLanguage(sub.label ?: return@map),
|
||||
sub.src ?: return@map
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val title: String?,
|
||||
val eps: Int?,
|
||||
val id: Int?,
|
||||
val epsId: Int?,
|
||||
)
|
||||
|
||||
data class Sources(
|
||||
@JsonProperty("Video") val video: String?,
|
||||
@JsonProperty("ThirdParty") val thirdParty: String?,
|
||||
)
|
||||
|
||||
data class Subtitle(
|
||||
@JsonProperty("src") val src: String?,
|
||||
@JsonProperty("label") val label: String?,
|
||||
)
|
||||
|
||||
data class Responses(
|
||||
@JsonProperty("data") val data: ArrayList<Media>? = arrayListOf(),
|
||||
)
|
||||
|
||||
data class Media(
|
||||
@JsonProperty("episodesCount") val episodesCount: Int?,
|
||||
@JsonProperty("thumbnail") val thumbnail: String?,
|
||||
@JsonProperty("id") val id: Int?,
|
||||
@JsonProperty("title") val title: String?,
|
||||
)
|
||||
|
||||
data class Episodes(
|
||||
@JsonProperty("id") val id: Int?,
|
||||
@JsonProperty("number") val number: Int?,
|
||||
@JsonProperty("sub") val sub: Int?,
|
||||
)
|
||||
|
||||
data class MediaDetail(
|
||||
@JsonProperty("description") val description: String?,
|
||||
@JsonProperty("releaseDate") val releaseDate: String?,
|
||||
@JsonProperty("status") val status: String?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("country") val country: String?,
|
||||
@JsonProperty("episodes") val episodes: ArrayList<Episodes>? = arrayListOf(),
|
||||
@JsonProperty("thumbnail") val thumbnail: String?,
|
||||
@JsonProperty("id") val id: Int?,
|
||||
@JsonProperty("title") val title: String?,
|
||||
)
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class KisskhProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(KisskhProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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=melomovie.com&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,195 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbUrl
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class MeloMovieProvider : MainAPI() {
|
||||
override var name = "MeloMovie"
|
||||
override var mainUrl = "https://melomovie.com"
|
||||
override val instantLinkLoading = true
|
||||
override val hasQuickSearch = true
|
||||
override val hasChromecastSupport = false // MKV FILES CANT BE PLAYED ON A CHROMECAST
|
||||
|
||||
data class MeloMovieSearchResult(
|
||||
@JsonProperty("id") val id: Int,
|
||||
@JsonProperty("imdb_code") val imdbId: String,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("type") val type: Int, // 1 = MOVIE, 2 = TV-SERIES
|
||||
@JsonProperty("year") val year: Int?, // 1 = MOVIE, 2 = TV-SERIES
|
||||
//"mppa" for tags
|
||||
)
|
||||
|
||||
data class MeloMovieLink(
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("link") val link: String
|
||||
)
|
||||
|
||||
override suspend fun quickSearch(query: String): List<SearchResponse> {
|
||||
return search(query)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/movie/search/?name=$query"
|
||||
val returnValue: ArrayList<SearchResponse> = ArrayList()
|
||||
val response = app.get(url).text
|
||||
val mapped = response.let { mapper.readValue<List<MeloMovieSearchResult>>(it) }
|
||||
if (mapped.isEmpty()) return returnValue
|
||||
|
||||
for (i in mapped) {
|
||||
val currentUrl = "$mainUrl/movie/${i.id}"
|
||||
val currentPoster = "$mainUrl/assets/images/poster/${i.imdbId}.jpg"
|
||||
if (i.type == 2) { // TV-SERIES
|
||||
returnValue.add(
|
||||
TvSeriesSearchResponse(
|
||||
i.title,
|
||||
currentUrl,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
currentPoster,
|
||||
i.year,
|
||||
null
|
||||
)
|
||||
)
|
||||
} else if (i.type == 1) { // MOVIE
|
||||
returnValue.add(
|
||||
MovieSearchResponse(
|
||||
i.title,
|
||||
currentUrl,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
currentUrl,
|
||||
i.year
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
// http not https, the links are not https!
|
||||
private fun fixUrl(url: String): String {
|
||||
if (url.isEmpty()) return ""
|
||||
|
||||
if (url.startsWith("//")) {
|
||||
return "http:$url"
|
||||
}
|
||||
if (!url.startsWith("http")) {
|
||||
return "http://$url"
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private fun serializeData(element: Element): List<MeloMovieProvider.MeloMovieLink> {
|
||||
val eps = element.select("> tbody > tr")
|
||||
val parsed = eps.mapNotNull {
|
||||
try {
|
||||
val tds = it.select("> td")
|
||||
val name = tds[if (tds.size == 5) 1 else 0].text()
|
||||
val url = fixUrl(tds.last()!!.selectFirst("> a")!!.attr("data-lnk").replace(" ", "%20"))
|
||||
MeloMovieLink(name, url)
|
||||
} catch (e: Exception) {
|
||||
MeloMovieLink("", "")
|
||||
}
|
||||
}.filter { it.link != "" && it.name != "" }
|
||||
return parsed
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val links = parseJson<List<MeloMovieLink>>(data)
|
||||
for (link in links) {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
link.name,
|
||||
link.link,
|
||||
"",
|
||||
getQualityFromName(link.name),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val response = app.get(url).text
|
||||
|
||||
//backdrop = imgurl
|
||||
fun findUsingRegex(src: String): String? {
|
||||
return src.toRegex().find(response)?.groups?.get(1)?.value ?: return null
|
||||
}
|
||||
|
||||
val imdbUrl = findUsingRegex("var imdb = \"(.*?)\"")
|
||||
val document = Jsoup.parse(response)
|
||||
val poster = document.selectFirst("img.img-fluid")!!.attr("src")
|
||||
val type = findUsingRegex("var posttype = ([0-9]*)")?.toInt() ?: return null
|
||||
val titleInfo = document.selectFirst("div.movie_detail_title > div > div > h1")
|
||||
val title = titleInfo!!.ownText()
|
||||
val year =
|
||||
titleInfo.selectFirst("> a")?.text()?.replace("(", "")?.replace(")", "")?.toIntOrNull()
|
||||
val plot = document.selectFirst("div.col-lg-12 > p")!!.text()
|
||||
|
||||
if (type == 1) { // MOVIE
|
||||
val serialize = document.selectFirst("table.accordion__list")
|
||||
?: throw ErrorLoadingException("No links found")
|
||||
return newMovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
TvType.Movie,
|
||||
serializeData(serialize)
|
||||
) {
|
||||
this.posterUrl = poster
|
||||
this.year = year
|
||||
this.plot = plot
|
||||
addImdbUrl(imdbUrl)
|
||||
}
|
||||
} else if (type == 2) {
|
||||
val episodes = ArrayList<Episode>()
|
||||
val seasons = document.select("div.accordion__card")
|
||||
?: throw ErrorLoadingException("No episodes found")
|
||||
for (s in seasons) {
|
||||
val season =
|
||||
s.selectFirst("> div.card-header > button > span")!!.text()
|
||||
.replace("Season: ", "").toIntOrNull()
|
||||
val localEpisodes = s.select("> div.collapse > div > div > div.accordion__card")
|
||||
for (e in localEpisodes) {
|
||||
val episode =
|
||||
e.selectFirst("> div.card-header > button > span")!!.text()
|
||||
.replace("Episode: ", "").toIntOrNull()
|
||||
val links =
|
||||
e.selectFirst("> div.collapse > div > table.accordion__list") ?: continue
|
||||
val data = serializeData(links)
|
||||
episodes.add(newEpisode(data) {
|
||||
this.season = season
|
||||
this.episode = episode
|
||||
})
|
||||
}
|
||||
}
|
||||
episodes.reverse()
|
||||
return newTvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
TvType.TvSeries,
|
||||
episodes
|
||||
) {
|
||||
this.posterUrl = poster
|
||||
this.year = year
|
||||
this.plot = plot
|
||||
addImdbUrl(imdbUrl)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class MeloMovieProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(MeloMovieProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"AnimeMovie",
|
||||
"TvSeries",
|
||||
"Movie",
|
||||
)
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,286 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
class NginxProvider : MainAPI() {
|
||||
override var name = "Nginx"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie)
|
||||
|
||||
companion object {
|
||||
var loginCredentials: String? = null
|
||||
var overrideUrl: String? = null
|
||||
const val ERROR_STRING = "No nginx url specified in the settings"
|
||||
}
|
||||
|
||||
private fun getAuthHeader(): Map<String, String> {
|
||||
val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING)
|
||||
mainUrl = url
|
||||
println("OVERRIDING URL TO $overrideUrl")
|
||||
if (mainUrl == "NONE" || mainUrl.isBlank()) {
|
||||
throw ErrorLoadingException(ERROR_STRING)
|
||||
}
|
||||
|
||||
val localCredentials = loginCredentials
|
||||
if (localCredentials == null || localCredentials.trim() == ":") {
|
||||
return mapOf("Authorization" to "Basic ") // no Authorization headers
|
||||
}
|
||||
|
||||
val basicAuthToken =
|
||||
base64Encode(localCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load
|
||||
|
||||
return mapOf("Authorization" to "Basic $basicAuthToken")
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val authHeader =
|
||||
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
// url can be tvshow.nfo for series or mediaRootUrl for movies
|
||||
|
||||
val mainRootDocument = app.get(url, authHeader).document
|
||||
|
||||
val nfoUrl = url + mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo")
|
||||
.attr("href") // metadata url file
|
||||
|
||||
val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file
|
||||
|
||||
val isMovie = !nfoUrl.contains("tvshow.nfo")
|
||||
|
||||
val title = metadataDocument.selectFirst("title")!!.text()
|
||||
|
||||
val description = metadataDocument.selectFirst("plot")!!.text()
|
||||
|
||||
if (isMovie) {
|
||||
val poster = metadataDocument.selectFirst("thumb")!!.text()
|
||||
val trailer = metadataDocument.select("trailer").mapNotNull {
|
||||
it?.text()?.replace(
|
||||
"plugin://plugin.video.youtube/play/?video_id=",
|
||||
"https://www.youtube.com/watch?v="
|
||||
)
|
||||
}
|
||||
val partialUrl =
|
||||
mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href")
|
||||
.replace(".nfo", ".")
|
||||
val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull()
|
||||
val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull()
|
||||
val tagsList = metadataDocument.select("genre")
|
||||
.mapNotNull { // all the tags like action, thriller ...
|
||||
it?.text()
|
||||
|
||||
}
|
||||
|
||||
|
||||
val dataList =
|
||||
mainRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
|
||||
"href",
|
||||
partialUrl
|
||||
)
|
||||
|
||||
val data = url + dataList.firstNotNullOf { item ->
|
||||
item.takeIf {
|
||||
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
|
||||
}
|
||||
}.attr("href").toString() // exclude poster and nfo (metadata) file
|
||||
|
||||
return newMovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
TvType.Movie,
|
||||
data
|
||||
) {
|
||||
this.year = date
|
||||
this.plot = description
|
||||
this.rating = ratingAverage
|
||||
this.tags = tagsList
|
||||
addTrailer(trailer)
|
||||
addPoster(poster, authHeader)
|
||||
}
|
||||
} else // a tv serie
|
||||
{
|
||||
val list = ArrayList<Pair<Int, String>>()
|
||||
val mediaRootUrl = url.replace("tvshow.nfo", "")
|
||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||
val mediaRootDocument = app.get(mediaRootUrl, authHeader).document
|
||||
val seasons =
|
||||
mediaRootDocument.getElementsByAttributeValueContaining("href", "Season%20")
|
||||
|
||||
|
||||
val tagsList = metadataDocument.select("genre")
|
||||
.mapNotNull { // all the tags like action, thriller ...; unused variable
|
||||
it?.text()
|
||||
}
|
||||
|
||||
//val actorsList = document.select("actor")
|
||||
// ?.mapNotNull { // all the tags like action, thriller ...; unused variable
|
||||
// it?.text()
|
||||
// }
|
||||
|
||||
seasons.forEach { element ->
|
||||
val season =
|
||||
element.attr("href").replace("Season%20", "").replace("/", "").toIntOrNull()
|
||||
val href = mediaRootUrl + element.attr("href")
|
||||
if (season != null && season > 0 && href.isNotBlank()) {
|
||||
list.add(Pair(season, href))
|
||||
}
|
||||
}
|
||||
|
||||
if (list.isEmpty()) throw ErrorLoadingException("No Seasons Found")
|
||||
|
||||
val episodeList = ArrayList<Episode>()
|
||||
|
||||
|
||||
list.apmap { (seasonInt, seasonString) ->
|
||||
val seasonDocument = app.get(seasonString, authHeader).document
|
||||
val episodes = seasonDocument.getElementsByAttributeValueContaining(
|
||||
"href",
|
||||
".nfo"
|
||||
) // get metadata
|
||||
episodes.forEach { episode ->
|
||||
val nfoDocument = app.get(
|
||||
seasonString + episode.attr("href"),
|
||||
authHeader
|
||||
).document // get episode metadata file
|
||||
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
|
||||
val poster =
|
||||
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
|
||||
val name = nfoDocument.selectFirst("title")!!.text()
|
||||
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
|
||||
val date = nfoDocument.selectFirst("aired")?.text()
|
||||
val plot = nfoDocument.selectFirst("plot")?.text()
|
||||
|
||||
val dataList = seasonDocument.getElementsByAttributeValueContaining(
|
||||
"href",
|
||||
episode.attr("href").replace(".nfo", "")
|
||||
)
|
||||
val data = seasonString + dataList.firstNotNullOf { item ->
|
||||
item.takeIf {
|
||||
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
|
||||
}
|
||||
}.attr("href").toString() // exclude poster and nfo (metadata) file
|
||||
|
||||
episodeList.add(
|
||||
newEpisode(data) {
|
||||
this.name = name
|
||||
this.season = seasonInt
|
||||
this.episode = epNum
|
||||
this.posterUrl = poster // will require headers too
|
||||
this.description = plot
|
||||
addDate(date)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) {
|
||||
this.name = title
|
||||
this.url = url
|
||||
this.episodes = episodeList
|
||||
this.plot = description
|
||||
this.tags = tagsList
|
||||
addPoster(posterUrl, authHeader)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
// loadExtractor(data, null) { callback(it.copy(headers=authHeader)) }
|
||||
val authHeader =
|
||||
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
data,
|
||||
data, // referer not needed
|
||||
Qualities.Unknown.value,
|
||||
false,
|
||||
authHeader,
|
||||
)
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val authHeader =
|
||||
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
|
||||
val document = app.get(mainUrl, authHeader).document
|
||||
val categories = document.select("a")
|
||||
val returnList = categories.mapNotNull {
|
||||
val categoryTitle = it.text() // get the category title like Movies or Series
|
||||
if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir
|
||||
val href = it?.attr("href")
|
||||
val categoryPath = fixUrlNull(href?.trim())
|
||||
?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
|
||||
|
||||
val categoryDocument = app.get(
|
||||
categoryPath,
|
||||
authHeader
|
||||
).document // queries the page http://192.168.1.10/media/Movies/
|
||||
val contentLinks = categoryDocument.select("a")
|
||||
val currentList = contentLinks.mapNotNull { head ->
|
||||
if (head.attr("href") != "../") {
|
||||
try {
|
||||
val mediaRootUrl =
|
||||
categoryPath + head.attr("href")// like http://192.168.1.10/media/Series/Chernobyl/
|
||||
val mediaDocument = app.get(mediaRootUrl, authHeader).document
|
||||
val nfoFilename = mediaDocument.getElementsByAttributeValueContaining(
|
||||
"href",
|
||||
".nfo"
|
||||
)[0].attr("href")
|
||||
val isMovieType = nfoFilename != "tvshow.nfo"
|
||||
val nfoPath =
|
||||
mediaRootUrl + nfoFilename // must exist or will raise errors, only the first one is taken
|
||||
val nfoContent =
|
||||
app.get(nfoPath, authHeader).document // all the metadata
|
||||
|
||||
if (isMovieType) {
|
||||
val movieName = nfoContent.select("title").text()
|
||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||
return@mapNotNull newMovieSearchResponse(
|
||||
movieName,
|
||||
mediaRootUrl,
|
||||
TvType.Movie,
|
||||
) {
|
||||
addPoster(posterUrl, authHeader)
|
||||
}
|
||||
} else { // tv serie
|
||||
val serieName = nfoContent.select("title").text()
|
||||
|
||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||
|
||||
newTvSeriesSearchResponse(
|
||||
serieName,
|
||||
nfoPath,
|
||||
TvType.TvSeries,
|
||||
) {
|
||||
addPoster(posterUrl, authHeader)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { // can cause issues invisible errors
|
||||
null
|
||||
//logError(e) // not working because it changes the return type of currentList to Any
|
||||
}
|
||||
} else null
|
||||
}
|
||||
if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir
|
||||
HomePageList(categoryTitle, currentList)
|
||||
} else null
|
||||
} else null // the path is ../ which is parent directory
|
||||
}
|
||||
// if (returnList.isEmpty()) return null // maybe doing nothing idk
|
||||
return HomePageResponse(returnList)
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class NginxProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(NginxProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"Anime",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=9anime.id&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,357 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class NineAnimeProvider : MainAPI() {
|
||||
override var mainUrl = "https://9anime.id"
|
||||
override var name = "9Anime"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(TvType.Anime)
|
||||
override val hasQuickSearch = true
|
||||
|
||||
// taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/NineAnime.kt
|
||||
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
|
||||
companion object {
|
||||
private const val nineAnimeKey =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
private const val cipherKey = "kMXzgyNzT3k5dYab"
|
||||
|
||||
fun encodeVrf(text: String, mainKey: String): String {
|
||||
return encode(
|
||||
encrypt(
|
||||
cipher(mainKey, encode(text)),
|
||||
nineAnimeKey
|
||||
)//.replace("""=+$""".toRegex(), "")
|
||||
)
|
||||
}
|
||||
|
||||
fun decodeVrf(text: String, mainKey: String): String {
|
||||
return decode(cipher(mainKey, decrypt(text, nineAnimeKey)))
|
||||
}
|
||||
|
||||
fun encrypt(input: String, key: String): String {
|
||||
if (input.any { it.code > 255 }) throw Exception("illegal characters!")
|
||||
var output = ""
|
||||
for (i in input.indices step 3) {
|
||||
val a = intArrayOf(-1, -1, -1, -1)
|
||||
a[0] = input[i].code shr 2
|
||||
a[1] = (3 and input[i].code) shl 4
|
||||
if (input.length > i + 1) {
|
||||
a[1] = a[1] or (input[i + 1].code shr 4)
|
||||
a[2] = (15 and input[i + 1].code) shl 2
|
||||
}
|
||||
if (input.length > i + 2) {
|
||||
a[2] = a[2] or (input[i + 2].code shr 6)
|
||||
a[3] = 63 and input[i + 2].code
|
||||
}
|
||||
for (n in a) {
|
||||
if (n == -1) output += "="
|
||||
else {
|
||||
if (n in 0..63) output += key[n]
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
fun cipher(key: String, text: String): String {
|
||||
val arr = IntArray(256) { it }
|
||||
|
||||
var u = 0
|
||||
var r: Int
|
||||
arr.indices.forEach {
|
||||
u = (u + arr[it] + key[it % key.length].code) % 256
|
||||
r = arr[it]
|
||||
arr[it] = arr[u]
|
||||
arr[u] = r
|
||||
}
|
||||
u = 0
|
||||
var c = 0
|
||||
|
||||
return text.indices.map { j ->
|
||||
c = (c + 1) % 256
|
||||
u = (u + arr[c]) % 256
|
||||
r = arr[c]
|
||||
arr[c] = arr[u]
|
||||
arr[u] = r
|
||||
(text[j].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
|
||||
}.joinToString("")
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun decrypt(input: String, key: String): String {
|
||||
val t = if (input.replace("""[\t\n\f\r]""".toRegex(), "").length % 4 == 0) {
|
||||
input.replace("""==?$""".toRegex(), "")
|
||||
} else input
|
||||
if (t.length % 4 == 1 || t.contains("""[^+/0-9A-Za-z]""".toRegex())) throw Exception("bad input")
|
||||
var i: Int
|
||||
var r = ""
|
||||
var e = 0
|
||||
var u = 0
|
||||
for (o in t.indices) {
|
||||
e = e shl 6
|
||||
i = key.indexOf(t[o])
|
||||
e = e or i
|
||||
u += 6
|
||||
if (24 == u) {
|
||||
r += ((16711680 and e) shr 16).toChar()
|
||||
r += ((65280 and e) shr 8).toChar()
|
||||
r += (255 and e).toChar()
|
||||
e = 0
|
||||
u = 0
|
||||
}
|
||||
}
|
||||
return if (12 == u) {
|
||||
e = e shr 4
|
||||
r + e.toChar()
|
||||
} else {
|
||||
if (18 == u) {
|
||||
e = e shr 2
|
||||
r += ((65280 and e) shr 8).toChar()
|
||||
r += (255 and e).toChar()
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
fun encode(input: String): String =
|
||||
java.net.URLEncoder.encode(input, "utf-8").replace("+", "%20")
|
||||
|
||||
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
|
||||
}
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"$mainUrl/ajax/home/widget/trending?page=" to "Trending",
|
||||
"$mainUrl/ajax/home/widget/updated-all?page=" to "All",
|
||||
"$mainUrl/ajax/home/widget/updated-sub?page=" to "Recently Updated (SUB)",
|
||||
"$mainUrl/ajax/home/widget/updated-dub?page=" to "Recently Updated (DUB)",
|
||||
"$mainUrl/ajax/home/widget/updated-china?page=" to "Recently Updated (Chinese)",
|
||||
"$mainUrl/ajax/home/widget/random?page=" to "Random",
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(
|
||||
page: Int,
|
||||
request: MainPageRequest
|
||||
): HomePageResponse {
|
||||
val url = request.data + page
|
||||
val home = Jsoup.parse(
|
||||
app.get(
|
||||
url
|
||||
).parsed<Response>().html
|
||||
).select("div.item").mapNotNull { element ->
|
||||
val title = element.selectFirst(".info > .name") ?: return@mapNotNull null
|
||||
val link = title.attr("href")
|
||||
val poster = element.selectFirst(".poster > a > img")?.attr("src")
|
||||
val meta = element.selectFirst(".poster > a > .meta > .inner > .left")
|
||||
val subbedEpisodes = meta?.selectFirst(".sub")?.text()?.toIntOrNull()
|
||||
val dubbedEpisodes = meta?.selectFirst(".dub")?.text()?.toIntOrNull()
|
||||
|
||||
newAnimeSearchResponse(title.text() ?: return@mapNotNull null, link) {
|
||||
this.posterUrl = poster
|
||||
addDubStatus(
|
||||
dubbedEpisodes != null,
|
||||
subbedEpisodes != null,
|
||||
dubbedEpisodes,
|
||||
subbedEpisodes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newHomePageResponse(request.name, home)
|
||||
}
|
||||
|
||||
data class Response(
|
||||
@JsonProperty("result") val html: String
|
||||
)
|
||||
|
||||
data class QuickSearchResponse(
|
||||
//@JsonProperty("status") val status: Int? = null,
|
||||
@JsonProperty("result") val result: QuickSearchResult? = null,
|
||||
//@JsonProperty("message") val message: String? = null,
|
||||
//@JsonProperty("messages") val messages: ArrayList<String> = arrayListOf()
|
||||
)
|
||||
|
||||
data class QuickSearchResult(
|
||||
@JsonProperty("html") val html: String? = null,
|
||||
//@JsonProperty("linkMore") val linkMore: String? = null
|
||||
)
|
||||
|
||||
override suspend fun quickSearch(query: String): List<SearchResponse>? {
|
||||
val vrf = encodeVrf(query, cipherKey)
|
||||
val url =
|
||||
"$mainUrl/ajax/anime/search?keyword=$query&vrf=$vrf"
|
||||
val response = app.get(url).parsedSafe<QuickSearchResponse>()
|
||||
val document = Jsoup.parse(response?.result?.html ?: return null)
|
||||
return document.select(".items > a").mapNotNull { element ->
|
||||
val link = fixUrl(element?.attr("href") ?: return@mapNotNull null)
|
||||
val title = element.selectFirst(".info > .name")?.text() ?: return@mapNotNull null
|
||||
newAnimeSearchResponse(title, link) {
|
||||
posterUrl = element.selectFirst(".poster > span > img")?.attr("src")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val vrf = encodeVrf(query, cipherKey)
|
||||
//?language%5B%5D=${if (selectDub) "dubbed" else "subbed"}&
|
||||
val url =
|
||||
"$mainUrl/filter?keyword=${encode(query)}&vrf=${vrf}&page=1"
|
||||
return app.get(url).document.select("#list-items div.ani.poster.tip > a").mapNotNull {
|
||||
val link = fixUrl(it.attr("href") ?: return@mapNotNull null)
|
||||
val img = it.select("img")
|
||||
val title = img.attr("alt")
|
||||
newAnimeSearchResponse(title, link) {
|
||||
posterUrl = img.attr("src")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val validUrl = url.replace("https://9anime.to", mainUrl)
|
||||
val doc = app.get(validUrl).document
|
||||
|
||||
val meta = doc.selectFirst("#w-info") ?: throw ErrorLoadingException("Could not find info")
|
||||
val ratingElement = meta.selectFirst(".brating > #w-rating")
|
||||
val id = ratingElement?.attr("data-id") ?: throw ErrorLoadingException("Could not find id")
|
||||
val binfo =
|
||||
meta.selectFirst(".binfo") ?: throw ErrorLoadingException("Could not find binfo")
|
||||
val info = binfo.selectFirst(".info") ?: throw ErrorLoadingException("Could not find info")
|
||||
|
||||
val title = (info.selectFirst(".title") ?: info.selectFirst(".d-title"))?.text()
|
||||
?: throw ErrorLoadingException("Could not find title")
|
||||
|
||||
val vrf = encodeVrf(id, cipherKey)
|
||||
val episodeListUrl = "$mainUrl/ajax/episode/list/$id?vrf=$vrf"
|
||||
val body =
|
||||
app.get(episodeListUrl).parsedSafe<Response>()?.html
|
||||
?: throw ErrorLoadingException("Could not parse json with cipherKey=$cipherKey id=$id url=\n$episodeListUrl")
|
||||
|
||||
val subEpisodes = ArrayList<Episode>()
|
||||
val dubEpisodes = ArrayList<Episode>()
|
||||
|
||||
//TODO RECOMMENDATIONS
|
||||
|
||||
Jsoup.parse(body).body().select(".episodes > ul > li > a").mapNotNull { element ->
|
||||
val ids = element.attr("data-ids").split(",", limit = 2)
|
||||
|
||||
val epNum = element.attr("data-num")
|
||||
.toIntOrNull() // might fuck up on 7.5 ect might use data-slug instead
|
||||
val epTitle = element.selectFirst("span.d-title")?.text()
|
||||
//val filler = element.hasClass("filler")
|
||||
ids.getOrNull(1)?.let { dub ->
|
||||
dubEpisodes.add(
|
||||
Episode(
|
||||
"$mainUrl/ajax/server/list/$dub?vrf=${encodeVrf(dub, cipherKey)}",
|
||||
epTitle,
|
||||
episode = epNum
|
||||
)
|
||||
)
|
||||
}
|
||||
ids.getOrNull(0)?.let { sub ->
|
||||
subEpisodes.add(
|
||||
Episode(
|
||||
"$mainUrl/ajax/server/list/$sub?vrf=${encodeVrf(sub, cipherKey)}",
|
||||
epTitle,
|
||||
episode = epNum
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||
addEpisodes(DubStatus.Dubbed, dubEpisodes)
|
||||
addEpisodes(DubStatus.Subbed, subEpisodes)
|
||||
|
||||
plot = info.selectFirst(".synopsis > .shorting > .content")?.text()
|
||||
posterUrl = binfo.selectFirst(".poster > span > img")?.attr("src")
|
||||
rating = ratingElement.attr("data-score").toFloat().times(1000f).toInt()
|
||||
|
||||
info.select(".bmeta > .meta > div").forEach { element ->
|
||||
when (element.ownText()) {
|
||||
"Genre: " -> {
|
||||
tags = element.select("span > a").mapNotNull { it?.text() }
|
||||
}
|
||||
"Duration: " -> {
|
||||
duration = getDurationFromString(element.selectFirst("span")?.text())
|
||||
}
|
||||
"Type: " -> {
|
||||
type = when (element.selectFirst("span > a")?.text()) {
|
||||
"ONA" -> TvType.OVA
|
||||
else -> {
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
"Status: " -> {
|
||||
showStatus = when (element.selectFirst("span")?.text()) {
|
||||
"Releasing" -> ShowStatus.Ongoing
|
||||
"Completed" -> ShowStatus.Completed
|
||||
else -> {
|
||||
showStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Result(
|
||||
@JsonProperty("url")
|
||||
val url: String? = null
|
||||
)
|
||||
|
||||
data class Links(
|
||||
@JsonProperty("result")
|
||||
val result: Result? = null
|
||||
)
|
||||
|
||||
//TODO 9anime outro into {"status":200,"result":{"url":"","skip_data":{"intro_begin":67,"intro_end":154,"outro_begin":1337,"outro_end":1415,"count":3}},"message":"","messages":[]}
|
||||
private suspend fun getEpisodeLinks(id: String): Links? {
|
||||
return app.get("$mainUrl/ajax/server/$id?vrf=${encodeVrf(id, cipherKey)}").parsedSafe()
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val body = app.get(data).parsed<Response>().html
|
||||
val document = Jsoup.parse(body)
|
||||
|
||||
document.select("li").apmap {
|
||||
try {
|
||||
val name = it.text()
|
||||
val encodedStreamUrl =
|
||||
getEpisodeLinks(it.attr("data-link-id"))?.result?.url ?: return@apmap
|
||||
val url = decodeVrf(encodedStreamUrl, cipherKey)
|
||||
if (!loadExtractor(url, mainUrl, subtitleCallback, callback)) {
|
||||
callback(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
name,
|
||||
url,
|
||||
mainUrl,
|
||||
Qualities.Unknown.value,
|
||||
url.contains(".m3u8")
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class NineAnimeProviderPlugin : Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(NineAnimeProvider())
|
||||
registerMainAPI(WcoProvider())
|
||||
registerExtractorAPI(Mcloud())
|
||||
registerExtractorAPI(Vidstreamz())
|
||||
registerExtractorAPI(Vizcloud())
|
||||
registerExtractorAPI(Vizcloud2())
|
||||
registerExtractorAPI(VizcloudOnline())
|
||||
registerExtractorAPI(VizcloudXyz())
|
||||
registerExtractorAPI(VizcloudLive())
|
||||
registerExtractorAPI(VizcloudInfo())
|
||||
registerExtractorAPI(MwvnVizcloudInfo())
|
||||
registerExtractorAPI(VizcloudDigital())
|
||||
registerExtractorAPI(VizcloudCloud())
|
||||
registerExtractorAPI(VizcloudSite())
|
||||
registerExtractorAPI(WcoStream())
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.util.*
|
||||
|
||||
|
||||
class WcoProvider : MainAPI() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
override var mainUrl = "https://wcostream.cc"
|
||||
override var name = "WCO Stream"
|
||||
override val hasQuickSearch = true
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.Anime,
|
||||
TvType.OVA
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val urls = listOf(
|
||||
Pair("$mainUrl/ajax/list/recently_updated?type=tv", "Recently Updated Anime"),
|
||||
Pair("$mainUrl/ajax/list/recently_updated?type=movie", "Recently Updated Movies"),
|
||||
Pair("$mainUrl/ajax/list/recently_added?type=tv", "Recently Added Anime"),
|
||||
Pair("$mainUrl/ajax/list/recently_added?type=movie", "Recently Added Movies"),
|
||||
)
|
||||
|
||||
val items = ArrayList<HomePageList>()
|
||||
for (i in urls) {
|
||||
try {
|
||||
val response = JSONObject(
|
||||
app.get(
|
||||
i.first,
|
||||
).text
|
||||
).getString("html") // I won't make a dataclass for this shit
|
||||
val document = Jsoup.parse(response)
|
||||
val results = document.select("div.flw-item").map {
|
||||
val filmPoster = it.selectFirst("> div.film-poster")
|
||||
val filmDetail = it.selectFirst("> div.film-detail")
|
||||
val nameHeader = filmDetail!!.selectFirst("> h3.film-name > a")
|
||||
val title = nameHeader!!.text().replace(" (Dub)", "")
|
||||
val href =
|
||||
nameHeader.attr("href").replace("/watch/", "/anime/")
|
||||
.replace(Regex("-episode-.*"), "/")
|
||||
val isDub =
|
||||
filmPoster!!.selectFirst("> div.film-poster-quality")?.text()
|
||||
?.contains("DUB")
|
||||
?: false
|
||||
val poster = filmPoster.selectFirst("> img")!!.attr("data-src")
|
||||
val set: EnumSet<DubStatus> =
|
||||
EnumSet.of(if (isDub) DubStatus.Dubbed else DubStatus.Subbed)
|
||||
AnimeSearchResponse(title, href, this.name, TvType.Anime, poster, null, set)
|
||||
}
|
||||
items.add(HomePageList(i.second, results))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
|
||||
private fun fixAnimeLink(url: String): String {
|
||||
val regex = "watch/([a-zA-Z\\-0-9]*)-episode".toRegex()
|
||||
val (aniId) = regex.find(url)!!.destructured
|
||||
return "$mainUrl/anime/$aniId"
|
||||
}
|
||||
|
||||
private fun parseSearchPage(soup: Document): List<SearchResponse> {
|
||||
val items = soup.select(".film_list-wrap > .flw-item")
|
||||
if (items.isEmpty()) return ArrayList()
|
||||
return items.map { i ->
|
||||
val href = fixAnimeLink(i.selectFirst("a")!!.attr("href"))
|
||||
val img = fixUrl(i.selectFirst("img")!!.attr("data-src"))
|
||||
val title = i.selectFirst("img")!!.attr("title")
|
||||
val isDub = !i.select(".pick.film-poster-quality").isEmpty()
|
||||
val year =
|
||||
i.selectFirst(".film-detail.film-detail-fix > div > span:nth-child(1)")!!.text()
|
||||
.toIntOrNull()
|
||||
val type =
|
||||
i.selectFirst(".film-detail.film-detail-fix > div > span:nth-child(3)")!!.text()
|
||||
|
||||
if (getType(type) == TvType.AnimeMovie) {
|
||||
MovieSearchResponse(
|
||||
title, href, this.name, TvType.AnimeMovie, img, year
|
||||
)
|
||||
} else {
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
img,
|
||||
year,
|
||||
EnumSet.of(if (isDub) DubStatus.Dubbed else DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/search"
|
||||
val response =
|
||||
app.get(url, params = mapOf("keyword" to query))
|
||||
var document = Jsoup.parse(response.text)
|
||||
val returnValue = parseSearchPage(document).toMutableList()
|
||||
|
||||
while (!document.select(".pagination").isEmpty()) {
|
||||
val link = document.select("a.page-link[rel=\"next\"]")
|
||||
if (!link.isEmpty() && returnValue.size < 40) {
|
||||
val extraResponse = app.get(fixUrl(link[0].attr("href"))).text
|
||||
document = Jsoup.parse(extraResponse)
|
||||
returnValue.addAll(parseSearchPage(document))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return returnValue.distinctBy { it.url }
|
||||
}
|
||||
|
||||
override suspend fun quickSearch(query: String): List<SearchResponse> {
|
||||
val response = JSONObject(
|
||||
app.post(
|
||||
"https://wcostream.cc/ajax/search",
|
||||
data = mapOf("keyword" to query)
|
||||
).text
|
||||
).getString("html") // I won't make a dataclass for this shit
|
||||
val document = Jsoup.parse(response)
|
||||
|
||||
return document.select("a.nav-item").mapNotNull {
|
||||
val title = it.selectFirst("img")?.attr("title") ?: return@mapNotNull null
|
||||
val img = it?.selectFirst("img")?.attr("src") ?: return@mapNotNull null
|
||||
val href = it?.attr("href") ?: return@mapNotNull null
|
||||
val isDub = title.contains("(Dub)")
|
||||
val filmInfo = it.selectFirst(".film-infor")
|
||||
val year = filmInfo?.select("span")?.get(0)?.text()?.toIntOrNull()
|
||||
val type = filmInfo?.select("span")?.get(1)?.text().toString()
|
||||
if (getType(type) == TvType.AnimeMovie) {
|
||||
MovieSearchResponse(
|
||||
title, href, this.name, TvType.AnimeMovie, img, year
|
||||
)
|
||||
} else {
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
img,
|
||||
year,
|
||||
EnumSet.of(if (isDub) DubStatus.Dubbed else DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val response = app.get(url, timeout = 120).text
|
||||
val document = Jsoup.parse(response)
|
||||
|
||||
val japaneseTitle =
|
||||
document.selectFirst("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(1)")
|
||||
?.text()?.trim()?.replace("Other names:", "")?.trim()
|
||||
|
||||
val canonicalTitle = document.selectFirst("meta[name=\"title\"]")
|
||||
?.attr("content")?.split("| W")?.get(0).toString()
|
||||
|
||||
val isDubbed = canonicalTitle.contains("Dub")
|
||||
val episodeNodes = document.select(".tab-content .nav-item > a")
|
||||
|
||||
val episodes = ArrayList(episodeNodes?.map {
|
||||
Episode(it.attr("href"))
|
||||
} ?: ArrayList())
|
||||
|
||||
val statusElem =
|
||||
document.selectFirst("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(2)")
|
||||
val status = when (statusElem?.text()?.replace("Status:", "")?.trim()) {
|
||||
"Ongoing" -> ShowStatus.Ongoing
|
||||
"Completed" -> ShowStatus.Completed
|
||||
else -> null
|
||||
}
|
||||
val yearText =
|
||||
document.selectFirst("div.elements div.row > div:nth-child(2) > div.row-line:nth-child(4)")
|
||||
?.text()
|
||||
val year = yearText?.replace("Date release:", "")?.trim()?.split("-")?.get(0)?.toIntOrNull()
|
||||
|
||||
val poster = document.selectFirst(".film-poster-img")?.attr("src")
|
||||
val type = document.selectFirst("span.item.mr-1 > a")?.text()?.trim()
|
||||
|
||||
val synopsis = document.selectFirst(".description > p")?.text()?.trim()
|
||||
val genre =
|
||||
document.select("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(5) > a")
|
||||
.map { it?.text()?.trim().toString() }
|
||||
|
||||
return newAnimeLoadResponse(canonicalTitle, url, getType(type ?: "")) {
|
||||
japName = japaneseTitle
|
||||
engName = canonicalTitle
|
||||
posterUrl = poster
|
||||
this.year = year
|
||||
addEpisodes(if (isDubbed) DubStatus.Dubbed else DubStatus.Subbed, episodes)
|
||||
showStatus = status
|
||||
plot = synopsis
|
||||
tags = genre
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val response = app.get(data).text
|
||||
val servers = Jsoup.parse(response).select("#servers-list > ul > li").map {
|
||||
mapOf(
|
||||
"link" to it?.selectFirst("a")?.attr("data-embed"),
|
||||
"title" to it?.selectFirst("span")?.text()?.trim()
|
||||
)
|
||||
}
|
||||
|
||||
for (server in servers) {
|
||||
WcoStream().getSafeUrl(server["link"].toString(), null, subtitleCallback, callback)
|
||||
Mcloud().getSafeUrl(server["link"].toString(), null, subtitleCallback, callback)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.NineAnimeProvider.Companion.cipher
|
||||
import com.lagradost.NineAnimeProvider.Companion.encrypt
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
class Vidstreamz : WcoStream() {
|
||||
override var mainUrl = "https://vidstreamz.online"
|
||||
}
|
||||
|
||||
open class Mcloud : WcoStream() {
|
||||
override var name = "Mcloud"
|
||||
override var mainUrl = "https://mcloud.to"
|
||||
override val requiresReferer = true
|
||||
}
|
||||
|
||||
class Vizcloud : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud2.ru"
|
||||
}
|
||||
|
||||
class Vizcloud2 : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud2.online"
|
||||
}
|
||||
|
||||
class VizcloudOnline : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.online"
|
||||
}
|
||||
|
||||
class VizcloudXyz : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.xyz"
|
||||
}
|
||||
|
||||
class VizcloudLive : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.live"
|
||||
}
|
||||
|
||||
class VizcloudInfo : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.info"
|
||||
}
|
||||
|
||||
class MwvnVizcloudInfo : WcoStream() {
|
||||
override var mainUrl = "https://mwvn.vizcloud.info"
|
||||
}
|
||||
|
||||
class VizcloudDigital : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.digital"
|
||||
}
|
||||
|
||||
class VizcloudCloud : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.cloud"
|
||||
}
|
||||
|
||||
class VizcloudSite : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.site"
|
||||
}
|
||||
|
||||
open class WcoStream : ExtractorApi() {
|
||||
override var name = "VidStream" // Cause works for animekisa and wco
|
||||
override var mainUrl = "https://vidstream.pro"
|
||||
override val requiresReferer = false
|
||||
private val regex = Regex("(.+?/)e(?:mbed)?/([a-zA-Z0-9]+)")
|
||||
|
||||
companion object {
|
||||
// taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/extractors/VizCloud.kt
|
||||
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
|
||||
private var lastChecked = 0L
|
||||
private const val jsonLink =
|
||||
"https://raw.githubusercontent.com/chenkaslowankiya/BruhFlow/main/keys.json"
|
||||
private var cipherKey: VizCloudKey? = null
|
||||
suspend fun getKey(): VizCloudKey {
|
||||
cipherKey =
|
||||
if (cipherKey != null && (lastChecked - System.currentTimeMillis()) < 1000 * 60 * 30) cipherKey!!
|
||||
else {
|
||||
lastChecked = System.currentTimeMillis()
|
||||
app.get(jsonLink).parsed()
|
||||
}
|
||||
return cipherKey!!
|
||||
}
|
||||
|
||||
data class VizCloudKey(
|
||||
@JsonProperty("cipherKey") val cipherKey: String,
|
||||
@JsonProperty("mainKey") val mainKey: String,
|
||||
@JsonProperty("encryptKey") val encryptKey: String,
|
||||
@JsonProperty("dashTable") val dashTable: String
|
||||
)
|
||||
|
||||
private const val baseTable =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=/_"
|
||||
|
||||
private fun dashify(id: String, dashTable: String): String {
|
||||
val table = dashTable.split(" ")
|
||||
return id.mapIndexedNotNull { i, c ->
|
||||
table.getOrNull((baseTable.indexOf(c) * 16) + (i % 16))
|
||||
}.joinToString("-")
|
||||
}
|
||||
}
|
||||
|
||||
//private val key = "LCbu3iYC7ln24K7P" // key credits @Modder4869
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val group = regex.find(url)?.groupValues!!
|
||||
|
||||
val host = group[1]
|
||||
val viz = getKey()
|
||||
val id = encrypt(
|
||||
cipher(
|
||||
viz.cipherKey,
|
||||
encrypt(group[2], viz.encryptKey).also { println(it) }
|
||||
).also { println(it) },
|
||||
viz.encryptKey
|
||||
).also { println(it) }
|
||||
|
||||
val link =
|
||||
"${host}mediainfo/${dashify(id, viz.dashTable)}?key=${viz.mainKey}" //
|
||||
val response = app.get(link, referer = referer)
|
||||
|
||||
data class Sources(@JsonProperty("file") val file: String)
|
||||
data class Media(@JsonProperty("sources") val sources: List<Sources>)
|
||||
data class Data(@JsonProperty("media") val media: Media)
|
||||
data class Response(@JsonProperty("data") val data: Data)
|
||||
|
||||
|
||||
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
||||
return response.parsed<Response>().data.media.sources.map {
|
||||
ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 3
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
description = "Uses TMDB"
|
||||
authors = listOf("Blatzar")
|
||||
|
||||
/**
|
||||
* 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=olgply.com&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,115 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.metaproviders.TmdbLink
|
||||
import com.lagradost.cloudstream3.metaproviders.TmdbProvider
|
||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.nicehttp.requestCreator
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
|
||||
class OlgplyProvider : TmdbProvider() {
|
||||
override var mainUrl = "https://olgply.com"
|
||||
override val apiName = "Olgply"
|
||||
override var name = "Olgply"
|
||||
override val instantLinkLoading = true
|
||||
override val useMetaLoadResponse = true
|
||||
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie)
|
||||
|
||||
private suspend fun loadLinksWithWebView(
|
||||
url: String,
|
||||
// subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val foundVideo = WebViewResolver(
|
||||
Regex("""\.m3u8|i7njdjvszykaieynzsogaysdgb0hm8u1mzubmush4maopa4wde\.com""")
|
||||
).resolveUsingWebView(
|
||||
requestCreator(
|
||||
"GET", url, referer = "https://olgply.xyz/"
|
||||
)
|
||||
)
|
||||
.first ?: return
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
"Movies4Discord",
|
||||
foundVideo.url.toString(),
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val mappedData = parseJson<TmdbLink>(data)
|
||||
val tmdbId = mappedData.tmdbID ?: return false
|
||||
val jsRegex = Regex("""eval\(.*\);""")
|
||||
|
||||
val apiUrl =
|
||||
"https://olgply.xyz/${tmdbId}${mappedData.season?.let { "/$it" } ?: ""}${mappedData.episode?.let { "/$it" } ?: ""}"
|
||||
// val html =
|
||||
// app.get(apiUrl, referer = "https://olgply.xyz/").text
|
||||
// val rhino = Context.enter()
|
||||
// rhino.optimizationLevel = -1
|
||||
// val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
// val documentJs = """
|
||||
// Plyr = function(){};
|
||||
//
|
||||
// hlsPrototype = {
|
||||
// loadSource(url) {
|
||||
// this.url = url;
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// function Hls() {};
|
||||
// Hls.isSupported = function(){return true};
|
||||
//
|
||||
// Hls.prototype = hlsPrototype;
|
||||
// Hls.prototype.constructor = Hls;
|
||||
//
|
||||
// document = {
|
||||
// "querySelector" : function() {}
|
||||
// };
|
||||
// """.trimIndent()
|
||||
//
|
||||
// val foundJs = jsRegex.find(html)?.groupValues?.getOrNull(0) ?: return false
|
||||
// try {
|
||||
// rhino.evaluateString(scope, documentJs + foundJs, "JavaScript", 1, null)
|
||||
// } catch (e: Exception) {
|
||||
// }
|
||||
//
|
||||
// val hls = scope.get("hls", scope) as? ScriptableObject
|
||||
//
|
||||
// if (hls != null) {
|
||||
// callback.invoke(
|
||||
// ExtractorLink(
|
||||
// this.name,
|
||||
// this.name,
|
||||
// hls["url"].toString(),
|
||||
// this.mainUrl + "/",
|
||||
// Qualities.Unknown.value,
|
||||
// headers = mapOf("range" to "bytes=0-"),
|
||||
// isM3u8 = true
|
||||
// )
|
||||
// )
|
||||
// } else {
|
||||
// Disgraceful fallback, but the js for Movies4Discord refuses to work correctly :(
|
||||
loadLinksWithWebView(apiUrl, callback)
|
||||
// }
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class OlgplyProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(OlgplyProvider())
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 2
|
||||
|
||||
|
||||
cloudstream {
|
||||
// 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=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,6 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
class DopeboxProvider : SflixProvider() {
|
||||
override var mainUrl = "https://dopebox.to"
|
||||
override var name = "Dopebox"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
class HDTodayProvider : SflixProvider() {
|
||||
override var mainUrl = "https://hdtoday.cc"
|
||||
override var name = "HDToday"
|
||||
}
|
|
@ -1,747 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/search/${query.replace(" ", "-")}"
|
||||
val html = app.get(url).text
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
return document.select("div.flw-item").map {
|
||||
val title = it.select("h2.film-name").text()
|
||||
val href = fixUrl(it.select("a").attr("href"))
|
||||
val year = it.select("span.fdi-item").text().toIntOrNull()
|
||||
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 {
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
image,
|
||||
year,
|
||||
null,
|
||||
quality = quality
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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())
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
class SolarmovieProvider : SflixProvider() {
|
||||
override var mainUrl = "https://solarmovie.pe"
|
||||
override var name = "Solarmovie"
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
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/")
|
||||
}
|
||||
}
|
|
@ -1,371 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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=secretlink.xyz&sz=24"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -1,263 +0,0 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class SoaptwoDayProvider : MainAPI() {
|
||||
override var mainUrl = "https://secretlink.xyz" //Probably a rip off, but it has no captcha
|
||||
override var name = "Soap2Day"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
Pair("$mainUrl/movielist?page=", "Movies"),
|
||||
Pair("$mainUrl/tvlist?page=", "TV Series"),
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(
|
||||
page: Int,
|
||||
request : MainPageRequest
|
||||
): HomePageResponse {
|
||||
val url = request.data + page
|
||||
|
||||
val soup = app.get(url).document
|
||||
val home =
|
||||
soup.select("div.container div.row div.col-sm-12.col-lg-12 div.row div.col-sm-12.col-lg-12 .col-xs-6")
|
||||
.map {
|
||||
val title = it.selectFirst("h5 a")!!.text()
|
||||
val link = it.selectFirst("a")!!.attr("href")
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
fixUrl(it.selectFirst("img")!!.attr("src")),
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
return newHomePageResponse(request.name, home)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val doc = app.get("$mainUrl/search/keyword/$query").document
|
||||
return doc.select("div.container div.row div.col-sm-12.col-lg-12 div.row div.col-sm-12.col-lg-12 .col-xs-6")
|
||||
.map {
|
||||
val title = it.selectFirst("h5 a")!!.text()
|
||||
val image = fixUrl(it.selectFirst("img")!!.attr("src"))
|
||||
val href = fixUrl(it.selectFirst("a")!!.attr("href"))
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
image,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val soup = app.get(url).document
|
||||
val title = soup.selectFirst(".hidden-lg > div:nth-child(1) > h4")?.text() ?: ""
|
||||
val description = soup.selectFirst("p#wrap")?.text()?.trim()
|
||||
val poster =
|
||||
soup.selectFirst(".col-md-5 > div:nth-child(1) > div:nth-child(1) > img")?.attr("src")
|
||||
val episodes = mutableListOf<Episode>()
|
||||
soup.select("div.alert").forEach {
|
||||
val season = it?.selectFirst("h4")?.text()?.filter { c -> c.isDigit() }?.toIntOrNull()
|
||||
it?.select("div > div > a")?.forEach { entry ->
|
||||
val link = fixUrlNull(entry?.attr("href")) ?: return@forEach
|
||||
val text = entry?.text() ?: ""
|
||||
val name = text.replace(Regex("(^(\\d+)\\.)"), "")
|
||||
val epNum = text.substring(0, text.indexOf(".")).toIntOrNull()
|
||||
episodes.add(
|
||||
Episode(
|
||||
name = name,
|
||||
data = link,
|
||||
season = season,
|
||||
episode = epNum
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
val otherInfoBody = soup.select("div.col-sm-8 div.panel-body").toString()
|
||||
//Fetch casts
|
||||
val casts = otherInfoBody.substringAfter("Stars : ")
|
||||
.substringBefore("Genre : ").let {
|
||||
Jsoup.parse(it).select("a")
|
||||
}.mapNotNull {
|
||||
val castName = it?.text() ?: return@mapNotNull null
|
||||
ActorData(
|
||||
Actor(
|
||||
name = castName
|
||||
)
|
||||
)
|
||||
}
|
||||
//Fetch year
|
||||
val year = otherInfoBody.substringAfter("<h4>Release : </h4>")
|
||||
.substringBefore("<div").let {
|
||||
//Log.i(this.name, "Result => year string: $it")
|
||||
Jsoup.parse(it).select("p")[1]
|
||||
}?.text()?.take(4)?.toIntOrNull()
|
||||
//Fetch genres
|
||||
val genre = otherInfoBody.substringAfter("<h4>Genre : </h4>")
|
||||
.substringBefore("<h4>Release : </h4>").let {
|
||||
//Log.i(this.name, "Result => genre string: $it")
|
||||
Jsoup.parse(it).select("a")
|
||||
}.mapNotNull { it?.text()?.trim() ?: return@mapNotNull null }
|
||||
|
||||
return when (val tvType = if (episodes.isEmpty()) TvType.Movie else TvType.TvSeries) {
|
||||
TvType.TvSeries -> {
|
||||
TvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
tvType,
|
||||
episodes.reversed(),
|
||||
fixUrlNull(poster),
|
||||
year = year,
|
||||
description,
|
||||
actors = casts,
|
||||
tags = genre
|
||||
)
|
||||
}
|
||||
TvType.Movie -> {
|
||||
MovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
tvType,
|
||||
url,
|
||||
fixUrlNull(poster),
|
||||
year = year,
|
||||
description,
|
||||
actors = casts,
|
||||
tags = genre
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerJson(
|
||||
@JsonProperty("0") val zero: String?,
|
||||
@JsonProperty("key") val key: Boolean?,
|
||||
@JsonProperty("val") val stream: String?,
|
||||
@JsonProperty("val_bak") val streambackup: String?,
|
||||
@JsonProperty("pos") val pos: Int?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("subs") val subs: List<Subs>?,
|
||||
@JsonProperty("prev_epi_title") val prevEpiTitle: String?,
|
||||
@JsonProperty("prev_epi_url") val prevEpiUrl: String?,
|
||||
@JsonProperty("next_epi_title") val nextEpiTitle: String?,
|
||||
@JsonProperty("next_epi_url") val nextEpiUrl: String?
|
||||
)
|
||||
|
||||
data class Subs(
|
||||
@JsonProperty("id") val id: Int?,
|
||||
@JsonProperty("movieId") val movieId: Int?,
|
||||
@JsonProperty("tvId") val tvId: Int?,
|
||||
@JsonProperty("episodeId") val episodeId: Int?,
|
||||
@JsonProperty("default") val default: Int?,
|
||||
@JsonProperty("IsShow") val IsShow: Int?,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("path") val path: String?,
|
||||
@JsonProperty("downlink") val downlink: String?,
|
||||
@JsonProperty("source_file_name") val sourceFileName: String?,
|
||||
@JsonProperty("createtime") val createtime: Int?
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val doc = app.get(data).document
|
||||
val idplayer = doc.selectFirst("#divU")?.text()
|
||||
val idplayer2 = doc.selectFirst("#divP")?.text()
|
||||
val movieid = doc.selectFirst("div.row input#hId")!!.attr("value")
|
||||
val tvType = try {
|
||||
doc.selectFirst(".col-md-5 > div:nth-child(1) > div:nth-child(1) > img")!!.attr("src")
|
||||
?: ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
val ajaxlink =
|
||||
if (tvType.contains("movie")) "$mainUrl/home/index/GetMInfoAjax" else "$mainUrl/home/index/GetEInfoAjax"
|
||||
listOf(
|
||||
idplayer,
|
||||
idplayer2,
|
||||
).mapNotNull { playerID ->
|
||||
val url = app.post(
|
||||
ajaxlink,
|
||||
headers = mapOf(
|
||||
"Host" to "secretlink.xyz",
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Accept" to "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With" to "XMLHttpRequest",
|
||||
"Origin" to "https://secretlink.xyz",
|
||||
"DNT" to "1",
|
||||
"Connection" to "keep-alive",
|
||||
"Referer" to data,
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "same-origin",
|
||||
),
|
||||
data = mapOf(
|
||||
Pair("pass", movieid),
|
||||
Pair("param", playerID ?: ""),
|
||||
)
|
||||
).text.replace("\\\"", "\"").replace("\"{", "{").replace("}\"", "}")
|
||||
.replace("\\\\\\/", "\\/")
|
||||
val json = parseJson<ServerJson>(url)
|
||||
listOfNotNull(
|
||||
json.stream,
|
||||
json.streambackup
|
||||
).apmap { stream ->
|
||||
val cleanstreamurl = stream.replace("\\/", "/").replace("\\\\\\", "")
|
||||
if (cleanstreamurl.isNotBlank()) {
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"Soap2Day",
|
||||
"Soap2Day",
|
||||
cleanstreamurl,
|
||||
"https://soap2day.ac",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
json.subs?.forEach { subtitle ->
|
||||
val sublink = mainUrl + subtitle.path
|
||||
listOf(
|
||||
sublink,
|
||||
subtitle.downlink
|
||||
).mapNotNull { subs ->
|
||||
if (subs != null) {
|
||||
if (subs.isNotBlank()) {
|
||||
subtitleCallback(
|
||||
SubtitleFile(subtitle.name, subs)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class SoaptwoDayProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(SoaptwoDayProvider())
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
// description = "Lorem Ipsum"
|
||||
// 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(
|
||||
"Anime",
|
||||
"Movie",
|
||||
"AnimeMovie",
|
||||
"TvSeries",
|
||||
)
|
||||
iconUrl = "https://raw.githubusercontent.com/recloudstream/cloudstream-extensions/master/SuperStream/icon.png"
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue