parent
4e5b31404a
commit
9eb85f400e
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,404 @@
|
|||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,206 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,274 @@
|
|||
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?,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,119 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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",
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,562 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,131 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,198 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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
|
||||
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=bflix.ru&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,300 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.lagradost
|
||||
|
||||
class FmoviesToProvider : BflixProvider() {
|
||||
override var mainUrl = "https://fmovies.to"
|
||||
override var name = "Fmovies.to"
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
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")
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.lagradost
|
||||
|
||||
class SflixProProvider : BflixProvider() {
|
||||
override var mainUrl = "https://sflix.pro"
|
||||
override var name = "Sflix.pro"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// use an integer for version numbers
|
||||
version = 3
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
description = "The Crunchyroll provider allows you to watch all the shows that are on Crunchyroll."
|
||||
authors = listOf("Sir Aguacata (KillerDogeEmpire)")
|
||||
|
||||
/**
|
||||
* 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=crunchyroll.com&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,46 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import okhttp3.*
|
||||
import okhttp3.internal.parseCookie
|
||||
|
||||
/**
|
||||
* An HTTP session manager.
|
||||
*
|
||||
* This class simply keeps cookies across requests. No security about which site should use which cookies.
|
||||
*
|
||||
*/
|
||||
|
||||
class CustomSession(
|
||||
client: OkHttpClient
|
||||
) : Requests() {
|
||||
var cookies = mutableMapOf<String, Cookie>()
|
||||
|
||||
init {
|
||||
this.baseClient = client
|
||||
.newBuilder()
|
||||
.addInterceptor {
|
||||
val time = System.currentTimeMillis()
|
||||
val request = it.request()
|
||||
request.headers.forEach { header ->
|
||||
if (header.first.equals("cookie", ignoreCase = true)) {
|
||||
val cookie = parseCookie(time, request.url, header.second) ?: return@forEach
|
||||
cookies += cookie.name to cookie
|
||||
}
|
||||
}
|
||||
it.proceed(request)
|
||||
}
|
||||
.cookieJar(CustomCookieJar())
|
||||
.build()
|
||||
}
|
||||
|
||||
inner class CustomCookieJar : CookieJar {
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return this@CustomSession.cookies.values.toList()
|
||||
}
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
this@CustomSession.cookies += cookies.map { it.name to it }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,497 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.capitalize
|
||||
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 com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.*
|
||||
|
||||
private fun String.toAscii() = this.map { it.code }.joinToString()
|
||||
|
||||
class KrunchyGeoBypasser {
|
||||
companion object {
|
||||
const val BYPASS_SERVER = "https://cr-unblocker.us.to/start_session"
|
||||
val headers = mapOf(
|
||||
"accept" to "*/*",
|
||||
// "Accept-Encoding" to "gzip, deflate",
|
||||
"connection" to "keep-alive",
|
||||
// "Referer" to "https://google.com/",
|
||||
"user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36".toAscii()
|
||||
)
|
||||
var sessionId: String? = null
|
||||
|
||||
// val interceptor = CookieInterceptor()
|
||||
val session = CustomSession(app.baseClient)
|
||||
}
|
||||
|
||||
data class KrunchySession(
|
||||
@JsonProperty("data") var data: DataInfo? = DataInfo(),
|
||||
@JsonProperty("error") var error: Boolean? = null,
|
||||
@JsonProperty("code") var code: String? = null
|
||||
)
|
||||
|
||||
data class DataInfo(
|
||||
@JsonProperty("session_id") var sessionId: String? = null,
|
||||
@JsonProperty("country_code") var countryCode: String? = null,
|
||||
)
|
||||
|
||||
private suspend fun getSessionId(): Boolean {
|
||||
return try {
|
||||
val response = app.get(BYPASS_SERVER, params = mapOf("version" to "1.1")).text
|
||||
val json = parseJson<KrunchySession>(response)
|
||||
sessionId = json.data?.sessionId
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
sessionId = null
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun autoLoadSession(): Boolean {
|
||||
if (sessionId != null) return true
|
||||
getSessionId()
|
||||
// Do not spam the api!
|
||||
delay(3000)
|
||||
return autoLoadSession()
|
||||
}
|
||||
|
||||
suspend fun geoBypassRequest(url: String): NiceResponse {
|
||||
autoLoadSession()
|
||||
return session.get(url, headers = headers, cookies = mapOf("session_id" to sessionId!!))
|
||||
}
|
||||
}
|
||||
|
||||
class KrunchyProvider : MainAPI() {
|
||||
companion object {
|
||||
val crUnblock = KrunchyGeoBypasser()
|
||||
val episodeNumRegex = Regex("""Episode (\d+)""")
|
||||
}
|
||||
|
||||
// Do not make https! It will fail!
|
||||
override var mainUrl = "http://www.crunchyroll.com"
|
||||
override var name: String = "Crunchyroll"
|
||||
override var lang = "en"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
TvType.AnimeMovie,
|
||||
TvType.Anime,
|
||||
TvType.OVA
|
||||
)
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"$mainUrl/videos/anime/popular/ajax_page?pg=" to "Popular",
|
||||
"$mainUrl/videos/anime/simulcasts/ajax_page" to "Simulcasts"
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
println("GETMAINPAGE ")
|
||||
val categoryData = request.data
|
||||
|
||||
val paginated = categoryData.endsWith("=")
|
||||
val pagedLink = if (paginated) categoryData + page else categoryData
|
||||
val items = mutableListOf<HomePageList>()
|
||||
|
||||
// Only fetch page at first-time load of homepage
|
||||
if (page <= 1 && request.name == "Popular") {
|
||||
val doc = Jsoup.parse(crUnblock.geoBypassRequest(mainUrl).text)
|
||||
val featured = doc.select(".js-featured-show-list > li").mapNotNull { anime ->
|
||||
val url =
|
||||
fixUrlNull(anime?.selectFirst("a")?.attr("href")) ?: return@mapNotNull null
|
||||
val imgEl = anime.selectFirst("img")
|
||||
val name = imgEl?.attr("alt") ?: ""
|
||||
val posterUrl = imgEl?.attr("src")?.replace("small", "full")
|
||||
AnimeSearchResponse(
|
||||
name = name,
|
||||
url = url,
|
||||
apiName = this.name,
|
||||
type = TvType.Anime,
|
||||
posterUrl = posterUrl,
|
||||
dubStatus = EnumSet.of(DubStatus.Subbed)
|
||||
)
|
||||
}
|
||||
val recent =
|
||||
doc.select("div.welcome-countdown-day:contains(Now Showing) li").mapNotNull {
|
||||
val link =
|
||||
fixUrlNull(it.selectFirst("a")?.attr("href")) ?: return@mapNotNull null
|
||||
val name = it.selectFirst("span.welcome-countdown-name")?.text() ?: ""
|
||||
val img = it.selectFirst("img")?.attr("src")?.replace("medium", "full")
|
||||
val dubstat = if (name.contains("Dub)", true)) EnumSet.of(DubStatus.Dubbed) else
|
||||
EnumSet.of(DubStatus.Subbed)
|
||||
val details = it.selectFirst("span.welcome-countdown-details")?.text()
|
||||
val epnum =
|
||||
if (details.isNullOrBlank()) null else episodeNumRegex.find(details)?.value?.replace(
|
||||
"Episode ",
|
||||
""
|
||||
) ?: "0"
|
||||
val episodesMap = mutableMapOf<DubStatus, Int>()
|
||||
episodesMap[DubStatus.Subbed] = epnum?.toIntOrNull() ?: 0
|
||||
episodesMap[DubStatus.Dubbed] = epnum?.toIntOrNull() ?: 0
|
||||
AnimeSearchResponse(
|
||||
name = "★ $name ★",
|
||||
url = link.replace(Regex("(\\/episode.*)"), ""),
|
||||
apiName = this.name,
|
||||
type = TvType.Anime,
|
||||
posterUrl = fixUrlNull(img),
|
||||
dubStatus = dubstat,
|
||||
episodes = episodesMap
|
||||
)
|
||||
}
|
||||
if (recent.isNotEmpty()) {
|
||||
items.add(
|
||||
HomePageList(
|
||||
name = "Now Showing",
|
||||
list = recent,
|
||||
)
|
||||
)
|
||||
}
|
||||
if (featured.isNotEmpty()) {
|
||||
items.add(HomePageList("Featured", featured))
|
||||
}
|
||||
}
|
||||
|
||||
if (paginated || !paginated && page <= 1) {
|
||||
crUnblock.geoBypassRequest(pagedLink).let { respText ->
|
||||
val soup = Jsoup.parse(respText.text)
|
||||
|
||||
val episodes = soup.select("li").mapNotNull {
|
||||
val innerA = it.selectFirst("a") ?: return@mapNotNull null
|
||||
val urlEps = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null
|
||||
AnimeSearchResponse(
|
||||
name = innerA.attr("title"),
|
||||
url = urlEps,
|
||||
apiName = this.name,
|
||||
type = TvType.Anime,
|
||||
posterUrl = it.selectFirst("img")?.attr("src"),
|
||||
dubStatus = EnumSet.of(DubStatus.Subbed)
|
||||
)
|
||||
}
|
||||
if (episodes.isNotEmpty()) {
|
||||
items.add(
|
||||
HomePageList(
|
||||
name = request.name,
|
||||
list = episodes,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.isNotEmpty()) {
|
||||
return newHomePageResponse(items)
|
||||
}
|
||||
throw ErrorLoadingException()
|
||||
}
|
||||
|
||||
// Maybe fuzzy match in the future
|
||||
private fun getCloseMatches(sequence: String, items: Collection<String>): List<String> {
|
||||
val a = sequence.trim().lowercase()
|
||||
|
||||
return items.mapNotNull { item ->
|
||||
val b = item.trim().lowercase()
|
||||
if (b.contains(a))
|
||||
item
|
||||
else if (a.contains(b))
|
||||
item
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
||||
private data class CrunchyAnimeData(
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("img") var img: String,
|
||||
@JsonProperty("link") var link: String
|
||||
)
|
||||
|
||||
private data class CrunchyJson(
|
||||
@JsonProperty("data") val data: List<CrunchyAnimeData>,
|
||||
)
|
||||
|
||||
|
||||
override suspend fun search(query: String): ArrayList<SearchResponse> {
|
||||
val json =
|
||||
crUnblock.geoBypassRequest("http://www.crunchyroll.com/ajax/?req=RpcApiSearch_GetSearchCandidates").text.split(
|
||||
"*/"
|
||||
)[0].replace("\\/", "/")
|
||||
val data = parseJson<CrunchyJson>(
|
||||
json.split("\n").mapNotNull { if (!it.startsWith("/")) it else null }.joinToString("\n")
|
||||
).data
|
||||
|
||||
val results = getCloseMatches(query, data.map { it.name })
|
||||
if (results.isEmpty()) return ArrayList()
|
||||
val searchResutls = ArrayList<SearchResponse>()
|
||||
|
||||
var count = 0
|
||||
for (anime in data) {
|
||||
if (count == results.size) {
|
||||
break
|
||||
}
|
||||
if (anime.name == results[count]) {
|
||||
val dubstat =
|
||||
if (anime.name.contains("Dub)", true)) EnumSet.of(DubStatus.Dubbed) else
|
||||
EnumSet.of(DubStatus.Subbed)
|
||||
anime.link = fixUrl(anime.link)
|
||||
anime.img = anime.img.replace("small", "full")
|
||||
searchResutls.add(
|
||||
AnimeSearchResponse(
|
||||
name = anime.name,
|
||||
url = anime.link,
|
||||
apiName = this.name,
|
||||
type = TvType.Anime,
|
||||
posterUrl = anime.img,
|
||||
dubStatus = dubstat,
|
||||
)
|
||||
)
|
||||
++count
|
||||
}
|
||||
}
|
||||
|
||||
return searchResutls
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val soup = Jsoup.parse(crUnblock.geoBypassRequest(url).text)
|
||||
val title = soup.selectFirst("#showview-content-header .ellipsis")?.text()?.trim()
|
||||
val posterU = soup.selectFirst(".poster")?.attr("src")
|
||||
|
||||
val p = soup.selectFirst(".description")
|
||||
var description = p?.selectFirst(".more")?.text()?.trim()
|
||||
if (description.isNullOrBlank()) {
|
||||
description = p?.selectFirst("span")?.text()?.trim()
|
||||
}
|
||||
|
||||
val genres = soup.select(".large-margin-bottom > ul:nth-child(2) li:nth-child(2) a")
|
||||
.map { it.text().capitalize() }
|
||||
val year = genres.filter { it.toIntOrNull() != null }.map { it.toInt() }.sortedBy { it }
|
||||
.getOrNull(0)
|
||||
|
||||
val subEpisodes = mutableListOf<Episode>()
|
||||
val dubEpisodes = mutableListOf<Episode>()
|
||||
val premiumSubEpisodes = mutableListOf<Episode>()
|
||||
val premiumDubEpisodes = mutableListOf<Episode>()
|
||||
soup.select(".season").forEach {
|
||||
val seasonName = it.selectFirst("a.season-dropdown")?.text()?.trim()
|
||||
it.select(".episode").forEach { ep ->
|
||||
val epTitle = ep.selectFirst(".short-desc")?.text()
|
||||
|
||||
val epNum = episodeNumRegex.find(
|
||||
ep.selectFirst("span.ellipsis")?.text().toString()
|
||||
)?.destructured?.component1()
|
||||
var poster = ep.selectFirst("img.landscape")?.attr("data-thumbnailurl")
|
||||
val poster2 = ep.selectFirst("img")?.attr("src")
|
||||
if (poster.isNullOrBlank()) {
|
||||
poster = poster2
|
||||
}
|
||||
|
||||
var epDesc =
|
||||
(if (epNum == null) "" else "Episode $epNum") + (if (!seasonName.isNullOrEmpty()) " - $seasonName" else "")
|
||||
val isPremium = poster?.contains("widestar", ignoreCase = true) ?: false
|
||||
if (isPremium) {
|
||||
epDesc = "★ $epDesc ★"
|
||||
}
|
||||
|
||||
val epi = Episode(
|
||||
fixUrl(ep.attr("href")),
|
||||
"$epTitle",
|
||||
posterUrl = poster?.replace("widestar", "full")?.replace("wide", "full"),
|
||||
description = epDesc
|
||||
)
|
||||
if (isPremium && seasonName != null && (seasonName.contains("Dub") || seasonName.contains(
|
||||
"Russian"
|
||||
) || seasonName.contains("Spanish"))
|
||||
) {
|
||||
premiumDubEpisodes.add(epi)
|
||||
} else if (isPremium) {
|
||||
premiumSubEpisodes.add(epi)
|
||||
} else if (seasonName != null && (seasonName.contains("Dub"))) {
|
||||
dubEpisodes.add(epi)
|
||||
} else {
|
||||
subEpisodes.add(epi)
|
||||
}
|
||||
}
|
||||
}
|
||||
val recommendations =
|
||||
soup.select(".other-series > ul li")?.mapNotNull { element ->
|
||||
val recTitle =
|
||||
element.select("span.ellipsis[dir=auto]").text() ?: return@mapNotNull null
|
||||
val image = element.select("img")?.attr("src")
|
||||
val recUrl = fixUrl(element.select("a").attr("href"))
|
||||
AnimeSearchResponse(
|
||||
recTitle,
|
||||
fixUrl(recUrl),
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
fixUrl(image!!),
|
||||
dubStatus =
|
||||
if (recTitle.contains("(DUB)") || recTitle.contains("Dub")) EnumSet.of(
|
||||
DubStatus.Dubbed
|
||||
) else EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
return newAnimeLoadResponse(title.toString(), url, TvType.Anime) {
|
||||
this.posterUrl = posterU
|
||||
this.engName = title
|
||||
if (subEpisodes.isNotEmpty()) addEpisodes(DubStatus.Subbed, subEpisodes.reversed())
|
||||
if (dubEpisodes.isNotEmpty()) addEpisodes(DubStatus.Dubbed, dubEpisodes.reversed())
|
||||
// TODO add arbitrary seasons
|
||||
|
||||
//if (premiumDubEpisodes.isNotEmpty()) addEpisodes(
|
||||
// DubStatus.PremiumDub,
|
||||
// premiumDubEpisodes.reversed()
|
||||
// )
|
||||
// if (premiumSubEpisodes.isNotEmpty()) addEpisodes(
|
||||
// DubStatus.PremiumSub,
|
||||
// premiumSubEpisodes.reversed()
|
||||
// )
|
||||
this.plot = description
|
||||
this.tags = genres
|
||||
this.year = year
|
||||
this.recommendations = recommendations
|
||||
}
|
||||
}
|
||||
|
||||
data class Subtitles(
|
||||
@JsonProperty("language") val language: String,
|
||||
@JsonProperty("url") val url: String,
|
||||
@JsonProperty("title") val title: String?,
|
||||
@JsonProperty("format") val format: String?
|
||||
)
|
||||
|
||||
data class Streams(
|
||||
@JsonProperty("format") val format: String?,
|
||||
@JsonProperty("audio_lang") val audioLang: String?,
|
||||
@JsonProperty("hardsub_lang") val hardsubLang: String?,
|
||||
@JsonProperty("url") val url: String,
|
||||
@JsonProperty("resolution") val resolution: String?,
|
||||
@JsonProperty("title") var title: String?
|
||||
) {
|
||||
fun title(): String {
|
||||
return when {
|
||||
this.hardsubLang == "enUS" && this.audioLang == "jaJP" -> "Hardsub (English)"
|
||||
this.hardsubLang == "esLA" && this.audioLang == "jaJP" -> "Hardsub (Latino)"
|
||||
this.hardsubLang == "esES" && this.audioLang == "jaJP" -> "Hardsub (Español España)"
|
||||
this.audioLang == "esLA" -> "Latino"
|
||||
this.audioLang == "esES" -> "Español España"
|
||||
this.audioLang == "enUS" -> "English (US)"
|
||||
else -> "RAW"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class KrunchyVideo(
|
||||
@JsonProperty("streams") val streams: List<Streams>,
|
||||
@JsonProperty("subtitles") val subtitles: List<Subtitles>,
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val contentRegex = Regex("""vilos\.config\.media = (\{.+\})""")
|
||||
val response = crUnblock.geoBypassRequest(data)
|
||||
|
||||
val hlsHelper = M3u8Helper()
|
||||
|
||||
val dat = contentRegex.find(response.text)?.destructured?.component1()
|
||||
|
||||
if (!dat.isNullOrEmpty()) {
|
||||
val json = parseJson<KrunchyVideo>(dat)
|
||||
val streams = ArrayList<Streams>()
|
||||
|
||||
for (stream in json.streams) {
|
||||
if (
|
||||
listOf(
|
||||
"adaptive_hls", "adaptive_dash",
|
||||
"multitrack_adaptive_hls_v2",
|
||||
"vo_adaptive_dash", "vo_adaptive_hls",
|
||||
"trailer_hls",
|
||||
).contains(stream.format)
|
||||
) {
|
||||
if (stream.format!!.contains("adaptive") && listOf(
|
||||
"jaJP",
|
||||
"esLA",
|
||||
"esES",
|
||||
"enUS"
|
||||
)
|
||||
.contains(stream.audioLang) && (listOf(
|
||||
"esLA",
|
||||
"esES",
|
||||
"enUS",
|
||||
null
|
||||
).contains(stream.hardsubLang))
|
||||
// && URI(stream.url).path.endsWith(".m3u")
|
||||
) {
|
||||
stream.title = stream.title()
|
||||
streams.add(stream)
|
||||
}
|
||||
// Premium eps
|
||||
else if (stream.format == "trailer_hls" && listOf(
|
||||
"jaJP",
|
||||
"esLA",
|
||||
"esES",
|
||||
"enUS"
|
||||
).contains(stream.audioLang) &&
|
||||
(listOf("esLA", "esES", "enUS", null).contains(stream.hardsubLang))
|
||||
) {
|
||||
stream.title = stream.title()
|
||||
streams.add(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
streams.apmap { stream ->
|
||||
if (stream.url.contains("m3u8") && stream.format!!.contains("adaptive")) {
|
||||
hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(stream.url, null), false)
|
||||
.forEach {
|
||||
callback(
|
||||
ExtractorLink(
|
||||
"Crunchyroll",
|
||||
"Crunchy - ${stream.title}",
|
||||
it.streamUrl,
|
||||
"",
|
||||
getQualityFromName(it.quality.toString()),
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (stream.format == "trailer_hls") {
|
||||
val premiumStream = stream.url
|
||||
.replace("\\/", "/")
|
||||
.replace(Regex("\\/clipFrom.*?index.m3u8"), "").replace("'_,'", "'_'")
|
||||
.replace(stream.url.split("/")[2], "fy.v.vrv.co")
|
||||
callback(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
"Crunchy - ${stream.title} ★",
|
||||
premiumStream,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
false
|
||||
)
|
||||
)
|
||||
} else null
|
||||
}
|
||||
json.subtitles.forEach {
|
||||
val langclean = it.language.replace("esLA", "Spanish")
|
||||
.replace("enUS", "English")
|
||||
.replace("esES", "Spanish (Spain)")
|
||||
subtitleCallback(
|
||||
SubtitleFile(langclean, it.url)
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class CrunchyrollProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(KrunchyProvider())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,270 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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")
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=eja.tv&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,120 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,412 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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",
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,116 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,222 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,174 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,152 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,208 @@
|
|||
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?,
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,195 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,357 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
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"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,263 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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",
|
||||
"Movie",
|
||||
"OVA",
|
||||
)
|
||||
|
||||
iconUrl = "https://www.google.com/s2/favicons?domain=tenshi.moe&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,352 @@
|
|||
package com.lagradost
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.network.DdosGuardKiller
|
||||
import com.lagradost.cloudstream3.network.getHeaders
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import org.jsoup.nodes.Document
|
||||
import java.net.URI
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class TenshiProvider : MainAPI() {
|
||||
companion object {
|
||||
//var token: String? = null
|
||||
//var cookie: Map<String, String> = mapOf()
|
||||
|
||||
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://tenshi.moe"
|
||||
override var name = "Tenshi.moe"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA)
|
||||
private var ddosGuardKiller = DdosGuardKiller(true)
|
||||
|
||||
/*private fun loadToken(): Boolean {
|
||||
return try {
|
||||
val response = get(mainUrl)
|
||||
cookie = response.cookies
|
||||
val document = Jsoup.parse(response.text)
|
||||
token = document.selectFirst("""meta[name="csrf-token"]""").attr("content")
|
||||
token != null
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}*/
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document
|
||||
for (section in soup.select("#content > section")) {
|
||||
try {
|
||||
if (section.attr("id") == "toplist-tabs") {
|
||||
for (top in section.select(".tab-content > [role=\"tabpanel\"]")) {
|
||||
val title = "Top - " + top.attr("id").split("-")[1].replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.UK
|
||||
) else it.toString()
|
||||
}
|
||||
val anime = top.select("li > a").map {
|
||||
AnimeSearchResponse(
|
||||
it.selectFirst(".thumb-title")!!.text(),
|
||||
fixUrl(it.attr("href")),
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
it.selectFirst("img")!!.attr("src"),
|
||||
null,
|
||||
EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
items.add(HomePageList(title, anime))
|
||||
}
|
||||
} else {
|
||||
val title = section.selectFirst("h2")!!.text()
|
||||
val anime = section.select("li > a").map {
|
||||
AnimeSearchResponse(
|
||||
it.selectFirst(".thumb-title")?.text() ?: "",
|
||||
fixUrl(it.attr("href")),
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
it.selectFirst("img")!!.attr("src"),
|
||||
null,
|
||||
EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
items.add(HomePageList(title, anime))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
private fun getIsMovie(type: String, id: Boolean = false): Boolean {
|
||||
if (!id) return type == "Movie"
|
||||
|
||||
val movies = listOf("rrso24fa", "e4hqvtym", "bl5jdbqn", "u4vtznut", "37t6h2r4", "cq4azcrj")
|
||||
val aniId = type.replace("$mainUrl/anime/", "")
|
||||
return movies.contains(aniId)
|
||||
}
|
||||
|
||||
private fun parseSearchPage(soup: Document): List<SearchResponse> {
|
||||
val items = soup.select("ul.thumb > li > a")
|
||||
return items.map {
|
||||
val href = fixUrl(it.attr("href"))
|
||||
val img = fixUrl(it.selectFirst("img")!!.attr("src"))
|
||||
val title = it.attr("title")
|
||||
if (getIsMovie(href, true)) {
|
||||
MovieSearchResponse(
|
||||
title, href, this.name, TvType.Movie, img, null
|
||||
)
|
||||
} else {
|
||||
AnimeSearchResponse(
|
||||
title,
|
||||
href,
|
||||
this.name,
|
||||
TvType.Anime,
|
||||
img,
|
||||
null,
|
||||
EnumSet.of(DubStatus.Subbed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun dateParser(dateString: String?): Date? {
|
||||
if (dateString == null) return null
|
||||
try {
|
||||
val format = SimpleDateFormat("dd 'of' MMM',' yyyy")
|
||||
val data = format.parse(
|
||||
dateString.replace("th ", " ").replace("st ", " ").replace("nd ", " ")
|
||||
.replace("rd ", " ")
|
||||
) ?: return null
|
||||
return data
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// data class TenshiSearchResponse(
|
||||
// @JsonProperty("url") var url : String,
|
||||
// @JsonProperty("title") var title : String,
|
||||
// @JsonProperty("cover") var cover : String,
|
||||
// @JsonProperty("genre") var genre : String,
|
||||
// @JsonProperty("year") var year : Int,
|
||||
// @JsonProperty("type") var type : String,
|
||||
// @JsonProperty("eps") var eps : String,
|
||||
// @JsonProperty("cen") var cen : String
|
||||
// )
|
||||
|
||||
// override suspend fun quickSearch(query: String): ArrayList<SearchResponse>? {
|
||||
// if (!autoLoadToken()) return quickSearch(query)
|
||||
// val url = "$mainUrl/anime/search"
|
||||
// val response = khttp.post(
|
||||
// url,
|
||||
// data=mapOf("q" to query),
|
||||
// headers=mapOf("x-csrf-token" to token, "x-requested-with" to "XMLHttpRequest"),
|
||||
// cookies = cookie
|
||||
//
|
||||
// )
|
||||
//
|
||||
// val items = mapper.readValue<List<TenshiSearchResponse>>(response.text)
|
||||
//
|
||||
// if (items.isEmpty()) return ArrayList()
|
||||
//
|
||||
// val returnValue = ArrayList<SearchResponse>()
|
||||
// for (i in items) {
|
||||
// val href = fixUrl(i.url)
|
||||
// val title = i.title
|
||||
// val img = fixUrl(i.cover)
|
||||
// val year = i.year
|
||||
//
|
||||
// returnValue.add(
|
||||
// if (getIsMovie(i.type)) {
|
||||
// MovieSearchResponse(
|
||||
// title, href, getSlug(href), this.name, TvType.Movie, img, year
|
||||
// )
|
||||
// } else {
|
||||
// AnimeSearchResponse(
|
||||
// title, href, getSlug(href), this.name,
|
||||
// TvType.Anime, img, year, null,
|
||||
// EnumSet.of(DubStatus.Subbed),
|
||||
// null, null
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// return returnValue
|
||||
// }
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/anime"
|
||||
var document = app.get(
|
||||
url,
|
||||
params = mapOf("q" to query),
|
||||
cookies = mapOf("loop-view" to "thumb"),
|
||||
interceptor = ddosGuardKiller
|
||||
).document
|
||||
|
||||
val returnValue = parseSearchPage(document).toMutableList()
|
||||
|
||||
while (!document.select("""a.page-link[rel="next"]""").isEmpty()) {
|
||||
val link = document.selectFirst("""a.page-link[rel="next"]""")?.attr("href")
|
||||
if (!link.isNullOrBlank()) {
|
||||
document = app.get(
|
||||
link,
|
||||
cookies = mapOf("loop-view" to "thumb"),
|
||||
interceptor = ddosGuardKiller
|
||||
).document
|
||||
returnValue.addAll(parseSearchPage(document))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
var document = app.get(
|
||||
url,
|
||||
cookies = mapOf("loop-view" to "thumb"),
|
||||
interceptor = ddosGuardKiller
|
||||
).document
|
||||
|
||||
val canonicalTitle = document.selectFirst("header.entry-header > h1.mb-3")!!.text().trim()
|
||||
val episodeNodes = document.select("li[class*=\"episode\"] > a").toMutableList()
|
||||
val totalEpisodePages = if (document.select(".pagination").size > 0)
|
||||
document.select(".pagination .page-item a.page-link:not([rel])").last()!!.text()
|
||||
.toIntOrNull()
|
||||
else 1
|
||||
|
||||
if (totalEpisodePages != null && totalEpisodePages > 1) {
|
||||
for (pageNum in 2..totalEpisodePages) {
|
||||
document = app.get(
|
||||
"$url?page=$pageNum",
|
||||
cookies = mapOf("loop-view" to "thumb"),
|
||||
interceptor = ddosGuardKiller
|
||||
).document
|
||||
episodeNodes.addAll(document.select("li[class*=\"episode\"] > a"))
|
||||
}
|
||||
}
|
||||
|
||||
val episodes = ArrayList(episodeNodes.map {
|
||||
val title = it.selectFirst(".episode-title")?.text()?.trim()
|
||||
newEpisode(it.attr("href")) {
|
||||
this.name = if (title == "No Title") null else title
|
||||
this.posterUrl = it.selectFirst("img")?.attr("src")
|
||||
addDate(dateParser(it?.selectFirst(".episode-date")?.text()?.trim()))
|
||||
this.description = it.attr("data-content").trim()
|
||||
}
|
||||
})
|
||||
|
||||
val similarAnime = document.select("ul.anime-loop > li > a").mapNotNull { element ->
|
||||
val href = element.attr("href") ?: return@mapNotNull null
|
||||
val title =
|
||||
element.selectFirst("> .overlay > .thumb-title")?.text() ?: return@mapNotNull null
|
||||
val img = element.selectFirst("> img")?.attr("src")
|
||||
AnimeSearchResponse(title, href, this.name, TvType.Anime, img)
|
||||
}
|
||||
|
||||
val type = document.selectFirst("a[href*=\"$mainUrl/type/\"]")?.text()?.trim()
|
||||
|
||||
return newAnimeLoadResponse(canonicalTitle, url, getType(type ?: "")) {
|
||||
recommendations = similarAnime
|
||||
posterUrl = document.selectFirst("img.cover-image")?.attr("src")
|
||||
plot = document.selectFirst(".entry-description > .card-body")?.text()?.trim()
|
||||
tags =
|
||||
document.select("li.genre.meta-data > span.value")
|
||||
.map { it?.text()?.trim().toString() }
|
||||
|
||||
synonyms =
|
||||
document.select("li.synonym.meta-data > div.info-box > span.value")
|
||||
.map { it?.text()?.trim().toString() }
|
||||
|
||||
engName =
|
||||
document.selectFirst("span.value > span[title=\"English\"]")?.parent()?.text()
|
||||
?.trim()
|
||||
japName =
|
||||
document.selectFirst("span.value > span[title=\"Japanese\"]")?.parent()?.text()
|
||||
?.trim()
|
||||
|
||||
val pattern = Regex("(\\d{4})")
|
||||
val yearText = document.selectFirst("li.release-date .value")!!.text()
|
||||
year = pattern.find(yearText)?.groupValues?.get(1)?.toIntOrNull()
|
||||
|
||||
addEpisodes(DubStatus.Subbed, episodes)
|
||||
|
||||
showStatus = when (document.selectFirst("li.status > .value")?.text()?.trim()) {
|
||||
"Ongoing" -> ShowStatus.Ongoing
|
||||
"Completed" -> ShowStatus.Completed
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val soup = app.get(data, interceptor = ddosGuardKiller).document
|
||||
|
||||
data class Quality(
|
||||
@JsonProperty("src") val src: String,
|
||||
@JsonProperty("size") val size: Int
|
||||
)
|
||||
|
||||
for (source in soup.select("""[aria-labelledby="mirror-dropdown"] > li > a.dropdown-item""")) {
|
||||
val release = source.text().replace("/", "").trim()
|
||||
val sourceHTML = app.get(
|
||||
"https://tenshi.moe/embed?v=${source.attr("href").split("v=")[1].split("&")[0]}",
|
||||
headers = mapOf("Referer" to data), interceptor = ddosGuardKiller
|
||||
).text
|
||||
|
||||
val match = Regex("""sources: (\[(?:.|\s)+?type: ['"]video/.*?['"](?:.|\s)+?])""").find(
|
||||
sourceHTML
|
||||
)
|
||||
if (match != null) {
|
||||
val qualities = parseJson<List<Quality>>(
|
||||
match.destructured.component1()
|
||||
.replace("'", "\"")
|
||||
.replace(Regex("""(\w+): """), "\"\$1\": ")
|
||||
.replace(Regex("""\s+"""), "")
|
||||
.replace(",}", "}")
|
||||
.replace(",]", "]")
|
||||
)
|
||||
qualities.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
"${this.name} $release",
|
||||
fixUrl(it.src),
|
||||
this.mainUrl,
|
||||
getQualityFromName("${it.size}"),
|
||||
headers = getHeaders(emptyMap(),
|
||||
ddosGuardKiller.savedCookiesMap[URI(this.mainUrl).host]
|
||||
?: emptyMap()
|
||||
).toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class TenshiProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(TenshiProvider())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=theflix.to&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,602 @@
|
|||
package com.lagradost
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
class TheFlixToProvider : MainAPI() {
|
||||
companion object {
|
||||
var latestCookies: Map<String, String> = emptyMap()
|
||||
}
|
||||
|
||||
override var name = "TheFlix.to"
|
||||
override var mainUrl = "https://theflix.to"
|
||||
override val instantLinkLoading = false
|
||||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
|
||||
|
||||
|
||||
data class HomeJson(
|
||||
@JsonProperty("props") val props: HomeProps = HomeProps(),
|
||||
)
|
||||
|
||||
data class HomeProps(
|
||||
@JsonProperty("pageProps") val pageProps: PageProps = PageProps(),
|
||||
)
|
||||
|
||||
data class PageProps(
|
||||
@JsonProperty("moviesListTrending") val moviesListTrending: MoviesListTrending = MoviesListTrending(),
|
||||
@JsonProperty("moviesListNewArrivals") val moviesListNewArrivals: MoviesListNewArrivals = MoviesListNewArrivals(),
|
||||
@JsonProperty("tvsListTrending") val tvsListTrending: TvsListTrending = TvsListTrending(),
|
||||
@JsonProperty("tvsListNewEpisodes") val tvsListNewEpisodes: TvsListNewEpisodes = TvsListNewEpisodes(),
|
||||
)
|
||||
|
||||
|
||||
data class MoviesListTrending(
|
||||
@JsonProperty("docs") val docs: ArrayList<Docs> = arrayListOf(),
|
||||
@JsonProperty("total") val total: Int? = null,
|
||||
@JsonProperty("page") val page: Int? = null,
|
||||
@JsonProperty("limit") val limit: Int? = null,
|
||||
@JsonProperty("pages") val pages: Int? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
)
|
||||
|
||||
data class MoviesListNewArrivals(
|
||||
@JsonProperty("docs") val docs: ArrayList<Docs> = arrayListOf(),
|
||||
@JsonProperty("total") val total: Int? = null,
|
||||
@JsonProperty("page") val page: Int? = null,
|
||||
@JsonProperty("limit") val limit: Int? = null,
|
||||
@JsonProperty("pages") val pages: Int? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
)
|
||||
|
||||
data class TvsListTrending(
|
||||
@JsonProperty("docs") val docs: ArrayList<Docs> = arrayListOf(),
|
||||
@JsonProperty("total") val total: Int? = null,
|
||||
@JsonProperty("page") val page: Int? = null,
|
||||
@JsonProperty("limit") val limit: Int? = null,
|
||||
@JsonProperty("pages") val pages: Int? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
)
|
||||
|
||||
data class TvsListNewEpisodes(
|
||||
@JsonProperty("docs") val docs: ArrayList<Docs> = arrayListOf(),
|
||||
@JsonProperty("total") val total: Int? = null,
|
||||
@JsonProperty("page") val page: Int? = null,
|
||||
@JsonProperty("limit") val limit: Int? = null,
|
||||
@JsonProperty("pages") val pages: Int? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
)
|
||||
|
||||
data class Docs(
|
||||
@JsonProperty("name") val name: String = String(),
|
||||
@JsonProperty("originalLanguage") val originalLanguage: String? = null,
|
||||
@JsonProperty("popularity") val popularity: Double? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("status") val status: String? = null,
|
||||
@JsonProperty("voteAverage") val voteAverage: Double? = null,
|
||||
@JsonProperty("voteCount") val voteCount: Int? = null,
|
||||
@JsonProperty("cast") val cast: String? = null,
|
||||
@JsonProperty("director") val director: String? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("posterUrl") val posterUrl: String? = null,
|
||||
@JsonProperty("releaseDate") val releaseDate: String? = null,
|
||||
@JsonProperty("createdAt") val createdAt: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("conversionDate") val conversionDate: String? = null,
|
||||
@JsonProperty("id") val id: Int? = null,
|
||||
@JsonProperty("available") val available: Boolean? = null,
|
||||
@JsonProperty("videos" ) val videos : ArrayList<String>? = arrayListOf(),
|
||||
)
|
||||
|
||||
|
||||
private suspend fun getCookies(): Map<String, String> {
|
||||
// val cookieResponse = app.post(
|
||||
// "https://theflix.to:5679/authorization/session/continue?contentUsageType=Viewing",
|
||||
// headers = mapOf(
|
||||
// "Host" to "theflix.to:5679",
|
||||
// "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0",
|
||||
// "Accept" to "application/json, text/plain,"
|
||||
// "Accept-Language" to "en-US,en;q=0.5",
|
||||
// "Content-Type" to "application/json;charset=utf-8",
|
||||
// "Content-Length" to "35",
|
||||
// "Origin" to "https://theflix.to",
|
||||
// "DNT" to "1",
|
||||
// "Connection" to "keep-alive",
|
||||
// "Referer" to "https://theflix.to/",
|
||||
// "Sec-Fetch-Dest" to "empty",
|
||||
// "Sec-Fetch-Mode" to "cors",
|
||||
// "Sec-Fetch-Site" to "same-site",)).okhttpResponse.headers.values("Set-Cookie")
|
||||
|
||||
val cookies = app.post(
|
||||
"$mainUrl:5679/authorization/session/continue?contentUsageType=Viewing",
|
||||
headers = mapOf(
|
||||
"Host" to "theflix.to:5679",
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Accept" to "application/json, text/plain, */*",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
"Content-Type" to "application/json;charset=utf-8",
|
||||
"Content-Length" to "35",
|
||||
"Origin" to mainUrl,
|
||||
"DNT" to "1",
|
||||
"Connection" to "keep-alive",
|
||||
"Referer" to mainUrl,
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "same-site",)
|
||||
).cookies
|
||||
/* val cookieRegex = Regex("(theflix\\..*?id\\=[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
|
||||
val findcookie = cookieRegex.findAll(cookieResponse.toString()).map { it.value }.toList()
|
||||
val cookiesstring = findcookie.toString().replace(", ","; ").replace("[","").replace("]","")
|
||||
val cookiesmap = mapOf("Cookie" to cookiesstring) */
|
||||
latestCookies = cookies
|
||||
return latestCookies
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val doc = app.get(mainUrl).document
|
||||
val scriptText = doc.selectFirst("script[type=application/json]")!!.data()
|
||||
if (scriptText.contains("moviesListTrending")) {
|
||||
val json = parseJson<HomeJson>(scriptText)
|
||||
val homePageProps = json.props.pageProps
|
||||
listOf(
|
||||
Triple(
|
||||
homePageProps.moviesListNewArrivals.docs,
|
||||
homePageProps.moviesListNewArrivals.type,
|
||||
"New Movie arrivals"
|
||||
),
|
||||
Triple(
|
||||
homePageProps.moviesListTrending.docs,
|
||||
homePageProps.moviesListTrending.type,
|
||||
"Trending Movies"
|
||||
),
|
||||
Triple(
|
||||
homePageProps.tvsListTrending.docs,
|
||||
homePageProps.tvsListTrending.type,
|
||||
"Trending TV Series"
|
||||
),
|
||||
Triple(
|
||||
homePageProps.tvsListNewEpisodes.docs,
|
||||
homePageProps.tvsListNewEpisodes.type,
|
||||
"New Episodes"
|
||||
)
|
||||
).map { (docs, type, homename) ->
|
||||
val home = docs.map { info ->
|
||||
val title = info.name
|
||||
val poster = info.posterUrl
|
||||
val typeinfo =
|
||||
if (type?.contains("TV") == true) TvType.TvSeries else TvType.Movie
|
||||
val link =
|
||||
if (typeinfo == TvType.Movie) "$mainUrl/movie/${info.id}-${cleanTitle(title)}"
|
||||
else "$mainUrl/tv-show/${info.id}-${cleanTitle(title).replace("?","")}/season-1/episode-1"
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
typeinfo,
|
||||
poster,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
items.add(HomePageList(homename, home))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
data class SearchJson(
|
||||
@JsonProperty("props") val props: SearchProps = SearchProps(),
|
||||
)
|
||||
|
||||
data class SearchProps(
|
||||
@JsonProperty("pageProps") val pageProps: SearchPageProps = SearchPageProps(),
|
||||
)
|
||||
|
||||
data class SearchPageProps(
|
||||
@JsonProperty("mainList") val mainList: SearchMainList = SearchMainList(),
|
||||
)
|
||||
|
||||
data class SearchMainList(
|
||||
@JsonProperty("docs") val docs: ArrayList<Docs> = arrayListOf(),
|
||||
@JsonProperty("total") val total: Int? = null,
|
||||
@JsonProperty("page") val page: Int? = null,
|
||||
@JsonProperty("limit") val limit: Int? = null,
|
||||
@JsonProperty("pages") val pages: Int? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
)
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val search = ArrayList<SearchResponse>()
|
||||
val urls = listOf(
|
||||
"$mainUrl/movies/trending?search=$query",
|
||||
"$mainUrl/tv-shows/trending?search=$query"
|
||||
)
|
||||
urls.apmap { url ->
|
||||
val doc = app.get(url).document
|
||||
val scriptText = doc.selectFirst("script[type=application/json]")!!.data()
|
||||
if (scriptText.contains("pageProps")) {
|
||||
val json = parseJson<SearchJson>(scriptText)
|
||||
val searchPageProps = json.props.pageProps.mainList
|
||||
val pair = listOf(Pair(searchPageProps.docs, searchPageProps.type))
|
||||
pair.map { (docs, type) ->
|
||||
docs.map { info ->
|
||||
val title = info.name
|
||||
val poster = info.posterUrl
|
||||
val typeinfo =
|
||||
if (type?.contains("TV") == true) TvType.TvSeries else TvType.Movie
|
||||
val link = if (typeinfo == TvType.Movie) "$mainUrl/movie/${info.id}-${
|
||||
cleanTitle(title)
|
||||
}"
|
||||
else "$mainUrl/tv-show/${info.id}-${cleanTitle(title)}/season-1/episode-1"
|
||||
if (typeinfo == TvType.Movie) {
|
||||
search.add(
|
||||
MovieSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
poster,
|
||||
null
|
||||
)
|
||||
)
|
||||
} else {
|
||||
search.add(
|
||||
TvSeriesSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
poster,
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return search
|
||||
}
|
||||
data class LoadMain (
|
||||
@JsonProperty("props" ) val props : LoadProps? = LoadProps(),
|
||||
@JsonProperty("page" ) val page : String? = null,
|
||||
@JsonProperty("buildId" ) val buildId : String? = null,
|
||||
@JsonProperty("runtimeConfig" ) val runtimeConfig : RuntimeConfig? = RuntimeConfig(),
|
||||
@JsonProperty("isFallback" ) val isFallback : Boolean? = null,
|
||||
@JsonProperty("gssp" ) val gssp : Boolean? = null,
|
||||
@JsonProperty("customServer" ) val customServer : Boolean? = null,
|
||||
@JsonProperty("appGip" ) val appGip : Boolean? = null
|
||||
)
|
||||
|
||||
data class LoadProps (
|
||||
@JsonProperty("pageProps" ) val pageProps : LoadPageProps? = LoadPageProps(),
|
||||
@JsonProperty("__N_SSP" ) val _NSSP : Boolean? = null
|
||||
)
|
||||
|
||||
data class LoadPageProps (
|
||||
@JsonProperty("selectedTv" ) val selectedTv : TheFlixMetadata? = TheFlixMetadata(),
|
||||
@JsonProperty("movie") val movie: TheFlixMetadata? = TheFlixMetadata(),
|
||||
@JsonProperty("recommendationsList" ) val recommendationsList : RecommendationsList? = RecommendationsList(),
|
||||
@JsonProperty("basePageSegments" ) val basePageSegments : ArrayList<String>? = arrayListOf()
|
||||
)
|
||||
|
||||
data class TheFlixMetadata (
|
||||
@JsonProperty("episodeRuntime" ) val episodeRuntime : Int? = null,
|
||||
@JsonProperty("name" ) val name : String? = null,
|
||||
@JsonProperty("numberOfSeasons" ) val numberOfSeasons : Int? = null,
|
||||
@JsonProperty("numberOfEpisodes" ) val numberOfEpisodes : Int? = null,
|
||||
@JsonProperty("originalLanguage" ) val originalLanguage : String? = null,
|
||||
@JsonProperty("popularity" ) val popularity : Double? = null,
|
||||
@JsonProperty("status" ) val status : String? = null,
|
||||
@JsonProperty("voteAverage" ) val voteAverage : Double? = null,
|
||||
@JsonProperty("voteCount" ) val voteCount : Int? = null,
|
||||
@JsonProperty("cast" ) val cast : String? = null,
|
||||
@JsonProperty("director" ) val director : String? = null,
|
||||
@JsonProperty("overview" ) val overview : String? = null,
|
||||
@JsonProperty("posterUrl" ) val posterUrl : String? = null,
|
||||
@JsonProperty("releaseDate" ) val releaseDate : String? = null,
|
||||
@JsonProperty("createdAt" ) val createdAt : String? = null,
|
||||
@JsonProperty("updatedAt" ) val updatedAt : String? = null,
|
||||
@JsonProperty("id" ) val id : Int? = null,
|
||||
@JsonProperty("available" ) val available : Boolean? = null,
|
||||
@JsonProperty("genres" ) val genres : ArrayList<Genres>? = arrayListOf(),
|
||||
@JsonProperty("seasons" ) val seasons : ArrayList<Seasons>? = arrayListOf(),
|
||||
@JsonProperty("videos" ) val videos : ArrayList<String>? = arrayListOf(),
|
||||
@JsonProperty("runtime" ) val runtime : Int? = null,
|
||||
)
|
||||
data class Seasons(
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("numberOfEpisodes") val numberOfEpisodes: Int? = null,
|
||||
@JsonProperty("seasonNumber") val seasonNumber: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("posterUrl") val posterUrl: String? = null,
|
||||
@JsonProperty("releaseDate") val releaseDate: String? = null,
|
||||
@JsonProperty("createdAt") val createdAt: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("id") val id: Int? = null,
|
||||
@JsonProperty("episodes") val episodes: ArrayList<Episodes>? = arrayListOf()
|
||||
)
|
||||
|
||||
data class Episodes(
|
||||
@JsonProperty("episodeNumber") val episodeNumber: Int? = null,
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("seasonNumber") val seasonNumber: Int? = null,
|
||||
@JsonProperty("voteAverage") val voteAverage: Double? = null,
|
||||
@JsonProperty("voteCount") val voteCount: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("releaseDate") val releaseDate: String? = null,
|
||||
@JsonProperty("createdAt") val createdAt: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("id") val id: Int? = null,
|
||||
@JsonProperty("videos") val videos: ArrayList<String>? = arrayListOf()
|
||||
)
|
||||
|
||||
|
||||
data class Genres (
|
||||
@JsonProperty("name" ) val name : String? = null,
|
||||
@JsonProperty("id" ) val id : Int? = null
|
||||
)
|
||||
|
||||
data class RuntimeConfig (
|
||||
@JsonProperty("AddThisService" ) val AddThisService : RuntimeConfigData? = RuntimeConfigData(),
|
||||
@JsonProperty("Application" ) val Application : RuntimeConfigData? = RuntimeConfigData(),
|
||||
@JsonProperty("GtmService" ) val GtmService : RuntimeConfigData? = RuntimeConfigData(),
|
||||
@JsonProperty("Services" ) val Services : RuntimeConfigData? = RuntimeConfigData(),
|
||||
)
|
||||
|
||||
data class RuntimeConfigData(
|
||||
@JsonProperty("PublicId" ) val PublicId : String? = null,
|
||||
@JsonProperty("ContentUsageType" ) val ContentUsageType : String? = null,
|
||||
@JsonProperty("IsDevelopmentMode" ) val IsDevelopmentMode : Boolean? = null,
|
||||
@JsonProperty("IsDevelopmentOrProductionMode" ) val IsDevelopmentOrProductionMode : Boolean? = null,
|
||||
@JsonProperty("IsProductionMode" ) val IsProductionMode : Boolean? = null,
|
||||
@JsonProperty("IsStagingMode" ) val IsStagingMode : Boolean? = null,
|
||||
@JsonProperty("IsTestMode" ) val IsTestMode : Boolean? = null,
|
||||
@JsonProperty("Mode" ) val Mode : String? = null,
|
||||
@JsonProperty("Name" ) val Name : String? = null,
|
||||
@JsonProperty("Url" ) val Url : String? = null,
|
||||
@JsonProperty("UseFilterInfoInUrl" ) val UseFilterInfoInUrl : Boolean? = null,
|
||||
@JsonProperty("TrackingId" ) val TrackingId : String? = null,
|
||||
@JsonProperty("Server" ) val Server : Server? = Server(),
|
||||
@JsonProperty("TmdbServer" ) val TmdbServer : TmdbServer? = TmdbServer(),
|
||||
)
|
||||
|
||||
data class TmdbServer (
|
||||
@JsonProperty("Url" ) val Url : String? = null
|
||||
)
|
||||
|
||||
|
||||
data class Server (
|
||||
@JsonProperty("Url" ) val Url : String? = null
|
||||
)
|
||||
|
||||
data class RecommendationsList (
|
||||
@JsonProperty("docs" ) val docs : ArrayList<Docs> = arrayListOf(),
|
||||
@JsonProperty("total" ) val total : Int? = null,
|
||||
@JsonProperty("page" ) val page : Int? = null,
|
||||
@JsonProperty("limit" ) val limit : Int? = null,
|
||||
@JsonProperty("pages" ) val pages : Int? = null,
|
||||
@JsonProperty("type" ) val type : String? = null,
|
||||
)
|
||||
|
||||
private fun cleanTitle(title: String): String {
|
||||
val dotTitle = title.substringBefore("/season")
|
||||
if (dotTitle.contains(Regex("\\..\\."))) { //For titles containing more than two dots (S.W.A.T.)
|
||||
return (dotTitle.removeSuffix(".")
|
||||
.replace(" - ", "-")
|
||||
.replace(".", "-").replace(" ", "-")
|
||||
.replace("-&", "")
|
||||
.replace(Regex("(:|-&)"), "")
|
||||
.replace("'", "-")).lowercase()
|
||||
}
|
||||
return (title
|
||||
.replace(" - ", "-")
|
||||
.replace(" ", "-")
|
||||
.replace("-&", "")
|
||||
.replace("/", "-")
|
||||
.replace(Regex("(:|-&|\\.)"), "")
|
||||
.replace("'", "-")).lowercase()
|
||||
}
|
||||
|
||||
private suspend fun getLoadMan(url: String): LoadMain {
|
||||
getCookies()
|
||||
val og = app.get(url, headers = latestCookies)
|
||||
val soup = og.document
|
||||
val script = soup.selectFirst("script[type=application/json]")!!.data()
|
||||
return parseJson(script)
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val tvtype = if (url.contains("movie")) TvType.Movie else TvType.TvSeries
|
||||
val json = getLoadMan(url)
|
||||
val episodes = ArrayList<Episode>()
|
||||
val isMovie = tvtype == TvType.Movie
|
||||
val pageMain = json.props?.pageProps
|
||||
|
||||
val metadata: TheFlixMetadata? = if (isMovie) pageMain?.movie else pageMain?.selectedTv
|
||||
|
||||
val available = metadata?.available
|
||||
|
||||
val comingsoon = !available!!
|
||||
|
||||
val movieId = metadata.id
|
||||
|
||||
val movietitle = metadata.name
|
||||
|
||||
val poster = metadata.posterUrl
|
||||
|
||||
val description = metadata.overview
|
||||
|
||||
if (!isMovie) {
|
||||
metadata.seasons?.map { seasons ->
|
||||
val seasonPoster = seasons.posterUrl ?: metadata.posterUrl
|
||||
seasons.episodes?.forEach { epi ->
|
||||
val episodenu = epi.episodeNumber
|
||||
val seasonum = epi.seasonNumber
|
||||
val title = epi.name
|
||||
val epDesc = epi.overview
|
||||
val test = epi.videos
|
||||
val ratinginfo = (epi.voteAverage)?.times(10)?.toInt()
|
||||
val rating = if (ratinginfo?.equals(0) == true) null else ratinginfo
|
||||
val eps = Episode(
|
||||
"$mainUrl/tv-show/$movieId-${cleanTitle(movietitle!!)}/season-$seasonum/episode-$episodenu",
|
||||
title,
|
||||
seasonum,
|
||||
episodenu,
|
||||
description = epDesc!!,
|
||||
posterUrl = seasonPoster,
|
||||
rating = rating,
|
||||
)
|
||||
if (test!!.isNotEmpty()) {
|
||||
episodes.add(eps)
|
||||
} else {
|
||||
//Nothing, will prevent seasons/episodes with no videos to be added
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val rating = metadata.voteAverage?.toFloat()?.times(1000)?.toInt()
|
||||
|
||||
val tags = metadata.genres?.mapNotNull { it.name }
|
||||
|
||||
val recommendationsitem = pageMain?.recommendationsList?.docs?.map { loadDocs ->
|
||||
val title = loadDocs.name
|
||||
val posterrec = loadDocs.posterUrl
|
||||
val link = if (isMovie) "$mainUrl/movie/${loadDocs.id}-${cleanTitle(title)}"
|
||||
else "$mainUrl/tv-show/${loadDocs.id}-${cleanTitle(title)}/season-1/episode-1"
|
||||
MovieSearchResponse(
|
||||
title,
|
||||
link,
|
||||
this.name,
|
||||
tvtype,
|
||||
posterrec,
|
||||
year = null
|
||||
)
|
||||
}
|
||||
|
||||
val year = metadata.releaseDate?.substringBefore("-")
|
||||
|
||||
val runtime = metadata.runtime?.div(60) ?: metadata.episodeRuntime?.div(60)
|
||||
val cast = metadata.cast?.split(",")
|
||||
|
||||
return when (tvtype) {
|
||||
TvType.TvSeries -> {
|
||||
return newTvSeriesLoadResponse(movietitle!!, url, TvType.TvSeries, episodes) {
|
||||
this.posterUrl = poster
|
||||
this.year = year?.toIntOrNull()
|
||||
this.plot = description
|
||||
this.duration = runtime
|
||||
addActors(cast)
|
||||
this.tags = tags
|
||||
this.recommendations = recommendationsitem
|
||||
this.comingSoon = comingsoon
|
||||
this.rating = rating
|
||||
}
|
||||
}
|
||||
TvType.Movie -> {
|
||||
newMovieLoadResponse(movietitle!!, url, TvType.Movie, url) {
|
||||
this.year = year?.toIntOrNull()
|
||||
this.posterUrl = poster
|
||||
this.plot = description
|
||||
this.duration = runtime
|
||||
addActors(cast)
|
||||
this.tags = tags
|
||||
this.recommendations = recommendationsitem
|
||||
this.comingSoon = comingsoon
|
||||
this.rating = rating
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class VideoData (
|
||||
@JsonProperty("url" ) val url : String? = null,
|
||||
@JsonProperty("id" ) val id : String? = null,
|
||||
@JsonProperty("type" ) val type : String? = null,
|
||||
)
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
val json = getLoadMan(data)
|
||||
val authhost = json.runtimeConfig?.Services?.Server?.Url
|
||||
val isMovie = data.contains("/movie/")
|
||||
val qualityReg = Regex("(\\d+p)")
|
||||
if (isMovie){
|
||||
json.props?.pageProps?.movie?.videos?.apmap { id ->
|
||||
val jsonmovie = app.get("$authhost/movies/videos/$id/request-access?contentUsageType=Viewing",
|
||||
headers = latestCookies).parsedSafe<VideoData>() ?: return@apmap false
|
||||
val extractedlink = jsonmovie.url
|
||||
if (!extractedlink.isNullOrEmpty()) {
|
||||
val quality = qualityReg.find(extractedlink)?.value ?: ""
|
||||
callback(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
extractedlink,
|
||||
"",
|
||||
getQualityFromName(quality),
|
||||
false
|
||||
)
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
val dataRegex = Regex("(season-(\\d+)\\/episode-(\\d+))")
|
||||
val cleandatainfo = dataRegex.find(data)?.value?.replace(Regex("(season-|episode-)"),"")?.replace("/","x")
|
||||
val tesatt = cleandatainfo.let { str ->
|
||||
str?.split("x")?.mapNotNull { subStr -> subStr.toIntOrNull() }
|
||||
}
|
||||
val epID = tesatt?.getOrNull(1)
|
||||
val seasonid = tesatt?.getOrNull(0)
|
||||
json.props?.pageProps?.selectedTv?.seasons?.map {
|
||||
it.episodes?.map {
|
||||
val epsInfo = Triple(it.seasonNumber, it.episodeNumber, it.videos)
|
||||
if (epsInfo.first == seasonid && epsInfo.second == epID) {
|
||||
epsInfo.third?.apmap { id ->
|
||||
val jsonserie = app.get("$authhost/tv/videos/$id/request-access?contentUsageType=Viewing", headers = latestCookies).parsedSafe<VideoData>() ?: return@apmap false
|
||||
val extractedlink = jsonserie.url
|
||||
if (!extractedlink.isNullOrEmpty()) {
|
||||
val quality = qualityReg.find(extractedlink)?.value ?: ""
|
||||
callback(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
extractedlink,
|
||||
"",
|
||||
getQualityFromName(quality),
|
||||
false
|
||||
)
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class TheFlixToProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(TheFlixToProvider())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// 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=www.vmovee.watch&sz=%size%"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,125 @@
|
|||
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.getQualityFromName
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class VMoveeProvider : MainAPI() {
|
||||
override var name = "VMovee"
|
||||
override var mainUrl = "https://www.vmovee.watch"
|
||||
|
||||
override val supportedTypes = setOf(TvType.Movie)
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/?s=$query"
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
val searchItems = document.select("div.search-page > div.result-item > article")
|
||||
if (searchItems.size == 0) return ArrayList()
|
||||
val returnValue = ArrayList<SearchResponse>()
|
||||
for (item in searchItems) {
|
||||
val details = item.selectFirst("> div.details")
|
||||
val imgHolder = item.selectFirst("> div.image > div.thumbnail > a")
|
||||
// val href = imgHolder.attr("href")
|
||||
val poster = imgHolder!!.selectFirst("> img")!!.attr("data-lazy-src")
|
||||
val isTV = imgHolder.selectFirst("> span")!!.text() == "TV"
|
||||
if (isTV) continue // no TV support yet
|
||||
|
||||
val titleHolder = details!!.selectFirst("> div.title > a")
|
||||
val title = titleHolder!!.text()
|
||||
val href = titleHolder.attr("href")
|
||||
val meta = details.selectFirst("> div.meta")
|
||||
val year = meta!!.selectFirst("> span.year")!!.text().toIntOrNull()
|
||||
// val rating = parseRating(meta.selectFirst("> span.rating").text().replace("IMDb ", ""))
|
||||
// val descript = details.selectFirst("> div.contenido").text()
|
||||
returnValue.add(
|
||||
if (isTV) TvSeriesSearchResponse(title, href, this.name, TvType.TvSeries, poster, year, null)
|
||||
else MovieSearchResponse(title, href, this.name, TvType.Movie, poster, year)
|
||||
)
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
data class LoadLinksAjax(
|
||||
@JsonProperty("embed_url")
|
||||
val embedUrl: String,
|
||||
)
|
||||
|
||||
data class ReeoovAPIData(
|
||||
@JsonProperty("file")
|
||||
val file: String,
|
||||
@JsonProperty("label")
|
||||
val label: String,
|
||||
)
|
||||
|
||||
data class ReeoovAPI(
|
||||
@JsonProperty("data")
|
||||
val data: List<ReeoovAPIData>,
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
|
||||
val url = "$mainUrl/dashboard/admin-ajax.php"
|
||||
val post =
|
||||
app.post(
|
||||
url,
|
||||
headers = mapOf("referer" to url),
|
||||
data = mapOf("action" to "doo_player_ajax", "post" to data, "nume" to "2", "type" to "movie")
|
||||
).text
|
||||
|
||||
val ajax = parseJson<LoadLinksAjax>(post)
|
||||
var realUrl = ajax.embedUrl
|
||||
if (realUrl.startsWith("//")) {
|
||||
realUrl = "https:$realUrl"
|
||||
}
|
||||
|
||||
val request = app.get(realUrl)
|
||||
val prefix = "https://reeoov.tube/v/"
|
||||
if (request.url.startsWith(prefix)) {
|
||||
val apiUrl = "https://reeoov.tube/api/source/${request.url.removePrefix(prefix)}"
|
||||
val apiResponse = app.post(
|
||||
apiUrl,
|
||||
headers = mapOf("Referer" to request.url),
|
||||
data = mapOf("r" to "https://www.vmovee.watch/", "d" to "reeoov.tube")
|
||||
).text
|
||||
val apiData = parseJson<ReeoovAPI>(apiResponse)
|
||||
for (d in apiData.data) {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name + " " + d.label,
|
||||
d.file,
|
||||
"https://reeoov.tube/",
|
||||
getQualityFromName(d.label),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val response = app.get(url).text
|
||||
val document = Jsoup.parse(response)
|
||||
|
||||
val sheader = document.selectFirst("div.sheader")
|
||||
|
||||
val poster = sheader!!.selectFirst("> div.poster > img")!!.attr("data-lazy-src")
|
||||
val data = sheader.selectFirst("> div.data")
|
||||
val title = data!!.selectFirst("> h1")!!.text()
|
||||
val descript = document.selectFirst("div#info > div")!!.text()
|
||||
val id = document.select("div.starstruck").attr("data-id")
|
||||
|
||||
return MovieLoadResponse(title, url, this.name, TvType.Movie, id, poster, null, descript, null, null)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
package com.lagradost
|
||||
|
||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import android.content.Context
|
||||
|
||||
@CloudstreamPlugin
|
||||
class VMoveeProviderPlugin: Plugin() {
|
||||
override fun load(context: Context) {
|
||||
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||
registerMainAPI(VMoveeProvider())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// use an integer for version numbers
|
||||
version = 1
|
||||
|
||||
|
||||
cloudstream {
|
||||
language = "en"
|
||||
// All of these properties are optional, you can safely remove them
|
||||
|
||||
description = "Includes many providers with the same layout as Vidstream"
|
||||
// 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",
|
||||
)
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue