New source: HentaiYabu (#687)

This commit is contained in:
Claudemirovsky
2022-07-23 17:33:56 -03:00
committed by GitHub
parent 367c2d006e
commit 9092d6f1af
13 changed files with 716 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.hentaiyabu.HYUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hentaiyabu.com"
android:pathPattern="/hentai/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,18 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'HentaiYabu'
pkgNameSuffix = 'pt.hentaiyabu'
extClass = '.HentaiYabu'
extVersionCode = 1
libVersion = '12'
containsNsfw = true
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu
object HYConstants {
const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
const val USER_AGENT = "Mozilla/5.0 (Linux; Android 10; SM-A307GT Build/QP1A.190711.020;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/103.0.5060.71 Mobile Safari/537.36"
const val PREFERRED_QUALITY = "preferred_quality"
const val PREFIX_SEARCH_SLUG = "slug:"
val QUALITY_LIST = arrayOf("SD", "HD")
val PLAYER_REGEX = Regex("""label: "(\w+)",.*file: "(.*?)"""")
}

View File

@ -0,0 +1,275 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object HYFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray()
) {
fun toQueryPart() = vals[state].second
}
open class TriStateFilterList(name: String, values: List<TriState>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
private class TriStateVal(name: String) : AnimeFilter.TriState(name)
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.getFirst<R>().let {
(it as QueryPartFilter).toQueryPart()
}
}
class InitialLetterFilter : QueryPartFilter("Primeira letra", HYFiltersData.initialLetter)
class EpisodeFilter : AnimeFilter.Text("Episódios")
class EpisodeFilterMode : QueryPartFilter("Modo de filtro", HYFiltersData.episodeFilterMode)
class SortFilter : AnimeFilter.Sort(
"Ordenar",
HYFiltersData.orders.map { it.first }.toTypedArray(),
Selection(0, true)
)
class GenresFilter : TriStateFilterList(
"Gêneros",
HYFiltersData.genres.map { TriStateVal(it) }
)
val filterList = AnimeFilterList(
InitialLetterFilter(),
SortFilter(),
AnimeFilter.Separator(),
EpisodeFilter(),
EpisodeFilterMode(),
AnimeFilter.Separator(),
GenresFilter(),
)
data class FilterSearchParams(
val initialLetter: String = "",
val episodesFilterMode: String = ">=",
var numEpisodes: Int = 0,
var orderAscending: Boolean = true,
var sortBy: String = "",
val blackListedGenres: ArrayList<String> = ArrayList(),
val includedGenres: ArrayList<String> = ArrayList(),
var animeName: String = ""
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
val searchParams = FilterSearchParams(
filters.asQueryPart<InitialLetterFilter>(),
filters.asQueryPart<EpisodeFilterMode>(),
)
searchParams.numEpisodes = try {
filters.getFirst<EpisodeFilter>().state.toInt()
} catch (e: NumberFormatException) { 0 }
filters.getFirst<SortFilter>().state?.let {
val order = HYFiltersData.orders[it.index].second
searchParams.orderAscending = it.ascending
searchParams.sortBy = order
}
filters.getFirst<GenresFilter>()
.state.forEach { genre ->
if (genre.isIncluded()) {
searchParams.includedGenres.add(genre.name)
} else if (genre.isExcluded()) {
searchParams.blackListedGenres.add(genre.name)
}
}
return searchParams
}
private fun mustRemove(anime: SearchResultDto, params: FilterSearchParams): Boolean {
val epFilterMode = params.episodesFilterMode
return when {
params.animeName != "" && params.animeName.lowercase() !in anime.title.lowercase() -> true
anime.title == "null" -> true
params.initialLetter != "" && !anime.title.startsWith(params.initialLetter) -> true
params.blackListedGenres.size > 0 && params.blackListedGenres.any {
it.lowercase() in anime.genre.lowercase()
} -> true
params.includedGenres.size > 0 && params.includedGenres.any {
it.lowercase() !in anime.genre.lowercase()
} -> true
params.numEpisodes > 0 -> {
when (epFilterMode) {
"==" -> params.numEpisodes != anime.videos
">=" -> params.numEpisodes >= anime.videos
"<=" -> params.numEpisodes <= anime.videos
else -> false
}
}
else -> false
}
}
fun MutableList<SearchResultDto>.applyFilterParams(params: FilterSearchParams) {
this.removeAll { anime -> mustRemove(anime, params) }
when (params.sortBy) {
"A-Z" -> {
if (!params.orderAscending)
this.reverse()
}
"num" -> {
if (params.orderAscending)
this.sortBy { it.videos }
else
this.sortByDescending { it.videos }
}
}
}
private object HYFiltersData {
val orders = arrayOf(
Pair("Alfabeticamente", "A-Z"),
Pair("Por número de eps", "num")
)
val initialLetter = arrayOf(Pair("Qualquer uma", "")) + ('A'..'Z').map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val episodeFilterMode = arrayOf(
Pair("Maior ou igual", ">="),
Pair("Menor ou igual", "<="),
Pair("Igual", "=="),
)
val genres = arrayOf(
"Ahegao",
"Anal",
"Artes Marciais",
"Ashikoki",
"Aventura",
"Ação",
"BDSM",
"Bara",
"Boquete",
"Boys Love",
"Brinquedos",
"Brinquedos Sexuais",
"Bukkake",
"Bunda Grande",
"Chikan",
"Científica",
"Comédia",
"Cosplay",
"Creampie",
"Dark Skin",
"Demônio",
"Drama",
"Dupla Penetração",
"Ecchi",
"Elfos",
"Empregada",
"Enfermeira",
"Eroge",
"Erótico",
"Escolar",
"Esporte",
"Estupro",
"Facial",
"Fantasia",
"Femdom",
"Ficção",
"Ficção Científica",
"Futanari",
"Gang Bang",
"Garotas De Escritório",
"Gender Bender",
"Gerakuro",
"Gokkun",
"Golden Shower",
"Gore",
"Gozando Dentro",
"Grupo",
"Grávida",
"Guerra",
"Gyaru",
"Harém",
"Hipnose",
"Histórico",
"Horror",
"Incesto",
"Jogos Eróticos",
"Josei",
"Kemono",
"Kemonomimi",
"Lactação",
"Lolicon",
"Magia",
"Maid",
"Masturbação",
"Mecha",
"Menage",
"Metrô",
"Milf",
"Mind Break",
"Mind Control",
"Mistério",
"Moe",
"Monstros",
"Médico",
"Nakadashi",
"Nerd",
"Netorare",
"Ninjas",
"Óculos",
"Oral",
"Orgia",
"Paizuri",
"Paródia",
"Peitões",
"Pelos Pubianos",
"Pettanko",
"Policial",
"Preservativo",
"Professor",
"Psicológico",
"Punição",
"Raio-X",
"Romance",
"Ronin",
"Sci-Fi",
"Seinen",
"Sexo Público",
"Shotacon",
"Shoujo Ai",
"Shounen",
"Shounen Ai",
"Slice Of Life",
"Sobrenatural",
"Submissão",
"Succubus",
"Super Poder",
"Swimsuit",
"Tentáculos",
"Terror",
"Tetas",
"Thriller",
"Traição",
"Trem",
"Vampiros",
"Vanilla",
"Vida Escolar",
"Virgem",
"Voyeur",
"Yaoi",
"Yuri",
"Zoofilia",
)
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://hentaiyabu.com/hentai/<slug>intents
* and redirects them to the main Aniyomi process.
*/
class HYUrlActivity : Activity() {
private val TAG = "HYUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val searchQuery = HYConstants.PREFIX_SEARCH_SLUG + slug
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.toString())
}
} else {
Log.e(TAG, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,291 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.hentaiyabu.HYFilters.applyFilterParams
import eu.kanade.tachiyomi.animeextension.pt.hentaiyabu.extractors.PlayerOneExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
class HentaiYabu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "HentaiYabu"
override val baseUrl = "https://hentaiyabu.com"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json = Json {
ignoreUnknownKeys = true
}
private var searchJson: List<SearchResultDto>? = null
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Accept-Language", HYConstants.ACCEPT_LANGUAGE)
.add("Referer", baseUrl)
.add("User-Agent", HYConstants.USER_AGENT)
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.main-index > div.index-size > div.episodes-container > div.anime-episode"
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
override fun popularAnimeFromElement(element: Element): SAnime {
val anime: SAnime = SAnime.create()
val img = element.selectFirst("img")
val elementA = element.selectFirst("a")
anime.setUrlWithoutDomain(elementA.attr("href"))
anime.title = element.selectFirst("h3").text()
anime.thumbnail_url = img.attr("src")
return anime
}
override fun popularAnimeNextPageSelector() = throw Exception("not used")
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "div.left-single div.anime-episode"
override fun episodeListParse(response: Response): List<SEpisode> {
val url = response.request.url.toString()
val doc = if (url.contains("/video/")) {
getRealDoc(response.asJsoup())
} else {
response.asJsoup()
}
return doc.select(episodeListSelector()).map {
episodeFromElement(it)
}.reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val elementA = element.selectFirst("a")
episode.setUrlWithoutDomain(elementA.attr("href"))
val name = element.selectFirst("h3").text()
val epName = name.substringAfterLast(" ")
episode.name = epName
episode.episode_number = try {
epName.substringAfter(" ").substringBefore(" ").toFloat()
} catch (e: NumberFormatException) { 0F }
return episode
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document: Document = response.asJsoup()
val html: String = document.html()
val videoList = mutableListOf<Video>()
val kanraElement = document.selectFirst("script:containsData(kanra.dev)")
if (kanraElement != null) {
val kanraUrl = kanraElement.html()
.substringAfter("src='")
.substringBefore("'")
val kanraVideos = PlayerOneExtractor(client).videoListFromKanraUrl(kanraUrl)
videoList.addAll(kanraVideos)
} else {
val extracted = PlayerOneExtractor()
.videoListFromHtml(html)
videoList.addAll(extracted)
}
return videoList
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = throw Exception("not used")
override fun searchAnimeParse(response: Response) = throw Exception("not used")
private fun searchAnimeParse(result: SearchResultDto): SAnime {
val anime: SAnime = SAnime.create()
anime.title = result.title
anime.url = "/hentai/${result.slug}"
anime.thumbnail_url = "$baseUrl/${result.cover}"
return anime
}
override fun searchAnimeNextPageSelector() = throw Exception("not used")
override fun searchAnimeSelector() = throw Exception("not used")
private fun searchAnimeByIdParse(response: Response, slug: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/hentai/$slug"
return AnimesPage(listOf(details), false)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(HYConstants.PREFIX_SEARCH_SLUG)) {
val slug = query.removePrefix(HYConstants.PREFIX_SEARCH_SLUG)
client.newCall(GET("$baseUrl/hentai/$slug"))
.asObservableSuccess()
.map { response ->
searchAnimeByIdParse(response, slug)
}
} else {
val params = HYFilters.getSearchParameters(filters)
Observable.just(searchAnimeRequest(page, query, params))
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filterParams: HYFilters.FilterSearchParams): AnimesPage {
filterParams.animeName = query
if (searchJson == null) {
val body = client.newCall(GET("$baseUrl/api/show.php"))
.execute()
.body?.string().orEmpty()
searchJson = json.decodeFromString<List<SearchResultDto>>(body)
}
val mutableJson = searchJson!!.toMutableList()
mutableJson.applyFilterParams(filterParams)
val results = mutableJson.chunked(30)
val hasNextPage = results.size > page
val currentPage = if (results.size == 0) {
emptyList<SAnime>()
} else {
results.get(page - 1).map { searchAnimeParse(it) }
}
return AnimesPage(currentPage, hasNextPage)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val doc = getRealDoc(document)
val img = doc.selectFirst("div.anime-cover")
val infos = img.selectFirst("div.anime-info-right")
anime.thumbnail_url = img.selectFirst("img").attr("src")
anime.title = infos.selectFirst("h1").text()
anime.genre = infos.getInfo("Generos")
?.replace(".", "") // Prevents things like "Yuri."
anime.status = parseStatus(infos.getInfo("Status"))
anime.description = doc.selectFirst("div.anime-description").text()
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector() = "div#pagination a:contains(Próxima)"
override fun latestUpdatesSelector() = "div.releases-box div.anime-episode"
override fun latestUpdatesFromElement(element: Element) =
popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = HYConstants.PREFERRED_QUALITY
title = "Qualidade preferida"
entries = HYConstants.QUALITY_LIST
entryValues = HYConstants.QUALITY_LIST
setDefaultValue(HYConstants.QUALITY_LIST.last())
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
override fun getFilterList(): AnimeFilterList = HYFilters.filterList
// ============================= Utilities ==============================
private fun getRealDoc(document: Document): Document {
val elementA = document.selectFirst("div.anime-thumb-single > a")
if (elementA != null) {
val url = elementA.attr("href")
val req = client.newCall(GET(url)).execute()
return req.asJsoup()
} else {
return document
}
}
private fun Element.getInfo(tag: String): String? {
val item = this.selectFirst("div:contains($tag) > i").parent()
val text = item?.text()
val info = text?.substringAfter(tag + ": ") ?: ""
return when {
info == "" -> null
else -> info
}
}
private fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()) {
"Completo" -> SAnime.COMPLETED
"Em lançamento" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(HYConstants.PREFERRED_QUALITY, null)
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
when {
quality != null && video.quality.contains(quality) -> {
newList.add(preferred, video)
preferred++
}
else -> newList.add(video)
}
}
return newList
}
}

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class SearchResultDto(
val title: String,
val genre: String,
@Serializable(with = IntSerializer::class)
val videos: Int,
val cover: String,
val type: String,
val slug: String
)
object IntSerializer : JsonTransformingSerializer<Int>(Int.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return try {
JsonPrimitive(element.toString().toInt())
} catch (e: Exception) { JsonPrimitive(-1) }
}
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu.extractors
import eu.kanade.tachiyomi.animeextension.pt.hentaiyabu.HYConstants
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class PlayerOneExtractor(private val client: OkHttpClient? = null) {
private val KANRA_REGEX = Regex("""(?s)label: "(\w+)".*?file: "(.*?)"""")
private val PREFIX = "Player 1"
fun videoListFromHtml(html: String, regex: Regex = HYConstants.PLAYER_REGEX, headers: Headers? = null): List<Video> {
return regex.findAll(html).map { it ->
val quality = "$PREFIX (${it.groupValues[1]})"
val videoUrl = it.groupValues[2]
Video(videoUrl, quality, videoUrl, null, headers)
}.toList()
}
fun videoListFromKanraUrl(url: String): List<Video> {
val headers = Headers.headersOf(
"User-Agent", HYConstants.USER_AGENT,
)
val res = client!!.newCall(GET(url, headers)).execute()
val html = res.body?.string().orEmpty()
return videoListFromHtml(html, KANRA_REGEX, headers)
}
}