diff --git a/src/pt/brmangas/AndroidManifest.xml b/src/pt/brmangas/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/pt/brmangas/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/pt/brmangas/build.gradle b/src/pt/brmangas/build.gradle
new file mode 100644
index 000000000..4ebbc7504
--- /dev/null
+++ b/src/pt/brmangas/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'BR Mangás'
+ pkgNameSuffix = 'pt.brmangas'
+ extClass = '.BrMangas'
+ extVersionCode = 1
+ libVersion = '1.2'
+ containsNsfw = true
+}
+
+dependencies {
+ implementation project(':lib-ratelimit')
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/brmangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/brmangas/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..5baddbed3
Binary files /dev/null and b/src/pt/brmangas/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/brmangas/res/mipmap-mdpi/ic_launcher.png b/src/pt/brmangas/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..fc72a9f8b
Binary files /dev/null and b/src/pt/brmangas/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/brmangas/res/mipmap-xhdpi/ic_launcher.png b/src/pt/brmangas/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..e9559301a
Binary files /dev/null and b/src/pt/brmangas/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/brmangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/brmangas/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d0073c77e
Binary files /dev/null and b/src/pt/brmangas/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/brmangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/brmangas/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..2b14e2591
Binary files /dev/null and b/src/pt/brmangas/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/brmangas/res/web_hi_res_512.png b/src/pt/brmangas/res/web_hi_res_512.png
new file mode 100644
index 000000000..c69ee102f
Binary files /dev/null and b/src/pt/brmangas/res/web_hi_res_512.png differ
diff --git a/src/pt/brmangas/src/eu/kanade/tachiyomi/extension/pt/brmangas/BrMangas.kt b/src/pt/brmangas/src/eu/kanade/tachiyomi/extension/pt/brmangas/BrMangas.kt
new file mode 100644
index 000000000..517cf58df
--- /dev/null
+++ b/src/pt/brmangas/src/eu/kanade/tachiyomi/extension/pt/brmangas/BrMangas.kt
@@ -0,0 +1,145 @@
+package eu.kanade.tachiyomi.extension.pt.brmangas
+
+import eu.kanade.tachiyomi.annotations.Nsfw
+import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
+import eu.kanade.tachiyomi.network.GET
+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 okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.util.concurrent.TimeUnit
+
+@Nsfw
+class BrMangas : ParsedHttpSource() {
+
+ override val name = "BR Mangás"
+
+ override val baseUrl = "https://brmangas.com"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+ .addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS))
+ .build()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Accept", ACCEPT)
+ .add("Accept-Language", ACCEPT_LANGUAGE)
+ .add("Referer", "$baseUrl/")
+
+ override fun popularMangaRequest(page: Int): Request {
+ val listPath = if (page == 1) "" else "page/${page - 1}"
+ val newHeaders = headersBuilder()
+ .set("Referer", "$baseUrl/lista-de-mangas/$listPath")
+ .build()
+
+ val pageStr = if (page != 1) "page/$page" else ""
+ return GET("$baseUrl/lista-de-mangas/$pageStr", newHeaders)
+ }
+
+ override fun popularMangaSelector(): String = "div.listagem.row div.item a[title]"
+
+ override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
+ val thumbnailEl = element.select("img").first()!!
+
+ title = element.select("h2.titulo").first()!!.text()
+ thumbnail_url = when {
+ thumbnailEl.hasAttr("original-src") -> thumbnailEl.attr("original-src")
+ else -> thumbnailEl.attr("src")
+ }
+ setUrlWithoutDomain(element.attr("href"))
+ }
+
+ override fun popularMangaNextPageSelector() = "div.navigation a.next"
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ val listPath = if (page == 1) "" else "category/page/${page - 1}"
+ val newHeaders = headersBuilder()
+ .set("Referer", "$baseUrl/$listPath")
+ .build()
+
+ val pageStr = if (page != 1) "page/$page" else ""
+ return GET("$baseUrl/category/mangas/$pageStr", newHeaders)
+ }
+
+ override fun latestUpdatesSelector() = popularMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = baseUrl.toHttpUrlOrNull()!!.newBuilder()
+ .addQueryParameter("s", query)
+
+ return GET(url.toString(), headers)
+ }
+
+ override fun searchMangaSelector() = popularMangaSelector()
+
+ override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector(): String? = null
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ val infoElement = document.select("div.serie-geral div.infoall").first()!!
+
+ title = document.select("title").first().text().substringBeforeLast(" - ")
+ genre = infoElement.select("a.category.tag").joinToString { it.text() }
+ description = document.select("div.manga_sinopse ~ p").text().trim()
+ thumbnail_url = infoElement.select("div.serie-capa img").first()!!.attr("src")
+ }
+
+ override fun chapterListParse(response: Response): List {
+ return super.chapterListParse(response).reversed()
+ }
+
+ override fun chapterListSelector() = "ul.capitulos li.row a"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ name = element.text()
+ setUrlWithoutDomain(element.attr("href"))
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("script:containsData(imageArray)").first()!!
+ .data()
+ .substringAfter("[")
+ .substringBefore("]")
+ .split(",")
+ .mapIndexed { i, imageUrl ->
+ val fixedImageUrl = imageUrl
+ .replace("\\\"", "")
+ .replace("\\/", "/")
+ Page(i, document.location(), fixedImageUrl)
+ }
+ }
+
+ override fun imageUrlParse(document: Document) = ""
+
+ override fun imageRequest(page: Page): Request {
+ val newHeaders = headersBuilder()
+ .set("Accept", ACCEPT_IMAGE)
+ .set("Referer", page.url)
+ .build()
+
+ return GET(page.imageUrl!!, newHeaders)
+ }
+
+ companion object {
+ private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," +
+ "image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
+ private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
+ private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5"
+ }
+}
diff --git a/src/pt/muitomanga/AndroidManifest.xml b/src/pt/muitomanga/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/pt/muitomanga/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/pt/muitomanga/build.gradle b/src/pt/muitomanga/build.gradle
new file mode 100644
index 000000000..ef3c21bea
--- /dev/null
+++ b/src/pt/muitomanga/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Muito Mangá'
+ pkgNameSuffix = 'pt.muitomanga'
+ extClass = '.MuitoManga'
+ extVersionCode = 1
+ libVersion = '1.2'
+ containsNsfw = true
+}
+
+dependencies {
+ implementation project(':lib-ratelimit')
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/muitomanga/res/mipmap-hdpi/ic_launcher.png b/src/pt/muitomanga/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..5c6f60282
Binary files /dev/null and b/src/pt/muitomanga/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/muitomanga/res/mipmap-mdpi/ic_launcher.png b/src/pt/muitomanga/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..93d32f4be
Binary files /dev/null and b/src/pt/muitomanga/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/muitomanga/res/mipmap-xhdpi/ic_launcher.png b/src/pt/muitomanga/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..542190d40
Binary files /dev/null and b/src/pt/muitomanga/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/muitomanga/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/muitomanga/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..53dbded84
Binary files /dev/null and b/src/pt/muitomanga/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/muitomanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/muitomanga/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..e6156bbd4
Binary files /dev/null and b/src/pt/muitomanga/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/muitomanga/res/web_hi_res_512.png b/src/pt/muitomanga/res/web_hi_res_512.png
new file mode 100644
index 000000000..56954efa1
Binary files /dev/null and b/src/pt/muitomanga/res/web_hi_res_512.png differ
diff --git a/src/pt/muitomanga/src/eu/kanade/tachiyomi/extension/pt/muitomanga/MuitoManga.kt b/src/pt/muitomanga/src/eu/kanade/tachiyomi/extension/pt/muitomanga/MuitoManga.kt
new file mode 100644
index 000000000..a56faaa0c
--- /dev/null
+++ b/src/pt/muitomanga/src/eu/kanade/tachiyomi/extension/pt/muitomanga/MuitoManga.kt
@@ -0,0 +1,230 @@
+package eu.kanade.tachiyomi.extension.pt.muitomanga
+
+import com.github.salomonbrys.kotson.array
+import com.github.salomonbrys.kotson.get
+import com.github.salomonbrys.kotson.obj
+import com.github.salomonbrys.kotson.string
+import com.google.gson.JsonElement
+import com.google.gson.JsonParser
+import eu.kanade.tachiyomi.annotations.Nsfw
+import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+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.Interceptor
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.OkHttpClient
+import okhttp3.Protocol
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import kotlin.math.ceil
+
+@Nsfw
+class MuitoManga : ParsedHttpSource() {
+
+ override val name = "Muito Mangá"
+
+ override val baseUrl = "https://muitomanga.com"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+ .addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS))
+ .addInterceptor(::directoryCacheIntercept)
+ .build()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Accept", ACCEPT)
+ .add("Accept-Language", ACCEPT_LANGUAGE)
+ .add("Referer", "$baseUrl/")
+
+ private val directoryCache: MutableMap = mutableMapOf()
+
+ override fun popularMangaRequest(page: Int): Request {
+ val newHeaders = headersBuilder()
+ .set("Accept", ACCEPT_JSON)
+ .set("Referer", "$baseUrl/lista-de-mangas")
+ .add("X-Page", page.toString())
+ .add("X-Requested-With", "XMLHttpRequest")
+ .build()
+
+ return GET("$baseUrl/lib/diretorio.json?pagina=1&tipo_pag=$DIRECTORY_TYPE_POPULAR&pega_busca=", newHeaders)
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val result = response.asJson().obj
+ val totalPages = ceil(result["encontrado"].array.size().toDouble() / ITEMS_PER_PAGE)
+ val currentPage = response.request.header("X-Page")!!.toInt()
+
+ val mangaList = result["encontrado"].array
+ .drop(ITEMS_PER_PAGE * (currentPage - 1))
+ .take(ITEMS_PER_PAGE)
+ .map(::popularMangaFromObject)
+
+ return MangasPage(mangaList, hasNextPage = currentPage < totalPages)
+ }
+
+ private fun popularMangaFromObject(obj: JsonElement): SManga = SManga.create().apply {
+ title = obj["titulo"].string
+ thumbnail_url = obj["imagem"].string
+ url = "/manga/" + obj["url"].string
+ }
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ val newHeaders = headersBuilder()
+ .set("Accept", ACCEPT_JSON)
+ .set("Referer", "$baseUrl/lista-de-mangas/mais-vistos")
+ .add("X-Page", page.toString())
+ .add("X-Requested-With", "XMLHttpRequest")
+ .build()
+
+ return GET("$baseUrl/lib/diretorio.json?pagina=1&tipo_pag=$DIRECTORY_TYPE_LATEST&pega_busca=", newHeaders)
+ }
+
+ override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/buscar".toHttpUrlOrNull()!!.newBuilder()
+ .addQueryParameter("q", query)
+
+ return GET(url.toString(), headers)
+ }
+
+ override fun searchMangaSelector() = "div.content_post div.anime"
+
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.select("h3 a").first()!!.text()
+ thumbnail_url = element.select("div.capaMangaBusca img").first()!!.attr("src")
+ setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
+ }
+
+ override fun searchMangaNextPageSelector(): String? = null
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ val infoElement = document.select("div.content_post").first()!!
+
+ title = document.select("div.content div.widget-title h1").first()!!.text()
+ author = infoElement.select("span.series_autor2").first()!!.text()
+ genre = infoElement.select("ul.lancamento-list a").joinToString { it.text() }
+ description = document.select("ul.lancamento-list ~ p").text().trim()
+ thumbnail_url = infoElement.select("div.capaMangaInfo img").first()!!.attr("data-src")
+ }
+
+ override fun chapterListSelector() = "div.manga-chapters div.single-chapter"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ name = element.select("a").first()!!.text()
+ date_upload = element.select("small[title]").first()!!.text().toDate()
+ scanlator = element.select("scanlator2 a").joinToString { it.text().trim() }
+ setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("script:containsData(imagens_cap)").first()!!
+ .data()
+ .substringAfter("[")
+ .substringBefore("]")
+ .split(",")
+ .mapIndexed { i, imageUrl ->
+ val fixedImageUrl = imageUrl
+ .replace("\"", "")
+ .replace("\\/", "/")
+ Page(i, document.location(), fixedImageUrl)
+ }
+ }
+
+ override fun imageUrlParse(document: Document) = ""
+
+ override fun imageRequest(page: Page): Request {
+ val newHeaders = headersBuilder()
+ .set("Accept", ACCEPT_IMAGE)
+ .set("Referer", page.url)
+ .build()
+
+ return GET(page.imageUrl!!, newHeaders)
+ }
+
+ override fun popularMangaSelector(): String = throw UnsupportedOperationException("Not used")
+
+ override fun popularMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used")
+
+ override fun popularMangaNextPageSelector(): String = 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")
+
+ private fun directoryCacheIntercept(chain: Interceptor.Chain): Response {
+ if (!chain.request().url.toString().contains("diretorio.json")) {
+ return chain.proceed(chain.request())
+ }
+
+ val directoryType = chain.request().url.queryParameter("tipo_pag")!!.toInt()
+
+ if (directoryCache.containsKey(directoryType)) {
+ val jsonContentType = "application/json; charset=UTF-8".toMediaTypeOrNull()
+ val responseBody = directoryCache[directoryType]!!.toResponseBody(jsonContentType)
+
+ return Response.Builder()
+ .code(200)
+ .protocol(Protocol.HTTP_1_1)
+ .request(chain.request())
+ .message("OK")
+ .body(responseBody)
+ .build()
+ }
+
+ val response = chain.proceed(chain.request())
+ val responseContentType = response.body!!.contentType()
+ val responseString = response.body!!.string()
+
+ directoryCache[directoryType] = responseString
+
+ return response.newBuilder()
+ .body(responseString.toResponseBody(responseContentType))
+ .build()
+ }
+
+ private fun String.toDate(): Long {
+ return try {
+ DATE_FORMATTER.parse(this)?.time ?: 0L
+ } catch (e: ParseException) {
+ 0L
+ }
+ }
+
+ private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
+
+ companion object {
+ private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," +
+ "image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
+ private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
+ private const val ACCEPT_JSON = "application/json, text/javascript, */*; q=0.01"
+ private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5"
+
+ private const val ITEMS_PER_PAGE = 21
+ private const val DIRECTORY_TYPE_POPULAR = 5
+ private const val DIRECTORY_TYPE_LATEST = 6
+
+ private val DATE_FORMATTER by lazy {
+ SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH)
+ }
+ }
+}