diff --git a/src/all/madara/build.gradle b/src/all/madara/build.gradle
new file mode 100644
index 000000000..024f56a4f
--- /dev/null
+++ b/src/all/madara/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ appName = 'Tachiyomi: Madara'
+ pkgNameSuffix = "all.madara"
+ extClass = '.MadaraFactory'
+ extVersionCode = 1
+ libVersion = '1.2'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/madara/res/mipmap-hdpi/ic_launcher.png b/src/all/madara/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..bc8eef503
Binary files /dev/null and b/src/all/madara/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/madara/res/mipmap-mdpi/ic_launcher.png b/src/all/madara/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..e51d65390
Binary files /dev/null and b/src/all/madara/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/madara/res/mipmap-xhdpi/ic_launcher.png b/src/all/madara/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..b011eaf9b
Binary files /dev/null and b/src/all/madara/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/madara/res/mipmap-xxhdpi/ic_launcher.png b/src/all/madara/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..dac0772db
Binary files /dev/null and b/src/all/madara/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/madara/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/madara/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c2706aa3f
Binary files /dev/null and b/src/all/madara/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/madara/res/web_hi_res_512.png b/src/all/madara/res/web_hi_res_512.png
new file mode 100644
index 000000000..a62cfe72f
Binary files /dev/null and b/src/all/madara/res/web_hi_res_512.png differ
diff --git a/src/all/madara/src/eu/kanade/tachiyomi/extension/all/madara/Madara.kt b/src/all/madara/src/eu/kanade/tachiyomi/extension/all/madara/Madara.kt
new file mode 100644
index 000000000..84074c120
--- /dev/null
+++ b/src/all/madara/src/eu/kanade/tachiyomi/extension/all/madara/Madara.kt
@@ -0,0 +1,211 @@
+package eu.kanade.tachiyomi.extension.all.madara
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import okhttp3.*
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.*
+
+abstract class Madara(
+ override val name: String,
+ override val baseUrl: String,
+ override val lang: String,
+ private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
+) : ParsedHttpSource() {
+
+ override val supportsLatest = true
+
+ // Popular Manga
+
+ override fun popularMangaSelector() = "div.page-item-detail"
+
+ override fun popularMangaFromElement(element: Element): SManga {
+ val manga = SManga.create()
+
+ with(element) {
+ select("div.post-title a").first()?.let {
+ manga.setUrlWithoutDomain(it.attr("href"))
+ manga.title = it.ownText()
+ }
+
+ select("img").first()?.let {
+ manga.thumbnail_url = it.absUrl(if(it.hasAttr("data-src")) "data-src" else "src")
+ }
+ }
+
+ return manga
+ }
+
+ // Latest Updates
+
+ override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
+
+ override fun latestUpdatesSelector() = "div.item__wrap"
+
+ override fun latestUpdatesNextPageSelector(): String? = null
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val mp = super.latestUpdatesParse(response)
+ val mangas = mp.mangas.distinctBy { it.url }
+ return MangasPage(mangas, mp.hasNextPage)
+ }
+
+ override fun latestUpdatesFromElement(element: Element): SManga {
+ // Even if it's different from the popular manga's list, the relevant classes are the same
+ return popularMangaFromElement(element)
+ }
+
+ // Search Manga
+
+ override fun searchMangaSelector() = "div.c-tabs-item__content"
+
+ override fun searchMangaFromElement(element: Element): SManga {
+ val manga = SManga.create()
+
+ with(element) {
+ select("div.post-title a").first()?.let {
+ manga.setUrlWithoutDomain(it.attr("href"))
+ manga.title = it.ownText()
+ }
+ select("img").first()?.let {
+ manga.thumbnail_url = it.absUrl(if(it.hasAttr("data-src")) "data-src" else "src")
+ }
+ select("div.mg_author div.summary-content a").first()?.let {
+ manga.author = it.text()
+ }
+ select("div.mg_artists div.summary-content a").first()?.let {
+ manga.artist = it.text()
+ }
+ select("div.mg_genres div.summary-content a").first()?.let {
+ manga.genre = it.text()
+ }
+ select("div.mg_status div.summary-content a").first()?.let {
+ manga.status = when(it.text()) {
+ // I don't know what's the corresponding for COMPLETED and LICENSED
+ // There's no support for "Canceled" or "On Hold"
+ "OnGoing" -> SManga.ONGOING
+ else -> SManga.UNKNOWN
+ }
+ }
+ }
+
+ return manga
+ }
+
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+ // Manga Details Parse
+
+ override fun mangaDetailsParse(document: Document): SManga {
+ val manga = SManga.create()
+
+ with(document) {
+ select("div.post-title h3").first()?.let {
+ manga.title = it.ownText()
+ }
+ select("div.author-content").first()?.let {
+ manga.author = it.text()
+ }
+ select("div.artist-content").first()?.let {
+ manga.artist = it.text()
+ }
+ select("div.description-summary div.summary__content p").let {
+ manga.description = it.joinToString(separator = "\n\n") { p ->
+ p.text().replace("
", "\n")
+ }
+ }
+ select("div.summary_image img").first()?.let {
+ manga.thumbnail_url = it.absUrl(if(it.hasAttr("data-src")) "data-src" else "src")
+ }
+ }
+
+ return manga
+ }
+
+ override fun chapterListSelector() = "div.listing-chapters_wrap li.wp-manga-chapter"
+
+ override fun chapterFromElement(element: Element): SChapter {
+ val chapter = SChapter.create()
+
+ with(element) {
+ select("a").first()?.let { urlElement ->
+ chapter.setUrlWithoutDomain(urlElement.attr("href").let {
+ it + if(!it.endsWith("?style=list")) "?style=list" else ""
+ })
+ chapter.name = urlElement.text()
+ }
+
+ select("span.chapter-release-date i").first()?.let {
+ chapter.date_upload = parseChapterDate(it.text()) ?: 0
+ }
+ }
+
+ return chapter
+ }
+
+ open fun parseChapterDate(date: String): Long? {
+ val lcDate = date.toLowerCase()
+ if (lcDate.endsWith(" ago"))
+ parseRelativeDate(lcDate)?.let { return it }
+
+ //Handle 'yesterday' and 'today', using midnight
+ if (lcDate.startsWith("ayer"))
+ return Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, -1) //yesterday
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }.timeInMillis
+ else if (lcDate.startsWith("today"))
+ return Calendar.getInstance().apply {
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }.timeInMillis
+
+ return dateFormat.parseOrNull(date)?.time
+ }
+
+ // Parses dates in this form:
+ // 21 horas ago
+ private fun parseRelativeDate(date: String): Long? {
+ val trimmedDate = date.split(" ")
+ if (trimmedDate[2] != "ago") return null
+ val number = trimmedDate[0].toIntOrNull() ?: return null
+
+ // Map English/Spanish unit to Java unit
+ val javaUnit = when (trimmedDate[1].removeSuffix("s")) {
+ "día", "day" -> Calendar.DAY_OF_MONTH
+ "hora", "hour" -> Calendar.HOUR
+ "min", "minute" -> Calendar.MINUTE
+ "segundo", "second" -> Calendar.SECOND
+ else -> return null
+ }
+
+ return Calendar.getInstance().apply { add(javaUnit, -number) }.timeInMillis
+ }
+
+ private fun SimpleDateFormat.parseOrNull(string: String): Date? {
+ return try {
+ parse(string)
+ } catch (e: ParseException) {
+ null
+ }
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("div.page-break").mapIndexed { index, element ->
+ Page(index, "", element.select("img").first()?.let{
+ it.absUrl(if(it.hasAttr("data-src")) "data-src" else "src")
+ })
+ }
+ }
+
+ override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
+}
\ No newline at end of file
diff --git a/src/all/madara/src/eu/kanade/tachiyomi/extension/all/madara/MadaraFactory.kt b/src/all/madara/src/eu/kanade/tachiyomi/extension/all/madara/MadaraFactory.kt
new file mode 100644
index 000000000..f082018a2
--- /dev/null
+++ b/src/all/madara/src/eu/kanade/tachiyomi/extension/all/madara/MadaraFactory.kt
@@ -0,0 +1,91 @@
+package eu.kanade.tachiyomi.extension.all.madara
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceFactory
+import eu.kanade.tachiyomi.source.model.FilterList
+import okhttp3.CacheControl
+import okhttp3.FormBody
+import okhttp3.Request
+import java.text.SimpleDateFormat
+import java.util.*
+
+class MadaraFactory : SourceFactory {
+ override fun createSources(): List = listOf(
+ LeviatanScans("en"),
+ LeviatanScans("es"),
+ Mangasushi(),
+ NinjaScans(),
+ ReadManhua(),
+ ZeroScans()
+ )
+}
+
+class LeviatanScans(lang: String) : LoadMadara("LeviatanScans", "https://leviatanscans.com", lang, dateFormat = SimpleDateFormat("MMMM dd, yy", Locale("es", "ES"))) {
+ override fun popularMangaSelector() = if(lang == "en") "div.page-item-detail:not(:contains(Capitulo))" else "div.page-item-detail:contains(Capitulo)"
+ override fun latestUpdatesSelector() = if(lang == "en") "div.item__wrap:not(:contains(Capitulo))" else "div.item__wrap:contains(Capitulo)"
+ override fun searchMangaSelector() = if(lang == "en") "div.c-tabs-item__content:not(:contains(Capitulo))" else "div.c-tabs-item__content:contains(Capitulo)"
+}
+class Mangasushi : LoadMadara("Mangasushi", "https://mangasushi.net", "en") {
+ override fun latestUpdatesSelector() = "div.page-item-detail"
+}
+class NinjaScans : PageMadara("NinjaScans", "https://ninjascans.com", "en", urlModifier = "/manhua")
+class ReadManhua : LoadMadara("ReadManhua", "https://readmanhua.net", "en", dateFormat = SimpleDateFormat("dd MMM yy", Locale.US))
+class ZeroScans : PageMadara("ZeroScans", "https://zeroscans.com", "en")
+
+open class LoadMadara(
+ name: String,
+ baseUrl: String,
+ lang: String,
+ dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
+) : Madara(name, baseUrl, lang, dateFormat) {
+ override fun popularMangaRequest(page: Int): Request {
+ val form = FormBody.Builder().apply {
+ add("action", "madara_load_more")
+ add("page", (page-1).toString())
+ add("template", "madara-core/content/content-archive")
+ add("vars[manga_archives_item_layout]", "default")
+ add("vars[meta_key]", "_latest_update")
+ add("vars[order]", "desc")
+ add("vars[paged]", (page-1).toString())
+ add("vars[post_status]", "publish")
+ add("vars[post_type]", "wp-manga")
+ add("vars[sidebar]", "right")
+ add("vars[template]", "archive")
+ }
+ return POST("$baseUrl/wp-admin/admin-ajax.php", headers, form.build(), CacheControl.FORCE_NETWORK)
+ }
+
+ override fun popularMangaNextPageSelector(): String? = "body:not(:has(.no-posts))"
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val form = FormBody.Builder().apply {
+ add("action", "madara_load_more")
+ add("page", (page-1).toString())
+ add("template", "madara-core/content/content-search")
+ add("vars[s]", query)
+ add("vars[orderby]", "")
+ add("vars[paged]", (page-1).toString())
+ add("vars[template]", "search")
+ add("vars[post_type]", "wp-manga")
+ add("vars[post_status]", "publish")
+ add("vars[manga_archives_item_layout]", "default")
+ }
+ return POST("$baseUrl/wp-admin/admin-ajax.php", headers, form.build(), CacheControl.FORCE_NETWORK)
+ }
+}
+
+open class PageMadara(
+ name: String,
+ baseUrl: String,
+ lang: String,
+ private val urlModifier: String = "/manga",
+ dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
+) : Madara(name, baseUrl, lang, dateFormat) {
+ override fun popularMangaRequest(page: Int): Request = GET("$baseUrl$urlModifier/page/$page", headers)
+ override fun popularMangaNextPageSelector() = "div.nav-previous"
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/page/$page/?s=$query&post_type=wp-manga", headers)
+
+}
\ No newline at end of file