diff --git a/src/pt/goyabu/AndroidManifest.xml b/src/pt/goyabu/AndroidManifest.xml
new file mode 100644
index 000000000..94339ee7a
--- /dev/null
+++ b/src/pt/goyabu/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/pt/goyabu/build.gradle b/src/pt/goyabu/build.gradle
new file mode 100644
index 000000000..1263411a7
--- /dev/null
+++ b/src/pt/goyabu/build.gradle
@@ -0,0 +1,14 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Goyabu'
+ pkgNameSuffix = 'pt.goyabu'
+ extClass = '.Goyabu'
+ extVersionCode = 1
+ libVersion = '12'
+}
+
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/goyabu/res/mipmap-hdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..b2b90b7a7
Binary files /dev/null and b/src/pt/goyabu/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/goyabu/res/mipmap-mdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..5adebc468
Binary files /dev/null and b/src/pt/goyabu/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/goyabu/res/mipmap-xhdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..8c8548ae0
Binary files /dev/null and b/src/pt/goyabu/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/goyabu/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d60fc7a76
Binary files /dev/null and b/src/pt/goyabu/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/goyabu/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/goyabu/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..186a5f1e6
Binary files /dev/null and b/src/pt/goyabu/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYConstants.kt b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYConstants.kt
new file mode 100644
index 000000000..b669615ed
--- /dev/null
+++ b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYConstants.kt
@@ -0,0 +1,11 @@
+package eu.kanade.tachiyomi.animeextension.pt.goyabu
+
+object GYConstants {
+ 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 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Mobile/15E148 Safari/604.1"
+ const val PREFERRED_QUALITY = "preferred_quality"
+ const val PREFERRED_PLAYER = "preferred_player"
+ val QUALITY_LIST = arrayOf("SD", "HD")
+ val PLAYER_NAMES = arrayOf("Player 1", "Player 2")
+ val PLAYER_REGEX = Regex("""label: "(\w+)",.*file: "(.*?)"""")
+}
diff --git a/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYFilters.kt b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYFilters.kt
new file mode 100644
index 000000000..809ef7687
--- /dev/null
+++ b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/GYFilters.kt
@@ -0,0 +1,230 @@
+package eu.kanade.tachiyomi.animeextension.pt.goyabu
+
+import eu.kanade.tachiyomi.animesource.model.AnimeFilter
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+
+object GYFilters {
+
+ 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 LanguageFilter : QueryPartFilter("Idioma", GYFiltersData.languages)
+ class InitialLetterFilter : QueryPartFilter("Primeira letra", GYFiltersData.initialLetter)
+
+ class EpisodeFilter : AnimeFilter.Text("Episódios")
+ class EpisodeFilterMode : QueryPartFilter("Modo de filtro", GYFiltersData.episodeFilterMode)
+ class SortFilter : AnimeFilter.Sort(
+ "Ordenar",
+ GYFiltersData.orders.map { it.first }.toTypedArray(),
+ Selection(0, true)
+ )
+
+ class GenresFilter : TriStateFilterList(
+ "Gêneros",
+ GYFiltersData.genres.map { TriStateVal(it) }
+ )
+
+ val filterList = AnimeFilterList(
+ LanguageFilter(),
+ InitialLetterFilter(),
+ SortFilter(),
+ AnimeFilter.Separator(),
+ EpisodeFilter(),
+ EpisodeFilterMode(),
+ AnimeFilter.Separator(),
+ GenresFilter(),
+ )
+
+ data class FilterSearchParams(
+ val language: String = "",
+ 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(),
+ filters.asQueryPart(),
+ )
+
+ searchParams.numEpisodes = try {
+ filters.getFirst().state.toInt()
+ } catch (e: NumberFormatException) { 0 }
+
+ filters.getFirst().state?.let {
+ val order = GYFiltersData.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.language != "" && params.language !in anime.type -> 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 GYFiltersData {
+
+ val languages = arrayOf(
+ Pair("Todos", ""),
+ Pair("Legendado", "Leg"),
+ Pair("Dublado", "Dub")
+ )
+
+ 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(
+ "Alien",
+ "Animação Chinesa",
+ "Anjos",
+ "Artes Marciais",
+ "Astronautas",
+ "Aventura",
+ "Ação",
+ "Carros",
+ "Comédia",
+ "Crianças",
+ "Demência",
+ "Demônios",
+ "Drama",
+ "Ecchi",
+ "Escolar",
+ "Espacial",
+ "Espaço",
+ "Esporte",
+ "Fantasia",
+ "Fantasmas",
+ "Ficção Científica",
+ "Harém",
+ "Histórico",
+ "Horror",
+ "Idol",
+ "Infantil",
+ "Isekai",
+ "Jogo",
+ "Josei",
+ "Magia",
+ "Mecha",
+ "Militar",
+ "Mistério",
+ "Monstros",
+ "Magia",
+ "Música",
+ "Otaku",
+ "Paródia",
+ "Piratas",
+ "Policial",
+ "Psicológico",
+ "RPG",
+ "Realidade Virtual",
+ "Romance",
+ "Samurai",
+ "Sci-Fi",
+ "Seinen",
+ "Shoujo",
+ "Shoujo Ai",
+ "Shounen",
+ "Shounen Ai",
+ "Slice of life",
+ "Sobrenatural",
+ "Super Poder",
+ "Supernatural",
+ "Superpotência",
+ "Suspense",
+ "Teatro",
+ "Terror",
+ "Thriller",
+ "Vampiro",
+ "Vida Escolar",
+ "Yaoi",
+ "Yuri"
+ )
+ }
+}
diff --git a/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/Goyabu.kt b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/Goyabu.kt
new file mode 100644
index 000000000..156f922f5
--- /dev/null
+++ b/src/pt/goyabu/src/eu/kanade/tachiyomi/animeextension/pt/goyabu/Goyabu.kt
@@ -0,0 +1,305 @@
+package eu.kanade.tachiyomi.animeextension.pt.goyabu
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animeextension.pt.goyabu.GYFilters.applyFilterParams
+import eu.kanade.tachiyomi.animeextension.pt.goyabu.extractors.PlayerOneExtractor
+import eu.kanade.tachiyomi.animeextension.pt.goyabu.extractors.PlayerTwoExtractor
+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.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 Goyabu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "Goyabu"
+
+ override val baseUrl = "https://goyabu.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", GYConstants.ACCEPT_LANGUAGE)
+ .add("Referer", baseUrl)
+
+ // ============================== Popular ===============================
+ override fun popularAnimeSelector(): String = "div.item > div.anime-episode"
+ override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
+
+ override fun popularAnimeFromElement(element: Element): SAnime {
+ val anime: SAnime = SAnime.create()
+ anime.setUrlWithoutDomain(element.selectFirst("a").attr("href"))
+ anime.title = element.selectFirst("h3").text()
+ anime.thumbnail_url = element.selectFirst("img").attr("src")
+ return anime
+ }
+
+ override fun popularAnimeNextPageSelector() = throw Exception("not used")
+
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ val document = response.asJsoup()
+ val content = document.select("div.episodes-container").get(2)
+ val animes = content.select(popularAnimeSelector()).map { element ->
+ popularAnimeFromElement(element)
+ }
+ return AnimesPage(animes, false)
+ }
+
+ // ============================== Episodes ==============================
+ override fun episodeListSelector(): String = "div.episodes-container > div.anime-episode"
+
+ private fun getAllEps(response: Response): List {
+ val epList = mutableListOf()
+ val url = response.request.url.toString()
+ val doc = if (url.contains("/videos/")) {
+ getRealDoc(response.asJsoup())
+ } else { response.asJsoup() }
+
+ val epElementList = doc.select(episodeListSelector())
+ epList.addAll(epElementList.map { episodeFromElement(it) })
+
+ val next = doc.selectFirst("div.naco > a.next")
+ if (next != null) {
+ val newResponse = client.newCall(GET(next.attr("href"))).execute()
+ epList.addAll(getAllEps(newResponse))
+ }
+ return epList
+ }
+
+ override fun episodeListParse(response: Response): List {
+ return getAllEps(response).reversed()
+ }
+
+ override fun episodeFromElement(element: Element): SEpisode {
+ val episode = SEpisode.create()
+
+ episode.setUrlWithoutDomain(element.selectFirst("a").attr("href"))
+ val epName = element.selectFirst("h3").text().substringAfter("– ")
+ 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