diff --git a/src/en/mangarockes/build.gradle b/src/en/mangarockes/build.gradle
new file mode 100644
index 000000000..f1ebfc77d
--- /dev/null
+++ b/src/en/mangarockes/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+    appName = 'Tachiyomi: MangaRock.es'
+    pkgNameSuffix = 'en.mangarockes'
+    extClass = '.MangaRockEs'
+    extVersionCode = 1
+    libVersion = '1.2'
+}
+
+dependencies {
+    compileOnly 'com.google.code.gson:gson:2.8.2'
+    compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/mangarockes/res/mipmap-hdpi/ic_launcher.png b/src/en/mangarockes/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..d781a98f4
Binary files /dev/null and b/src/en/mangarockes/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/mangarockes/res/mipmap-mdpi/ic_launcher.png b/src/en/mangarockes/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..f16c6ddad
Binary files /dev/null and b/src/en/mangarockes/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/mangarockes/res/mipmap-xhdpi/ic_launcher.png b/src/en/mangarockes/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..a44510632
Binary files /dev/null and b/src/en/mangarockes/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/mangarockes/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangarockes/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..873a21662
Binary files /dev/null and b/src/en/mangarockes/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/mangarockes/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mangarockes/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d312cafe9
Binary files /dev/null and b/src/en/mangarockes/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/mangarockes/res/web_hi_res_512.png b/src/en/mangarockes/res/web_hi_res_512.png
new file mode 100644
index 000000000..cb6ffacee
Binary files /dev/null and b/src/en/mangarockes/res/web_hi_res_512.png differ
diff --git a/src/en/mangarockes/src/eu/kanade/tachiyomi/extension/en/mangarockes/MangaRockEs.kt b/src/en/mangarockes/src/eu/kanade/tachiyomi/extension/en/mangarockes/MangaRockEs.kt
new file mode 100644
index 000000000..de24b8e16
--- /dev/null
+++ b/src/en/mangarockes/src/eu/kanade/tachiyomi/extension/en/mangarockes/MangaRockEs.kt
@@ -0,0 +1,315 @@
+package eu.kanade.tachiyomi.extension.en.mangarockes
+
+import com.github.salomonbrys.kotson.fromJson
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import okhttp3.HttpUrl
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.Calendar
+import kotlin.experimental.and
+import kotlin.experimental.xor
+
+
+class MangaRockEs : ParsedHttpSource() {
+
+    override val name = "MangaRock.es"
+
+    override val baseUrl = "https://mangarock.es"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    // Handles the page decoding
+    override val client: OkHttpClient = network.cloudflareClient.newBuilder().addInterceptor(fun(chain): Response {
+        val url = chain.request().url().toString()
+        val response = chain.proceed(chain.request())
+        if (!url.endsWith(".mri")) return response
+
+        val decoded: ByteArray = decodeMri(response)
+        val mediaType = MediaType.parse("image/webp")
+        val rb = ResponseBody.create(mediaType, decoded)
+        return response.newBuilder().body(rb).build()
+    }).build()
+
+    // Popular
+
+    override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/$page?sort=rank", headers)
+
+    override fun popularMangaSelector() = "div.col-five"
+
+    override fun popularMangaFromElement(element: Element): SManga {
+        return SManga.create().apply {
+            element.select("h3 a").let {
+                title = it.text()
+                setUrlWithoutDomain(it.attr("href"))
+            }
+            thumbnail_url = element.select("img").attr("abs:src")
+        }
+    }
+
+    override fun popularMangaNextPageSelector() = "a.page-link:contains(next)"
+
+    // Latest
+
+    override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga/latest/$page?sort=date", headers)
+
+    override fun latestUpdatesSelector() = "div.product-item-detail"
+
+    override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+    override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+
+    // Search
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        return if (query.isNotBlank()) {
+            GET("$baseUrl/search/${query.replace(" ", "+")}/$page", headers)
+        } else {
+            val url = HttpUrl.parse("$baseUrl/manga" + if (page > 1) "/$page" else "")!!.newBuilder()
+            filters.forEach { filter ->
+                when (filter) {
+                    is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
+                    is RankFilter -> url.addQueryParameter("rank", filter.toUriPart())
+                    is SortBy -> url.addQueryParameter("sort", filter.toUriPart())
+                    is GenreList -> {
+                        val genres = filter.state
+                            .filter { it.state }
+                            .joinToString(".") { it.uriPart }
+                        url.addQueryParameter("genres", genres)
+                    }
+                }
+            }
+            GET(url.toString(), headers)
+        }
+    }
+
+    override fun searchMangaSelector() = popularMangaSelector()
+
+    override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+    // Details
+
+    override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+        return SManga.create().apply {
+            document.select("div.block-info-manga").let { info ->
+                thumbnail_url = info.select("div.thumb_inner").attr("style")
+                    .substringAfter("'").substringBefore("'")
+                title = info.select("h1").text()
+                author = info.select("div.author_item").text()
+                status = info.select("div.status_chapter_item").text().substringBefore(" ").let {
+                    when {
+                        it.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
+                        it.contains("Completed", ignoreCase = true) -> SManga.COMPLETED
+                        else -> SManga.UNKNOWN
+                    }
+                }
+            }
+            genre = document.select("div.tags a").joinToString { it.text() }
+            description = document.select("div.full.summary p").filterNot { it.text().isNullOrEmpty() }.joinToString("\n")
+        }
+    }
+
+    // Chapters
+
+    override fun chapterListSelector(): String = "tbody[data-test=chapter-table] tr"
+
+    override fun chapterFromElement(element: Element): SChapter {
+        return SChapter.create().apply {
+            element.select("a").let {
+                name = it.text()
+                setUrlWithoutDomain(it.attr("href"))
+            }
+            date_upload = element.select("td").lastOrNull()?.text()?.let { date ->
+                if (date.contains("ago", ignoreCase = true)) {
+                    val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ")
+
+                    val calendar = Calendar.getInstance()
+                    when (trimmedDate[1]) {
+                        "day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
+                        "hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
+                        "minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
+                        "second" -> calendar.apply { add(Calendar.SECOND, -trimmedDate[0].toInt()) }
+                    }
+
+                    calendar.timeInMillis
+                } else {
+                    SimpleDateFormat("MMM d, yyyy", Locale.US).parse(date).time
+                }
+            } ?: 0
+        }
+    }
+
+    // Pages
+
+    private val gson by lazy { Gson() }
+
+    override fun pageListParse(response: Response): List<Page> {
+        val responseString = response.body()!!.string()
+        return Regex("""mangaData = (\[.*]);""", RegexOption.IGNORE_CASE).find(responseString)?.groupValues?.get(1)?.let { array ->
+            gson.fromJson<JsonArray>(array)
+                .mapIndexed { i, jsonElement -> Page(i, "", jsonElement.asJsonObject["url"].asString) }
+        }
+            ?: Regex("""getManga\(\d+, '(http.*)',""").findAll(responseString).toList()
+                .mapIndexed { i, mr -> Page(i, "", mr.groupValues[1]) }
+    }
+
+    override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException("This method should not be called!")
+
+    override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("This method should not be called!")
+
+    // Filters
+
+    // See drawWebpToCanvas function in the site's client.js file
+    // Extracted code: https://jsfiddle.net/6h2sLcs4/30/
+    private fun decodeMri(response: Response): ByteArray {
+        val data = response.body()!!.bytes()
+
+        // Decode file if it starts with "E" (space when XOR-ed later)
+        if (data[0] != 69.toByte()) return data
+
+        // Reconstruct WEBP header
+        // Doc: https://developers.google.com/speed/webp/docs/riff_container#webp_file_header
+        val buffer = ByteArray(data.size + 15)
+        val size = data.size + 7
+        buffer[0] = 82  // R
+        buffer[1] = 73  // I
+        buffer[2] = 70  // F
+        buffer[3] = 70  // F
+        buffer[4] = (255.toByte() and size.toByte())
+        buffer[5] = (size ushr 8).toByte() and 255.toByte()
+        buffer[6] = (size ushr 16).toByte() and 255.toByte()
+        buffer[7] = (size ushr 24).toByte() and 255.toByte()
+        buffer[8] = 87  // W
+        buffer[9] = 69  // E
+        buffer[10] = 66 // B
+        buffer[11] = 80 // P
+        buffer[12] = 86 // V
+        buffer[13] = 80 // P
+        buffer[14] = 56 // 8
+
+        // Decrypt file content using XOR cipher with 101 as the key
+        val cipherKey = 101.toByte()
+        for (r in data.indices) {
+            buffer[r + 15] = cipherKey xor data[r]
+        }
+
+        return buffer
+    }
+
+    // Filters
+
+    private class StatusFilter : UriPartFilter("Status", arrayOf(
+        Pair("All", "all"),
+        Pair("Completed", "completed"),
+        Pair("Ongoing", "ongoing")
+    ))
+
+    private class RankFilter : UriPartFilter("Rank", arrayOf(
+            Pair("All", "all"),
+            Pair("1 - 999", "1-999"),
+            Pair("1k - 2k", "1000-2000"),
+            Pair("2k - 3k", "2000-3000"),
+            Pair("3k - 4k", "3000-4000"),
+            Pair("4k - 5k", "4000-5000"),
+            Pair("5k - 6k", "5000-6000"),
+            Pair("6k - 7k", "6000-7000"),
+            Pair("7k - 8k", "7000-8000"),
+            Pair("8k - 9k", "8000-9000"),
+            Pair("9k - 19k", "9000-10000"),
+            Pair("10k - 11k", "10000-11000")
+    ))
+
+    private class SortBy : UriPartFilter("Sort by", arrayOf(
+            Pair("Name", "name"),
+            Pair("Rank", "rank")
+    ))
+
+    private class Genre(name: String, val uriPart: String) : Filter.CheckBox(name)
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
+
+    override fun getFilterList() = FilterList(
+            // Search and filter don't work at the same time
+            Filter.Header("NOTE: Ignored if using text search!"),
+            Filter.Separator(),
+            StatusFilter(),
+            RankFilter(),
+            SortBy(),
+            GenreList(getGenreList())
+    )
+
+    // [...document.querySelectorAll('._2DMqI .mdl-checkbox')].map(n => `Genre("${n.querySelector('.mdl-checkbox__label').innerText}", "${n.querySelector('input').dataset.oid}")`).sort().join(',\n')
+    // on https://mangarock.com/manga
+    private fun getGenreList() = listOf(
+            Genre("4-koma", "4-koma"),
+            Genre("Action", "action"),
+            Genre("Adult", "adult"),
+            Genre("Adventure", "adventure"),
+            Genre("Comedy", "comedy"),
+            Genre("Demons", "demons"),
+            Genre("Doujinshi", "doujinshi"),
+            Genre("Drama", "drama"),
+            Genre("Ecchi", "ecchi"),
+            Genre("Fantasy", "fantasy"),
+            Genre("Gender Bender", "gender-bender"),
+            Genre("Harem", "harem"),
+            Genre("Historical", "historical"),
+            Genre("Horror", "horror"),
+            Genre("Isekai", "isekai"),
+            Genre("Josei", "josei"),
+            Genre("Kids", "kids"),
+            Genre("Magic", "magic"),
+            Genre("Martial Arts", "martial-arts"),
+            Genre("Mature", "mature"),
+            Genre("Mecha", "mecha"),
+            Genre("Military", "military"),
+            Genre("Music", "music"),
+            Genre("Mystery", "mystery"),
+            Genre("One Shot", "one-shot"),
+            Genre("Parody", "parody"),
+            Genre("Police", "police"),
+            Genre("Psychological", "psychological"),
+            Genre("Romance", "romance"),
+            Genre("School Life", "school-life"),
+            Genre("Sci-Fi", "sci-fi"),
+            Genre("Seinen", "seinen"),
+            Genre("Shoujo Ai", "shoujo-ai"),
+            Genre("Shoujo", "shoujo"),
+            Genre("Shounen Ai", "shounen-ai"),
+            Genre("Shounen", "shounen"),
+            Genre("Slice of Life", "slice-of-life"),
+            Genre("Smut", "smut"),
+            Genre("Space", "space"),
+            Genre("Sports", "sports"),
+            Genre("Super Power", "super-power"),
+            Genre("Supernatural", "supernatural"),
+            Genre("Tragedy", "tragedy"),
+            Genre("Vampire", "vampire"),
+            Genre("Webtoons", "webtoons"),
+            Genre("Yaoi", "yaoi"),
+            Genre("Yuri", "yuri")
+    )
+
+    private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
+            Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
+        fun toUriPart() = vals[state].second
+    }
+
+}