diff --git a/src/pt/tsukimangas/build.gradle b/src/pt/tsukimangas/build.gradle
new file mode 100644
index 000000000..98f6e61b0
--- /dev/null
+++ b/src/pt/tsukimangas/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+    extName = 'Tsuki Mangás'
+    pkgNameSuffix = 'pt.tsukimangas'
+    extClass = '.TsukiMangas'
+    extVersionCode = 6
+    libVersion = '1.2'
+}
+
+dependencies {
+    implementation project(':lib-ratelimit')
+}
+
+apply from: "$rootDir/common.gradle"
+
diff --git a/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..6f76b7f4d
Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..1ecc240e9
Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..590cfe9b5
Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6dd850a9d
Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..cc9dc086a
Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/tsukimangas/res/web_hi_res_512.png b/src/pt/tsukimangas/res/web_hi_res_512.png
new file mode 100644
index 000000000..8f5f3f51d
Binary files /dev/null and b/src/pt/tsukimangas/res/web_hi_res_512.png differ
diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt
new file mode 100644
index 000000000..bc8fe76b7
--- /dev/null
+++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt
@@ -0,0 +1,349 @@
+package eu.kanade.tachiyomi.extension.pt.tsukimangas
+
+import com.github.salomonbrys.kotson.array
+import com.github.salomonbrys.kotson.int
+import com.github.salomonbrys.kotson.obj
+import com.github.salomonbrys.kotson.string
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.source.model.Filter
+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.HttpSource
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+
+class TsukiMangas : HttpSource() {
+
+    override val name = "Tsuki Mangás"
+
+    override val baseUrl = "https://tsukimangas.com"
+
+    override val lang = "pt-BR"
+
+    override val supportsLatest = true
+
+    private val rateLimitInterceptor = RateLimitInterceptor(150, 1, TimeUnit.MINUTES)
+
+    override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+        .addInterceptor(rateLimitInterceptor)
+        .build()
+
+    override fun headersBuilder(): Headers.Builder = Headers.Builder()
+        .add("Accept", ACCEPT)
+        .add("Accept-Language", ACCEPT_LANGUAGE)
+        .add("User-Agent", USER_AGENT)
+        .add("Referer", baseUrl)
+
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/api/melhores", headers)
+    }
+
+    override fun popularMangaParse(response: Response): MangasPage {
+        val result = response.asJson().array
+
+        val popularMangas = result.map { popularMangaItemParse(it.obj) }
+
+        return MangasPage(popularMangas, false)
+    }
+
+    private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
+        title = obj["TITULO"].string
+        thumbnail_url = baseUrl + "/imgs/" + obj["CAPA"].string.substringBefore("?")
+        url = "/manga/" + obj["URL"].string
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/api/lancamentos/$page", headers)
+    }
+
+    override fun latestUpdatesParse(response: Response): MangasPage {
+        val json = response.asJson().array
+
+        if (json.size() == 0)
+            return MangasPage(emptyList(), false)
+
+        val result = json[0].obj
+
+        val latestMangas = result["mangas"].array
+            .map { latestMangaItemParse(it.obj) }
+
+        // Latest pagination doesn't seen to have a lower end.
+        return MangasPage(latestMangas, true)
+    }
+
+    private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
+        title = obj["TITULO"].string
+        thumbnail_url = baseUrl + "/imgs/" + obj["CAPA"].string
+        url = "/manga/" + obj["URL"].string
+    }
+
+    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
+        return client.newCall(searchMangaRequest(page, query, filters))
+            .asObservableSuccess()
+            .map { response -> searchMangaParse(response) }
+            .onErrorReturn {
+                if (it.message!!.contains("404")) {
+                    return@onErrorReturn MangasPage(emptyList(), false)
+                }
+
+                throw it
+            }
+    }
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val newHeaders = headersBuilder()
+            .set("Referer", "$baseUrl/lista-mangas")
+            .build()
+
+        val pathQuery = if (query.isEmpty()) "all" else query
+
+        val genreFilter = if (filters.isEmpty()) null else filters[0] as GenreFilter
+        val genreQuery = genreFilter?.state
+            ?.filter { it.state }
+            ?.joinToString(",") { it.name } ?: "all"
+
+        val url = HttpUrl.parse("$baseUrl/api/generos")!!.newBuilder()
+            .addEncodedPathSegment(genreQuery)
+            .addPathSegment(page.toString())
+            .addEncodedPathSegment(pathQuery)
+            .toString()
+
+        return GET(url, newHeaders)
+    }
+
+    override fun searchMangaParse(response: Response): MangasPage {
+        val result = response.asJson().array
+
+        if (result.size() == 0)
+            return MangasPage(emptyList(), false)
+
+        val searchMangas = result.map { searchMangaItemParse(it.obj) }
+
+        val currentPage = response.request().url().toString()
+            .substringBeforeLast("/")
+            .substringAfterLast("/")
+            .toInt()
+        val lastPage = result[0].obj["page"].array[0].int
+        val hasNextPage = currentPage < lastPage
+
+        return MangasPage(searchMangas, hasNextPage)
+    }
+
+    private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
+        title = obj["TITULO"].string
+        thumbnail_url = baseUrl + "/imgs/" + obj["CAPA"].string.substringBefore("?")
+        url = "/manga/" + obj["URL"].string
+    }
+
+    // Workaround to allow "Open in browser" use the real URL.
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+        return client.newCall(mangaDetailsApiRequest(manga))
+            .asObservableSuccess()
+            .map { response ->
+                mangaDetailsParse(response).apply { initialized = true }
+            }
+    }
+
+    private fun mangaDetailsApiRequest(manga: SManga): Request {
+        val newHeaders = headersBuilder()
+            .set("Referer", baseUrl + manga.url)
+            .build()
+
+        val mangaSlug = manga.url.substringAfterLast("/")
+
+        return GET("$baseUrl/api/mangas/$mangaSlug", newHeaders)
+    }
+
+    override fun mangaDetailsRequest(manga: SManga): Request {
+        val newHeaders = headersBuilder()
+            .removeAll("Accept")
+            .build()
+
+        return GET(baseUrl + manga.url, newHeaders)
+    }
+
+    override fun mangaDetailsParse(response: Response): SManga {
+        val result = response.asJson().obj["manga"].array[0].obj
+
+        return SManga.create().apply {
+            title = result["TITULO"].string
+            thumbnail_url = baseUrl + "/imgs/" + result["CAPA"].string.substringBefore("?")
+            description = result["SINOPSE"].string
+            status = SManga.ONGOING
+            author = result["AUTOR"].string
+            artist = result["ARTISTA"].string
+            genre = result["GENEROS"].string
+        }
+    }
+
+    override fun chapterListRequest(manga: SManga): Request = chapterListRequestPaginated(manga.url, 1)
+
+    private fun chapterListRequestPaginated(mangaUrl: String, page: Int): Request {
+        val slug = mangaUrl.substringAfterLast("/")
+
+        val newHeaders = headersBuilder()
+            .set("Referer", baseUrl + mangaUrl)
+            .build()
+
+        return GET("$baseUrl/api/capitulospag/$slug/DESC/$page", newHeaders)
+    }
+
+    override fun chapterListParse(response: Response): List<SChapter> {
+        var result = response.asJson().array
+
+        if (result.size() == 0)
+            return emptyList()
+
+        val mangaUrl = response.request().header("Referer")!!.substringAfter(baseUrl)
+        val mangaSlug = mangaUrl.substringAfterLast("/")
+        var page = 1
+
+        val chapters = mutableListOf<SChapter>()
+
+        while (result.size() != 0) {
+            chapters += result
+                .map { chapterListItemParse(it.obj, mangaSlug) }
+                .toMutableList()
+
+            val newRequest = chapterListRequestPaginated(mangaUrl, ++page)
+            result = client.newCall(newRequest).execute().asJson().array
+        }
+
+        return chapters
+    }
+
+    private fun chapterListItemParse(obj: JsonObject, slug: String): SChapter = SChapter.create().apply {
+        name = "Cap. " + obj["NUMERO"].string +
+            (if (obj["TITULO"].string.isNotEmpty()) " - " + obj["TITULO"].string else "")
+        chapter_number = obj["NUMERO"].string.toFloatOrNull() ?: -1f
+        scanlator = obj["scans"].array.joinToString { it.obj["NOME"].string }
+        date_upload = obj["DATA"].string.substringBefore("T").toDate()
+        url = "/leitor/$slug/" + obj["NUMERO"].string
+    }
+
+    override fun pageListRequest(chapter: SChapter): Request {
+        val newHeaders = headersBuilder()
+            .set("Referer", baseUrl + chapter.url)
+            .build()
+
+        return GET("$baseUrl/api" + chapter.url, newHeaders)
+    }
+
+    override fun pageListParse(response: Response): List<Page> {
+        val result = response.asJson().array
+
+        return result.mapIndexed { i, page -> Page(i, baseUrl + "/", page.obj["IMG"].string) }
+    }
+
+    override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
+
+    override fun imageUrlParse(response: Response): String = ""
+
+    override fun imageRequest(page: Page): Request {
+        val newHeaders = headersBuilder()
+            .set("Accept", ACCEPT_IMAGE)
+            .set("Accept-Language", ACCEPT_LANGUAGE)
+            .set("Referer", page.url)
+            .build()
+
+        return GET(page.imageUrl!!, newHeaders)
+    }
+
+    private class Genre(name: String) : Filter.CheckBox(name)
+
+    private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Gêneros", genres)
+
+    override fun getFilterList(): FilterList = FilterList(GenreFilter(getGenreList()))
+
+    // [...document.querySelectorAll(".multiselect__element span span")]
+    //     .map(i => `Genre("${i.innerHTML}")`).join(",\n")
+    private fun getGenreList(): List<Genre> = listOf(
+        Genre("4-koma"),
+        Genre("Adulto"),
+        Genre("Artes Marciais"),
+        Genre("Aventura"),
+        Genre("Ação"),
+        Genre("Bender"),
+        Genre("Comédia"),
+        Genre("Drama"),
+        Genre("Ecchi"),
+        Genre("Esporte"),
+        Genre("Fantasia"),
+        Genre("Ficção"),
+        Genre("Gastronomia"),
+        Genre("Gender"),
+        Genre("Guerra"),
+        Genre("Harém"),
+        Genre("Histórico"),
+        Genre("Horror"),
+        Genre("Isekai"),
+        Genre("Josei"),
+        Genre("Magia"),
+        Genre("Manhua"),
+        Genre("Manhwa"),
+        Genre("Mecha"),
+        Genre("Medicina"),
+        Genre("Militar"),
+        Genre("Mistério"),
+        Genre("Musical"),
+        Genre("One-Shot"),
+        Genre("Psicológico"),
+        Genre("Romance"),
+        Genre("Sci-fi"),
+        Genre("Seinen"),
+        Genre("Shoujo"),
+        Genre("Shoujo Ai"),
+        Genre("Shounen"),
+        Genre("Shounen Ai"),
+        Genre("Slice of Life"),
+        Genre("Sobrenatural"),
+        Genre("Super Poderes"),
+        Genre("Suspense"),
+        Genre("Terror"),
+        Genre("Thriller"),
+        Genre("Tragédia"),
+        Genre("Vida Escolar"),
+        Genre("Webtoon"),
+        Genre("Yaoi"),
+        Genre("Yuri"),
+        Genre("Zumbi")
+    )
+
+    private fun String.toDate(): Long {
+        return try {
+            DATE_FORMATTER.parse(this)?.time ?: 0L
+        } catch (e: ParseException) {
+            0L
+        }
+    }
+
+    private fun Response.asJson(): JsonElement = JSON_PARSER.parse(body()!!.string())
+
+    companion object {
+        private const val ACCEPT = "application/json, text/plain, */*"
+        private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,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"
+        // By request of site owner. Detailed at Issue #4912 (in Portuguese).
+        private val USER_AGENT = "Tachiyomi " + System.getProperty("http.agent")
+
+        private val JSON_PARSER by lazy { JsonParser() }
+
+        private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
+    }
+}