feat(src/es): VerPelisTop added & HLA fixed (Kohi-den/extensions-source#1073)

Closes Kohi-den/extensions-source#1066
Closes Kohi-den/extensions-source#1056

Co-authored-by: imper1aldev <23511335+imper1aldev@users.noreply.github.com>

(cherry picked from commit 178e85bcd50940355d0d7032ad44779f8f8dcc50)
This commit is contained in:
imperialdev
2025-08-24 11:14:36 -05:00
committed by Cuong-Tran
parent 0f45d3ea95
commit 0b27030c44
2 changed files with 208 additions and 185 deletions

View File

@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.animeextension.es.hentaila
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -19,7 +18,6 @@ import eu.kanade.tachiyomi.lib.universalextractor.UniversalExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import extensions.utils.getPreferencesLazy
@@ -28,6 +26,8 @@ import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
@@ -36,12 +36,10 @@ class Hentaila : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Hentaila"
override val baseUrl = "https://www5.hentaila.com"
override val baseUrl = "https://hentaila.com"
override val lang = "es"
private val json: Json by injectLazy()
override val supportsLatest = true
private val preferences by getPreferencesLazy()
@@ -69,123 +67,60 @@ class Hentaila : ConfigurableAnimeSource, AnimeHttpSource() {
}
}
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/catalogo?order=popular&page=$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select("section.latest-hentais div.slider > div.item")
val elements = document.select("[class*=\"group/item\"]")
val hasNext = document.select(".flex span.pointer-events-none:not([class*=\"max-sm:hidden\"]) ~ a").any()
val animes = elements.map { element ->
SAnime.create().apply {
setUrlWithoutDomain(element.select("h2.h-title a").attr("abs:href"))
title = element.selectFirst("h2.h-title a")!!.text()
thumbnail_url = element.selectFirst("figure.bg img")!!.attr("abs:src").replace("/fondos/", "/portadas/")
setUrlWithoutDomain(element.select("a").attr("abs:href"))
title = element.selectFirst("header > h3")!!.text()
thumbnail_url = element.selectFirst(".bg-current img")!!.attr("abs:src")
description = element.selectFirst(".line-clamp-6")?.text()
}
}
return AnimesPage(animes, false)
return AnimesPage(animes, hasNext)
}
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/catalogo?order=latest_released&page=$page", headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select("section.episodes div.grid article.hentai")
val animes = elements.map { element ->
SAnime.create().apply {
setUrlWithoutDomain(element.select("a").attr("abs:href").replace("/ver/", "/hentai-").substringBeforeLast("-"))
title = element.selectFirst("h2.h-title")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("abs:src").replace("/thumbs/", "/portadas/")
}
}
return AnimesPage(animes.distinctBy { it.url }, false)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.filterIsInstance<GenreFilter>().firstOrNull()
if (query.isNotEmpty()) {
require(query.length >= 2) { "La búsqueda debe tener al menos 2 caracteres" }
return POST("$baseUrl/api/search", headers, FormBody.Builder().add("value", query).build())
val params = HentailaFilters.getSearchParameters(filters)
return when {
query.isNotEmpty() -> GET("$baseUrl/catalogo?search=$query&page=$page")
params.filter.isNotBlank() -> GET("$baseUrl/catalogo${params.getQuery()}&page=$page")
else -> popularAnimeRequest(page)
}
val urlBuilder = "$baseUrl/${if (genreFilter?.state != 0) "genero/${genreFilter?.toUriPart()}" else "directorio?p=$page"}"
.toHttpUrl().newBuilder()
filterList.forEach { filter ->
when (filter) {
is OrderFilter -> urlBuilder.addQueryParameter("filter", filter.toUriPart())
is StatusOngoingFilter -> if (filter.state) urlBuilder.addQueryParameter("status[1]", "on")
is StatusCompletedFilter -> if (filter.state) urlBuilder.addQueryParameter("status[2]", "on")
is UncensoredFilter -> if (filter.state) urlBuilder.addQueryParameter("uncensored", "on")
else -> {}
}
}
return GET(urlBuilder.build().toString(), headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
if (response.request.url.toString().startsWith("$baseUrl/api/search")) {
val jsonString = response.body.string()
val results = json.decodeFromString<List<HentailaDto>>(jsonString)
val animeList = results.map { anime ->
SAnime.create().apply {
title = anime.title
thumbnail_url = "$baseUrl/uploads/portadas/${anime.id}.jpg"
setUrlWithoutDomain("$baseUrl/hentai-${anime.slug}")
}
}
return AnimesPage(animeList, false)
}
val document = response.asJsoup()
val animeList = document.select("div.columns main section.section div.grid.hentais article.hentai").map {
SAnime.create().apply {
title = it.select("header.h-header h2").text()
setUrlWithoutDomain(it.select("a").attr("abs:href"))
thumbnail_url = it.select("div.h-thumb figure img").attr("abs:src")
}
}
val hasNextPage = document.select("a.btn.rnd.npd.fa-arrow-right").any()
return AnimesPage(animeList, hasNextPage)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
return SAnime.create().apply {
thumbnail_url = baseUrl + document.selectFirst("div.h-thumb figure img")!!.attr("src")
with(document.selectFirst("article.hentai-single")!!) {
title = selectFirst("header.h-header h1")!!.text()
description = select("div.h-content p").text()
genre = select("footer.h-footer nav.genres a.btn.sm").joinToString { it.text() }
status = if (selectFirst("span.status-on") != null) {
SAnime.ONGOING
} else {
SAnime.COMPLETED
}
}
thumbnail_url = document.selectFirst("img.aspect-poster")!!.attr("abs:src")
title = document.selectFirst("header h1.text-lead")!!.text()
description = document.select(".items-start > .entry > p").text()
genre = document.select("header .items-center a.rounded-full").joinToString { it.text() }
status = if (document.select("header .text-sm span").any { it.text().contains("En emisión") }) SAnime.ONGOING else SAnime.COMPLETED
}
}
override fun episodeListParse(response: Response): List<SEpisode> {
val episodes = mutableListOf<SEpisode>()
val animeId = response.request.url.toString().substringAfter("hentai-").lowercase()
val jsoup = response.asJsoup()
jsoup.select("div.episodes-list article").forEach { it ->
val epNum = it.select("a").attr("href").replace("/ver/$animeId-", "")
val episode = SEpisode.create().apply {
return jsoup.select(".from-mute article[class*=\"group/item\"]").reversed().map { it ->
val href = it.select("a").attr("href")
val epNum = href.split("/").last()
SEpisode.create().apply {
episode_number = epNum.toFloat()
name = "Episodio $epNum"
url = "/ver/$animeId-$epNum"
date_upload = it.select(".h-header time").text().toDate()
setUrlWithoutDomain(href)
}
episodes.add(episode)
}
return episodes
}
/*--------------------------------Video extractors------------------------------------*/
@@ -200,30 +135,38 @@ class Hentaila : ConfigurableAnimeSource, AnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoServers = document.selectFirst("script:containsData(var videos = [)")!!.data()
.substringAfter("videos = ").substringBefore(";")
.replace("[[", "").replace("]]", "")
val videoServers = document.selectFirst("script:containsData(embeds:{)")!!.data()
.substringAfter("embeds:{").substringBefore("uses:")
val videoServerList = videoServers.split("],[")
return videoServerList.parallelCatchingFlatMapBlocking {
val server = it.split(",").map { a -> a.replace("\"", "") }
val urlServer = server[1].replace("\\/", "/")
val nameServer = server[0]
when (nameServer.lowercase()) {
val videoServerList = fetchUrls(videoServers)
return videoServerList.parallelCatchingFlatMapBlocking { urlServer ->
val matched = conventions.firstOrNull { (_, names) -> names.any { it.lowercase() in urlServer.lowercase() } }?.first
when (matched) {
"streamwish" -> streamWishExtractor.videosFromUrl(urlServer, videoNameGen = { "StreamWish:$it" })
"voe" -> voeExtractor.videosFromUrl(urlServer)
"arc" -> listOf(Video(urlServer.substringAfter("#"), "Arc", urlServer.substringAfter("#")))
"yupi" -> yourUploadExtractor.videoFromUrl(urlServer, headers = headers)
"yourupload" -> yourUploadExtractor.videoFromUrl(urlServer, headers = headers)
"mp4upload" -> mp4uploadExtractor.videosFromUrl(urlServer, headers = headers)
"burst" -> burstCloudExtractor.videoFromUrl(urlServer, headers = headers)
"vidhide", "streamhide", "guccihide", "streamvid" -> streamHideVidExtractor.videosFromUrl(urlServer)
"burstcloud" -> burstCloudExtractor.videoFromUrl(urlServer, headers = headers)
"vidhide" -> streamHideVidExtractor.videosFromUrl(urlServer)
"sendvid" -> sendvidExtractor.videosFromUrl(urlServer)
else -> universalExtractor.videosFromUrl(urlServer, headers)
}
}
}
private val conventions = listOf(
"streamwish" to listOf("wishembed", "streamwish", "strwish", "wish", "Kswplayer", "Swhoi", "Multimovies", "Uqloads", "neko-stream", "swdyu", "iplayerhls", "streamgg"),
"voe" to listOf("voe", "tubelessceliolymph", "simpulumlamerop", "urochsunloath", "nathanfromsubject", "yip.", "metagnathtuggers", "donaldlineelse"),
"arc" to listOf("arc"),
"mp4upload" to listOf("mp4upload"),
"yourupload" to listOf("yourupload", "yupi"),
"burstcloud" to listOf("burstcloud", "burst"),
"vidhide" to listOf("ahvsh", "streamhide", "guccihide", "streamvid", "vidhide", "kinoger", "smoothpre", "dhtpre", "peytonepre", "earnvids", "ryderjet"),
"sendvid" to listOf("sendvid"),
"mediafire" to listOf("mediafire"),
)
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
@@ -236,85 +179,15 @@ class Hentaila : ConfigurableAnimeSource, AnimeHttpSource() {
).reversed()
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
AnimeFilter.Separator(),
OrderFilter(),
AnimeFilter.Separator(),
StatusOngoingFilter(),
StatusCompletedFilter(),
AnimeFilter.Separator(),
UncensoredFilter(),
)
override fun getFilterList(): AnimeFilterList = HentailaFilters.FILTER_LIST
private class StatusOngoingFilter : AnimeFilter.CheckBox("En Emision")
private class StatusCompletedFilter : AnimeFilter.CheckBox("Finalizado")
private class UncensoredFilter : AnimeFilter.CheckBox("Sin Censura")
private class OrderFilter : UriPartFilter(
"Ordenar por",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("Populares", "popular"),
Pair("Recientes", "recent"),
),
)
private class GenreFilter : UriPartFilter(
"Generos",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("3D", "3d"),
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Casadas", "casadas"),
Pair("Chikan", "chikan"),
Pair("Ecchi", "ecchi"),
Pair("Enfermeras", "enfermeras"),
Pair("Futanari", "futanari"),
Pair("Escolares", "escolares"),
Pair("Gore", "gore"),
Pair("Hardcore", "hardcore"),
Pair("Harem", "harem"),
Pair("Incesto", "incesto"),
Pair("Juegos Sexuales", "juegos-sexuales"),
Pair("Milfs", "milfs"),
Pair("Maids", "maids"),
Pair("Netorare", "netorare"),
Pair("Ninfomania", "ninfomania"),
Pair("Ninjas", "ninjas"),
Pair("Orgias", "orgias"),
Pair("Romance", "romance"),
Pair("Shota", "shota"),
Pair("Softcore", "softcore"),
Pair("Succubus", "succubus"),
Pair("Teacher", "teacher"),
Pair("Tentaculos", "tentaculos"),
Pair("Tetonas", "tetonas"),
Pair("Vanilla", "vanilla"),
Pair("Violacion", "violacion"),
Pair("Virgenes", "virgenes"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Bondage", "bondage"),
Pair("Elfas", "elfas"),
Pair("Petit", "petit"),
Pair("Threesome", "threesome"),
Pair("Paizuri", "paizuri"),
Pair("Gal", "gal"),
Pair("Oyakodon", "oyakodon"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
fun fetchUrls(text: String?): List<String> {
if (text.isNullOrEmpty()) {
return listOf()
}
val linkRegex =
Regex("""(https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))""")
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {

View File

@@ -0,0 +1,150 @@
package eu.kanade.tachiyomi.animeextension.es.hentaila
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import java.util.Calendar
object HentailaFilters {
open class QueryPartFilter(displayName: String, val vals: Array<Pair<String, String>>) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart(name: String) = vals[state].second.takeIf { it.isNotEmpty() }?.let { "&$name=${vals[state].second}" } ?: run { "" }
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
open class CheckBoxFilter(name: String) : AnimeFilter.CheckBox(name) {
fun toQueryPart(name: String): String {
return this.state.takeIf { it }?.let { "&$name=true" } ?: run { "" }
}
}
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name=").let {
if (it.isBlank()) {
""
} else {
"&$name=$it"
}
}
}
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
}
private inline fun <reified R> AnimeFilterList.checkAsQueryPart(name: String): String {
return (this.getFirst<R>() as CheckBoxFilter).toQueryPart(name)
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private fun String.changePrefix() = this.takeIf { it.startsWith("&") }?.let { this.replaceFirst("&", "?") } ?: run { this }
data class FilterSearchParams(val filter: String = "") { fun getQuery() = filter.changePrefix() }
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(HentailaFiltersData.GENRES, "genre") +
filters.asQueryPart<YearMinFilter>("minYear") +
filters.asQueryPart<YearMaxFilter>("maxYear") +
filters.asQueryPart<StatusFilter>("status") +
filters.asQueryPart<SortFilter>("order") +
filters.checkAsQueryPart<CheckBoxFilter>("uncensored"),
)
}
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenresFilter(),
YearMinFilter(),
YearMaxFilter(),
StatusFilter(),
AnimeFilter.Separator(),
CensoredFilter(),
AnimeFilter.Separator(),
SortFilter(),
)
class GenresFilter : CheckBoxFilterList("Género", HentailaFiltersData.GENRES.map { CheckBoxVal(it.first, false) })
class YearMinFilter : QueryPartFilter("Min. Año", arrayOf(Pair("Todos", "")) + HentailaFiltersData.YEARS)
class YearMaxFilter : QueryPartFilter("Max. Año", arrayOf(Pair("Todos", "")) + HentailaFiltersData.YEARS.reversed())
class StatusFilter : QueryPartFilter("Estado", HentailaFiltersData.STATUS)
class SortFilter : QueryPartFilter("Ordenar por", HentailaFiltersData.SORT)
class CensoredFilter : CheckBoxFilter("Sin censura")
private object HentailaFiltersData {
val GENRES = arrayOf(
Pair("3D", "3d"),
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Casadas", "casadas"),
Pair("Chikan", "chikan"),
Pair("Ecchi", "ecchi"),
Pair("Enfermeras", "enfermeras"),
Pair("Escolares", "escolares"),
Pair("Futanari", "futanari"),
Pair("Gore", "gore"),
Pair("Hardcore", "hardcore"),
Pair("Harem", "harem"),
Pair("Incesto", "incesto"),
Pair("Juegos Sexuales", "juegos-sexuales"),
Pair("Suspenso", "suspenso"),
Pair("Milfs", "milfs"),
Pair("Maids", "maids"),
Pair("Netorare", "netorare"),
Pair("Ninfomania", "ninfomania"),
Pair("Ninjas", "ninjas"),
Pair("Orgias", "orgias"),
Pair("Romance", "romance"),
Pair("Shota", "shota"),
Pair("Softcore", "softcore"),
Pair("Succubus", "succubus"),
Pair("Teacher", "teacher"),
Pair("Tentaculos", "tentaculos"),
Pair("Tetonas", "tetonas"),
Pair("Vanilla", "vanilla"),
Pair("Violacion", "violacion"),
Pair("Virgenes", "virgenes"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Bondage", "bondage"),
Pair("Elfas", "elfas"),
Pair("Petit", "petit"),
Pair("Threesome", "threesome"),
Pair("Paizuri", "paizuri"),
Pair("Gal", "gal"),
Pair("Oyakodon", "oyakodon"),
)
val YEARS = (1991..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.toTypedArray()
val STATUS = arrayOf(
Pair("Todos", ""),
Pair("Finalizado", "finalizado"),
Pair("Próximamente", "proximamente"),
Pair("En emisión", "emision"),
)
val SORT = arrayOf(
Pair("Predeterminado", ""),
Pair("Puntuación", "score"),
Pair("Populares", "popular"),
Pair("Título", "title"),
Pair("Últimos agregados", "latest_added"),
Pair("Últimos estrenos", "latest_released"),
)
}
}