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.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://154.26.137.28" 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", true) || t.contains("Special")) TvType.OVA else if (t.contains("Movie", true)) 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 { 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() .replace("Subtitle Indonesia", "").trim() val poster = document.selectFirst("div.entry-content > img")?.attr("src") val type = document.select("tbody th:contains(Tipe)").next().text().lowercase() val year = document.select("tbody th:contains(Dirilis)").next().text().trim().toIntOrNull() val (malId, anilistId, image, cover) = getTracker(title, type, year) val episodes = document.select("ul.daftar > li").map { val episode = Regex("Episode\\s?([0-9]+)").find( it.select("a").text().trim() )?.groupValues?.getOrNull(0) val link = fixUrl(it.select("a").attr("href")) Episode(link, episode = episode?.toIntOrNull()) }.reversed() return newAnimeLoadResponse(title, url, getType(type)) { posterUrl = image ?: poster backgroundPosterUrl = cover ?: image ?: poster this.year = year 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() } addMalId(malId) addAniListId(anilistId?.toIntOrNull()) } } 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("https://aghanim.xyz/tools/redirect/") -> { val link = "https://rasa-cintaku-semakin-berantai.xyz/v/${ iframe.substringAfter("id=").substringBefore("&token") }" loadExtractor(link, mainUrl, subtitleCallback, callback) } 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 } private suspend fun getTracker(title: String?, type: String?, year: Int?): Tracker { val res = app.get("https://api.consumet.org/meta/anilist/$title") .parsedSafe()?.results?.find { media -> (media.title?.english.equals(title, true) || media.title?.romaji.equals( title, true )) || (media.type.equals(type, true) && media.releaseDate == year) } return Tracker(res?.malId, res?.aniId, res?.image, res?.cover) } data class Tracker( val malId: Int? = null, val aniId: String? = null, val image: String? = null, val cover: String? = null, ) data class Title( @JsonProperty("romaji") val romaji: String? = null, @JsonProperty("english") val english: String? = null, ) data class Results( @JsonProperty("id") val aniId: String? = null, @JsonProperty("malId") val malId: Int? = null, @JsonProperty("title") val title: Title? = null, @JsonProperty("releaseDate") val releaseDate: Int? = null, @JsonProperty("type") val type: String? = null, @JsonProperty("image") val image: String? = null, @JsonProperty("cover") val cover: String? = null, ) data class AniSearch( @JsonProperty("results") val results: ArrayList? = arrayListOf(), ) }