2023-01-02 22:52:08 +00:00
|
|
|
package com.hexated
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
|
|
|
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
|
2023-02-03 03:16:37 +00:00
|
|
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
2023-01-02 22:52:08 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
|
|
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
|
|
|
import org.jsoup.nodes.Document
|
|
|
|
import org.jsoup.nodes.Element
|
|
|
|
import java.net.URI
|
|
|
|
|
|
|
|
class YugenAnime : MainAPI() {
|
2023-12-24 00:58:43 +00:00
|
|
|
override var mainUrl = "https://yugenanime.sx"
|
2023-01-02 22:52:08 +00:00
|
|
|
override var name = "YugenAnime"
|
|
|
|
override val hasMainPage = true
|
|
|
|
override var lang = "en"
|
|
|
|
override val hasDownloadSupport = true
|
|
|
|
|
2023-02-03 03:16:37 +00:00
|
|
|
override val supportedSyncNames = setOf(
|
|
|
|
SyncIdName.Anilist,
|
|
|
|
SyncIdName.MyAnimeList
|
|
|
|
)
|
|
|
|
|
2023-01-02 22:52:08 +00:00
|
|
|
override val supportedTypes = setOf(
|
|
|
|
TvType.Anime,
|
|
|
|
TvType.AnimeMovie,
|
|
|
|
TvType.OVA
|
|
|
|
)
|
|
|
|
|
|
|
|
companion object {
|
2024-01-01 12:09:03 +00:00
|
|
|
private const val consumetAnilist = "https://consumet-instance.vercel.app/meta/anilist"
|
|
|
|
private const val consumetMal = "https://consumet-instance.vercel.app/meta/mal"
|
2023-02-03 03:16:37 +00:00
|
|
|
|
2023-01-02 22:52:08 +00:00
|
|
|
fun getType(t: String): TvType {
|
|
|
|
return if (t.contains("OVA", true) || t.contains("Special", true)) TvType.OVA
|
|
|
|
else if (t.contains("Movie", true)) TvType.AnimeMovie
|
|
|
|
else TvType.Anime
|
|
|
|
}
|
|
|
|
|
|
|
|
fun getStatus(t: String): ShowStatus {
|
|
|
|
return when (t) {
|
|
|
|
"Finished Airing" -> ShowStatus.Completed
|
|
|
|
"Currently Airing" -> ShowStatus.Ongoing
|
|
|
|
else -> ShowStatus.Completed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override val mainPage = mainPageOf(
|
|
|
|
"$mainUrl/trending/?page=" to "Trending",
|
|
|
|
"$mainUrl/latest/?page=" to "Recently Released",
|
|
|
|
"$mainUrl/best/?page=" to "Most Popular Series",
|
|
|
|
"$mainUrl/new/?page=" to "New to YugenAnime",
|
|
|
|
)
|
|
|
|
|
|
|
|
override suspend fun getMainPage(
|
|
|
|
page: Int,
|
|
|
|
request: MainPageRequest
|
|
|
|
): HomePageResponse {
|
|
|
|
val items = mutableListOf<HomePageList>()
|
|
|
|
val document = app.get(request.data + page).document
|
|
|
|
val home = document.select("div.cards-grid a, ul.ep-grid li.ep-card").mapNotNull {
|
|
|
|
it.toSearchResult()
|
|
|
|
}
|
2023-01-02 23:21:34 +00:00
|
|
|
items.add(HomePageList(request.name, home))
|
2023-01-02 22:52:08 +00:00
|
|
|
return newHomePageResponse(items)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun Element.toSearchResult(): AnimeSearchResponse? {
|
|
|
|
val title = this.attr("title").ifBlank { this.select("div.ep-origin-name").text() }
|
|
|
|
.ifBlank { this.select("span.anime-name").text() } ?: return null
|
|
|
|
val href = fixUrl(this.attr("href").ifBlank { this.select("a.ep-details").attr("href") })
|
|
|
|
val posterUrl = fixUrlNull(this.selectFirst("img.lozad")?.attr("data-src"))
|
|
|
|
val epNum =
|
|
|
|
this.select("a.ep-thumbnail").attr("title").substringBefore(":").filter { it.isDigit() }
|
|
|
|
.toIntOrNull()
|
|
|
|
return newAnimeSearchResponse(title, href, TvType.Anime) {
|
|
|
|
this.posterUrl = posterUrl
|
|
|
|
addDubStatus(dubExist = true, subExist = true, dubEpisodes = epNum, subEpisodes = epNum)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun search(query: String): List<SearchResponse> {
|
2023-12-24 00:58:43 +00:00
|
|
|
val document = app.get("$mainUrl/discover/?q=$query").document
|
2023-01-02 22:52:08 +00:00
|
|
|
return document.select("div.cards-grid a.anime-meta").mapNotNull {
|
|
|
|
it.toSearchResult()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 03:16:37 +00:00
|
|
|
override suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
|
|
|
|
val syncId = id.split("/").last()
|
|
|
|
val url = if (name == SyncIdName.Anilist) {
|
|
|
|
"${consumetAnilist}/info/$syncId"
|
|
|
|
} else {
|
|
|
|
"${consumetMal}/info/$syncId"
|
|
|
|
}
|
|
|
|
val res = app.get(url).parsedSafe<SyncInfo>()
|
|
|
|
|
|
|
|
val title = res?.title?.romaji ?: res?.title?.english
|
|
|
|
val year = res?.startDate?.year
|
|
|
|
val season = res?.season
|
|
|
|
|
|
|
|
val document = app.get("$mainUrl/discover/?q=$title").document
|
|
|
|
val syncUrl = document.select("div.cards-grid a.anime-meta").find {
|
|
|
|
it.attr("title").equals(title, true) || it.select("div.anime-details span").text().equals("$season $year", true)
|
|
|
|
}?.attr("href")
|
|
|
|
|
|
|
|
return fixUrl(syncUrl ?: return null)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-01-02 22:52:08 +00:00
|
|
|
override suspend fun load(url: String): LoadResponse? {
|
|
|
|
val document = app.get(url).document
|
|
|
|
|
|
|
|
val title = document.selectFirst("div.content h1")?.text() ?: return null
|
|
|
|
val poster = document.selectFirst("img.cover")?.attr("src")
|
|
|
|
val tags = document.getPageContent("Genres").split(",").map { it.trim() }
|
|
|
|
val type = getType(document.getPageContent("Format"))
|
|
|
|
val year = document.getPageContent("Premiered").filter { it.isDigit() }.toIntOrNull()
|
|
|
|
val status = getStatus(document.getPageContent("Status"))
|
|
|
|
val description = document.select("p.description").text()
|
|
|
|
|
|
|
|
val malId = document.getExternalId("MyAnimeList")
|
|
|
|
val anilistId = document.getExternalId("AniList")
|
|
|
|
|
|
|
|
val trailer = document.selectFirst("iframe.lozad.video")?.attr("src")
|
|
|
|
|
2023-01-04 04:38:01 +00:00
|
|
|
val episodes = mutableListOf<Episode>()
|
2023-01-04 04:39:43 +00:00
|
|
|
for(page in 1..50) {
|
2023-01-04 04:38:01 +00:00
|
|
|
val doc = app.get("${url}watch/?page=$page").document
|
|
|
|
val currentPage = doc.select("ul.pagination div.btn.btn-default").text().toIntOrNull() ?: 1
|
|
|
|
if(page > currentPage) break
|
|
|
|
doc.select("ul.ep-grid li.ep-card").map { eps ->
|
|
|
|
val epsTitle = eps.select("a.ep-title").text()
|
|
|
|
val link = fixUrl(eps.select("a.ep-title").attr("href"))
|
|
|
|
val episode = epsTitle.substringBefore(":").filter { it.isDigit() }.toIntOrNull()
|
|
|
|
episodes.add(Episode(link, name = epsTitle.substringAfter(":").trim(), episode = episode))
|
|
|
|
}
|
2023-01-02 22:52:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return newAnimeLoadResponse(title, url, type) {
|
|
|
|
engName = title
|
|
|
|
posterUrl = poster
|
|
|
|
this.year = year
|
|
|
|
addEpisodes(DubStatus.Subbed, episodes)
|
|
|
|
showStatus = status
|
|
|
|
plot = description
|
|
|
|
this.tags = tags
|
|
|
|
addMalId(malId)
|
|
|
|
addAniListId(anilistId)
|
|
|
|
addTrailer(trailer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun loadLinks(
|
|
|
|
data: String,
|
|
|
|
isCasting: Boolean,
|
|
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
|
|
callback: (ExtractorLink) -> Unit
|
|
|
|
): Boolean {
|
|
|
|
|
|
|
|
val episode = data.removeSuffix("/").split("/").last()
|
|
|
|
val dubData = data.substringBeforeLast("/$episode").let { "$it-dub/$episode" }
|
|
|
|
|
|
|
|
listOf(data, dubData).apmap { url ->
|
|
|
|
val doc = app.get(url).document
|
|
|
|
val iframe = doc.select("iframe#main-embed").attr("src") ?: return@apmap null
|
|
|
|
val id = iframe.removeSuffix("/").split("/").lastOrNull() ?: return@apmap null
|
|
|
|
val source = app.post(
|
2023-01-02 22:53:22 +00:00
|
|
|
"$mainUrl/api/embed/", data = mapOf(
|
2023-01-02 22:52:08 +00:00
|
|
|
"id" to id,
|
|
|
|
"ac" to "0"
|
|
|
|
), referer = iframe,
|
|
|
|
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
|
|
|
).parsedSafe<Sources>()?.hls?.distinct()?.firstOrNull() ?: return@apmap null
|
|
|
|
|
|
|
|
val isDub = if (url.contains("-dub")) "dub" else "sub"
|
|
|
|
|
|
|
|
M3u8Helper.generateM3u8(
|
|
|
|
"${getSourceType(getBaseUrl(source))} [$isDub]",
|
|
|
|
source,
|
|
|
|
""
|
|
|
|
).forEach(callback)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun Document.getExternalId(str: String): Int? {
|
|
|
|
return this.select("div.anime-metadetails > div:contains(External Links) a:contains($str)")
|
|
|
|
.attr("href").removeSuffix("/").split("/").lastOrNull()?.toIntOrNull()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun Document.getPageContent(str: String): String {
|
|
|
|
return this.select("div.anime-metadetails > div:contains($str) span.description").text()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getBaseUrl(url: String): String {
|
|
|
|
return URI(url).let {
|
|
|
|
"${it.scheme}://${it.host}"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getSourceType(url: String): String {
|
|
|
|
return when {
|
2023-01-02 23:21:34 +00:00
|
|
|
url.contains("cache", true) -> "Cache"
|
2023-01-08 11:33:40 +00:00
|
|
|
url.contains("allanime", true) -> "Crunchyroll-AL"
|
|
|
|
else -> Regex("\\.(\\S+)\\.").find(url)?.groupValues?.getOrNull(1)?.let { fixTitle(it) } ?: this.name
|
2023-01-02 22:52:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data class Sources(
|
|
|
|
@JsonProperty("hls") val hls: List<String>? = null,
|
|
|
|
)
|
|
|
|
|
2023-02-03 03:16:37 +00:00
|
|
|
data class SyncTitle(
|
|
|
|
@JsonProperty("romaji") val romaji: String? = null,
|
|
|
|
@JsonProperty("english") val english: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class StartDate(
|
|
|
|
@JsonProperty("year") val year: Int? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class SyncInfo(
|
|
|
|
@JsonProperty("title") val title: SyncTitle? = null,
|
|
|
|
@JsonProperty("startDate") val startDate: StartDate? = null,
|
|
|
|
@JsonProperty("season") val season: String? = null,
|
|
|
|
)
|
|
|
|
|
2023-12-24 00:58:43 +00:00
|
|
|
}
|