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