mirror of
https://github.com/recloudstream/cloudstream-extensions.git
synced 2024-08-15 03:03:54 +00:00
Add anime providers
This commit is contained in:
parent
b74b8614db
commit
b3f33b368a
139 changed files with 9873 additions and 9 deletions
22
AA_BLANK/build.gradle.kts
Normal file
22
AA_BLANK/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AA_BLANK/src/main/AndroidManifest.xml
Normal file
2
AA_BLANK/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
22
AllAnimeProvider/build.gradle.kts
Normal file
22
AllAnimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AllAnimeProvider/src/main/AndroidManifest.xml
Normal file
2
AllAnimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
AniPlayProvider/build.gradle.kts
Normal file
22
AniPlayProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AniPlayProvider/src/main/AndroidManifest.xml
Normal file
2
AniPlayProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
215
AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProvider.kt
Normal file
215
AniPlayProvider/src/main/kotlin/com/lagradost/AniPlayProvider.kt
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
class AniPlayProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://aniplay.it"
|
||||||
|
override var name = "AniPlay"
|
||||||
|
override var lang = "it"
|
||||||
|
override val hasMainPage = true
|
||||||
|
private val dubIdentifier = " (ITA)"
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getStatus(t: String?): ShowStatus? {
|
||||||
|
return when (t?.lowercase()) {
|
||||||
|
"completato" -> ShowStatus.Completed
|
||||||
|
"in corso" -> ShowStatus.Ongoing
|
||||||
|
else -> null // "annunciato"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getType(t: String?): TvType {
|
||||||
|
return when (t?.lowercase()) {
|
||||||
|
"ona" -> TvType.OVA
|
||||||
|
"movie" -> TvType.AnimeMovie
|
||||||
|
else -> TvType.Anime //"serie", "special"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDub(title: String): Boolean{
|
||||||
|
return title.contains(dubIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ApiPoster(
|
||||||
|
@JsonProperty("imageFull") val posterUrl: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiMainPageAnime(
|
||||||
|
@JsonProperty("animeId") val id: Int,
|
||||||
|
@JsonProperty("episodeNumber") val episode: String?,
|
||||||
|
@JsonProperty("animeTitle") val title: String,
|
||||||
|
@JsonProperty("animeType") val type: String,
|
||||||
|
@JsonProperty("fullHd") val fullHD: Boolean,
|
||||||
|
@JsonProperty("animeVerticalImages") val posters: List<ApiPoster>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiSearchResult(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("status") val status: String,
|
||||||
|
@JsonProperty("type") val type: String,
|
||||||
|
@JsonProperty("verticalImages") val posters: List<ApiPoster>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiGenres(
|
||||||
|
@JsonProperty("description") val name: String
|
||||||
|
)
|
||||||
|
data class ApiWebsite(
|
||||||
|
@JsonProperty("listWebsiteId") val websiteId: Int,
|
||||||
|
@JsonProperty("url") val url: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiEpisode(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("title") val title: String?,
|
||||||
|
@JsonProperty("episodeNumber") val number: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun ApiEpisode.toEpisode() : Episode? {
|
||||||
|
val number = this.number.toIntOrNull() ?: return null
|
||||||
|
return Episode(
|
||||||
|
data = "$mainUrl/api/episode/${this.id}",
|
||||||
|
episode = number,
|
||||||
|
name = this.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ApiSeason(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("name") val name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun ApiSeason.toEpisodeList(url: String) : List<Episode> {
|
||||||
|
return app.get("$url/season/${this.id}").parsed<List<ApiEpisode>>().mapNotNull { it.toEpisode() }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ApiAnime(
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("alternativeTitle") val japTitle: String?,
|
||||||
|
@JsonProperty("episodeDuration") val duration: Int,
|
||||||
|
@JsonProperty("storyline") val plot: String,
|
||||||
|
@JsonProperty("type") val type: String,
|
||||||
|
@JsonProperty("status") val status: String,
|
||||||
|
@JsonProperty("genres") val genres: List<ApiGenres>,
|
||||||
|
@JsonProperty("verticalImages") val posters: List<ApiPoster>,
|
||||||
|
@JsonProperty("listWebsites") val websites: List<ApiWebsite>,
|
||||||
|
@JsonProperty("episodes") val episodes: List<ApiEpisode>,
|
||||||
|
@JsonProperty("seasons") val seasons: List<ApiSeason>?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiEpisodeUrl(
|
||||||
|
@JsonProperty("videoUrl") val url: String
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val response = app.get("$mainUrl/api/home/latest-episodes?page=0").parsed<List<ApiMainPageAnime>>()
|
||||||
|
|
||||||
|
val results = response.map{
|
||||||
|
val isDub = isDub(it.title)
|
||||||
|
newAnimeSearchResponse(
|
||||||
|
name = if (isDub) it.title.replace(dubIdentifier, "") else it.title,
|
||||||
|
url = "$mainUrl/api/anime/${it.id}",
|
||||||
|
type = getType(it.type),
|
||||||
|
){
|
||||||
|
addDubStatus(isDub, it.episode?.toIntOrNull())
|
||||||
|
this.posterUrl = it.posters.first().posterUrl
|
||||||
|
this.quality = if (it.fullHD) SearchQuality.HD else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HomePageResponse(listOf(HomePageList("Ultime uscite",results)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val response = app.get("$mainUrl/api/anime/advanced-search?page=0&size=36&query=$query").parsed<List<ApiSearchResult>>()
|
||||||
|
|
||||||
|
return response.map {
|
||||||
|
val isDub = isDub(it.title)
|
||||||
|
|
||||||
|
newAnimeSearchResponse(
|
||||||
|
name = if (isDub) it.title.replace(dubIdentifier, "") else it.title,
|
||||||
|
url = "$mainUrl/api/anime/${it.id}",
|
||||||
|
type = getType(it.type),
|
||||||
|
){
|
||||||
|
addDubStatus(isDub)
|
||||||
|
this.posterUrl = it.posters.first().posterUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
|
||||||
|
val response = app.get(url).parsed<ApiAnime>()
|
||||||
|
|
||||||
|
val tags: List<String> = response.genres.map { it.name }
|
||||||
|
|
||||||
|
val malId: Int? = response.websites.find { it.websiteId == 1 }?.url?.removePrefix("https://myanimelist.net/anime/")?.split("/")?.first()?.toIntOrNull()
|
||||||
|
val aniListId: Int? = response.websites.find { it.websiteId == 4 }?.url?.removePrefix("https://anilist.co/anime/")?.split("/")?.first()?.toIntOrNull()
|
||||||
|
|
||||||
|
val episodes = if (response.seasons.isNullOrEmpty()) response.episodes.mapNotNull { it.toEpisode() } else response.seasons.map{ it.toEpisodeList(url) }.flatten()
|
||||||
|
val isDub = isDub(response.title)
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(response.title, url, getType(response.type)) {
|
||||||
|
this.name = if (isDub) response.title.replace(dubIdentifier, "") else response.title
|
||||||
|
this.japName = response.japTitle
|
||||||
|
this.plot = response.plot
|
||||||
|
this.tags = tags
|
||||||
|
this.showStatus = getStatus(response.status)
|
||||||
|
addPoster(response.posters.first().posterUrl)
|
||||||
|
addEpisodes(if (isDub) DubStatus.Dubbed else DubStatus.Subbed, episodes)
|
||||||
|
addMalId(malId)
|
||||||
|
addAniListId(aniListId)
|
||||||
|
addDuration(response.duration.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val episode = app.get(data).parsed<ApiEpisodeUrl>()
|
||||||
|
|
||||||
|
if(episode.url.contains(".m3u8")){
|
||||||
|
val m3u8Helper = M3u8Helper()
|
||||||
|
val streams = m3u8Helper.m3u8Generation(M3u8Helper.M3u8Stream(episode.url,Qualities.Unknown.value), false)
|
||||||
|
|
||||||
|
streams.forEach {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
it.streamUrl,
|
||||||
|
referer = mainUrl,
|
||||||
|
quality = it.quality ?: Qualities.Unknown.value,
|
||||||
|
isM3u8 = it.streamUrl.contains(".m3u8"))) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
episode.url,
|
||||||
|
referer = mainUrl,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = 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 AniPlayProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AniPlayProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AniflixProvider/build.gradle.kts
Normal file
22
AniflixProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AniflixProvider/src/main/AndroidManifest.xml
Normal file
2
AniflixProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
274
AniflixProvider/src/main/kotlin/com/lagradost/AniflixProvider.kt
Normal file
274
AniflixProvider/src/main/kotlin/com/lagradost/AniflixProvider.kt
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeFlickProvider/build.gradle.kts
Normal file
22
AnimeFlickProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeFlickProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeFlickProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeIndoProvider/build.gradle.kts
Normal file
22
AnimeIndoProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeIndoProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeIndoProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,192 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import com.lagradost.nicehttp.NiceResponse
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class AnimeIndoProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://animeindo.sbs"
|
||||||
|
override var name = "AnimeIndo"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override var lang = "id"
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||||
|
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(t: String): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"Finished Airing" -> ShowStatus.Completed
|
||||||
|
"Currently Airing" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun request(url: String): NiceResponse {
|
||||||
|
val req = app.get(
|
||||||
|
url,
|
||||||
|
cookies = mapOf("recaptcha_cookie" to "#Asia/Jakarta#-420#win32#Windows#0,false,false#Google Inc. (Intel)~ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0)")
|
||||||
|
)
|
||||||
|
if (req.isSuccessful) {
|
||||||
|
return req
|
||||||
|
} else {
|
||||||
|
val document = app.get(url).document
|
||||||
|
val captchaKey =
|
||||||
|
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||||
|
.attr("src").substringAfter("render=").substringBefore("&")
|
||||||
|
val token = getCaptchaToken(url, captchaKey)
|
||||||
|
return app.post(
|
||||||
|
url,
|
||||||
|
data = mapOf(
|
||||||
|
"action" to "recaptcha_for_all",
|
||||||
|
"token" to "$token",
|
||||||
|
"sitekey" to captchaKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/anime-terbaru/page/" to "Anime Terbaru",
|
||||||
|
"$mainUrl/donghua-terbaru/page/" to "Donghua Terbaru"
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(
|
||||||
|
page: Int,
|
||||||
|
request: MainPageRequest
|
||||||
|
): HomePageResponse {
|
||||||
|
val document = request(request.data + page).document
|
||||||
|
val home = document.select("div.post-show > article").mapNotNull {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, home)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProperAnimeLink(uri: String): String {
|
||||||
|
return if (uri.contains("/anime/")) {
|
||||||
|
uri
|
||||||
|
} else {
|
||||||
|
var title = uri.substringAfter("$mainUrl/")
|
||||||
|
title = when {
|
||||||
|
(title.contains("-episode")) && !(title.contains("-movie")) -> Regex("(.+)-episode").find(
|
||||||
|
title
|
||||||
|
)?.groupValues?.get(1).toString()
|
||||||
|
(title.contains("-movie")) -> Regex("(.+)-movie").find(title)?.groupValues?.get(
|
||||||
|
1
|
||||||
|
).toString()
|
||||||
|
else -> title
|
||||||
|
}
|
||||||
|
"$mainUrl/anime/$title"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResult(): AnimeSearchResponse? {
|
||||||
|
val title = this.selectFirst("div.title")?.text()?.trim() ?: return null
|
||||||
|
val href = getProperAnimeLink(this.selectFirst("a")!!.attr("href"))
|
||||||
|
val posterUrl = this.select("img[itemprop=image]").attr("src").toString()
|
||||||
|
val type = getType(this.select("div.type").text().trim())
|
||||||
|
val epNum =
|
||||||
|
this.selectFirst("span.episode")?.ownText()?.replace(Regex("[^0-9]"), "")?.trim()
|
||||||
|
?.toIntOrNull()
|
||||||
|
return newAnimeSearchResponse(title, href, type) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
addSub(epNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val link = "$mainUrl/?s=$query"
|
||||||
|
val document = request(link).document
|
||||||
|
|
||||||
|
return document.select(".site-main.relat > article").map {
|
||||||
|
val title = it.selectFirst("div.title > h2")!!.ownText().trim()
|
||||||
|
val href = it.selectFirst("a")!!.attr("href")
|
||||||
|
val posterUrl = it.selectFirst("img")!!.attr("src").toString()
|
||||||
|
val type = getType(it.select("div.type").text().trim())
|
||||||
|
newAnimeSearchResponse(title, href, type) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val document = request(url).document
|
||||||
|
|
||||||
|
val title = document.selectFirst("h1.entry-title")?.text().toString().trim()
|
||||||
|
val poster = document.selectFirst("div.thumb > img[itemprop=image]")?.attr("src")
|
||||||
|
val tags = document.select("div.genxed > a").map { it.text() }
|
||||||
|
val type = getType(
|
||||||
|
document.selectFirst("div.info-content > div.spe > span:nth-child(6)")?.ownText()
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
val year = Regex("\\d, ([0-9]*)").find(
|
||||||
|
document.select("div.info-content > div.spe > span:nth-child(9) > time").text()
|
||||||
|
)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
|
val status = getStatus(
|
||||||
|
document.selectFirst("div.info-content > div.spe > span:nth-child(1)")!!.ownText()
|
||||||
|
.trim()
|
||||||
|
)
|
||||||
|
val description = document.select("div[itemprop=description] > p").text()
|
||||||
|
val trailer = document.selectFirst("div.player-embed iframe")?.attr("src")
|
||||||
|
val episodes = document.select("div.lstepsiode.listeps ul li").mapNotNull {
|
||||||
|
val header = it.selectFirst("span.lchx > a") ?: return@mapNotNull null
|
||||||
|
val name = header.text().trim()
|
||||||
|
val episode = header.text().trim().replace("Episode", "").trim().toIntOrNull()
|
||||||
|
val link = fixUrl(header.attr("href"))
|
||||||
|
Episode(link, name = name, episode = episode)
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, type) {
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
this.year = year
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
this.tags = tags
|
||||||
|
addTrailer(trailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val document = request(data).document
|
||||||
|
document.select("div.itemleft > .mirror > option").mapNotNull {
|
||||||
|
fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("src"))
|
||||||
|
}.apmap {
|
||||||
|
if (it.startsWith("https://uservideo.xyz")) {
|
||||||
|
app.get(it, referer = "$mainUrl/").document.select("iframe").attr("src")
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}.apmap {
|
||||||
|
loadExtractor(it, 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 AnimeIndoProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimeIndoProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimePaheProvider/build.gradle.kts
Normal file
22
AnimePaheProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimePaheProvider/src/main/AndroidManifest.xml
Normal file
2
AnimePaheProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeSailProvider/build.gradle.kts
Normal file
22
AnimeSailProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeSailProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeSailProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,191 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import com.lagradost.nicehttp.NiceResponse
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class AnimeSailProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://111.90.143.42"
|
||||||
|
override var name = "AnimeSail"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override var lang = "id"
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||||
|
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(t: String): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"Completed" -> ShowStatus.Completed
|
||||||
|
"Ongoing" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun request(url: String, ref: String? = null): NiceResponse {
|
||||||
|
return app.get(
|
||||||
|
url,
|
||||||
|
headers = mapOf("Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"),
|
||||||
|
cookies = mapOf("_as_ipin_ct" to "ID"),
|
||||||
|
referer = ref
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/page/" to "Episode Terbaru",
|
||||||
|
"$mainUrl/movie-terbaru/page/" to "Movie Terbaru",
|
||||||
|
"$mainUrl/genres/donghua/page/" to "Donghua"
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
|
val document = request(request.data + page).document
|
||||||
|
val home = document.select("article").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, home)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProperAnimeLink(uri: String): String {
|
||||||
|
return if (uri.contains("/anime/")) {
|
||||||
|
uri
|
||||||
|
} else {
|
||||||
|
var title = uri.substringAfter("$mainUrl/")
|
||||||
|
title = when {
|
||||||
|
(title.contains("-episode")) && !(title.contains("-movie")) -> title.substringBefore(
|
||||||
|
"-episode"
|
||||||
|
)
|
||||||
|
(title.contains("-movie")) -> title.substringBefore("-movie")
|
||||||
|
else -> title
|
||||||
|
}
|
||||||
|
|
||||||
|
"$mainUrl/anime/$title"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResult(): AnimeSearchResponse {
|
||||||
|
val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString())
|
||||||
|
val title = this.select(".tt > h2").text().trim()
|
||||||
|
val posterUrl = fixUrlNull(this.selectFirst("div.limit img")?.attr("src"))
|
||||||
|
val epNum = this.selectFirst(".tt > h2")?.text()?.let {
|
||||||
|
Regex("Episode\\s?([0-9]+)").find(it)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
}
|
||||||
|
return newAnimeSearchResponse(title, href, TvType.Anime) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
addSub(epNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val link = "$mainUrl/?s=$query"
|
||||||
|
val document = request(link).document
|
||||||
|
|
||||||
|
return document.select("div.listupd article").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val document = request(url).document
|
||||||
|
|
||||||
|
val title = document.selectFirst("h1.entry-title")?.text().toString().trim()
|
||||||
|
val type = getType(
|
||||||
|
document.select("tbody th:contains(Tipe)").next().text()
|
||||||
|
)
|
||||||
|
val episodes = document.select("ul.daftar > li").map {
|
||||||
|
val header = it.select("a").text().trim()
|
||||||
|
val name =
|
||||||
|
Regex("(Episode\\s?[0-9]+)").find(header)?.groupValues?.getOrNull(0) ?: header
|
||||||
|
val link = fixUrl(it.select("a").attr("href"))
|
||||||
|
Episode(link, name = name)
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, type) {
|
||||||
|
posterUrl = document.selectFirst("div.entry-content > img")?.attr("src")
|
||||||
|
this.year =
|
||||||
|
document.select("tbody th:contains(Dirilis)").next().text().trim().toIntOrNull()
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus =
|
||||||
|
getStatus(document.select("tbody th:contains(Status)").next().text().trim())
|
||||||
|
plot = document.selectFirst("div.entry-content > p")?.text()
|
||||||
|
this.tags =
|
||||||
|
document.select("tbody th:contains(Genre)").next().select("a").map { it.text() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val document = request(data).document
|
||||||
|
|
||||||
|
document.select(".mobius > .mirror > option").apmap {
|
||||||
|
safeApiCall {
|
||||||
|
val iframe = fixUrl(
|
||||||
|
Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src")
|
||||||
|
?: throw ErrorLoadingException("No iframe found")
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
iframe.startsWith("$mainUrl/utils/player/arch/") || iframe.startsWith(
|
||||||
|
"$mainUrl/utils/player/race/"
|
||||||
|
) -> request(iframe, ref = data).document.select("source").attr("src")
|
||||||
|
.let { link ->
|
||||||
|
val source =
|
||||||
|
when {
|
||||||
|
iframe.contains("/arch/") -> "Arch"
|
||||||
|
iframe.contains("/race/") -> "Race"
|
||||||
|
else -> this.name
|
||||||
|
}
|
||||||
|
val quality =
|
||||||
|
Regex("\\.([0-9]{3,4})\\.").find(link)?.groupValues?.get(1)
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = source,
|
||||||
|
name = source,
|
||||||
|
url = link,
|
||||||
|
referer = mainUrl,
|
||||||
|
quality = quality?.toIntOrNull() ?: Qualities.Unknown.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// skip for now
|
||||||
|
// iframe.startsWith("$mainUrl/utils/player/fichan/") -> ""
|
||||||
|
// iframe.startsWith("$mainUrl/utils/player/blogger/") -> ""
|
||||||
|
iframe.startsWith("$mainUrl/utils/player/framezilla/") || iframe.startsWith("https://uservideo.xyz") -> {
|
||||||
|
request(iframe, ref = data).document.select("iframe").attr("src")
|
||||||
|
.let { link ->
|
||||||
|
loadExtractor(fixUrl(link), mainUrl, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
loadExtractor(iframe, mainUrl, 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 AnimeSailProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimeSailProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeSaturnProvider/build.gradle.kts
Normal file
22
AnimeSaturnProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeSaturnProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeSaturnProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,201 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addRating
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class AnimeSaturnProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://www.animesaturn.cc"
|
||||||
|
override var name = "AnimeSaturn"
|
||||||
|
override var lang = "it"
|
||||||
|
override val hasMainPage = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getStatus(t: String?): ShowStatus? {
|
||||||
|
return when (t?.lowercase()) {
|
||||||
|
"finito" -> ShowStatus.Completed
|
||||||
|
"in corso" -> ShowStatus.Ongoing
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResult(): AnimeSearchResponse {
|
||||||
|
|
||||||
|
var title = this.select("a.badge-archivio").first()!!.text()
|
||||||
|
var isDubbed = false
|
||||||
|
|
||||||
|
if (title.contains(" (ITA)")){
|
||||||
|
title = title.replace(" (ITA)", "")
|
||||||
|
isDubbed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = this.select("a.badge-archivio").first()!!.attr("href")
|
||||||
|
|
||||||
|
val posterUrl = this.select("img.locandina-archivio[src]").first()!!.attr("src")
|
||||||
|
|
||||||
|
return newAnimeSearchResponse(title, url, TvType.Anime) {
|
||||||
|
addDubStatus(isDubbed)
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toEpisode(): Episode? {
|
||||||
|
var episode = this.text().split(" ")[1]
|
||||||
|
if(episode.contains(".")) return null
|
||||||
|
if(episode.contains("-"))
|
||||||
|
episode = episode.split("-")[0]
|
||||||
|
|
||||||
|
return Episode(
|
||||||
|
data = this.attr("href"),
|
||||||
|
episode = episode.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val document = app.get(mainUrl).document
|
||||||
|
val list = ArrayList<HomePageList>()
|
||||||
|
document.select("div.container:has(span.badge-saturn)").forEach {
|
||||||
|
val tabName = it.select("span.badge-saturn").first()!!.text()
|
||||||
|
if (tabName.equals("Ultimi episodi")) return@forEach
|
||||||
|
val results = ArrayList<AnimeSearchResponse>()
|
||||||
|
it.select(".main-anime-card").forEach { card ->
|
||||||
|
var title = card.select("a[title]").first()!!.attr("title")
|
||||||
|
var isDubbed = false
|
||||||
|
if(title.contains(" (ITA)")){
|
||||||
|
title = title.replace(" (ITA)", "")
|
||||||
|
isDubbed = true
|
||||||
|
}
|
||||||
|
val posterUrl = card.select("img.new-anime").first()!!.attr("src")
|
||||||
|
val url = card.select("a").first()!!.attr("href")
|
||||||
|
|
||||||
|
results.add(newAnimeSearchResponse(title, url, TvType.Anime){
|
||||||
|
addDubStatus(isDubbed)
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
list.add(HomePageList(tabName, results))
|
||||||
|
}
|
||||||
|
return HomePageResponse(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val document = app.get("$mainUrl/animelist?search=$query").document
|
||||||
|
return document.select("div.item-archivio").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
|
||||||
|
val document = app.get(url).document
|
||||||
|
|
||||||
|
val title = document.select("img.cover-anime").first()!!.attr("alt")
|
||||||
|
val japTitle = document.select("div.box-trasparente-alternativo").first()!!.text()
|
||||||
|
val posterUrl = document.select("img.cover-anime[src]").first()!!.attr("src")
|
||||||
|
var malId : Int? = null
|
||||||
|
var aniListId : Int? = null
|
||||||
|
|
||||||
|
document.select("[rel=\"noopener noreferrer\"]").forEach {
|
||||||
|
if(it.attr("href").contains("myanimelist"))
|
||||||
|
malId = it.attr("href").removeSuffix("/").split('/').last().toIntOrNull()
|
||||||
|
else
|
||||||
|
aniListId = it.attr("href").removeSuffix("/").split('/').last().toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
val plot = document.select("div#shown-trama").first()?.text()
|
||||||
|
|
||||||
|
val tags = document.select("a.generi-as").map { it.text() }
|
||||||
|
|
||||||
|
val details : List<String>? = document.select("div.container:contains(Stato: )").first()?.text()?.split(" ")
|
||||||
|
var status : String? = null
|
||||||
|
var duration : String? = null
|
||||||
|
var year : String? = null
|
||||||
|
var score : String? = null
|
||||||
|
|
||||||
|
val isDubbed = document.select("div.anime-title-as").first()!!.text().contains("(ITA)")
|
||||||
|
|
||||||
|
if (!details.isNullOrEmpty()) {
|
||||||
|
details.forEach {
|
||||||
|
val index = details.indexOf(it) +1
|
||||||
|
when (it) {
|
||||||
|
"Stato:" -> status = details[index]
|
||||||
|
"episodi:" -> duration = details[index]
|
||||||
|
"uscita:" -> year = details[index + 2]
|
||||||
|
"Voto:" -> score = details[index].split("/")[0]
|
||||||
|
else -> return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val episodes = document.select("a.bottone-ep").mapNotNull{ it.toEpisode() }
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||||
|
this.engName = title
|
||||||
|
this.japName = japTitle
|
||||||
|
this.year = year?.toIntOrNull()
|
||||||
|
this.plot = plot
|
||||||
|
this.tags = tags
|
||||||
|
this.showStatus = getStatus(status)
|
||||||
|
addPoster(posterUrl)
|
||||||
|
addRating(score)
|
||||||
|
addEpisodes(if (isDubbed) DubStatus.Dubbed else DubStatus.Subbed, episodes)
|
||||||
|
addMalId(malId)
|
||||||
|
addAniListId(aniListId)
|
||||||
|
addDuration(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val page = app.get(data).document
|
||||||
|
val episodeLink = page.select("div.card-body > a[href]").find {it1 ->
|
||||||
|
it1.attr("href").contains("watch?")
|
||||||
|
}?.attr("href")
|
||||||
|
|
||||||
|
val episodePage = app.get(episodeLink!!).document
|
||||||
|
val episodeUrl: String?
|
||||||
|
var isM3U8 = false
|
||||||
|
|
||||||
|
if(episodePage.select("video.afterglow > source").isNotEmpty()) //Old player
|
||||||
|
episodeUrl = episodePage.select("video.afterglow > source").first()!!.attr("src")
|
||||||
|
|
||||||
|
else{ //New player
|
||||||
|
val script = episodePage.select("script").find {
|
||||||
|
it.toString().contains("jwplayer('player_hls').setup({")
|
||||||
|
}!!.toString()
|
||||||
|
episodeUrl = script.split(" ").find { it.contains(".m3u8") and !it.contains(".replace") }!!.replace("\"","").replace(",", "")
|
||||||
|
isM3U8 = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
episodeUrl!!,
|
||||||
|
isM3u8 = isM3U8,
|
||||||
|
referer = "https://www.animesaturn.io/", //Some servers need the old host as referer, and the new ones accept it too
|
||||||
|
quality = Qualities.Unknown.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 AnimeSaturnProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimeSaturnProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeWorldProvider/build.gradle.kts
Normal file
22
AnimeWorldProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeWorldProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeWorldProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
File diff suppressed because one or more lines are too long
|
@ -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 AnimeWorldProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimeWorldProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimefenixProvider/build.gradle.kts
Normal file
22
AnimefenixProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimefenixProvider/src/main/AndroidManifest.xml
Normal file
2
AnimefenixProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,249 @@
|
||||||
|
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 com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class AnimefenixProvider:MainAPI() {
|
||||||
|
|
||||||
|
override var mainUrl = "https://animefenix.com"
|
||||||
|
override var name = "Animefenix"
|
||||||
|
override var lang = "es"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val hasChromecastSupport = true
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA,
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getDubStatus(title: String): DubStatus {
|
||||||
|
return if (title.contains("Latino") || title.contains("Castellano"))
|
||||||
|
DubStatus.Dubbed
|
||||||
|
else DubStatus.Subbed
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val urls = listOf(
|
||||||
|
Pair("$mainUrl/", "Animes"),
|
||||||
|
Pair("$mainUrl/animes?type[]=movie&order=default", "Peliculas", ),
|
||||||
|
Pair("$mainUrl/animes?type[]=ova&order=default", "OVA's", ),
|
||||||
|
)
|
||||||
|
|
||||||
|
val items = ArrayList<HomePageList>()
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
HomePageList(
|
||||||
|
"Últimos episodios",
|
||||||
|
app.get(mainUrl).document.select(".capitulos-grid div.item").map {
|
||||||
|
val title = it.selectFirst("div.overtitle")?.text()
|
||||||
|
val poster = it.selectFirst("a img")?.attr("src")
|
||||||
|
val epRegex = Regex("(-(\\d+)\$|-(\\d+)\\.(\\d+))")
|
||||||
|
val url = it.selectFirst("a")?.attr("href")?.replace(epRegex,"")
|
||||||
|
?.replace("/ver/","/")
|
||||||
|
val epNum = it.selectFirst(".is-size-7")?.text()?.replace("Episodio ","")?.toIntOrNull()
|
||||||
|
newAnimeSearchResponse(title!!, url!!) {
|
||||||
|
this.posterUrl = poster
|
||||||
|
addDubStatus(getDubStatus(title), epNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
urls.apmap { (url, name) ->
|
||||||
|
val response = app.get(url)
|
||||||
|
val soup = Jsoup.parse(response.text)
|
||||||
|
val home = soup.select(".list-series article").map {
|
||||||
|
val title = it.selectFirst("h3 a")?.text()
|
||||||
|
val poster = it.selectFirst("figure img")?.attr("src")
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title!!,
|
||||||
|
it.selectFirst("a")?.attr("href") ?: "",
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
poster,
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(HomePageList(name, home))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size <= 0) throw ErrorLoadingException()
|
||||||
|
return HomePageResponse(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
return app.get("$mainUrl/animes?q=$query").document.select(".list-series article").map {
|
||||||
|
val title = it.selectFirst("h3 a")?.text()
|
||||||
|
val href = it.selectFirst("a")?.attr("href")
|
||||||
|
val image = it.selectFirst("figure img")?.attr("src")
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title!!,
|
||||||
|
href!!,
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
fixUrl(image ?: ""),
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
|
||||||
|
DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = Jsoup.parse(app.get(url, timeout = 120).text)
|
||||||
|
val poster = doc.selectFirst(".image > img")?.attr("src")
|
||||||
|
val title = doc.selectFirst("h1.title.has-text-orange")?.text()
|
||||||
|
val description = doc.selectFirst("p.has-text-light")?.text()
|
||||||
|
val genres = doc.select(".genres a").map { it.text() }
|
||||||
|
val status = when (doc.selectFirst(".is-narrow-desktop a.button")?.text()) {
|
||||||
|
"Emisión" -> ShowStatus.Ongoing
|
||||||
|
"Finalizado" -> ShowStatus.Completed
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val episodes = doc.select(".anime-page__episode-list li").map {
|
||||||
|
val name = it.selectFirst("span")?.text()
|
||||||
|
val link = it.selectFirst("a")?.attr("href")
|
||||||
|
Episode(link!!, name)
|
||||||
|
}.reversed()
|
||||||
|
val type = if (doc.selectFirst("ul.has-text-light")?.text()
|
||||||
|
!!.contains("Película") && episodes.size == 1
|
||||||
|
) TvType.AnimeMovie else TvType.Anime
|
||||||
|
return newAnimeLoadResponse(title!!, url, type) {
|
||||||
|
japName = null
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
plot = description
|
||||||
|
tags = genres
|
||||||
|
showStatus = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanStreamID(input: String): String = input.replace(Regex("player=.*&code=|&"),"")
|
||||||
|
|
||||||
|
data class Amazon (
|
||||||
|
@JsonProperty("file") var file : String? = null,
|
||||||
|
@JsonProperty("type") var type : String? = null,
|
||||||
|
@JsonProperty("label") var label : String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun cleanExtractor(
|
||||||
|
source: String,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
callback(
|
||||||
|
ExtractorLink(
|
||||||
|
source,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
"",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val soup = app.get(data).document
|
||||||
|
val script = soup.selectFirst(".player-container script")?.data()
|
||||||
|
if (script!!.contains("var tabsArray =")) {
|
||||||
|
val sourcesRegex = Regex("player=.*&code(.*)&")
|
||||||
|
val test = sourcesRegex.findAll(script).toList()
|
||||||
|
test.apmap {
|
||||||
|
val codestream = it.value
|
||||||
|
val links = when {
|
||||||
|
codestream.contains("player=2&") -> "https://embedsito.com/v/"+cleanStreamID(codestream)
|
||||||
|
codestream.contains("player=3&") -> "https://www.mp4upload.com/embed-"+cleanStreamID(codestream)+".html"
|
||||||
|
codestream.contains("player=6&") -> "https://www.yourupload.com/embed/"+cleanStreamID(codestream)
|
||||||
|
codestream.contains("player=12&") -> "http://ok.ru/videoembed/"+cleanStreamID(codestream)
|
||||||
|
codestream.contains("player=4&") -> "https://sendvid.com/"+cleanStreamID(codestream)
|
||||||
|
codestream.contains("player=9&") -> "AmaNormal https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream)
|
||||||
|
codestream.contains("player=11&") -> "AmazonES https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream)
|
||||||
|
codestream.contains("player=22&") -> "Fireload https://www.animefenix.com/stream/fl.php?v="+cleanStreamID(codestream)
|
||||||
|
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
loadExtractor(links, data, subtitleCallback, callback)
|
||||||
|
|
||||||
|
argamap({
|
||||||
|
if (links.contains("AmaNormal")) {
|
||||||
|
val doc = app.get(links.replace("AmaNormal ","")).document
|
||||||
|
doc.select("script").map { script ->
|
||||||
|
if (script.data().contains("sources: [{\"file\"")) {
|
||||||
|
val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","")
|
||||||
|
val json = parseJson<Amazon>(text)
|
||||||
|
if (json.file != null) {
|
||||||
|
cleanExtractor(
|
||||||
|
"Amazon",
|
||||||
|
"Amazon ${json.label}",
|
||||||
|
json.file!!,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.contains("AmazonES")) {
|
||||||
|
val amazonES = links.replace("AmazonES ", "")
|
||||||
|
val doc = app.get("$amazonES&ext=es").document
|
||||||
|
doc.select("script").map { script ->
|
||||||
|
if (script.data().contains("sources: [{\"file\"")) {
|
||||||
|
val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","")
|
||||||
|
val json = parseJson<Amazon>(text)
|
||||||
|
if (json.file != null) {
|
||||||
|
cleanExtractor(
|
||||||
|
"AmazonES",
|
||||||
|
"AmazonES ${json.label}",
|
||||||
|
json.file!!,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (links.contains("Fireload")) {
|
||||||
|
val doc = app.get(links.replace("Fireload ", "")).document
|
||||||
|
doc.select("script").map { script ->
|
||||||
|
if (script.data().contains("sources: [{\"file\"")) {
|
||||||
|
val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","")
|
||||||
|
val json = parseJson<Amazon>(text)
|
||||||
|
val testurl = if (json.file?.contains("fireload") == true) {
|
||||||
|
app.get("https://${json.file}").text
|
||||||
|
} else null
|
||||||
|
if (testurl?.contains("error") == true) {
|
||||||
|
//
|
||||||
|
} else if (json.file?.contains("fireload") == true) {
|
||||||
|
cleanExtractor(
|
||||||
|
"Fireload",
|
||||||
|
"Fireload ${json.label}",
|
||||||
|
"https://"+json.file!!,
|
||||||
|
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 AnimefenixProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimefenixProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeflvIOProvider/build.gradle.kts
Normal file
22
AnimeflvIOProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeflvIOProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeflvIOProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,239 @@
|
||||||
|
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.M3u8Helper.Companion.generateM3u8
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class AnimeflvIOProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://animeflv.io" //Also scrapes from animeid.to
|
||||||
|
override var name = "Animeflv.io"
|
||||||
|
override var lang = "es"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val hasChromecastSupport = true
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA,
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
|
val items = ArrayList<HomePageList>()
|
||||||
|
val urls = listOf(
|
||||||
|
Pair("$mainUrl/series", "Series actualizadas"),
|
||||||
|
Pair("$mainUrl/peliculas", "Peliculas actualizadas"),
|
||||||
|
)
|
||||||
|
items.add(
|
||||||
|
HomePageList(
|
||||||
|
"Estrenos",
|
||||||
|
app.get(mainUrl).document.select("div#owl-demo-premiere-movies .pull-left").map {
|
||||||
|
val title = it.selectFirst("p")?.text() ?: ""
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
it.selectFirst("img")?.attr("src"),
|
||||||
|
it.selectFirst("span.year").toString().toIntOrNull(),
|
||||||
|
EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
urls.apmap { (url, name) ->
|
||||||
|
val soup = app.get(url).document
|
||||||
|
val home = soup.select("div.item-pelicula").map {
|
||||||
|
val title = it.selectFirst(".item-detail p")?.text() ?: ""
|
||||||
|
val poster = it.selectFirst("figure img")?.attr("src")
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
poster,
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
|
||||||
|
DubStatus.Dubbed
|
||||||
|
) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(HomePageList(name, home))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size <= 0) throw ErrorLoadingException()
|
||||||
|
return HomePageResponse(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val headers = mapOf(
|
||||||
|
"Host" to "animeflv.io",
|
||||||
|
"User-Agent" to USER_AGENT,
|
||||||
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
|
"DNT" to "1",
|
||||||
|
"Alt-Used" to "animeflv.io",
|
||||||
|
"Connection" to "keep-alive",
|
||||||
|
"Referer" to "https://animeflv.io",
|
||||||
|
)
|
||||||
|
val url = "$mainUrl/search.html?keyword=$query"
|
||||||
|
val document = app.get(
|
||||||
|
url,
|
||||||
|
headers = headers
|
||||||
|
).document
|
||||||
|
return document.select(".item-pelicula.pull-left").map {
|
||||||
|
val title = it.selectFirst("div.item-detail p")?.text() ?: ""
|
||||||
|
val href = fixUrl(it.selectFirst("a")?.attr("href") ?: "")
|
||||||
|
var image = it.selectFirst("figure img")?.attr("src") ?: ""
|
||||||
|
val isMovie = href.contains("/pelicula/")
|
||||||
|
if (image.contains("/static/img/picture.png")) {
|
||||||
|
image = ""
|
||||||
|
}
|
||||||
|
if (isMovie) {
|
||||||
|
MovieSearchResponse(
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
this.name,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
image,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
image,
|
||||||
|
null,
|
||||||
|
EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
|
// Gets the url returned from searching.
|
||||||
|
val soup = app.get(url).document
|
||||||
|
val title = soup.selectFirst(".info-content h1")?.text()
|
||||||
|
val description = soup.selectFirst("span.sinopsis")?.text()?.trim()
|
||||||
|
val poster: String? = soup.selectFirst(".poster img")?.attr("src")
|
||||||
|
val episodes = soup.select(".item-season-episodes a").map { li ->
|
||||||
|
val href = fixUrl(li.selectFirst("a")?.attr("href") ?: "")
|
||||||
|
val name = li.selectFirst("a")?.text() ?: ""
|
||||||
|
Episode(
|
||||||
|
href, name,
|
||||||
|
)
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
|
val year = Regex("(\\d*)").find(soup.select(".info-half").text())
|
||||||
|
|
||||||
|
val tvType = if (url.contains("/pelicula/")) TvType.AnimeMovie else TvType.Anime
|
||||||
|
val genre = soup.select(".content-type-a a")
|
||||||
|
.map { it?.text()?.trim().toString().replace(", ", "") }
|
||||||
|
val duration = Regex("""(\d*)""").find(
|
||||||
|
soup.select("p.info-half:nth-child(4)").text()
|
||||||
|
)
|
||||||
|
|
||||||
|
return when (tvType) {
|
||||||
|
TvType.Anime -> {
|
||||||
|
return newAnimeLoadResponse(title ?: "", url, tvType) {
|
||||||
|
japName = null
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
this.year = null
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
plot = description
|
||||||
|
tags = genre
|
||||||
|
|
||||||
|
showStatus = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TvType.AnimeMovie -> {
|
||||||
|
MovieLoadResponse(
|
||||||
|
title ?: "",
|
||||||
|
url,
|
||||||
|
this.name,
|
||||||
|
tvType,
|
||||||
|
url,
|
||||||
|
poster,
|
||||||
|
year.toString().toIntOrNull(),
|
||||||
|
description,
|
||||||
|
null,
|
||||||
|
genre,
|
||||||
|
duration.toString().toIntOrNull(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MainJson(
|
||||||
|
@JsonProperty("source") val source: List<Source>,
|
||||||
|
@JsonProperty("source_bk") val sourceBk: String?,
|
||||||
|
@JsonProperty("track") val track: List<String>?,
|
||||||
|
@JsonProperty("advertising") val advertising: List<String>?,
|
||||||
|
@JsonProperty("linkiframe") val linkiframe: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
@JsonProperty("default") val default: String,
|
||||||
|
@JsonProperty("type") val type: String
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get(data).document.select("li.tab-video").apmap {
|
||||||
|
val url = fixUrl(it.attr("data-video"))
|
||||||
|
if (url.contains("animeid")) {
|
||||||
|
val ajaxurl = url.replace("streaming.php", "ajax.php")
|
||||||
|
val ajaxurltext = app.get(ajaxurl).text
|
||||||
|
val json = parseJson<MainJson>(ajaxurltext)
|
||||||
|
json.source.forEach { source ->
|
||||||
|
if (source.file.contains("m3u8")) {
|
||||||
|
generateM3u8(
|
||||||
|
"Animeflv.io",
|
||||||
|
source.file,
|
||||||
|
"https://animeid.to",
|
||||||
|
headers = mapOf("Referer" to "https://animeid.to")
|
||||||
|
).apmap {
|
||||||
|
callback(
|
||||||
|
ExtractorLink(
|
||||||
|
"Animeflv.io",
|
||||||
|
"Animeflv.io",
|
||||||
|
it.url,
|
||||||
|
"https://animeid.to",
|
||||||
|
getQualityFromName(it.quality.toString()),
|
||||||
|
it.url.contains("m3u8")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
"$name ${source.label}",
|
||||||
|
source.file,
|
||||||
|
"https://animeid.to",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
isM3u8 = source.file.contains("m3u8")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadExtractor(url, 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 AnimeflvIOProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimeflvIOProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimeflvnetProvider/build.gradle.kts
Normal file
22
AnimeflvnetProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimeflvnetProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeflvnetProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,182 @@
|
||||||
|
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.loadExtractor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class AnimeflvnetProvider : MainAPI() {
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA
|
||||||
|
else if (t.contains("Película")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDubStatus(title: String): DubStatus {
|
||||||
|
return if (title.contains("Latino") || title.contains("Castellano"))
|
||||||
|
DubStatus.Dubbed
|
||||||
|
else DubStatus.Subbed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var mainUrl = "https://www3.animeflv.net"
|
||||||
|
override var name = "Animeflv.net"
|
||||||
|
override var lang = "es"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val hasChromecastSupport = true
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA,
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val urls = listOf(
|
||||||
|
Pair("$mainUrl/browse?type[]=movie&order=updated", "Películas"),
|
||||||
|
Pair("$mainUrl/browse?status[]=2&order=default", "Animes"),
|
||||||
|
Pair("$mainUrl/browse?status[]=1&order=rating", "En emision"),
|
||||||
|
)
|
||||||
|
val items = ArrayList<HomePageList>()
|
||||||
|
items.add(
|
||||||
|
HomePageList(
|
||||||
|
"Últimos episodios",
|
||||||
|
app.get(mainUrl).document.select("main.Main ul.ListEpisodios li").mapNotNull {
|
||||||
|
val title = it.selectFirst("strong.Title")?.text() ?: return@mapNotNull null
|
||||||
|
val poster = it.selectFirst("span img")?.attr("src") ?: return@mapNotNull null
|
||||||
|
val epRegex = Regex("(-(\\d+)\$)")
|
||||||
|
val url = it.selectFirst("a")?.attr("href")?.replace(epRegex, "")
|
||||||
|
?.replace("ver/", "anime/") ?: return@mapNotNull null
|
||||||
|
val epNum =
|
||||||
|
it.selectFirst("span.Capi")?.text()?.replace("Episodio ", "")?.toIntOrNull()
|
||||||
|
newAnimeSearchResponse(title, url) {
|
||||||
|
this.posterUrl = fixUrl(poster)
|
||||||
|
addDubStatus(getDubStatus(title), epNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
for ((url, name) in urls) {
|
||||||
|
try {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val home = doc.select("ul.ListAnimes li article").mapNotNull {
|
||||||
|
val title = it.selectFirst("h3.Title")?.text() ?: return@mapNotNull null
|
||||||
|
val poster = it.selectFirst("figure img")?.attr("src") ?: return@mapNotNull null
|
||||||
|
newAnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
fixUrl(it.selectFirst("a")?.attr("href") ?: return@mapNotNull null)
|
||||||
|
) {
|
||||||
|
this.posterUrl = fixUrl(poster)
|
||||||
|
addDubStatus(getDubStatus(title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(HomePageList(name, home))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.size <= 0) throw ErrorLoadingException()
|
||||||
|
return HomePageResponse(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SearchObject(
|
||||||
|
@JsonProperty("id") val id: String,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("type") val type: String,
|
||||||
|
@JsonProperty("last_id") val lastId: String,
|
||||||
|
@JsonProperty("slug") val slug: String
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val response = app.post(
|
||||||
|
"https://www3.animeflv.net/api/animes/search",
|
||||||
|
data = mapOf(Pair("value", query))
|
||||||
|
).text
|
||||||
|
val json = parseJson<List<SearchObject>>(response)
|
||||||
|
return json.map { searchr ->
|
||||||
|
val title = searchr.title
|
||||||
|
val href = "$mainUrl/anime/${searchr.slug}"
|
||||||
|
val image = "$mainUrl/uploads/animes/covers/${searchr.id}.jpg"
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
fixUrl(image),
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
|
||||||
|
DubStatus.Subbed
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val episodes = ArrayList<Episode>()
|
||||||
|
val title = doc.selectFirst("h1.Title")!!.text()
|
||||||
|
val poster = doc.selectFirst("div.AnimeCover div.Image figure img")?.attr("src")!!
|
||||||
|
val description = doc.selectFirst("div.Description p")?.text()
|
||||||
|
val type = doc.selectFirst("span.Type")?.text() ?: ""
|
||||||
|
val status = when (doc.selectFirst("p.AnmStts span")?.text()) {
|
||||||
|
"En emision" -> ShowStatus.Ongoing
|
||||||
|
"Finalizado" -> ShowStatus.Completed
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val genre = doc.select("nav.Nvgnrs a")
|
||||||
|
.map { it?.text()?.trim().toString() }
|
||||||
|
|
||||||
|
doc.select("script").map { script ->
|
||||||
|
if (script.data().contains("var episodes = [")) {
|
||||||
|
val data = script.data().substringAfter("var episodes = [").substringBefore("];")
|
||||||
|
data.split("],").forEach {
|
||||||
|
val epNum = it.removePrefix("[").substringBefore(",")
|
||||||
|
// val epthumbid = it.removePrefix("[").substringAfter(",").substringBefore("]")
|
||||||
|
val animeid = doc.selectFirst("div.Strs.RateIt")?.attr("data-id")
|
||||||
|
val epthumb = "https://cdn.animeflv.net/screenshots/$animeid/$epNum/th_3.jpg"
|
||||||
|
val link = url.replace("/anime/", "/ver/") + "-$epNum"
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
link,
|
||||||
|
null,
|
||||||
|
posterUrl = epthumb,
|
||||||
|
episode = epNum.toIntOrNull()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newAnimeLoadResponse(title, url, getType(type)) {
|
||||||
|
posterUrl = fixUrl(poster)
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes.reversed())
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
tags = genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get(data).document.select("script").apmap { script ->
|
||||||
|
if (script.data().contains("var videos = {") || script.data()
|
||||||
|
.contains("var anime_id =") || script.data().contains("server")
|
||||||
|
) {
|
||||||
|
val videos = script.data().replace("\\/", "/")
|
||||||
|
fetchUrls(videos).map {
|
||||||
|
it.replace("https://embedsb.com/e/", "https://watchsb.com/e/")
|
||||||
|
.replace("https://ok.ru", "http://ok.ru")
|
||||||
|
}.apmap {
|
||||||
|
loadExtractor(it, 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 AnimeflvnetProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(AnimeflvnetProvider())
|
||||||
|
}
|
||||||
|
}
|
22
AnimekisaProvider/build.gradle.kts
Normal file
22
AnimekisaProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
AnimekisaProvider/src/main/AndroidManifest.xml
Normal file
2
AnimekisaProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -141,14 +141,14 @@ class DoramasYTProvider : MainAPI() {
|
||||||
val encodedurl = it.select("p").attr("data-player")
|
val encodedurl = it.select("p").attr("data-player")
|
||||||
val urlDecoded = base64Decode(encodedurl)
|
val urlDecoded = base64Decode(encodedurl)
|
||||||
val url = (urlDecoded).replace("https://doramasyt.com/reproductor?url=", "")
|
val url = (urlDecoded).replace("https://doramasyt.com/reproductor?url=", "")
|
||||||
// if (url.startsWith("https://www.fembed.com")) {
|
if (url.startsWith("https://www.fembed.com")) {
|
||||||
// val extractor = FEmbed()
|
val extractor = FEmbed()
|
||||||
// extractor.getUrl(url).forEach { link ->
|
extractor.getUrl(url).forEach { link ->
|
||||||
// callback.invoke(link)
|
callback.invoke(link)
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
loadExtractor(url, mainUrl, subtitleCallback, callback)
|
loadExtractor(url, mainUrl, subtitleCallback, callback)
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
class FEmbed: XStreamCdn() {
|
||||||
|
override val name: String = "FEmbed"
|
||||||
|
override val mainUrl: String = "https://www.fembed.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class XStreamCdn : ExtractorApi() {
|
||||||
|
override val name: String = "XStreamCdn"
|
||||||
|
override val mainUrl: String = "https://embedsito.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
open var domainUrl: String = "embedsito.com"
|
||||||
|
|
||||||
|
private data class ResponseData(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
//val type: String // Mp4
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ResponseJson(
|
||||||
|
@JsonProperty("success") val success: Boolean,
|
||||||
|
@JsonProperty("data") val data: List<ResponseData>?
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getExtractorUrl(id: String): String {
|
||||||
|
return "$domainUrl/api/source/$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val headers = mapOf(
|
||||||
|
"Referer" to url,
|
||||||
|
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
||||||
|
)
|
||||||
|
val id = url.trimEnd('/').split("/").last()
|
||||||
|
val newUrl = "https://${domainUrl}/api/source/${id}"
|
||||||
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
with(app.post(newUrl, headers = headers)) {
|
||||||
|
if (this.code != 200) return listOf()
|
||||||
|
val text = this.text
|
||||||
|
if (text.isEmpty()) return listOf()
|
||||||
|
if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf()
|
||||||
|
AppUtils.parseJson<ResponseJson?>(text)?.let {
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
it.data.forEach { data ->
|
||||||
|
extractedLinksList.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name = name,
|
||||||
|
data.file,
|
||||||
|
url,
|
||||||
|
getQualityFromName(data.label),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractedLinksList
|
||||||
|
}
|
||||||
|
}
|
22
DubbedAnimeProvider/build.gradle.kts
Normal file
22
DubbedAnimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
DubbedAnimeProvider/src/main/AndroidManifest.xml
Normal file
2
DubbedAnimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
GogoanimeProvider/build.gradle.kts
Normal file
22
GogoanimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
GogoanimeProvider/src/main/AndroidManifest.xml
Normal file
2
GogoanimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
GomunimeProvider/build.gradle.kts
Normal file
22
GomunimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
GomunimeProvider/src/main/AndroidManifest.xml
Normal file
2
GomunimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,232 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
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 com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
|
class GomunimeProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://185.231.223.76"
|
||||||
|
override var name = "Gomunime"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override var lang = "id"
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||||
|
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(t: String): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"Completed" -> ShowStatus.Completed
|
||||||
|
"Ongoing" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"e" to "Episode Baru",
|
||||||
|
"c" to "Completed",
|
||||||
|
"la" to "Live Action",
|
||||||
|
"t" to "Trending"
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(
|
||||||
|
page: Int,
|
||||||
|
request: MainPageRequest
|
||||||
|
): HomePageResponse {
|
||||||
|
val home = Jsoup.parse(
|
||||||
|
(app.post(
|
||||||
|
url = "$mainUrl/wp-admin/admin-ajax.php/wp-admin/admin-ajax.php",
|
||||||
|
headers = mapOf("Referer" to mainUrl),
|
||||||
|
data = mapOf(
|
||||||
|
"action" to "home_ajax",
|
||||||
|
"fungsi" to request.data,
|
||||||
|
"pag" to "$page"
|
||||||
|
)
|
||||||
|
).parsedSafe<Response>()?.html ?: throw ErrorLoadingException("Invalid Json reponse"))
|
||||||
|
).select("li").mapNotNull {
|
||||||
|
val title = it.selectFirst("a.name")?.text()?.trim() ?: return@mapNotNull null
|
||||||
|
val href = getProperAnimeLink(it.selectFirst("a")!!.attr("href"))
|
||||||
|
val posterUrl = it.selectFirst("img")?.attr("src")
|
||||||
|
val type = getType(it.selectFirst(".taglist > span")!!.text().trim())
|
||||||
|
val epNum = it.select(".tag.ep").text().replace(Regex("[^0-9]"), "").trim()
|
||||||
|
.toIntOrNull()
|
||||||
|
newAnimeSearchResponse(title, href, type) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
addSub(epNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newHomePageResponse(request.name, home)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProperAnimeLink(uri: String): String {
|
||||||
|
return if (uri.contains("-episode")) {
|
||||||
|
val href =
|
||||||
|
"$mainUrl/anime/" + Regex("\\w\\d/(.*)-episode.*").find(uri)?.groupValues?.get(1)
|
||||||
|
.toString()
|
||||||
|
when {
|
||||||
|
href.contains("pokemon") -> href.replace(Regex("-[0-9]+"), "")
|
||||||
|
else -> href
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val link = "$mainUrl/?s=$query"
|
||||||
|
val document = app.get(link).document
|
||||||
|
|
||||||
|
return document.select(".anime-list > li").map {
|
||||||
|
val title = it.selectFirst("a.name")!!.text()
|
||||||
|
val poster = it.selectFirst("img")!!.attr("src")
|
||||||
|
val tvType = getType(it.selectFirst(".taglist > span")?.text().toString())
|
||||||
|
val href = fixUrl(it.selectFirst("a.name")!!.attr("href"))
|
||||||
|
|
||||||
|
newAnimeSearchResponse(title, href, tvType) {
|
||||||
|
this.posterUrl = poster
|
||||||
|
addDubStatus(dubExist = false, subExist = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val document = app.get(url).document
|
||||||
|
|
||||||
|
val title = document.selectFirst(".entry-title")?.text().toString()
|
||||||
|
val poster = document.selectFirst(".thumbposter > img")?.attr("data-lazy-src")
|
||||||
|
val tags = document.select(".genxed > a").map { it.text() }
|
||||||
|
|
||||||
|
val year = Regex("\\d, ([0-9]*)").find(
|
||||||
|
document.select("time[itemprop = datePublished]").text()
|
||||||
|
)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
|
val status = getStatus(document.selectFirst(".spe > span")!!.ownText())
|
||||||
|
val description = document.select("div[itemprop = description] > p").text()
|
||||||
|
val trailer = document.selectFirst("div.embed-responsive noscript iframe")?.attr("src")
|
||||||
|
val episodes = parseJson<List<EpisodeElement>>(
|
||||||
|
Regex("var episodelist = (\\[.*])").find(
|
||||||
|
document.select(".bixbox.bxcl.epcheck > script").toString().trim()
|
||||||
|
)?.groupValues?.get(1).toString().replace(Regex("""\\"""), "").trim()
|
||||||
|
).map {
|
||||||
|
val name =
|
||||||
|
Regex("(Episode\\s?[0-9]+)").find(it.epTitle.toString())?.groupValues?.getOrNull(0)
|
||||||
|
?: it.epTitle
|
||||||
|
val link = it.epLink
|
||||||
|
Episode(link, name)
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
this.year = year
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
this.tags = tags
|
||||||
|
addTrailer(trailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val document = app.get(data).document
|
||||||
|
|
||||||
|
val scriptData = document.select("aside.sidebar > script").dataNodes().toString()
|
||||||
|
val key = scriptData.substringAfter("var a_ray = '").substringBefore("';")
|
||||||
|
val title = scriptData.substringAfter("var judul_postingan = \"").substringBefore("\";")
|
||||||
|
|
||||||
|
val sources: List<Pair<String, String>> = app.post(
|
||||||
|
url = "https://path.gomuni.me/app/vapi.php",
|
||||||
|
data = mapOf("data" to key, "judul" to title, "func" to "mirror")
|
||||||
|
).document.select("div.gomunime-server-mirror").map {
|
||||||
|
Pair(
|
||||||
|
it.attr("data-vhash"),
|
||||||
|
it.attr("data-type")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.apmap {
|
||||||
|
safeApiCall {
|
||||||
|
when {
|
||||||
|
it.second.contains("frame") -> {
|
||||||
|
loadExtractor(it.first, data, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
it.second.contains("hls") -> {
|
||||||
|
app.post(
|
||||||
|
url = "https://path.gomuni.me/app/vapi.php",
|
||||||
|
data = mapOf("fid" to it.first, "func" to "hls")
|
||||||
|
).text.let { link ->
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
this.name,
|
||||||
|
link,
|
||||||
|
"$mainUrl/",
|
||||||
|
headers = mapOf("Origin" to mainUrl)
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.second.contains("mp4") -> {
|
||||||
|
app.post(
|
||||||
|
url = "https://path.gomuni.me/app/vapi.php",
|
||||||
|
data = mapOf("data" to it.first, "func" to "blogs")
|
||||||
|
).parsed<List<MobiSource>>().map {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = name,
|
||||||
|
name = "Mobi SD",
|
||||||
|
url = it.file,
|
||||||
|
referer = "$mainUrl/",
|
||||||
|
quality = Qualities.P360.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Response(
|
||||||
|
@JsonProperty("status") val status: Boolean,
|
||||||
|
@JsonProperty("html") val html: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MobiSource(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
@JsonProperty("type") val type: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class EpisodeElement(
|
||||||
|
@JsonProperty("data-index") val dataIndex: Long?,
|
||||||
|
@JsonProperty("ep-num") val epNum: String?,
|
||||||
|
@JsonProperty("ep-title") val epTitle: String?,
|
||||||
|
@JsonProperty("ep-link") val epLink: String,
|
||||||
|
@JsonProperty("ep-date") val epDate: 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 GomunimeProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(GomunimeProvider())
|
||||||
|
}
|
||||||
|
}
|
22
JKAnimeProvider/build.gradle.kts
Normal file
22
JKAnimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
JKAnimeProvider/src/main/AndroidManifest.xml
Normal file
2
JKAnimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
319
JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProvider.kt
Normal file
319
JKAnimeProvider/src/main/kotlin/com/lagradost/JKAnimeProvider.kt
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
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.M3u8Helper.Companion.generateM3u8
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class JKAnimeProvider : MainAPI() {
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA
|
||||||
|
else if (t.contains("Pelicula")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var mainUrl = "https://jkanime.net"
|
||||||
|
override var name = "JKAnime"
|
||||||
|
override var lang = "es"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val hasChromecastSupport = true
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA,
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val urls = listOf(
|
||||||
|
Pair(
|
||||||
|
"$mainUrl/directorio/?filtro=fecha&tipo=TV&estado=1&fecha=none&temporada=none&orden=desc",
|
||||||
|
"En emisión"
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"$mainUrl/directorio/?filtro=fecha&tipo=none&estado=none&fecha=none&temporada=none&orden=none",
|
||||||
|
"Animes"
|
||||||
|
),
|
||||||
|
Pair(
|
||||||
|
"$mainUrl/directorio/?filtro=fecha&tipo=Movie&estado=none&fecha=none&temporada=none&orden=none",
|
||||||
|
"Películas"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val items = ArrayList<HomePageList>()
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
HomePageList(
|
||||||
|
"Últimos episodios",
|
||||||
|
app.get(mainUrl).document.select(".listadoanime-home a.bloqq").map {
|
||||||
|
val title = it.selectFirst("h5")?.text()
|
||||||
|
val dubstat = if (title!!.contains("Latino") || title.contains("Castellano"))
|
||||||
|
DubStatus.Dubbed else DubStatus.Subbed
|
||||||
|
val poster =
|
||||||
|
it.selectFirst(".anime__sidebar__comment__item__pic img")?.attr("src") ?: ""
|
||||||
|
val epRegex = Regex("/(\\d+)/|/especial/|/ova/")
|
||||||
|
val url = it.attr("href").replace(epRegex, "")
|
||||||
|
val epNum =
|
||||||
|
it.selectFirst("h6")?.text()?.replace("Episodio ", "")?.toIntOrNull()
|
||||||
|
newAnimeSearchResponse(title, url) {
|
||||||
|
this.posterUrl = poster
|
||||||
|
addDubStatus(dubstat, epNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
urls.apmap { (url, name) ->
|
||||||
|
val soup = app.get(url).document
|
||||||
|
val home = soup.select(".g-0").map {
|
||||||
|
val title = it.selectFirst("h5 a")?.text()
|
||||||
|
val poster = it.selectFirst("img")?.attr("src") ?: ""
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title!!,
|
||||||
|
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
fixUrl(poster),
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
|
||||||
|
DubStatus.Dubbed
|
||||||
|
) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items.add(HomePageList(name, home))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size <= 0) throw ErrorLoadingException()
|
||||||
|
return HomePageResponse(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MainSearch(
|
||||||
|
@JsonProperty("animes") val animes: List<Animes>,
|
||||||
|
@JsonProperty("anime_types") val animeTypes: AnimeTypes
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Animes(
|
||||||
|
@JsonProperty("id") val id: String,
|
||||||
|
@JsonProperty("slug") val slug: String,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("image") val image: String,
|
||||||
|
@JsonProperty("synopsis") val synopsis: String,
|
||||||
|
@JsonProperty("type") val type: String,
|
||||||
|
@JsonProperty("status") val status: String,
|
||||||
|
@JsonProperty("thumbnail") val thumbnail: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AnimeTypes(
|
||||||
|
@JsonProperty("TV") val TV: String,
|
||||||
|
@JsonProperty("OVA") val OVA: String,
|
||||||
|
@JsonProperty("Movie") val Movie: String,
|
||||||
|
@JsonProperty("Special") val Special: String,
|
||||||
|
@JsonProperty("ONA") val ONA: String,
|
||||||
|
@JsonProperty("Music") val Music: String
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val main = app.get("$mainUrl/ajax/ajax_search/?q=$query").text
|
||||||
|
val json = parseJson<MainSearch>(main)
|
||||||
|
return json.animes.map {
|
||||||
|
val title = it.title
|
||||||
|
val href = "$mainUrl/${it.slug}"
|
||||||
|
val image = "https://cdn.jkanime.net/assets/images/animes/image/${it.slug}.jpg"
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
image,
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
|
||||||
|
DubStatus.Dubbed
|
||||||
|
) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url, timeout = 120).document
|
||||||
|
val poster = doc.selectFirst(".set-bg")?.attr("data-setbg")
|
||||||
|
val title = doc.selectFirst(".anime__details__title > h3")?.text()
|
||||||
|
val type = doc.selectFirst(".anime__details__text")?.text()
|
||||||
|
val description = doc.selectFirst(".anime__details__text > p")?.text()
|
||||||
|
val genres = doc.select("div.col-lg-6:nth-child(1) > ul:nth-child(1) > li:nth-child(2) > a")
|
||||||
|
.map { it.text() }
|
||||||
|
val status = when (doc.selectFirst("span.enemision")?.text()) {
|
||||||
|
"En emisión" -> ShowStatus.Ongoing
|
||||||
|
"Concluido" -> ShowStatus.Completed
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val animeID = doc.selectFirst("div.ml-2")?.attr("data-anime")?.toInt()
|
||||||
|
val animeeps = "$mainUrl/ajax/last_episode/$animeID/"
|
||||||
|
val jsoneps = app.get(animeeps).text
|
||||||
|
val lastepnum =
|
||||||
|
jsoneps.substringAfter("{\"number\":\"").substringBefore("\",\"title\"").toInt()
|
||||||
|
val episodes = (1..lastepnum).map {
|
||||||
|
val link = "${url.removeSuffix("/")}/$it"
|
||||||
|
Episode(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title!!, url, getType(type!!)) {
|
||||||
|
posterUrl = poster
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
tags = genres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Nozomi(
|
||||||
|
@JsonProperty("file") val file: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun streamClean(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
referer: String,
|
||||||
|
quality: String?,
|
||||||
|
callback: (ExtractorLink) -> Unit,
|
||||||
|
m3u8: Boolean
|
||||||
|
): Boolean {
|
||||||
|
callback(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
referer,
|
||||||
|
getQualityFromName(quality),
|
||||||
|
m3u8
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get(data).document.select("script").apmap { script ->
|
||||||
|
if (script.data().contains("var video = []")) {
|
||||||
|
val videos = script.data().replace("\\/", "/")
|
||||||
|
fetchUrls(videos).map {
|
||||||
|
it.replace("$mainUrl/jkfembed.php?u=", "https://embedsito.com/v/")
|
||||||
|
.replace("$mainUrl/jkokru.php?u=", "http://ok.ru/videoembed/")
|
||||||
|
.replace("$mainUrl/jkvmixdrop.php?u=", "https://mixdrop.co/e/")
|
||||||
|
.replace("$mainUrl/jk.php?u=", "$mainUrl/")
|
||||||
|
}.apmap { link ->
|
||||||
|
loadExtractor(link, data, subtitleCallback, callback)
|
||||||
|
if (link.contains("um2.php")) {
|
||||||
|
val doc = app.get(link, referer = data).document
|
||||||
|
val gsplaykey = doc.select("form input[value]").attr("value")
|
||||||
|
app.post(
|
||||||
|
"$mainUrl/gsplay/redirect_post.php",
|
||||||
|
headers = mapOf(
|
||||||
|
"Host" to "jkanime.net",
|
||||||
|
"User-Agent" to USER_AGENT,
|
||||||
|
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||||
|
"Accept-Language" to "en-US,en;q=0.5",
|
||||||
|
"Referer" to link,
|
||||||
|
"Content-Type" to "application/x-www-form-urlencoded",
|
||||||
|
"Origin" to "https://jkanime.net",
|
||||||
|
"DNT" to "1",
|
||||||
|
"Connection" to "keep-alive",
|
||||||
|
"Upgrade-Insecure-Requests" to "1",
|
||||||
|
"Sec-Fetch-Dest" to "iframe",
|
||||||
|
"Sec-Fetch-Mode" to "navigate",
|
||||||
|
"Sec-Fetch-Site" to "same-origin",
|
||||||
|
"TE" to "trailers",
|
||||||
|
"Pragma" to "no-cache",
|
||||||
|
"Cache-Control" to "no-cache",
|
||||||
|
),
|
||||||
|
data = mapOf(Pair("data", gsplaykey)),
|
||||||
|
allowRedirects = false
|
||||||
|
).okhttpResponse.headers.values("location").apmap { loc ->
|
||||||
|
val postkey = loc.replace("/gsplay/player.html#", "")
|
||||||
|
val nozomitext = app.post(
|
||||||
|
"$mainUrl/gsplay/api.php",
|
||||||
|
headers = mapOf(
|
||||||
|
"Host" to "jkanime.net",
|
||||||
|
"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://jkanime.net",
|
||||||
|
"DNT" to "1",
|
||||||
|
"Connection" to "keep-alive",
|
||||||
|
"Sec-Fetch-Dest" to "empty",
|
||||||
|
"Sec-Fetch-Mode" to "cors",
|
||||||
|
"Sec-Fetch-Site" to "same-origin",
|
||||||
|
),
|
||||||
|
data = mapOf(Pair("v", postkey)),
|
||||||
|
allowRedirects = false
|
||||||
|
).text
|
||||||
|
val json = parseJson<Nozomi>(nozomitext)
|
||||||
|
val nozomiurl = listOf(json.file)
|
||||||
|
if (nozomiurl.isEmpty()) null else
|
||||||
|
nozomiurl.forEach { url ->
|
||||||
|
val nozominame = "Nozomi"
|
||||||
|
streamClean(
|
||||||
|
nozominame,
|
||||||
|
url!!,
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
callback,
|
||||||
|
url.contains(".m3u8")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (link.contains("um.php")) {
|
||||||
|
val desutext = app.get(link, referer = data).text
|
||||||
|
val desuRegex = Regex("((https:|http:)//.*\\.m3u8)")
|
||||||
|
val file = desuRegex.find(desutext)?.value
|
||||||
|
val namedesu = "Desu"
|
||||||
|
generateM3u8(
|
||||||
|
namedesu,
|
||||||
|
file!!,
|
||||||
|
mainUrl,
|
||||||
|
).forEach { desurl ->
|
||||||
|
streamClean(
|
||||||
|
namedesu,
|
||||||
|
desurl.url,
|
||||||
|
mainUrl,
|
||||||
|
desurl.quality.toString(),
|
||||||
|
callback,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (link.contains("jkmedia")) {
|
||||||
|
app.get(
|
||||||
|
link,
|
||||||
|
referer = data,
|
||||||
|
allowRedirects = false
|
||||||
|
).okhttpResponse.headers.values("location").apmap { xtremeurl ->
|
||||||
|
val namex = "Xtreme S"
|
||||||
|
streamClean(
|
||||||
|
namex,
|
||||||
|
xtremeurl,
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
callback,
|
||||||
|
xtremeurl.contains(".m3u8")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 JKAnimeProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(JKAnimeProvider())
|
||||||
|
}
|
||||||
|
}
|
22
KawaiifuProvider/build.gradle.kts
Normal file
22
KawaiifuProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
KawaiifuProvider/src/main/AndroidManifest.xml
Normal file
2
KawaiifuProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
KimCartoonProvider/build.gradle.kts
Normal file
22
KimCartoonProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
KimCartoonProvider/src/main/AndroidManifest.xml
Normal file
2
KimCartoonProvider/src/main/AndroidManifest.xml
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
22
KuramanimeProvider/build.gradle.kts
Normal file
22
KuramanimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
KuramanimeProvider/src/main/AndroidManifest.xml
Normal file
2
KuramanimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,176 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class KuramanimeProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://kuramanime.com"
|
||||||
|
override var name = "Kuramanime"
|
||||||
|
override val hasQuickSearch = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override var lang = "id"
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||||
|
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(t: String): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"Selesai Tayang" -> ShowStatus.Completed
|
||||||
|
"Sedang Tayang" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/anime/ongoing?order_by=updated&page=" to "Sedang Tayang",
|
||||||
|
"$mainUrl/anime/finished?order_by=updated&page=" to "Selesai Tayang",
|
||||||
|
"$mainUrl/properties/season/summer-2022?order_by=most_viewed&page=" to "Dilihat Terbanyak Musim Ini",
|
||||||
|
"$mainUrl/anime/movie?order_by=updated&page=" to "Film Layar Lebar",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(
|
||||||
|
page: Int,
|
||||||
|
request: MainPageRequest
|
||||||
|
): HomePageResponse {
|
||||||
|
val document = app.get(request.data + page).document
|
||||||
|
|
||||||
|
val home = document.select("div.col-lg-4.col-md-6.col-sm-6").mapNotNull {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newHomePageResponse(request.name, home)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProperAnimeLink(uri: String): String {
|
||||||
|
return if (uri.contains("/episode")) {
|
||||||
|
Regex("(.*)/episode/.+").find(uri)?.groupValues?.get(1).toString() + "/"
|
||||||
|
} else {
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResult(): AnimeSearchResponse? {
|
||||||
|
val href = getProperAnimeLink(fixUrl(this.selectFirst("a")!!.attr("href")))
|
||||||
|
val title = this.selectFirst("h5 a")?.text() ?: return null
|
||||||
|
val posterUrl = fixUrl(this.select("div.product__item__pic.set-bg").attr("data-setbg"))
|
||||||
|
val episode = Regex("([0-9*])\\s?/").find(
|
||||||
|
this.select("div.ep span").text()
|
||||||
|
)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
|
||||||
|
return newAnimeSearchResponse(title, href, TvType.Anime) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
addSub(episode)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val link = "$mainUrl/anime?search=$query&order_by=oldest"
|
||||||
|
val document = app.get(link).document
|
||||||
|
|
||||||
|
return document.select(".product__item").mapNotNull {
|
||||||
|
val title = it.selectFirst("div.product__item__text > h5")!!.text().trim()
|
||||||
|
val poster = it.selectFirst("a > div")!!.attr("data-setbg")
|
||||||
|
val tvType =
|
||||||
|
getType(it.selectFirst(".product__item__text > ul > li")!!.text().toString())
|
||||||
|
val href = fixUrl(it.selectFirst("a")!!.attr("href"))
|
||||||
|
|
||||||
|
newAnimeSearchResponse(title, href, tvType) {
|
||||||
|
this.posterUrl = poster
|
||||||
|
addDubStatus(dubExist = false, subExist = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val document = app.get(url).document
|
||||||
|
|
||||||
|
val title = document.selectFirst(".anime__details__title > h3")!!.text().trim()
|
||||||
|
val poster = document.selectFirst(".anime__details__pic")?.attr("data-setbg")
|
||||||
|
val tags =
|
||||||
|
document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(1)")
|
||||||
|
.text().trim().replace("Genre: ", "").split(", ")
|
||||||
|
|
||||||
|
val year = Regex("[^0-9]").replace(
|
||||||
|
document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(5)")
|
||||||
|
.text().trim().replace("Musim: ", ""), ""
|
||||||
|
).toIntOrNull()
|
||||||
|
val status = getStatus(
|
||||||
|
document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(3)")
|
||||||
|
.text().trim().replace("Status: ", "")
|
||||||
|
)
|
||||||
|
val description = document.select(".anime__details__text > p").text().trim()
|
||||||
|
|
||||||
|
val episodes =
|
||||||
|
Jsoup.parse(document.select("#episodeLists").attr("data-content")).select("a").map {
|
||||||
|
val name = it.text().trim()
|
||||||
|
val link = it.attr("href")
|
||||||
|
Episode(link, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
val recommendations = document.select("div#randomList > a").mapNotNull {
|
||||||
|
val epHref = it.attr("href")
|
||||||
|
val epTitle = it.select("h5.sidebar-title-h5.px-2.py-2").text()
|
||||||
|
val epPoster = it.select(".product__sidebar__view__item.set-bg").attr("data-setbg")
|
||||||
|
|
||||||
|
newAnimeSearchResponse(epTitle, epHref, TvType.Anime) {
|
||||||
|
this.posterUrl = epPoster
|
||||||
|
addDubStatus(dubExist = false, subExist = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
this.year = year
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
this.tags = tags
|
||||||
|
this.recommendations = recommendations
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val servers = app.get(data).document
|
||||||
|
servers.select("video#player > source").map {
|
||||||
|
suspendSafeApiCall {
|
||||||
|
val url = it.attr("src")
|
||||||
|
val quality = it.attr("size").toInt()
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
referer = "$mainUrl/",
|
||||||
|
quality = quality
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 KuramanimeProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(KuramanimeProvider())
|
||||||
|
}
|
||||||
|
}
|
22
KuronimeProvider/build.gradle.kts
Normal file
22
KuronimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
KuronimeProvider/src/main/AndroidManifest.xml
Normal file
2
KuronimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,197 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
|
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.Element
|
||||||
|
|
||||||
|
class KuronimeProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://45.12.2.2"
|
||||||
|
override var name = "Kuronime"
|
||||||
|
override val hasQuickSearch = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override var lang = "id"
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||||
|
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(t: String): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"Completed" -> ShowStatus.Completed
|
||||||
|
"Ongoing" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/page/" to "New Episodes",
|
||||||
|
"$mainUrl/popular-anime/page/" to "Popular Anime",
|
||||||
|
"$mainUrl/movies/page/" to "Movies",
|
||||||
|
"$mainUrl/genres/donghua/page/" to "Donghua",
|
||||||
|
"$mainUrl/live-action/page/" to "Live Action",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(
|
||||||
|
page: Int,
|
||||||
|
request: MainPageRequest
|
||||||
|
): HomePageResponse {
|
||||||
|
val document = app.get(request.data + page).document
|
||||||
|
val home = document.select("article").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, home)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProperAnimeLink(uri: String): String {
|
||||||
|
return if (uri.contains("/anime/")) {
|
||||||
|
uri
|
||||||
|
} else {
|
||||||
|
var title = uri.substringAfter("$mainUrl/")
|
||||||
|
title = when {
|
||||||
|
(title.contains("-episode")) && !(title.contains("-movie")) -> Regex("nonton-(.+)-episode").find(
|
||||||
|
title
|
||||||
|
)?.groupValues?.get(1).toString()
|
||||||
|
(title.contains("-movie")) -> Regex("nonton-(.+)-movie").find(title)?.groupValues?.get(
|
||||||
|
1
|
||||||
|
).toString()
|
||||||
|
else -> title
|
||||||
|
}
|
||||||
|
|
||||||
|
"$mainUrl/anime/$title"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResult(): AnimeSearchResponse {
|
||||||
|
val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString())
|
||||||
|
val title = this.select(".bsuxtt, .tt > h4").text().trim()
|
||||||
|
val posterUrl = fixUrlNull(
|
||||||
|
this.selectFirst("div.view,div.bt")?.nextElementSibling()?.select("img")
|
||||||
|
?.attr("data-src")
|
||||||
|
)
|
||||||
|
val epNum = this.select(".ep").text().replace(Regex("[^0-9]"), "").trim().toIntOrNull()
|
||||||
|
val tvType = getType(this.selectFirst(".bt > span")?.text().toString())
|
||||||
|
return newAnimeSearchResponse(title, href, tvType) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
addSub(epNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val link = "$mainUrl/?s=$query"
|
||||||
|
val document = app.get(link).document
|
||||||
|
|
||||||
|
return document.select("article.bs").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val document = app.get(url).document
|
||||||
|
|
||||||
|
val title = document.selectFirst(".entry-title")?.text().toString().trim()
|
||||||
|
val poster = document.selectFirst("div.l[itemprop=image] > img")?.attr("data-src")
|
||||||
|
val tags = document.select(".infodetail > ul > li:nth-child(2) > a").map { it.text() }
|
||||||
|
val type = getType(
|
||||||
|
document.selectFirst(".infodetail > ul > li:nth-child(7)")?.ownText()?.trim().toString()
|
||||||
|
)
|
||||||
|
val trailer = document.selectFirst("div.tply iframe")?.attr("data-lazy-src")
|
||||||
|
val year = Regex("\\d, ([0-9]*)").find(
|
||||||
|
document.select(".infodetail > ul > li:nth-child(5)").text()
|
||||||
|
)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
|
val status = getStatus(
|
||||||
|
document.selectFirst(".infodetail > ul > li:nth-child(3)")!!.ownText()
|
||||||
|
.replace(Regex("\\W"), "")
|
||||||
|
)
|
||||||
|
val description = document.select("span.const > p").text()
|
||||||
|
|
||||||
|
val episodes = document.select("div.bixbox.bxcl > ul > li").map {
|
||||||
|
val name = it.selectFirst("a")?.text()?.trim()
|
||||||
|
val episode =
|
||||||
|
it.selectFirst("a")?.text()?.trim()?.replace("Episode", "")?.trim()?.toIntOrNull()
|
||||||
|
val link = it.selectFirst("a")!!.attr("href")
|
||||||
|
Episode(link, name = name, episode = episode)
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, type) {
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
this.year = year
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
addTrailer(trailer)
|
||||||
|
this.tags = tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun invokeKuroSource(
|
||||||
|
url: String,
|
||||||
|
sourceCallback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val doc = app.get(url, referer = "${mainUrl}/").document
|
||||||
|
|
||||||
|
doc.select("script").map { script ->
|
||||||
|
if (script.data().contains("function jalankan_jwp() {")) {
|
||||||
|
val data = script.data()
|
||||||
|
val doma = data.substringAfter("var doma = \"").substringBefore("\";")
|
||||||
|
val token = data.substringAfter("var token = \"").substringBefore("\";")
|
||||||
|
val pat = data.substringAfter("var pat = \"").substringBefore("\";")
|
||||||
|
val link = "$doma$token$pat/index.m3u8"
|
||||||
|
val quality =
|
||||||
|
Regex("\\d{3,4}p").find(doc.select("title").text())?.groupValues?.get(0)
|
||||||
|
|
||||||
|
sourceCallback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
link,
|
||||||
|
referer = "https://animeku.org/",
|
||||||
|
quality = getQualityFromName(quality),
|
||||||
|
headers = mapOf("Origin" to "https://animeku.org"),
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val document = app.get(data).document
|
||||||
|
val sources = document.select(".mobius > .mirror > option").mapNotNull {
|
||||||
|
fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("data-src"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.apmap {
|
||||||
|
safeApiCall {
|
||||||
|
when {
|
||||||
|
it.startsWith("https://animeku.org") -> invokeKuroSource(it, callback)
|
||||||
|
else -> loadExtractor(it, mainUrl, 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 KuronimeProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(KuronimeProvider())
|
||||||
|
}
|
||||||
|
}
|
22
MonoschinosProvider/build.gradle.kts
Normal file
22
MonoschinosProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
MonoschinosProvider/src/main/AndroidManifest.xml
Normal file
2
MonoschinosProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class MonoschinosProvider : MainAPI() {
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA
|
||||||
|
else if (t.contains("Pelicula")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDubStatus(title: String): DubStatus {
|
||||||
|
return if (title.contains("Latino") || title.contains("Castellano"))
|
||||||
|
DubStatus.Dubbed
|
||||||
|
else DubStatus.Subbed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var mainUrl = "https://monoschinos2.com"
|
||||||
|
override var name = "Monoschinos"
|
||||||
|
override var lang = "es"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val hasChromecastSupport = true
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA,
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val urls = listOf(
|
||||||
|
Pair("$mainUrl/emision", "En emisión"),
|
||||||
|
Pair(
|
||||||
|
"$mainUrl/animes?categoria=pelicula&genero=false&fecha=false&letra=false",
|
||||||
|
"Peliculas"
|
||||||
|
),
|
||||||
|
Pair("$mainUrl/animes", "Animes"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val items = ArrayList<HomePageList>()
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
HomePageList(
|
||||||
|
"Capítulos actualizados",
|
||||||
|
app.get(mainUrl, timeout = 120).document.select(".col-6").map {
|
||||||
|
val title = it.selectFirst("p.animetitles")?.text() ?: it.selectFirst(".animetitles")?.text() ?: ""
|
||||||
|
val poster = it.selectFirst(".animeimghv")!!.attr("data-src")
|
||||||
|
val epRegex = Regex("episodio-(\\d+)")
|
||||||
|
val url = it.selectFirst("a")?.attr("href")!!.replace("ver/", "anime/")
|
||||||
|
.replace(epRegex, "sub-espanol")
|
||||||
|
val epNum = (it.selectFirst(".positioning h5")?.text() ?: it.selectFirst("div.positioning p")?.text())?.toIntOrNull()
|
||||||
|
newAnimeSearchResponse(title, url) {
|
||||||
|
this.posterUrl = fixUrl(poster)
|
||||||
|
addDubStatus(getDubStatus(title), epNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
for (i in urls) {
|
||||||
|
try {
|
||||||
|
val home = app.get(i.first, timeout = 120).document.select(".col-6").map {
|
||||||
|
val title = it.selectFirst(".seristitles")!!.text()
|
||||||
|
val poster = it.selectFirst("img.animemainimg")!!.attr("src")
|
||||||
|
newAnimeSearchResponse(title, fixUrl(it.selectFirst("a")!!.attr("href"))) {
|
||||||
|
this.posterUrl = fixUrl(poster)
|
||||||
|
addDubStatus(getDubStatus(title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(HomePageList(i.second, home))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size <= 0) throw ErrorLoadingException()
|
||||||
|
return HomePageResponse(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): ArrayList<SearchResponse> {
|
||||||
|
val search =
|
||||||
|
app.get("$mainUrl/buscar?q=$query", timeout = 120).document.select(".col-6").map {
|
||||||
|
val title = it.selectFirst(".seristitles")!!.text()
|
||||||
|
val href = fixUrl(it.selectFirst("a")!!.attr("href"))
|
||||||
|
val image = it.selectFirst("img.animemainimg")!!.attr("src")
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
fixUrl(image),
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
|
||||||
|
DubStatus.Dubbed
|
||||||
|
) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ArrayList(search)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url, timeout = 120).document
|
||||||
|
val poster = doc.selectFirst(".chapterpic img")!!.attr("src")
|
||||||
|
val title = doc.selectFirst(".chapterdetails h1")!!.text()
|
||||||
|
val type = doc.selectFirst("div.chapterdetls2")!!.text()
|
||||||
|
val description = doc.selectFirst("p.textComplete")!!.text().replace("Ver menos", "")
|
||||||
|
val genres = doc.select(".breadcrumb-item a").map { it.text() }
|
||||||
|
val status = when (doc.selectFirst("button.btn1")?.text()) {
|
||||||
|
"Estreno" -> ShowStatus.Ongoing
|
||||||
|
"Finalizado" -> ShowStatus.Completed
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val episodes = doc.select("div.col-item").map {
|
||||||
|
val name = it.selectFirst("p.animetitles")!!.text()
|
||||||
|
val link = it.selectFirst("a")!!.attr("href")
|
||||||
|
val epThumb = it.selectFirst(".animeimghv")!!.attr("data-src")
|
||||||
|
Episode(link, name, posterUrl = epThumb)
|
||||||
|
}
|
||||||
|
return newAnimeLoadResponse(title, url, getType(type)) {
|
||||||
|
posterUrl = poster
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
tags = genres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get(data).document.select("div.playother p").forEach {
|
||||||
|
val encodedurl = it.select("p").attr("data-player")
|
||||||
|
val urlDecoded = base64Decode(encodedurl)
|
||||||
|
val url = (urlDecoded).replace("https://monoschinos2.com/reproductor?url=", "")
|
||||||
|
if (url.startsWith("https://www.fembed.com")) {
|
||||||
|
val extractor = FEmbed()
|
||||||
|
extractor.getUrl(url).forEach { link ->
|
||||||
|
callback.invoke(link)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadExtractor(url, mainUrl, 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 MonoschinosProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(MonoschinosProvider())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
class FEmbed: XStreamCdn() {
|
||||||
|
override val name: String = "FEmbed"
|
||||||
|
override val mainUrl: String = "https://www.fembed.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class XStreamCdn : ExtractorApi() {
|
||||||
|
override val name: String = "XStreamCdn"
|
||||||
|
override val mainUrl: String = "https://embedsito.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
open var domainUrl: String = "embedsito.com"
|
||||||
|
|
||||||
|
private data class ResponseData(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
//val type: String // Mp4
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ResponseJson(
|
||||||
|
@JsonProperty("success") val success: Boolean,
|
||||||
|
@JsonProperty("data") val data: List<ResponseData>?
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getExtractorUrl(id: String): String {
|
||||||
|
return "$domainUrl/api/source/$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val headers = mapOf(
|
||||||
|
"Referer" to url,
|
||||||
|
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
||||||
|
)
|
||||||
|
val id = url.trimEnd('/').split("/").last()
|
||||||
|
val newUrl = "https://${domainUrl}/api/source/${id}"
|
||||||
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
with(app.post(newUrl, headers = headers)) {
|
||||||
|
if (this.code != 200) return listOf()
|
||||||
|
val text = this.text
|
||||||
|
if (text.isEmpty()) return listOf()
|
||||||
|
if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf()
|
||||||
|
AppUtils.parseJson<ResponseJson?>(text)?.let {
|
||||||
|
if (it.success && it.data != null) {
|
||||||
|
it.data.forEach { data ->
|
||||||
|
extractedLinksList.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name = name,
|
||||||
|
data.file,
|
||||||
|
url,
|
||||||
|
getQualityFromName(data.label),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractedLinksList
|
||||||
|
}
|
||||||
|
}
|
22
MundoDonghuaProvider/build.gradle.kts
Normal file
22
MundoDonghuaProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
MundoDonghuaProvider/src/main/AndroidManifest.xml
Normal file
2
MundoDonghuaProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -0,0 +1,217 @@
|
||||||
|
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.M3u8Helper.Companion.generateM3u8
|
||||||
|
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class MundoDonghuaProvider : MainAPI() {
|
||||||
|
|
||||||
|
override var mainUrl = "https://www.mundodonghua.com"
|
||||||
|
override var name = "MundoDonghua"
|
||||||
|
override var lang = "es"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val hasChromecastSupport = true
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val urls = listOf(
|
||||||
|
Pair("$mainUrl/lista-donghuas", "Donghuas"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val items = ArrayList<HomePageList>()
|
||||||
|
items.add(
|
||||||
|
HomePageList(
|
||||||
|
"Últimos episodios",
|
||||||
|
app.get(mainUrl, timeout = 120).document.select("div.row .col-xs-4").map {
|
||||||
|
val title = it.selectFirst("h5")?.text() ?: ""
|
||||||
|
val poster = it.selectFirst(".fit-1 img")?.attr("src")
|
||||||
|
val epRegex = Regex("(\\/(\\d+)\$)")
|
||||||
|
val url = it.selectFirst("a")?.attr("href")?.replace(epRegex,"")?.replace("/ver/","/donghua/")
|
||||||
|
val epnumRegex = Regex("((\\d+)$)")
|
||||||
|
val epNum = epnumRegex.find(title)?.value?.toIntOrNull()
|
||||||
|
val dubstat = if (title.contains("Latino") || title.contains("Castellano")) DubStatus.Dubbed else DubStatus.Subbed
|
||||||
|
newAnimeSearchResponse(title.replace(Regex("Episodio|(\\d+)"),"").trim(), fixUrl(url ?: "")) {
|
||||||
|
this.posterUrl = fixUrl(poster ?: "")
|
||||||
|
addDubStatus(dubstat, epNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
urls.apmap { (url, name) ->
|
||||||
|
val home = app.get(url, timeout = 120).document.select(".col-xs-4").map {
|
||||||
|
val title = it.selectFirst(".fs-14")?.text() ?: ""
|
||||||
|
val poster = it.selectFirst(".fit-1 img")?.attr("src") ?: ""
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
fixUrl(poster),
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
|
||||||
|
DubStatus.Dubbed
|
||||||
|
) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(HomePageList(name, home))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size <= 0) throw ErrorLoadingException()
|
||||||
|
return HomePageResponse(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
return app.get("$mainUrl/busquedas/$query", timeout = 120).document.select(".col-xs-4").map {
|
||||||
|
val title = it.selectFirst(".fs-14")?.text() ?: ""
|
||||||
|
val href = fixUrl(it.selectFirst("a")?.attr("href") ?: "")
|
||||||
|
val image = it.selectFirst(".fit-1 img")?.attr("src")
|
||||||
|
AnimeSearchResponse(
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
this.name,
|
||||||
|
TvType.Anime,
|
||||||
|
fixUrl(image ?: ""),
|
||||||
|
null,
|
||||||
|
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
|
||||||
|
DubStatus.Dubbed
|
||||||
|
) else EnumSet.of(DubStatus.Subbed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url, timeout = 120).document
|
||||||
|
val poster = doc.selectFirst("head meta[property=og:image]")?.attr("content") ?: ""
|
||||||
|
val title = doc.selectFirst(".ls-title-serie")?.text() ?: ""
|
||||||
|
val description = doc.selectFirst("p.text-justify.fc-dark")?.text() ?: ""
|
||||||
|
val genres = doc.select("span.label.label-primary.f-bold").map { it.text() }
|
||||||
|
val status = when (doc.selectFirst("div.col-md-6.col-xs-6.align-center.bg-white.pt-10.pr-15.pb-0.pl-15 p span.badge.bg-default")?.text()) {
|
||||||
|
"En Emisión" -> ShowStatus.Ongoing
|
||||||
|
"Finalizada" -> ShowStatus.Completed
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val episodes = doc.select("ul.donghua-list a").map {
|
||||||
|
val name = it.selectFirst(".fs-16")?.text()
|
||||||
|
val link = it.attr("href")
|
||||||
|
Episode(fixUrl(link), name)
|
||||||
|
}.reversed()
|
||||||
|
val typeinfo = doc.select("div.row div.col-md-6.pl-15 p.fc-dark").text()
|
||||||
|
val tvType = if (typeinfo.contains(Regex("Tipo.*Pel.cula"))) TvType.AnimeMovie else TvType.Anime
|
||||||
|
return newAnimeLoadResponse(title, url, tvType) {
|
||||||
|
posterUrl = poster
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = status
|
||||||
|
plot = description
|
||||||
|
tags = genres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class Protea (
|
||||||
|
@JsonProperty("source") val source: List<Source>,
|
||||||
|
@JsonProperty("poster") val poster: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Source (
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String?,
|
||||||
|
@JsonProperty("type") val type: String?,
|
||||||
|
@JsonProperty("default") val default: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun cleanStream(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
qualityString: String?,
|
||||||
|
callback: (ExtractorLink) -> Unit,
|
||||||
|
isM3U8: Boolean
|
||||||
|
): Boolean {
|
||||||
|
callback(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
"",
|
||||||
|
getQualityFromName(qualityString),
|
||||||
|
isM3U8
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get(data).document.select("script").apmap { script ->
|
||||||
|
if (script.data().contains("eval(function(p,a,c,k,e")) {
|
||||||
|
val packedRegex = Regex("eval\\(function\\(p,a,c,k,e,.*\\)\\)")
|
||||||
|
packedRegex.findAll(script.data()).map {
|
||||||
|
it.value
|
||||||
|
}.toList().apmap {
|
||||||
|
val unpack = getAndUnpack(it).replace("diasfem","embedsito")
|
||||||
|
fetchUrls(unpack).apmap { url ->
|
||||||
|
loadExtractor(url, data, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
if (unpack.contains("protea_tab")) {
|
||||||
|
val protearegex = Regex("(protea_tab.*slug.*,type)")
|
||||||
|
val slug = protearegex.findAll(unpack).map {
|
||||||
|
it.value.replace(Regex("(protea_tab.*slug\":\")"),"").replace("\"},type","")
|
||||||
|
}.first()
|
||||||
|
val requestlink = "$mainUrl/api_donghua.php?slug=$slug"
|
||||||
|
val response = app.get(requestlink, headers =
|
||||||
|
mapOf("Host" to "www.mundodonghua.com",
|
||||||
|
"User-Agent" to USER_AGENT,
|
||||||
|
"Accept" to "*/*",
|
||||||
|
"Accept-Language" to "en-US,en;q=0.5",
|
||||||
|
"Referer" to data,
|
||||||
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
|
"DNT" to "1",
|
||||||
|
"Connection" to "keep-alive",
|
||||||
|
"Sec-Fetch-Dest" to "empty",
|
||||||
|
"Sec-Fetch-Mode" to "no-cors",
|
||||||
|
"Sec-Fetch-Site" to "same-origin",
|
||||||
|
"TE" to "trailers",
|
||||||
|
"Pragma" to "no-cache",
|
||||||
|
"Cache-Control" to "no-cache",)
|
||||||
|
).text.removePrefix("[").removeSuffix("]")
|
||||||
|
val json = parseJson<Protea>(response)
|
||||||
|
json.source.forEach { source ->
|
||||||
|
val protename = "Protea"
|
||||||
|
cleanStream(protename, fixUrl(source.file), source.label, callback, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unpack.contains("asura_player")) {
|
||||||
|
val asuraRegex = Regex("(asura_player.*type)")
|
||||||
|
asuraRegex.findAll(unpack).map {
|
||||||
|
it.value
|
||||||
|
}.toList().apmap { protea ->
|
||||||
|
val asuraname = "Asura"
|
||||||
|
val file = protea.substringAfter("{file:\"").substringBefore("\"")
|
||||||
|
generateM3u8(
|
||||||
|
asuraname,
|
||||||
|
file,
|
||||||
|
""
|
||||||
|
).forEach {
|
||||||
|
cleanStream(asuraname, it.url, it.quality.toString(), callback, 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 MundoDonghuaProviderPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
// All providers should be added in this manner. Please don't edit the providers list directly.
|
||||||
|
registerMainAPI(MundoDonghuaProvider())
|
||||||
|
}
|
||||||
|
}
|
22
NeonimeProvider/build.gradle.kts
Normal file
22
NeonimeProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// use an integer for version numbers
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// All of these properties are optional, you can safely remove them
|
||||||
|
|
||||||
|
// description = "Lorem Ipsum"
|
||||||
|
// authors = listOf("Cloudburst")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status int as the following:
|
||||||
|
* 0: Down
|
||||||
|
* 1: Ok
|
||||||
|
* 2: Slow
|
||||||
|
* 3: Beta only
|
||||||
|
* */
|
||||||
|
status = 1 // will be 3 if unspecified
|
||||||
|
|
||||||
|
// Set to true to get an 18+ symbol next to the plugin
|
||||||
|
adult = false // will be false if unspecified
|
||||||
|
}
|
2
NeonimeProvider/src/main/AndroidManifest.xml
Normal file
2
NeonimeProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
178
NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProvider.kt
Normal file
178
NeonimeProvider/src/main/kotlin/com/lagradost/NeonimeProvider.kt
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package com.lagradost
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class NeonimeProvider : MainAPI() {
|
||||||
|
override var mainUrl = "https://neonime.watch"
|
||||||
|
override var name = "Neonime"
|
||||||
|
override val hasQuickSearch = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override var lang = "id"
|
||||||
|
override val hasDownloadSupport = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getType(t: String): TvType {
|
||||||
|
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
|
||||||
|
else if (t.contains("Movie")) TvType.AnimeMovie
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(t: String): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"Ended" -> ShowStatus.Completed
|
||||||
|
"OnGoing" -> ShowStatus.Ongoing
|
||||||
|
"Ongoing" -> ShowStatus.Ongoing
|
||||||
|
"In Production" -> ShowStatus.Ongoing
|
||||||
|
"Returning Series" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/episode/page/" to "Episode Terbaru",
|
||||||
|
"$mainUrl/tvshows/page/" to "Anime Terbaru",
|
||||||
|
"$mainUrl/movies/page/" to "Movie",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
|
val document = app.get(request.data + page).document
|
||||||
|
val home = document.select("tbody tr,div.item").mapNotNull {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, home)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProperAnimeLink(uri: String): String {
|
||||||
|
return when {
|
||||||
|
uri.contains("/episode") -> {
|
||||||
|
val title = uri.substringAfter("$mainUrl/episode/").let { tt ->
|
||||||
|
val fixTitle = Regex("(.*)-\\d{1,2}x\\d+").find(tt)?.groupValues?.getOrNull(1).toString()
|
||||||
|
when {
|
||||||
|
!tt.contains("-season") && !tt.contains(Regex("-1x\\d+")) && !tt.contains("one-piece") -> "$fixTitle-season-${Regex("-(\\d{1,2})x\\d+").find(tt)?.groupValues?.getOrNull(1).toString()}"
|
||||||
|
tt.contains("-special") -> fixTitle.replace(Regex("-x\\d+"), "")
|
||||||
|
!fixTitle.contains("-subtitle-indonesia") -> "$fixTitle-subtitle-indonesia"
|
||||||
|
else -> fixTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// title = when {
|
||||||
|
// title.contains("youkoso-jitsuryoku") && !title.contains("-season") -> title.replace("-e-", "-e-tv-")
|
||||||
|
// else -> title
|
||||||
|
// }
|
||||||
|
|
||||||
|
"$mainUrl/tvshows/$title"
|
||||||
|
}
|
||||||
|
else -> uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResult(): AnimeSearchResponse? {
|
||||||
|
val title = this.selectFirst("td.bb a")?.ownText() ?: this.selectFirst("h2")?.text() ?: return null
|
||||||
|
val href = getProperAnimeLink(fixUrl(this.select("a").attr("href")))
|
||||||
|
val posterUrl = fixUrl(this.select("img").attr("data-src"))
|
||||||
|
val epNum = this.selectFirst("td.bb span")?.text()?.let { eps ->
|
||||||
|
Regex("Episode\\s?([0-9]+)").find(eps)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAnimeSearchResponse(title, href, TvType.Anime) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
addSub(epNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val link = "$mainUrl/?s=$query"
|
||||||
|
val document = app.get(link).document
|
||||||
|
|
||||||
|
return document.select("div.item.episode-home").mapNotNull {
|
||||||
|
val title = it.selectFirst("div.judul-anime > span")!!.text()
|
||||||
|
val poster = it.select("img").attr("data-src").toString().trim()
|
||||||
|
val episodes = it.selectFirst("div.fixyear > h2.text-center")!!
|
||||||
|
.text().replace(Regex("[^0-9]"), "").trim().toIntOrNull()
|
||||||
|
val tvType = getType(it.selectFirst("span.calidad2.episode")?.text().toString())
|
||||||
|
val href = getProperAnimeLink(fixUrl(it.selectFirst("a")!!.attr("href")))
|
||||||
|
|
||||||
|
newAnimeSearchResponse(title, href, tvType) {
|
||||||
|
this.posterUrl = poster
|
||||||
|
addSub(episodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val document = app.get(url).document
|
||||||
|
|
||||||
|
if (url.contains("movie") || url.contains("live-action")) {
|
||||||
|
val mTitle = document.selectFirst(".sbox > .data > h1[itemprop = name]")?.text().toString().trim()
|
||||||
|
val mTrailer = document.selectFirst("div.youtube_id iframe")?.attr("data-wpfc-original-src")?.substringAfterLast("html#")?.let{ "https://www.youtube.com/embed/$it"}
|
||||||
|
|
||||||
|
return newMovieLoadResponse(name = mTitle, url = url, type = TvType.Movie, dataUrl = url) {
|
||||||
|
posterUrl = document.selectFirst(".sbox > .imagen > .fix > img[itemprop = image]")?.attr("data-src")
|
||||||
|
year = document.selectFirst("a[href*=release-year]")!!.text().toIntOrNull()
|
||||||
|
plot = document.select("div[itemprop = description]").text().trim()
|
||||||
|
rating = document.select("span[itemprop = ratingValue]").text().toIntOrNull()
|
||||||
|
tags = document.select("p.meta_dd > a").map { it.text() }
|
||||||
|
addTrailer(mTrailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val title = document.select("h1[itemprop = name]").text().trim()
|
||||||
|
val trailer = document.selectFirst("div.youtube_id_tv iframe")?.attr("data-wpfc-original-src")?.substringAfterLast("html#")?.let{ "https://www.youtube.com/embed/$it"}
|
||||||
|
|
||||||
|
val episodes = document.select("ul.episodios > li").mapNotNull {
|
||||||
|
val header = it.selectFirst(".episodiotitle > a")?.ownText().toString()
|
||||||
|
val name = Regex("(Episode\\s?[0-9]+)").find(header)?.groupValues?.getOrNull(0) ?: header
|
||||||
|
val link = fixUrl(it.selectFirst(".episodiotitle > a")!!.attr("href"))
|
||||||
|
Episode(link, name)
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
|
return newAnimeLoadResponse(title, url, TvType.Anime) {
|
||||||
|
engName = title
|
||||||
|
posterUrl = document.selectFirst(".imagen > img")?.attr("data-src")
|
||||||
|
year = document.select("#info a[href*=\"-year/\"]").text().toIntOrNull()
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes)
|
||||||
|
showStatus = getStatus(document.select("div.metadatac > span").last()!!.text().trim())
|
||||||
|
plot = document.select("div[itemprop = description] > p").text().trim()
|
||||||
|
tags = document.select("#info a[href*=\"-genre/\"]").map { it.text() }
|
||||||
|
addTrailer(trailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val source = if(data.contains("movie") || data.contains("live-action")) {
|
||||||
|
app.get(data).document.select("#player2-1 > div[id*=div]").mapNotNull {
|
||||||
|
fixUrl(it.select("iframe").attr("data-src"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.get(data).document.select(".player2 > .embed2 > div[id*=player]").mapNotNull {
|
||||||
|
fixUrl(it.select("iframe").attr("data-src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.apmap {
|
||||||
|
loadExtractor(it, data, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue