diff --git a/multisrc/overrides/madara/templescanesp/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/madara/templescanesp/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index aa68ca46e..000000000
Binary files a/multisrc/overrides/madara/templescanesp/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/templescanesp/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/madara/templescanesp/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 3be9c027a..000000000
Binary files a/multisrc/overrides/madara/templescanesp/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/templescanesp/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/madara/templescanesp/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 8d84cda9f..000000000
Binary files a/multisrc/overrides/madara/templescanesp/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/templescanesp/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/madara/templescanesp/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index c709dc5a3..000000000
Binary files a/multisrc/overrides/madara/templescanesp/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/templescanesp/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/madara/templescanesp/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index b093413db..000000000
Binary files a/multisrc/overrides/madara/templescanesp/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/templescanesp/res/web_hi_res_512.png b/multisrc/overrides/madara/templescanesp/res/web_hi_res_512.png
deleted file mode 100644
index b15248d65..000000000
Binary files a/multisrc/overrides/madara/templescanesp/res/web_hi_res_512.png and /dev/null differ
diff --git a/multisrc/overrides/madara/templescanesp/src/TempleScanEsp.kt b/multisrc/overrides/madara/templescanesp/src/TempleScanEsp.kt
deleted file mode 100644
index 3c5318afa..000000000
--- a/multisrc/overrides/madara/templescanesp/src/TempleScanEsp.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-package eu.kanade.tachiyomi.extension.es.templescanesp
-
-import eu.kanade.tachiyomi.multisrc.madara.Madara
-import eu.kanade.tachiyomi.network.POST
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import okhttp3.FormBody
-import okhttp3.Request
-import org.jsoup.nodes.Element
-import java.text.SimpleDateFormat
-import java.util.Locale
-
-class TempleScanEsp : Madara(
- "TempleScan",
- "https://templescanesp.com",
- "es",
- SimpleDateFormat("dd.MM.yyyy", Locale("es")),
-) {
- override val mangaSubString = "series"
-
- override fun popularMangaSelector() = "div:has(> div#series-card)"
- override val popularMangaUrlSelector = "div#series-card a.series-link"
- override fun popularMangaNextPageSelector() = "body:not(:has(.no-posts))"
-
- override val mangaDetailsSelectorAuthor = "div.post-content_item:contains(Autor) div.summary-content"
- override val mangaDetailsSelectorArtist = "div.post-content_item:contains(Artista) div.summary-content"
- override val mangaDetailsSelectorStatus = "div.post-content_item:contains(Estado) div.summary-content"
-
- private fun loadMoreRequest(page: Int, metaKey: String): Request {
- val formBody = FormBody.Builder().apply {
- add("action", "madara_load_more")
- add("page", page.toString())
- add("template", "madara-core/content/content-archive")
- add("vars[paged]", "1")
- add("vars[orderby]", "meta_value_num")
- add("vars[template]", "archive")
- add("vars[sidebar]", "full")
- add("vars[meta_query][0][0][key]", "_wp_manga_chapter_type")
- add("vars[meta_query][0][0][value]", "manga")
- add("vars[meta_query][0][relation]", "AND")
- add("vars[meta_query][relation]", "AND")
- add("vars[post_type]", "wp-manga")
- add("vars[post_status]", "publish")
- add("vars[meta_key]", metaKey)
- add("vars[manga_archives_item_layout]", "big_thumbnail")
- }.build()
-
- val xhrHeaders = headersBuilder()
- .add("Content-Length", formBody.contentLength().toString())
- .add("Content-Type", formBody.contentType().toString())
- .add("X-Requested-With", "XMLHttpRequest")
- .build()
-
- return POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, formBody)
- }
-
- override fun popularMangaRequest(page: Int): Request {
- return loadMoreRequest(page - 1, "_wp_manga_views")
- }
-
- override fun latestUpdatesRequest(page: Int): Request {
- return loadMoreRequest(page - 1, "_latest_update")
- }
-
- override fun popularMangaFromElement(element: Element): SManga {
- val manga = SManga.create()
-
- with(element) {
- select(popularMangaUrlSelector).first()?.let {
- manga.setUrlWithoutDomain(it.attr("abs:href"))
- }
-
- select("div.series-box .series-title").first()?.let {
- manga.title = it.text()
- }
-
- select("img").first()?.let {
- manga.thumbnail_url = imageFromElement(it)
- }
- }
-
- return manga
- }
-
- override fun chapterFromElement(element: Element): SChapter {
- val chapter = SChapter.create()
-
- with(element) {
- select(chapterUrlSelector).first()?.let { urlElement ->
- chapter.url = urlElement.attr("abs:href").let {
- it.substringBefore("?style=paged") + if (!it.endsWith(chapterUrlSuffix)) chapterUrlSuffix else ""
- }
- chapter.name = urlElement.select("p").text()
- }
-
- chapter.date_upload = select("img:not(.thumb)").firstOrNull()?.attr("alt")?.let { parseRelativeDate(it) }
- ?: select("span a").firstOrNull()?.attr("title")?.let { parseRelativeDate(it) }
- ?: parseChapterDate(select(chapterDateSelector()).firstOrNull()?.text())
- }
-
- return chapter
- }
-}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
index 002dfc53d..f97f0df12 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
@@ -466,7 +466,6 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("Tatakae Scan", "https://tatakaescan.com", "pt-BR", isNsfw = true, overrideVersionCode = 2),
SingleLang("Taurus Fansub", "https://taurusmanga.com", "es", overrideVersionCode = 1),
SingleLang("TeenManhua", "https://teenmanhua.com", "en", overrideVersionCode = 1),
- SingleLang("TempleScan", "https://templescanesp.com", "es", isNsfw = true, className = "TempleScanEsp", overrideVersionCode = 1),
SingleLang("The Beginning After The End", "https://www.thebeginningaftertheend.fr", "fr", overrideVersionCode = 1),
SingleLang("The Guild", "https://theguildscans.com", "en"),
SingleLang("Time Naight", "https://timenaight.com", "tr"),
diff --git a/src/es/templescanesp/AndroidManifest.xml b/src/es/templescanesp/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/es/templescanesp/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/es/templescanesp/build.gradle b/src/es/templescanesp/build.gradle
new file mode 100644
index 000000000..e6d2f50b1
--- /dev/null
+++ b/src/es/templescanesp/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Temple Scan'
+ pkgNameSuffix = 'es.templescanesp'
+ extClass = '.TempleScanEsp'
+ extVersionCode = 33
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/es/templescanesp/res/mipmap-hdpi/ic_launcher.png b/src/es/templescanesp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..0e3fb7a52
Binary files /dev/null and b/src/es/templescanesp/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/es/templescanesp/res/mipmap-mdpi/ic_launcher.png b/src/es/templescanesp/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..282c12a4d
Binary files /dev/null and b/src/es/templescanesp/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/es/templescanesp/res/mipmap-xhdpi/ic_launcher.png b/src/es/templescanesp/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..50ea886d7
Binary files /dev/null and b/src/es/templescanesp/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/es/templescanesp/res/mipmap-xxhdpi/ic_launcher.png b/src/es/templescanesp/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6cd4a9e3f
Binary files /dev/null and b/src/es/templescanesp/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/es/templescanesp/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/templescanesp/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..3ec4e450e
Binary files /dev/null and b/src/es/templescanesp/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/es/templescanesp/res/web_hi_res_512.png b/src/es/templescanesp/res/web_hi_res_512.png
new file mode 100644
index 000000000..58e3613f9
Binary files /dev/null and b/src/es/templescanesp/res/web_hi_res_512.png differ
diff --git a/src/es/templescanesp/src/eu/kanade/tachiyomi/extension/es/templescanesp/TempleScanEsp.kt b/src/es/templescanesp/src/eu/kanade/tachiyomi/extension/es/templescanesp/TempleScanEsp.kt
new file mode 100644
index 000000000..610b63b40
--- /dev/null
+++ b/src/es/templescanesp/src/eu/kanade/tachiyomi/extension/es/templescanesp/TempleScanEsp.kt
@@ -0,0 +1,185 @@
+package eu.kanade.tachiyomi.extension.es.templescanesp
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
+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.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import uy.kohesive.injekt.injectLazy
+import java.lang.IllegalArgumentException
+import java.util.Calendar
+
+class TempleScanEsp : ParsedHttpSource() {
+
+ override val name = "Temple Scan"
+
+ override val baseUrl = "https://templescanesp.net"
+
+ override val lang = "es"
+
+ // Moved from Madara to individual extension
+ override val versionId: Int = 2
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.client.newBuilder()
+ .rateLimitHost(baseUrl.toHttpUrl(), 2)
+ .build()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Referer", baseUrl)
+
+ private val json: Json by injectLazy()
+
+ override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
+
+ override fun popularMangaSelector(): String = "div#div-diario figure, div#div-semanal figure, div#div-mensual figure"
+
+ override fun popularMangaNextPageSelector(): String? = null
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val mangasPage = super.popularMangaParse(response)
+ val distinctList = mangasPage.mangas.distinctBy { it.url }
+
+ return MangasPage(distinctList, mangasPage.hasNextPage)
+ }
+
+ override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
+ thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
+ title = element.selectFirst("figcaption")!!.text()
+ setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
+ }
+
+ override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
+
+ override fun latestUpdatesSelector(): String = "section.flex > div.grid > figure"
+
+ override fun latestUpdatesNextPageSelector(): String? = null
+
+ override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
+ thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
+ title = element.selectFirst("figcaption")!!.text()
+ setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ if (query.isNotEmpty()) {
+ if (query.length > 1) return GET("$baseUrl/comics#$query", headers)
+ throw Exception("La búsqueda debe tener al menos 2 caracteres")
+ }
+ return GET("$baseUrl/comics?page=$page", headers)
+ }
+
+ override fun searchMangaSelector(): String = "section.flex > div.grid > figure"
+
+ override fun searchMangaNextPageSelector(): String = "nav > ul.pagination > li > a[rel=next]"
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val query = response.request.url.fragment ?: return super.searchMangaParse(response)
+ val document = response.asJsoup()
+ val mangas = parseMangaList(document, query)
+ return MangasPage(mangas, false)
+ }
+
+ private fun parseMangaList(document: Document, query: String): List {
+ val docString = document.toString()
+ val mangaListJson = JSON_PROJECT_LIST.find(docString)?.destructured?.toList()?.get(0).orEmpty()
+
+ return try {
+ json.decodeFromString>(mangaListJson)
+ .filter { it.title.contains(query, ignoreCase = true) }
+ .map {
+ SManga.create().apply {
+ title = it.title
+ thumbnail_url = it.thumbnail
+ url = "/comic/${it.slug}"
+ }
+ }
+ } catch (_: IllegalArgumentException) {
+ emptyList()
+ }
+ }
+
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
+ title = element.selectFirst("figcaption")!!.text()
+ setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
+ }
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ with(document.select("section#section-sinopsis")) {
+ description = select("p").text()
+ genre = select("div.flex:has(div:containsOwn(Genders)) > div > a > span").joinToString { it.text() }
+ author = select("div.flex:has(div:containsOwn(Autor)) > div").text()
+ }
+ }
+
+ override fun chapterListSelector(): String = "section#section-list-cap div.grid-capitulos > div"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
+ name = element.selectFirst("div#name")!!.text()
+ date_upload = parseRelativeDate(element.selectFirst("time")!!.text())
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("main.contenedor-imagen > section img[src]").mapIndexed { i, element ->
+ Page(i, "", element.attr("abs:src"))
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used!")
+
+ override fun getFilterList(): FilterList {
+ return FilterList(
+ Filter.Header("Limpie la barra de búsqueda y haga click en 'Filtrar' para mostrar todas las series."),
+ )
+ }
+
+ private fun parseRelativeDate(date: String): Long {
+ val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
+ val cal = Calendar.getInstance()
+
+ return when {
+ WordSet("segundo", "second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
+ WordSet("minuto", "minute").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
+ WordSet("hora", "hour").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
+ WordSet("día", "dia", "day").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
+ WordSet("semana", "week").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
+ WordSet("mes", "month").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
+ WordSet("año", "year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
+ else -> 0
+ }
+ }
+
+ class WordSet(private vararg val words: String) {
+ fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
+ }
+
+ @Serializable
+ data class SerieDto(
+ @SerialName("nombre") val title: String,
+ val slug: String,
+ @SerialName("portada") val thumbnail: String,
+ )
+
+ companion object {
+ private val JSON_PROJECT_LIST = """proyectos\s*=\s*(\[[\s\S]+?\])\s*;""".toRegex()
+ }
+}