Merge pull request #9 from LagradOst/animepahe

Added AnimePahe
This commit is contained in:
Arjix 2021-08-18 03:21:50 +03:00 committed by GitHub
commit 64e5712b46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 535 additions and 0 deletions

View File

@ -30,6 +30,7 @@ object APIHolder {
val apis = arrayListOf(
TrailersToProvider(),
//ShiroProvider(), // v2 fucked me
AnimePaheProvider(),
TenshiProvider(),
WcoProvider(),
// MeloMovieProvider(), // Captcha for links

View File

@ -0,0 +1,534 @@
package com.lagradost.cloudstream3.animeproviders
import com.lagradost.cloudstream3.*
import khttp.structures.cookie.CookieJar
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.getQualityFromName
import org.jsoup.Jsoup
import java.util.*
import kotlin.collections.ArrayList
class AnimePaheProvider : MainAPI() {
companion object {
var cookies = CookieJar()
private fun getType(t: String): TvType {
return if (t.contains("OVA") || t.contains("Special")) TvType.ONA
else if (t.contains("Movie")) TvType.AnimeMovie
else TvType.Anime
}
fun generateSession(): Boolean {
if (cookies.entries.size != 0) return true
return try {
val response = khttp.get("https://animepahe.com/")
cookies = response.cookies
true
} catch (e: Exception) {
false
}
}
val YTSM = "ysmm = '([^']+)".toRegex()
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 val mainUrl: String
get() = "https://animepahe.com"
override val name: String
get() = "AnimePahe"
override val hasQuickSearch: Boolean
get() = false
override val hasMainPage: Boolean
get() = true
override val supportedTypes: Set<TvType>
get() = setOf(
TvType.AnimeMovie,
TvType.Anime,
TvType.ONA
)
override fun getMainPage(): HomePageResponse? {
data class Data (
@JsonProperty("id") val id : Int,
@JsonProperty("anime_id") val animeId : Int,
@JsonProperty("anime_title") val animeTitle : String,
@JsonProperty("episode") val episode : Int,
@JsonProperty("snapshot") val snapshot : String,
@JsonProperty("created_at") val createdAt : 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 = khttp.get(i.first)
val episodes = mapper.readValue<AnimePaheLatestReleases>(response.text).data.map {
AnimeSearchResponse(
it.animeTitle,
"https://pahe.win/a/${it.animeId}",
this.name,
TvType.Anime,
null,
null,
null,
EnumSet.of(DubStatus.Subbed),
null,
it.episode
)
}
items.add(HomePageList(i.second, episodes))
} catch (e: Exception) {
e.printStackTrace()
}
}
if(items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override fun search(query: String): ArrayList<SearchResponse> {
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>
)
val url = "https://animepahe.com/api?m=search&l=8&q=$query"
val headers = mapOf("referer" to "https://animepahe.com/")
val req = khttp.get(url, headers=headers)
val data = req.let { mapper.readValue<AnimePaheSearch>(it.text) }
return ArrayList(data.data.map {
AnimeSearchResponse(
it.title,
"https://pahe.win/a/${it.id}",
this.name,
TvType.Anime,
it.poster,
it.year,
null,
EnumSet.of(DubStatus.Subbed),
null,
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 fun generateListOfEpisodes(link: String): ArrayList<AnimeEpisode> {
try {
val attrs = link.split('/')
val id = attrs[attrs.size - 1]
val uri = "https://animepahe.com/api?m=release&id=$id&sort=episode_asc&page=1"
val headers = mapOf("referer" to "https://animepahe.com/")
val req = khttp.get(uri, headers = headers)
val data = req.let { mapper.readValue<AnimePaheAnimeData>(it.text) }
val lastPage = data.lastPage
val perPage = data.perPage
val total = data.total
var ep = 1
val episodes = ArrayList<AnimeEpisode>()
fun getEpisodeTitle(k: AnimeData): String {
return if (k.title.length == 0) {
"Episode ${k.episode}"
} else {
k.title
}
}
if (lastPage == 1 && perPage > total) {
data.data.forEach {
episodes.add(
AnimeEpisode(
"https://animepahe.com/api?m=links&id=${it.animeId}&session=${it.session}&p=kwik!!TRUE!!",
getEpisodeTitle(it),
if (it.snapshot.length == 0) {null} else {it.snapshot},
it.createdAt
)
)
}
} else {
for (page in 0 until lastPage) {
for (i in 0 until perPage) {
if (ep <= total) {
episodes.add(
AnimeEpisode(
"https://animepahe.com/api?m=release&id=${id}&sort=episode_asc&page=${page + 1}&ep=${ep}!!FALSE!!"
)
)
++ep
}
}
}
}
return episodes
} catch (e: Exception) {
return ArrayList<AnimeEpisode>()
}
}
override fun load(url: String): LoadResponse? {
return normalSafeApiCall {
val html = khttp.get(url).text
val doc = Jsoup.parse(html)
val title = doc.selectFirst(".anime-poster img")?.attr("alt")?.replace("Poster of ", "")
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) ?: ArrayList<AnimeEpisode>()
val year = """<strong>Aired:<\/strong>[^,]*, (\d+)""".toRegex().find(html)!!.destructured?.component1()?.toIntOrNull()
val status = when ("""<strong>Status:<\/strong>[^a]*a href=[\"']\/anime\/(.*?)[\"']""".toRegex().find(html)!!.destructured?.component1().toString()) {
"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()
}
}
AnimeLoadResponse(
title,
japTitle,
title.toString(),
url,
this.name,
getType(TvType.toString()),
poster,
year,
null,
episodes,
status,
synopsis,
if (!doc.select(".anime-genre > ul a").isEmpty()) {
ArrayList(doc.select(".anime-genre > ul a").map { it.text().toString() })
} else { null },
ArrayList(),
malId,
anilistId,
null,
trailer
)
}
}
private fun isNumber(s: String?): Boolean {
return s?.toIntOrNull() != null
}
private fun pow(base: Int, expo: Int): Long{
// does not handle infinity
var exponent = expo
var result: Long = 1
while (exponent != 0) {
result *= base.toLong()
--exponent
}
return result
}
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: String = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/";
val slice2 = characterMap.slice(0..s2-1)
var acc: Long = 0
for ((n, i) in content.reversed().withIndex()) {
acc += (when(isNumber("$i")) {
true -> "$i".toLong()
false -> "0".toLong()
}) * Math.pow(s1.toDouble(), 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<Pair<Int, Int>, Pair<Int, Int>>(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<Int, Int>(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 = String(android.util.Base64.decode(returnValue, android.util.Base64.DEFAULT), Charsets.ISO_8859_1)
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_adfly") val kwikAdfly : String
)
private data class AnimePaheEpisodeLoadLinks (
@JsonProperty("data") val data : List<Map<String, VideoQuality>>
)
private fun bypassAdfly(adflyUri: String): String {
if (!generateSession()) {
return bypassAdfly(adflyUri)
}
var responseCode = 302
var adflyContent: khttp.responses.Response? = null
var tries = 0
while (responseCode != 200 && tries < 20) {
adflyContent = khttp.get(khttp.get(adflyUri, cookies=cookies, allowRedirects = false).headers.getValue("location"), cookies=cookies, allowRedirects = false)
cookies.putAll(adflyContent.cookies.toMap())
responseCode = adflyContent.statusCode
++tries
}
if (tries > 19) {
throw Exception("Failed to bypass adfly.")
}
return decodeAdfly(YTSM.find(adflyContent?.text.toString())!!.destructured.component1())
}
private fun getStreamUrlFromKwik(adflyUri: String): String {
val fContent = khttp.get(bypassAdfly(adflyUri), headers=mapOf("referer" to "https://kwik.cx/"), cookies=cookies)
cookies.putAll(fContent.cookies.toMap())
val (fullString, key, v1, v2) = KWIK_PARAMS_RE.find(fContent.text.toString())!!.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: khttp.responses.Response? = null
var code = 419
var tries = 0
while (code != 302 && tries < 20) {
content = khttp.post(
uri,
allowRedirects = false,
data=mapOf("_token" to tok),
headers=mapOf("referer" to fContent.url),
cookies=cookieStrToMap(fContent.headers.getValue("set-cookie").replace("path=/,", ""))
)
code = content.statusCode
++tries
}
if (tries > 19) {
throw Exception("Failed to extract the stream uri from kwik.")
}
return content?.headers?.getValue("location").toString()
}
private fun extractVideoLinks(episodeLink: String): List<ExtractorLink> {
var link = episodeLink
val headers = mapOf("referer" to "https://animepahe.com/")
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 = khttp.get(link, headers = headers)
val jsonResponse = req.let { mapper.readValue<AnimePaheAnimeData>(it.text) }
val ep = ((jsonResponse.data.map {
if (it.episode == episodeNum) {
it
} else {
null
}
}).filterNotNull())[0]
link = "https://animepahe.com/api?m=links&id=${ep.animeId}&session=${ep.session}&p=kwik"
}
val req = khttp.get(link, headers = headers)
val data = mapper.readValue<AnimePaheEpisodeLoadLinks>(req.text)
val qualities = ArrayList<ExtractorLink>()
data.data.forEach {
it.entries.forEach { quality ->
qualities.add(
ExtractorLink(
"KWIK",
"KWIK - ${quality.key} [${quality.value.audio ?: "jpn"}]",
getStreamUrlFromKwik(quality.value.kwikAdfly),
"",
getQualityFromName(quality.key),
false
)
)
}
}
return qualities
}
override fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
for (server in extractVideoLinks(data)) {
callback.invoke(server)
}
return true
}
}