diff --git a/src/pt/mundohentai/AndroidManifest.xml b/src/pt/mundohentai/AndroidManifest.xml
new file mode 100644
index 000000000..389c51256
--- /dev/null
+++ b/src/pt/mundohentai/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/pt/mundohentai/build.gradle b/src/pt/mundohentai/build.gradle
new file mode 100644
index 000000000..d946e94f5
--- /dev/null
+++ b/src/pt/mundohentai/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Mundo Hentai'
+ pkgNameSuffix = 'pt.mundohentai'
+ extClass = '.MundoHentai'
+ extVersionCode = 3
+ isNsfw = true
+}
+
+dependencies {
+ implementation project(':lib-ratelimit')
+}
+
+apply from: "$rootDir/common.gradle"
+
diff --git a/src/pt/mundohentai/res/mipmap-hdpi/ic_launcher.png b/src/pt/mundohentai/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..071cff073
Binary files /dev/null and b/src/pt/mundohentai/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/mundohentai/res/mipmap-mdpi/ic_launcher.png b/src/pt/mundohentai/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..82e950931
Binary files /dev/null and b/src/pt/mundohentai/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/mundohentai/res/mipmap-xhdpi/ic_launcher.png b/src/pt/mundohentai/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..f4320c59b
Binary files /dev/null and b/src/pt/mundohentai/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/mundohentai/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/mundohentai/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..01ebdefba
Binary files /dev/null and b/src/pt/mundohentai/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/mundohentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/mundohentai/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..650b323ec
Binary files /dev/null and b/src/pt/mundohentai/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/mundohentai/res/web_hi_res_512.png b/src/pt/mundohentai/res/web_hi_res_512.png
new file mode 100644
index 000000000..cf69095de
Binary files /dev/null and b/src/pt/mundohentai/res/web_hi_res_512.png differ
diff --git a/src/pt/mundohentai/src/eu/kanade/tachiyomi/extension/pt/mundohentai/MundoHentai.kt b/src/pt/mundohentai/src/eu/kanade/tachiyomi/extension/pt/mundohentai/MundoHentai.kt
new file mode 100644
index 000000000..f8189ba7f
--- /dev/null
+++ b/src/pt/mundohentai/src/eu/kanade/tachiyomi/extension/pt/mundohentai/MundoHentai.kt
@@ -0,0 +1,189 @@
+package eu.kanade.tachiyomi.extension.pt.mundohentai
+
+import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.util.concurrent.TimeUnit
+
+class MundoHentai : ParsedHttpSource() {
+
+ override val name = "Mundo Hentai"
+
+ override val baseUrl = "https://mundohentaioficial.com"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = false
+
+ override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+ .addInterceptor(RateLimitInterceptor(1, 2, TimeUnit.SECONDS))
+ .build()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Referer", baseUrl)
+
+ private fun genericMangaFromElement(element: Element): SManga =
+ SManga.create().apply {
+ title = element.select("div.menu a.title").text()
+ thumbnail_url = element.attr("style")
+ .substringAfter("url(\"")
+ .substringBefore("\")")
+ url = element.select("a.absolute").attr("href")
+ }
+
+ // The source does not have a popular list page, so we use the Doujin list instead.
+ override fun popularMangaRequest(page: Int): Request {
+ val newHeaders = headersBuilder()
+ .set("Referer", if (page == 1) baseUrl else "$baseUrl/tipo/doujin/${page - 1}")
+ .build()
+
+ val pageStr = if (page != 1) "/$page" else ""
+ return GET("$baseUrl/tipo/doujin$pageStr", newHeaders)
+ }
+
+ override fun popularMangaSelector(): String = "ul.post-list li div.card:has(a.absolute[href^=/])"
+
+ override fun popularMangaFromElement(element: Element): SManga = genericMangaFromElement(element)
+
+ override fun popularMangaNextPageSelector() = "div.buttons:not(:has(a.selected + a.material-icons))"
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ if (query.isNotEmpty()) {
+ val url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder()
+ .addQueryParameter("q", query)
+ .toString()
+
+ return GET(url, headers)
+ }
+
+ val tagFilter = filters[1] as TagFilter
+ val tagSlug = tagFilter.values[tagFilter.state].slug
+
+ if (tagSlug.isEmpty()) {
+ return popularMangaRequest(page)
+ }
+
+ val newHeaders = headersBuilder()
+ .set("Referer", if (page == 1) "$baseUrl/categories" else "$baseUrl/tags/$tagSlug/${page - 1}")
+ .build()
+
+ val pageStr = if (page != 1) "/$page" else ""
+ return GET("$baseUrl/tags/$tagSlug$pageStr", newHeaders)
+ }
+
+ override fun searchMangaSelector() = popularMangaSelector() + ":not(:has(div.right-tape))"
+
+ override fun searchMangaFromElement(element: Element): SManga = genericMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+ override fun mangaDetailsParse(document: Document): SManga {
+ val post = document.select("div.post")
+
+ return SManga.create().apply {
+ author = post.select("div.tags div.tag:contains(Artista:) a.value").text()
+ genre = post.select("div.tags div.tag:contains(Tags:) a.value").joinToString { it.text() }
+ description = post.select("div.tags div.tag:contains(Tipo:)").text()
+ .plus("\n" + post.select("div.tags div.tag:contains(Cor:)").text())
+ status = SManga.COMPLETED
+ thumbnail_url = post.select("div.cover img").attr("src")
+ }
+ }
+
+ override fun chapterListSelector(): String = "div.post header.data div.float-buttons a.read"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ name = "Capítulo"
+ scanlator = element.parent().parent()
+ .select("div.tags div.tag:contains(Tradutor:) a.value")
+ .text()
+ chapter_number = 1f
+ url = element.attr("href")
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val newHeader = headersBuilder()
+ .set("Referer", "$baseUrl${chapter.url}".substringBeforeLast("/"))
+ .build()
+
+ return GET(baseUrl + chapter.url, newHeader)
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("div.gallery > img")
+ .mapIndexed { i, el ->
+ Page(i, document.location(), el.attr("src"))
+ }
+ }
+
+ override fun imageUrlParse(document: Document) = ""
+
+ override fun imageRequest(page: Page): Request {
+ val newHeaders = headersBuilder()
+ .set("Referer", page.url)
+ .build()
+
+ return GET(page.imageUrl!!, newHeaders)
+ }
+
+ override fun getFilterList(): FilterList = FilterList(
+ Filter.Header("Os filtros são ignorados na busca!"),
+ TagFilter(getTags())
+ )
+
+ data class Tag(val name: String, val slug: String) {
+ override fun toString(): String = name
+ }
+
+ private class TagFilter(tags: Array) : Filter.Select("Tag", tags)
+
+ private fun getTags(): Array = arrayOf(
+ Tag("-- Selecione --", ""),
+ Tag("Ahegao", "ahegao"),
+ Tag("Anal", "anal"),
+ Tag("Biquíni", "biquini"),
+ Tag("Chubby", "chubby"),
+ Tag("Colegial", "colegial"),
+ Tag("Creampie", "creampie"),
+ Tag("Dark Skin", "dark-skin"),
+ Tag("Dupla Penetração", "dupla-penetracao"),
+ Tag("Espanhola", "espanhola"),
+ Tag("Exibicionismo", "exibicionismo"),
+ Tag("Footjob", "footjob"),
+ Tag("Furry", "furry"),
+ Tag("Futanari", "futanari"),
+ Tag("Grupal", "grupal"),
+ Tag("Incesto", "incesto"),
+ Tag("Lingerie", "lingerie"),
+ Tag("MILF", "milf"),
+ Tag("Maiô", "maio"),
+ Tag("Masturbação", "masturbacao"),
+ Tag("Netorare", "netorare"),
+ Tag("Oral", "oral"),
+ Tag("Peitinhos", "peitinhos"),
+ Tag("Preservativo", "preservativo"),
+ Tag("Professora", "professora"),
+ Tag("Sex Toys", "sex-toys"),
+ Tag("Tentáculos", "tentaculos"),
+ Tag("Yaoi", "yaoi")
+ )
+
+ override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used")
+
+ override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used")
+
+ override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used")
+
+ override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException("Not used")
+}