diff --git a/src/pt/bakai/AndroidManifest.xml b/src/pt/bakai/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/pt/bakai/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/pt/bakai/build.gradle b/src/pt/bakai/build.gradle
new file mode 100644
index 000000000..48c2b1447
--- /dev/null
+++ b/src/pt/bakai/build.gradle
@@ -0,0 +1,15 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Bakai'
+ pkgNameSuffix = 'pt.bakai'
+ extClass = '.Bakai'
+ extVersionCode = 1
+}
+
+dependencies {
+ implementation project(':lib-ratelimit')
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/bakai/res/mipmap-hdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..4dc069e59
Binary files /dev/null and b/src/pt/bakai/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/bakai/res/mipmap-mdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..bf34eebcd
Binary files /dev/null and b/src/pt/bakai/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/bakai/res/mipmap-xhdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..3ebae1078
Binary files /dev/null and b/src/pt/bakai/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/bakai/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..22957bfab
Binary files /dev/null and b/src/pt/bakai/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/bakai/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1d637c67e
Binary files /dev/null and b/src/pt/bakai/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/bakai/res/web_hi_res_512.png b/src/pt/bakai/res/web_hi_res_512.png
new file mode 100644
index 000000000..53a8f8dee
Binary files /dev/null and b/src/pt/bakai/res/web_hi_res_512.png differ
diff --git a/src/pt/bakai/src/eu/kanade/tachiyomi/extension/pt/bakai/Bakai.kt b/src/pt/bakai/src/eu/kanade/tachiyomi/extension/pt/bakai/Bakai.kt
new file mode 100644
index 000000000..b5e2c9592
--- /dev/null
+++ b/src/pt/bakai/src/eu/kanade/tachiyomi/extension/pt/bakai/Bakai.kt
@@ -0,0 +1,138 @@
+package eu.kanade.tachiyomi.extension.pt.bakai
+
+import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
+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.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class Bakai : ParsedHttpSource() {
+
+ override val name = "Bakai"
+
+ override val baseUrl = "https://bakai.org"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+ .addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1, 2))
+ .addInterceptor(SpecificHostRateLimitInterceptor(CDN_URL.toHttpUrl(), 1, 1))
+ .build()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Referer", "$baseUrl/")
+
+ // Source doesn't have a popular list, so use latest instead.
+ override fun popularMangaRequest(page: Int): Request = latestUpdatesRequest(page)
+
+ override fun popularMangaSelector(): String = latestUpdatesSelector()
+
+ override fun popularMangaFromElement(element: Element): SManga = latestUpdatesFromElement(element)
+
+ override fun popularMangaNextPageSelector(): String = latestUpdatesNextPageSelector()
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ val path = if (page > 1) "home/page/$page/" else ""
+ return GET("$baseUrl/$path", headers)
+ }
+
+ override fun latestUpdatesSelector() = "#elCmsPageWrap ul.ipsGrid article.ipsBox"
+
+ override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.selectFirst("h2.ipsType_pageTitle a")!!.text().trim()
+ thumbnail_url = element.selectFirst("img.ipsImage[alt]")!!.attr("abs:src")
+ setUrlWithoutDomain(element.selectFirst("a[title]")!!.attr("href"))
+ }
+
+ override fun latestUpdatesNextPageSelector() =
+ "#elCmsPageWrap ul.ipsPagination li.ipsPagination_next:not(.ipsPagination_inactive)"
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/search/".toHttpUrl().newBuilder()
+ .addQueryParameter("q", query)
+ .addQueryParameter("type", "cms_records1")
+ .addQueryParameter("search_in", "titles")
+ .addQueryParameter("sortby", "relevancy")
+ .toString()
+
+ return GET(url, headers)
+ }
+
+ override fun searchMangaSelector() = "#elSearch_main ol.ipsStream li.ipsStreamItem"
+
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.selectFirst("h2.ipsStreamItem_title a")!!.text().trim()
+ thumbnail_url = element.selectFirst("span.ipsThumb img")!!.attr("abs:src")
+ setUrlWithoutDomain(element.selectFirst("h2.ipsStreamItem_title a")!!.attr("href"))
+ }
+
+ override fun searchMangaNextPageSelector(): String? = null
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ val infoElement = document.selectFirst(chapterListSelector())!!
+
+ author = infoElement.select("p:contains(Artista:) a")
+ .joinToString { it.text().trim() }
+ genre = infoElement.selectFirst("p:contains(Tags:) span.ipsBadge + span")!!.text()
+ status = SManga.COMPLETED
+ description = infoElement.select("section.ipsType_richText p")
+ .joinToString("\n\n") { it.text().trim() }
+ thumbnail_url = infoElement.selectFirst("div.cCmsRecord_image img.ipsImage")!!.attr("abs:src")
+ }
+
+ override fun chapterListSelector() = "#ipsLayout_contentWrapper article.ipsContained"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ name = element.selectFirst("p:contains(Tipo:) a")!!.text()
+ scanlator = element.select("p:contains(Tradução:) a").firstOrNull()
+ ?.text()?.trim()
+ date_upload = element.ownerDocument().select("div.ipsPageHeader__meta time").firstOrNull()
+ ?.attr("datetime")?.toDate() ?: 0L
+ setUrlWithoutDomain(element.ownerDocument().location())
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select(chapterListSelector() + " div.ipsGrid img.ipsImage")
+ .mapIndexed { i, element ->
+ Page(i, document.location(), element.attr("abs:data-src"))
+ }
+ }
+
+ 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)
+ }
+
+ private fun String.toDate(): Long {
+ return runCatching { DATE_FORMATTER.parse(trim())?.time }
+ .getOrNull() ?: 0L
+ }
+
+ companion object {
+ private const val CDN_URL = "https://img.bakai.org"
+
+ private const val ACCEPT_IMAGE = "image/webp,image/apng,image/*,*/*;q=0.8"
+
+ private val DATE_FORMATTER by lazy {
+ SimpleDateFormat("yyyy-mm-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
+ }
+ }
+}