diff --git a/src/es/leercapitulo/AndroidManifest.xml b/src/es/leercapitulo/AndroidManifest.xml
new file mode 100644
index 00000000..b4571bfa
--- /dev/null
+++ b/src/es/leercapitulo/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/es/leercapitulo/build.gradle b/src/es/leercapitulo/build.gradle
new file mode 100644
index 00000000..f50ce515
--- /dev/null
+++ b/src/es/leercapitulo/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'LeerCapitulo'
+ pkgNameSuffix = 'es.leercapitulo'
+ extClass = '.LeerCapitulo'
+ extVersionCode = 6
+}
+
+dependencies {
+ implementation(project(":lib-synchrony"))
+}
+
+apply from: "$rootDir/common.gradle"
\ No newline at end of file
diff --git a/src/es/leercapitulo/res/mipmap-hdpi/ic_launcher.png b/src/es/leercapitulo/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..6649ec5f
Binary files /dev/null and b/src/es/leercapitulo/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/es/leercapitulo/res/mipmap-mdpi/ic_launcher.png b/src/es/leercapitulo/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..f2df43ab
Binary files /dev/null and b/src/es/leercapitulo/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/es/leercapitulo/res/mipmap-xhdpi/ic_launcher.png b/src/es/leercapitulo/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..bdd03a25
Binary files /dev/null and b/src/es/leercapitulo/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/es/leercapitulo/res/mipmap-xxhdpi/ic_launcher.png b/src/es/leercapitulo/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..47d60c4c
Binary files /dev/null and b/src/es/leercapitulo/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/es/leercapitulo/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/leercapitulo/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b732e8ec
Binary files /dev/null and b/src/es/leercapitulo/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/es/leercapitulo/res/web_hi_res_512.png b/src/es/leercapitulo/res/web_hi_res_512.png
new file mode 100644
index 00000000..44da81cf
Binary files /dev/null and b/src/es/leercapitulo/res/web_hi_res_512.png differ
diff --git a/src/es/leercapitulo/src/eu/kanade/tachiyomi/extension/es/leercapitulo/LeerCapitulo.kt b/src/es/leercapitulo/src/eu/kanade/tachiyomi/extension/es/leercapitulo/LeerCapitulo.kt
new file mode 100644
index 00000000..69e1b563
--- /dev/null
+++ b/src/es/leercapitulo/src/eu/kanade/tachiyomi/extension/es/leercapitulo/LeerCapitulo.kt
@@ -0,0 +1,176 @@
+package eu.kanade.tachiyomi.extension.es.leercapitulo
+
+import android.util.Base64
+import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
+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 kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import uy.kohesive.injekt.injectLazy
+import java.nio.charset.Charset
+
+class LeerCapitulo : ParsedHttpSource() {
+ override val name = "LeerCapitulo"
+
+ override val lang = "es"
+
+ override val supportsLatest = true
+
+ private val json: Json by injectLazy()
+
+ override val baseUrl = "https://www.leercapitulo.com"
+
+ override val client = super.client.newBuilder()
+ .rateLimitHost(baseUrl.toHttpUrl(), 1, 3)
+ .build()
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
+
+ override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
+
+ override fun popularMangaSelector(): String = ".hot-manga > .thumbnails > a"
+
+ override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
+ setUrlWithoutDomain(element.attr("abs:href"))
+ title = element.attr("title")
+ thumbnail_url = element.selectFirst("img")!!.imgAttr()
+ }
+
+ override fun popularMangaNextPageSelector(): String? = null
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/search-autocomplete".toHttpUrl().newBuilder()
+ .addQueryParameter("term", query)
+
+ return GET(url.toString(), headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val mangas = json.decodeFromString>(response.body.string()).map {
+ SManga.create().apply {
+ setUrlWithoutDomain(it.link)
+ title = it.label
+ thumbnail_url = baseUrl + it.thumbnail
+ }
+ }
+
+ return MangasPage(mangas, hasNextPage = false)
+ }
+
+ override fun searchMangaSelector(): String = throw UnsupportedOperationException("Not used.")
+
+ override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used.")
+
+ override fun searchMangaNextPageSelector(): String? = null
+
+ override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(page)
+
+ override fun latestUpdatesSelector(): String = ".mainpage-manga"
+
+ override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
+ setUrlWithoutDomain(element.selectFirst(".media-body > a")!!.attr("abs:href"))
+ title = element.selectFirst("h4")!!.text()
+ thumbnail_url = element.selectFirst("img")!!.imgAttr()
+ }
+
+ override fun latestUpdatesNextPageSelector(): String? = null
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ title = document.selectFirst("h1")!!.text()
+
+ val altNames = document.selectFirst(".description-update > span:contains(TÃtulos Alternativos:) + :matchText")?.text()
+ val desc = document.selectFirst("#example2")!!.text()
+ description = when (altNames) {
+ null -> desc
+ else -> "$desc\n\nAlt name(s): $altNames"
+ }
+
+ genre = document.select(".description-update a[href^='/genre/']").joinToString { it.text() }
+ status = document.selectFirst(".description-update > span:contains(Estado:) + :matchText")!!.text().toStatus()
+ thumbnail_url = document.selectFirst(".cover-detail > img")!!.imgAttr()
+ }
+
+ override fun chapterListSelector(): String = ".chapter-list > ul > li"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ with(element.selectFirst("a.xanh")!!) {
+ setUrlWithoutDomain(attr("abs:href"))
+ name = text()
+ }
+ }
+
+ override fun pageListParse(document: Document): List {
+ val orderList = document.selectFirst("meta[property=ad:check]")?.attr("content")
+ ?.replace("[^\\d]+".toRegex(), "-")
+ ?.split("-")
+
+ val useReversedString = orderList?.any { it == "01" }
+
+ val arrayData = document.selectFirst("#array_data")!!.text()
+
+ val scriptUrl = document.selectFirst("section.bodycontainer > script[src$=.js]")?.attr("abs:src")
+ ?: throw Exception("Script not found")
+
+ val scriptData = client.newCall(GET(scriptUrl, headers)).execute().body.string()
+
+ val deobfuscatedScript = Deobfuscator.deobfuscateScript(scriptData)
+ ?: throw Exception("Unable to deobfuscate script")
+
+ val keyRegex = """'([A-Z0-9]{62})'""".toRegex(RegexOption.IGNORE_CASE)
+
+ val (key1, key2) = keyRegex.findAll(deobfuscatedScript).map { it.groupValues[1] }.toList()
+
+ val encodedUrls = arrayData.replace(Regex("[A-Z0-9]", RegexOption.IGNORE_CASE)) {
+ val index = key2.indexOf(it.value)
+ key1[index].toString()
+ }
+
+ val urlList = String(Base64.decode(encodedUrls, Base64.DEFAULT), Charset.forName("UTF-8")).split(",")
+
+ val sortedUrls = orderList?.map {
+ if (useReversedString == true) urlList[it.reversed().toInt()] else urlList[it.toInt()]
+ }?.reversed() ?: urlList
+
+ return sortedUrls.mapIndexed { i, image_url ->
+ Page(i, imageUrl = image_url)
+ }
+ }
+
+ private fun Element.imgAttr(): String = when {
+ hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
+ hasAttr("data-src") -> attr("abs:data-src")
+ else -> attr("abs:src")
+ }
+
+ override fun imageUrlParse(document: Document): String =
+ throw UnsupportedOperationException("Not used.")
+
+ private fun String.toStatus() = when (this) {
+ "Ongoing" -> SManga.ONGOING
+ "Paused" -> SManga.ON_HIATUS
+ "Completed" -> SManga.COMPLETED
+ "Cancelled" -> SManga.CANCELLED
+ else -> SManga.UNKNOWN
+ }
+
+ @Serializable
+ data class MangaDto(
+ val label: String,
+ val link: String,
+ val thumbnail: String,
+ val value: String,
+ )
+}