diff --git a/src/pt/hentaiyabu/AndroidManifest.xml b/src/pt/hentaiyabu/AndroidManifest.xml
new file mode 100644
index 000000000..648a5d4ff
--- /dev/null
+++ b/src/pt/hentaiyabu/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pt/hentaiyabu/build.gradle b/src/pt/hentaiyabu/build.gradle
new file mode 100644
index 000000000..4cd8d1083
--- /dev/null
+++ b/src/pt/hentaiyabu/build.gradle
@@ -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"
diff --git a/src/pt/hentaiyabu/res/mipmap-hdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..3bba6f76d
Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/hentaiyabu/res/mipmap-mdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..74d6e1971
Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/hentaiyabu/res/mipmap-xhdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..c511591b4
Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/hentaiyabu/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..88515b652
Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/hentaiyabu/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d27590920
Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYConstants.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYConstants.kt
new file mode 100644
index 000000000..5b3a8844f
--- /dev/null
+++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYConstants.kt
@@ -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: "(.*?)"""")
+}
diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYFilters.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYFilters.kt
new file mode 100644
index 000000000..65fdbc7b3
--- /dev/null
+++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYFilters.kt
@@ -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>
+ ) : AnimeFilter.Select(
+ displayName,
+ vals.map { it.first }.toTypedArray()
+ ) {
+ fun toQueryPart() = vals[state].second
+ }
+
+ open class TriStateFilterList(name: String, values: List) : AnimeFilter.Group(name, values)
+ private class TriStateVal(name: String) : AnimeFilter.TriState(name)
+
+ private inline fun AnimeFilterList.getFirst(): R {
+ return this.filterIsInstance().first()
+ }
+
+ private inline fun AnimeFilterList.asQueryPart(): String {
+ return this.getFirst().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 = ArrayList(),
+ val includedGenres: ArrayList = ArrayList(),
+ var animeName: String = ""
+ )
+
+ internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
+ val searchParams = FilterSearchParams(
+ filters.asQueryPart(),
+ filters.asQueryPart(),
+ )
+
+ searchParams.numEpisodes = try {
+ filters.getFirst().state.toInt()
+ } catch (e: NumberFormatException) { 0 }
+
+ filters.getFirst().state?.let {
+ val order = HYFiltersData.orders[it.index].second
+ searchParams.orderAscending = it.ascending
+ searchParams.sortBy = order
+ }
+
+ filters.getFirst()
+ .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.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",
+ )
+ }
+}
diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYUrlActivity.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYUrlActivity.kt
new file mode 100644
index 000000000..b78eb49d5
--- /dev/null
+++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYUrlActivity.kt
@@ -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/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)
+ }
+}
diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HentaiYabu.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HentaiYabu.kt
new file mode 100644
index 000000000..4a0e320e2
--- /dev/null
+++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HentaiYabu.kt
@@ -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? = null
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().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 {
+ 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