From 32de7342cbd29caf82eb8e8d38108e12a605f322 Mon Sep 17 00:00:00 2001 From: Arif Ramadan Date: Sun, 20 Oct 2019 05:17:37 +0700 Subject: [PATCH] [New] WP Manga Stream (#1648) * create wp manga stream manga list: - https://kiryuu.co - https://komikav.com - https://komikstation.com - https://komikcast.com - https://westmanga.info - https://komikgo.com * Update WPMangaStreamFactory.kt adding: - https://www.komikindo.web.id - https://www.maid.my.id * change source title, adding suffix "(WP Manga Stream)" * implement rate limit, and select image quality config * optimized code * update icon --- src/all/wpmangastream/build.gradle | 17 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 859 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 527 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 1114 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 2014 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 2910 bytes src/all/wpmangastream/res/web_hi_res_512.png | Bin 0 -> 10941 bytes .../all/wpmangastream/WPMangaStream.kt | 344 +++++++ .../all/wpmangastream/WPMangaStreamFactory.kt | 969 ++++++++++++++++++ 9 files changed, 1330 insertions(+) create mode 100644 src/all/wpmangastream/build.gradle create mode 100755 src/all/wpmangastream/res/mipmap-hdpi/ic_launcher.png create mode 100755 src/all/wpmangastream/res/mipmap-mdpi/ic_launcher.png create mode 100755 src/all/wpmangastream/res/mipmap-xhdpi/ic_launcher.png create mode 100755 src/all/wpmangastream/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 src/all/wpmangastream/res/mipmap-xxxhdpi/ic_launcher.png create mode 100755 src/all/wpmangastream/res/web_hi_res_512.png create mode 100644 src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStream.kt create mode 100644 src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStreamFactory.kt diff --git a/src/all/wpmangastream/build.gradle b/src/all/wpmangastream/build.gradle new file mode 100644 index 000000000..ed98ead44 --- /dev/null +++ b/src/all/wpmangastream/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: WP Manga Stream' + pkgNameSuffix = 'all.wpmangastream' + extClass = '.WPMangaStreamFactory' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + implementation project(':lib-ratelimit') + compileOnly project(':preference-stub') + compileOnly 'com.github.inorichi.injekt:injekt-core:65b0440' +} +apply from: "$rootDir/common.gradle" diff --git a/src/all/wpmangastream/res/mipmap-hdpi/ic_launcher.png b/src/all/wpmangastream/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..fa71abc5dd505c10a640b3121841bfe95714f4ad GIT binary patch literal 859 zcmV-h1El@Q6B84SjEz4( zKmPsx0002)?e5{=-~s{y#>U1jE-p||P@bNiczAedXlSvpu?6Ty2><{940KXXQveuU z!zi5o4vm7#H`!*Ud;kCfp-DtRRCr$P*I92GF%$*h3`tFad%exR!2kc5)%6k^yOuG1 z=u6H>L_FlvnFHe}AC#Z`5+|hJL@c3#5%i29*mT@-j5%P+q9Mbpa|ZL}`XXpZLyr z(aE8-Pi3JhloukZ6>?qZRkLg$f{5m*O97UbpDWTQ(uR; zUB@@-`{^z*o*Lyb_68EQMm%oR_oE^yIQ!T;C^$m~xpjP0PkF4N(7KSl%Nj&A0!57| z%jP7$P2?P@lS9sk8;v~;Wue2o**G~Bh^NrBEmTS|M7EoHD*&J`REA^))W zj^3UMz1D~$Q|6AsJoibpL1-c5cS7t|0JM|}P8C8#?jYoLsE0-Xko1L&`xKzSpld{- zfp!Ma3L#y{0|atc+&Ji=xeB3+-^fZF!R8qnN{|84M&1aGEK`OI6G9Lska&WIBIJ(w z0C~n~!jA2oBE+*s4v@-@EkpzjWymK$)>(J@_{o8PfzW>;LBx%Q257vK^Zkfy5J5u& zG;IhVkMPjmxr2gJhX6JWfb0HzjI=>O)0h|S4mJRHuxWxUQ-{DuZlk#)T3Nth-t;d* z5Jf6-mJNjoZf>n{Q{E;a4@jYld0aLR6VLB#H_-O;DX_hvw92 zhX_fSMhKkJ=*%f};&Yy%9yle?*R@@tT)zm4(%r6Hv@!y(xAed4b9cM^YR=`>QK3ZY lzM9kJ4pDF9P)t-sM{rC4 z000320UjP61qB5g8yf%s00II60000D4GsSN{t*!o0RaID3JL}W23J;BK|w)lYHP&A z#Pjm=l#`U@`F2000SeQchC$#g;7pa!Ha6A;&#!Mvu6@&p=i zn+-%I3gGes$qTfvG(SugNCe!=D-+;mgE`kF^JD<0`{}0v z*yxzR{RX&Pw}5Wjpi6Z#T>)d`K1eebaBe#D{N|iM7Q^|=%E0|1?415=z5z-wNNz6c Rl%W6s002ovPDHLkV1kBk(#`+? literal 0 HcmV?d00001 diff --git a/src/all/wpmangastream/res/mipmap-xhdpi/ic_launcher.png b/src/all/wpmangastream/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..c994c12ccdea7889e06fd25757e7e73c2c334e0b GIT binary patch literal 1114 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 eABN|{TVE!)+oGcmi=;-*IKsH%F>;&NZ&#=n+9 zdIkpWBjqtOACCLjd?_>GXQ6lul}d%EOGk6nmg%WluYFR>&9CF!z*1_gGJb0t7*oFafGSMc z(MJb5MfIpGwL9>{$-?S~KJCw~unS1lEP+|v%kCTuZ+z@eO~ZwzN$6pp#~A{x?eiSf z1zcdr8O#KmyCz>)&^@}RM%sPat+pvl0fGxj915>|$N3m{fm+;ouQMAT&yDgs?o-M{_^y`wBd3`dh?S> z4djcCl4>9jZ|lzMP#JIGib(CV9-qege& zHRca~1;>i*3u=@kC(!0O)+~9R=5wvcB+gDKc_NlA6XfZvI|naJjgEg4gI+`|Ni})y znPQ%m8~J8El>BX`qp2TdK@@0SDwZeD*_HZwliUs{Cq`wWew58`)6qk_n{Kf5D?5%# zV`%%+dVEz;et+M`Hms<=Dl2|cz*u(6QaAMi(rT=dV9_41K#dQ)Kyh6SOTN4v z#hL-i2T;?KGg5Ai_c%((FHgc)p#h-dquKp=7 zxy)U>(xZ~6*!nV4f))R+MN3u2m*M+k)~=2%*h5wWGilHNe4ycC*U|$L=e=#dm`7eu zs_$3f+wdIw)JK{_^v5N2Z-o>_AM#`Pid8f@zpxn`C=R7ESBkgZ#j-{m)c{-K@Ud^* zkiAe*Aqr4PO7V_qx&0dl%(I56b<@|C)7`5g>ebW~w#*x6G-Ru%yD3*BiLm5+K4O#r vx{8tJ3|UB$eVU!bU}!)acErPup$*zvr+*TAKe^cd^Z7DdyqzC9g{J)%mDxLn literal 0 HcmV?d00001 diff --git a/src/all/wpmangastream/res/web_hi_res_512.png b/src/all/wpmangastream/res/web_hi_res_512.png new file mode 100755 index 0000000000000000000000000000000000000000..047e5438b41809658596516c66f734617640256c GIT binary patch literal 10941 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 literal 0 HcmV?d00001 diff --git a/src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStream.kt b/src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStream.kt new file mode 100644 index 000000000..7cbc1c404 --- /dev/null +++ b/src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStream.kt @@ -0,0 +1,344 @@ +package eu.kanade.tachiyomi.extension.all.wpmangastream + +import android.annotation.SuppressLint +import android.app.Application +import android.content.SharedPreferences +import android.support.v7.preference.ListPreference +import android.support.v7.preference.PreferenceScreen +import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +abstract class WPMangaStream(override val name: String, override val baseUrl: String, override val lang: String) : ConfigurableSource, ParsedHttpSource() { + override val supportsLatest = true + + companion object { + private const val MID_QUALITY = 1 + private const val LOW_QUALITY = 2 + + private const val SHOW_THUMBNAIL_PREF_Title = "Default thumbnail quality" + private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault" + } + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + + val thumbsPref = ListPreference(screen.context).apply { + key = SHOW_THUMBNAIL_PREF_Title + title = SHOW_THUMBNAIL_PREF_Title + entries = arrayOf("Show high quality", "Show mid quality", "Show low quality") + entryValues = arrayOf("0", "1", "2") + summary = "%s" + + setOnPreferenceChangeListener { _, newValue -> + val selected = newValue as String + val index = this.findIndexOfValue(selected) + preferences.edit().putInt(SHOW_THUMBNAIL_PREF, index).commit() + } + } + screen.addPreference(thumbsPref) + } + + private fun getShowThumbnail(): Int = preferences.getInt(SHOW_THUMBNAIL_PREF, 0) + + private val rateLimitInterceptor = RateLimitInterceptor(4) + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .addNetworkInterceptor(rateLimitInterceptor) + .build() + + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/manga/page/$page/?order=popular", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/manga/page/$page/?order=latest", headers) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val builtUrl = if (page == 1) "$baseUrl/manga/" else "$baseUrl/manga/page/$page/" + val url = HttpUrl.parse(builtUrl)!!.newBuilder() + url.addQueryParameter("title", query) + url.addQueryParameter("page", page.toString()) + filters.forEach { filter -> + when (filter) { + is AuthorFilter -> { + url.addQueryParameter("author", filter.state) + } + is YearFilter -> { + url.addQueryParameter("yearx", filter.state) + } + is StatusFilter -> { + val status = when (filter.state) { + Filter.TriState.STATE_INCLUDE -> "completed" + Filter.TriState.STATE_EXCLUDE -> "ongoing" + else -> "" + } + url.addQueryParameter("status", status) + } + is TypeFilter -> { + url.addQueryParameter("type", filter.toUriPart()) + } + is SortByFilter -> { + url.addQueryParameter("order", filter.toUriPart()) + } + is GenreListFilter -> { + filter.state + .filter { it.state != Filter.TriState.STATE_IGNORE } + .forEach { url.addQueryParameter("genre[]", it.id) } + } + } + } + return GET(url.build().toString(), headers) + } + + override fun popularMangaSelector() = "div.bs" + override fun latestUpdatesSelector() = popularMangaSelector() + override fun searchMangaSelector() = popularMangaSelector() + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.select("div.limit img").attr("src") + element.select("div.bsx > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun popularMangaNextPageSelector() = "a.next.page-numbers" + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.spe").first() + val descElement = document.select(".infox > div.desc").first() + val sepName = infoElement.select(".spe > span:nth-child(3)").last() + val manga = SManga.create() + manga.author = sepName.ownText() + manga.artist = sepName.ownText() + val genres = mutableListOf() + infoElement.select(".spe > span:nth-child(1) > a").forEach { element -> + val genre = element.text() + genres.add(genre) + } + manga.genre = genres.joinToString(", ") + manga.status = parseStatus(infoElement.select(".spe > span:nth-child(2)").text()) + manga.description = descElement.select("p").text() + manga.thumbnail_url = document.select(".thumb > img:nth-child(1)").attr("src") + + return manga + } + + @SuppressLint("DefaultLocale") + internal open fun parseStatus(element: String): Int = when { + element.toLowerCase().contains("ongoing") -> SManga.ONGOING + element.toLowerCase().contains("completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "div.bxcl ul li" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select(".lchx > a").first() + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = 0 + return chapter + } + + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { + val basic = Regex("""Chapter\s([0-9]+)""") + when { + basic.containsMatchIn(chapter.name) -> { + basic.find(chapter.name)?.let { + chapter.chapter_number = it.groups[1]?.value!!.toFloat() + } + } + } + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + var i = 0 + document.select("div#readerarea img").forEach { element -> + val url = element.attr("src") + i++ + if (url.isNotEmpty()) { + pages.add(Page(i, "", url)) + } + } + return pages + } + + override fun imageUrlParse(document: Document) = "" + + override fun imageRequest(page: Page): Request { + val headers = Headers.Builder() + headers.apply { + add("Referer", baseUrl) + add("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/76.0.3809.100 Mobile Safari/537.36") + } + + if (page.imageUrl!!.contains("i0.wp.com")) { + headers.apply { + add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") + } + } + + return GET(getImageUrl(page.imageUrl!!, getShowThumbnail()), headers.build()) + } + + private fun getImageUrl(baseUrl: String, quality: Int): String { + var url = baseUrl + when(quality){ + LOW_QUALITY -> { + url = url.replace("https://", "") + url = "http://images.weserv.nl/?w=300&q=70&url=" + url + } + MID_QUALITY -> { + url = url.replace("https://", "") + url = "http://images.weserv.nl/?w=600&q=70&url=" + url + } + } + return url + } + + private class AuthorFilter : Filter.Text("Author") + + private class YearFilter : Filter.Text("Year") + + private class TypeFilter : UriPartFilter("Type", arrayOf( + Pair("Default", ""), + Pair("Manga", "Manga"), + Pair("Manhwa", "Manhwa"), + Pair("Manhua", "Manhua"), + Pair("Comic", "Comic") + )) + + protected class SortByFilter : UriPartFilter("Sort By", arrayOf( + Pair("Default", ""), + Pair("A-Z", "title"), + Pair("Z-A", "titlereverse"), + Pair("Latest Update", "update"), + Pair("Latest Added", "latest"), + Pair("Popular", "popular") + )) + + protected class StatusFilter : UriPartFilter("Status", arrayOf( + Pair("All", ""), + Pair("Ongoing", "ongoing"), + Pair("Completed", "completed") + )) + + protected class Genre(name: String, val id: String = name) : Filter.TriState(name) + protected class GenreListFilter(genres: List) : Filter.Group("Genre", genres) + + override fun getFilterList() = FilterList( + Filter.Header("NOTE: Ignored if using text search!"), + Filter.Separator(), + AuthorFilter(), + YearFilter(), + StatusFilter(), + TypeFilter(), + SortByFilter(), + GenreListFilter(getGenreList()) + ) + + protected open fun getGenreList(): List = listOf( + Genre("4 Koma", "4-koma"), + Genre("Action", "action"), + Genre("Adult", "adult"), + Genre("Adventure", "adventure"), + Genre("Comedy", "comedy"), + Genre("Completed", "completed"), + Genre("Cooking", "cooking"), + Genre("Crime", "crime"), + Genre("Demon", "demon"), + Genre("Demons", "demons"), + Genre("Doujinshi", "doujinshi"), + Genre("Drama", "drama"), + Genre("Ecchi", "ecchi"), + Genre("Fantasy", "fantasy"), + Genre("Game", "game"), + Genre("Games", "games"), + Genre("Gender Bender", "gender-bender"), + Genre("Gore", "gore"), + Genre("Harem", "harem"), + Genre("Historical", "historical"), + Genre("Horror", "horror"), + Genre("Isekai", "isekai"), + Genre("Josei", "josei"), + Genre("Magic", "magic"), + Genre("Manga", "manga"), + Genre("Manhua", "manhua"), + Genre("Manhwa", "manhwa"), + Genre("Martial Art", "martial-art"), + Genre("Martial Arts", "martial-arts"), + Genre("Mature", "mature"), + Genre("Mecha", "mecha"), + Genre("Military", "military"), + Genre("Monster", "monster"), + Genre("Monster Girls", "monster-girls"), + Genre("Monsters", "monsters"), + Genre("Music", "music"), + Genre("Mystery", "mystery"), + Genre("One-shot", "one-shot"), + Genre("Oneshot", "oneshot"), + Genre("Police", "police"), + Genre("Pshycological", "pshycological"), + Genre("Psychological", "psychological"), + Genre("Reincarnation", "reincarnation"), + Genre("Reverse Harem", "reverse-harem"), + Genre("Romancce", "romancce"), + Genre("Romance", "romance"), + Genre("Samurai", "samurai"), + Genre("School", "school"), + Genre("School Life", "school-life"), + Genre("Sci-fi", "sci-fi"), + Genre("Seinen", "seinen"), + Genre("Shoujo", "shoujo"), + Genre("Shoujo Ai", "shoujo-ai"), + Genre("Shounen", "shounen"), + Genre("Shounen Ai", "shounen-ai"), + Genre("Slice of Life", "slice-of-life"), + Genre("Sports", "sports"), + Genre("Super Power", "super-power"), + Genre("Supernatural", "supernatural"), + Genre("Thriller", "thriller"), + Genre("Time Travel", "time-travel"), + Genre("Tragedy", "tragedy"), + Genre("Vampire", "vampire"), + Genre("Webtoon", "webtoon"), + Genre("Webtoons", "webtoons"), + Genre("Yaoi", "yaoi"), + Genre("Yuri", "yuri"), + Genre("Zombies", "zombies") + ) + + open class UriPartFilter(displayName: String, private val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } +} diff --git a/src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStreamFactory.kt b/src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStreamFactory.kt new file mode 100644 index 000000000..ae14062cb --- /dev/null +++ b/src/all/wpmangastream/src/eu/kanade/tachiyomi/extension/all/wpmangastream/WPMangaStreamFactory.kt @@ -0,0 +1,969 @@ +package eu.kanade.tachiyomi.extension.all.wpmangastream + +import android.annotation.SuppressLint +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl +import okhttp3.Request +import okhttp3.Response +import org.json.JSONObject +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class WPMangaStreamFactory : SourceFactory { + override fun createSources(): List = listOf( + Kiryuu(), + KomikAV(), + KomikStation(), + KomikCast(), + WestManga(), + KomikGo(), + KomikIndo(), + MaidManga() + ) +} + +class Kiryuu : WPMangaStream("Kiryuu (WP Manga Stream)", "https://kiryuu.co", "id") +class KomikAV : WPMangaStream("Komik AV (WP Manga Stream)", "https://komikav.com", "id") +class KomikStation : WPMangaStream("Komik Station (WP Manga Stream)", "https://komikstation.com", "id") +class KomikCast : WPMangaStream("Komik Cast (WP Manga Stream)", "https://komikcast.com", "id") { + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/daftar-komik/page/$page/?order=popular", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/komik/page/$page/", headers) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = if (query.isNotBlank()) { + val url = HttpUrl.parse("$baseUrl/page/$page")!!.newBuilder() + val pattern = "\\s+".toRegex() + val q = query.replace(pattern, "+") + if (query.isNotEmpty()) { + url.addQueryParameter("s", q) + } else { + url.addQueryParameter("s", "") + } + url.toString() + } else { + val url = HttpUrl.parse("$baseUrl/daftar-komik/page/$page")!!.newBuilder() + var orderBy: String + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is StatusFilter -> url.addQueryParameter("status", arrayOf("", "ongoing", "completed")[filter.state]) + is GenreListFilter -> { + val genreInclude = mutableListOf() + filter.state.forEach { + if (it.state == 1) { + genreInclude.add(it.id) + } + } + if (genreInclude.isNotEmpty()) { + genreInclude.forEach { genre -> + url.addQueryParameter("genre[]", genre) + } + } + } + is SortByFilter -> { + orderBy = filter.toUriPart() + url.addQueryParameter("order", orderBy) + } + } + } + url.toString() + } + return GET(url, headers) + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.select("div.limit img").attr("src") + element.select("div.bigor > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.spe").first() + val sepName = infoElement.select(".spe > span:nth-child(4)").last() + val manga = SManga.create() + manga.author = sepName.ownText() + manga.artist = sepName.ownText() + val genres = mutableListOf() + infoElement.select(".spe > span:nth-child(1) > a").forEach { element -> + val genre = element.text() + genres.add(genre) + } + manga.genre = genres.joinToString(", ") + manga.status = parseStatus(infoElement.select(".spe > span:nth-child(2)").text()) + manga.description = document.select("div[^itemprop]").last().text() + manga.thumbnail_url = document.select(".thumb > img:nth-child(1)").attr("src") + + return manga + } + + override fun chapterListSelector() = "div.cl ul li" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + val timeElement = element.select("span.rightoff").first() + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = parseChapterDate(timeElement.text()) + return chapter + } + + private fun parseChapterDate(date: String): Long { + val value = date.split(' ')[0].toInt() + + return when { + "mins" in date -> Calendar.getInstance().apply { + add(Calendar.MINUTE, value * -1) + }.timeInMillis + "hours" in date -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + }.timeInMillis + "days" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * -1) + }.timeInMillis + "weeks" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * 7 * -1) + }.timeInMillis + "months" in date -> Calendar.getInstance().apply { + add(Calendar.MONTH, value * -1) + }.timeInMillis + "years" in date -> Calendar.getInstance().apply { + add(Calendar.YEAR, value * -1) + }.timeInMillis + "min" in date -> Calendar.getInstance().apply { + add(Calendar.MINUTE, value * -1) + }.timeInMillis + "hour" in date -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + }.timeInMillis + "day" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * -1) + }.timeInMillis + "week" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * 7 * -1) + }.timeInMillis + "month" in date -> Calendar.getInstance().apply { + add(Calendar.MONTH, value * -1) + }.timeInMillis + "year" in date -> Calendar.getInstance().apply { + add(Calendar.YEAR, value * -1) + }.timeInMillis + else -> { + return 0 + } + } + } + + override fun getFilterList() = FilterList( + Filter.Header("NOTE: Ignored if using text search!"), + Filter.Separator(), + SortByFilter(), + Filter.Separator(), + StatusFilter(), + Filter.Separator(), + GenreListFilter(getGenreList()) + ) +} +class WestManga : WPMangaStream("West Manga (WP Manga Stream)", "https://westmanga.info", "id") { + override fun popularMangaRequest(page: Int): Request { + val url = if (page == 1) "$baseUrl/manga-list/?popular" else "$baseUrl/manga-list/page/$page/?popular" + return GET(url, headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + val url = if (page == 1) "$baseUrl/manga-list/?latest" else "$baseUrl/manga-list/page/$page/?latest" + return GET(url, headers) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + var builtUrl = if (page == 1) "$baseUrl/manga/" else "$baseUrl/manga/page/$page/" + if (query != "") { + builtUrl = if (page == 1) "$baseUrl/?s=$query&post_type=manga" else "$baseUrl/page/2/?s=$query&post_type=manga" + } else if (filters.size > 0) { + filters.forEach { filter -> + when (filter) { + is SortByFilter -> { + builtUrl = if (page == 1) "$baseUrl/manga-list/?" + filter.toUriPart() else "$baseUrl/manga-list/page/$page/?" + filter.toUriPart() + } + is GenreListFilter -> { + builtUrl = if (page == 1) "$baseUrl/genre/" + filter.toUriPart() else "$baseUrl/genre/" + filter.toUriPart() + "/page/$page/" + } + } + } + } + val url = HttpUrl.parse(builtUrl)!!.newBuilder() + return GET(url.build().toString(), headers) + } + + override fun popularMangaSelector() = "div.result-search" + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.select("div.fletch > .img_search > img").attr("src") + element.select(".kanan_search > .search_title > .titlex > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun popularMangaNextPageSelector() = ".paginado>ul>li.dd + li.a" + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("table.attr").first() + val descElement = document.select("div.sin").first() + val sepName = infoElement.select("tr:nth-child(5)>td").first() + val manga = SManga.create() + manga.author = sepName.text() + manga.artist = sepName.text() + val genres = mutableListOf() + infoElement.select("tr:nth-child(6)>td > a").forEach { element -> + val genre = element.text() + genres.add(genre) + } + manga.genre = genres.joinToString(", ") + manga.status = parseStatus(infoElement.select("tr:nth-child(4)>td").text()) + manga.description = descElement.select("p").text() + manga.thumbnail_url = document.select(".topinfo > img").attr("src") + return manga + } + + @SuppressLint("DefaultLocale") + override fun parseStatus(element: String): Int = when { + element.toLowerCase().contains("publishing") -> SManga.ONGOING + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "div.cl ul li" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select(".leftoff > a").first() + val chapter = SChapter.create() + val timeElement = element.select("span.rightoff").first() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = parseChapterDate(timeElement.text()) + return chapter + } + + @SuppressLint("SimpleDateFormat") + private fun parseChapterDate(date: String): Long { + val sdf = SimpleDateFormat("MMM dd, yyyy") + val parse = sdf.parse(date) + val cal = Calendar.getInstance() + cal.time = parse + return cal.timeInMillis + } + + private class SortByFilter : UriPartFilter("Sort By", arrayOf( + Pair("Default", ""), + Pair("A-Z", "A-Z"), + Pair("Latest Added", "latest"), + Pair("Popular", "popular") + )) + + private class GenreListFilter : UriPartFilter("Genre", arrayOf( + Pair("Default", ""), + Pair("4-Koma", "4-koma"), + Pair("Action", "action"), + Pair("Adventure", "adventure"), + Pair("Comedy", "comedy"), + Pair("Cooking", "cooking"), + Pair("Demons", "demons"), + Pair("Drama", "drama"), + Pair("Ecchi", "ecchi"), + Pair("Fantasy", "fantasy"), + Pair("FantasyAction", "fantasyaction"), + Pair("Game", "game"), + Pair("Gender Bender", "gender-bender"), + Pair("Gore", "gore"), + Pair("Harem", "harem"), + Pair("Historical", "historical"), + Pair("Horro", "horro"), + Pair("Horror", "horror"), + Pair("Isekai", "isekai"), + Pair("Isekai Action", "isekai-action"), + Pair("Josei", "josei"), + Pair("Magic", "magic"), + Pair("Manga", "manga"), + Pair("Manhua", "manhua"), + Pair("Martial arts", "martial-arts"), + Pair("Mature", "mature"), + Pair("Mecha", "mecha"), + Pair("Medical", "medical"), + Pair("Music", "music"), + Pair("Mystery", "mystery"), + Pair("Oneshot", "oneshot"), + Pair("Project", "project"), + Pair("Psychological", "psychological"), + Pair("Romance", "romance"), + Pair("School", "school"), + Pair("School life", "school-life"), + Pair("Sci fi", "sci-fi"), + Pair("Seinen", "seinen"), + Pair("Shoujo", "shoujo"), + Pair("Shoujo Ai", "shoujo-ai"), + Pair("Shounen", "shounen"), + Pair("Slice of Life", "slice-of-life"), + Pair("Sports", "sports"), + Pair("Super Power", "super-power"), + Pair("Supernatural", "supernatural"), + Pair("Suspense", "suspense"), + Pair("Thriller", "thriller"), + Pair("Tragedy", "tragedy"), + Pair("Vampire", "vampire"), + Pair("Webtoons", "webtoons"), + Pair("Yuri", "yuri") + )) + + override fun getFilterList() = FilterList( + Filter.Header("NOTE: sort and genre can't be combined and ignored when using text search!"), + Filter.Separator(), + SortByFilter(), + GenreListFilter() + ) +} +class KomikGo : WPMangaStream("Komik GO (WP Manga Stream)", "https://komikgo.com", "id") { + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/page/$page?s&post_type=wp-manga&m_orderby=views", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/page/$page?s&post_type=wp-manga&m_orderby=latest", headers) + } + + override fun popularMangaSelector() = "div.c-tabs-item__content" + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.select("div.tab-thumb > a > img").attr("data-src") + element.select("div.tab-thumb > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/page/$page")!!.newBuilder() + url.addQueryParameter("post_type", "wp-manga") + val pattern = "\\s+".toRegex() + val q = query.replace(pattern, "+") + if (query.isNotEmpty()) { + url.addQueryParameter("s", q) + } else { + url.addQueryParameter("s", "") + } + + var orderBy: String + + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { +// is Status -> url.addQueryParameter("manga_status", arrayOf("", "completed", "ongoing")[filter.state]) + is GenreListFilter -> { + val genreInclude = mutableListOf() + filter.state.forEach { + if (it.state == 1) { + genreInclude.add(it.id) + } + } + if (genreInclude.isNotEmpty()) { + genreInclude.forEach { genre -> + url.addQueryParameter("genre[]", genre) + } + } + } + is StatusList -> { + val statuses = mutableListOf() + filter.state.forEach { + if (it.state == 1) { + statuses.add(it.id) + } + } + if (statuses.isNotEmpty()) { + statuses.forEach { status -> + url.addQueryParameter("status[]", status) + } + } + } + + is SortBy -> { + orderBy = filter.toUriPart() + url.addQueryParameter("m_orderby", orderBy) + } + is TextField -> url.addQueryParameter(filter.key, filter.state) + } + } + + return GET(url.toString(), headers) + } + + override fun popularMangaNextPageSelector() = "#navigation-ajax" + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.site-content").first() + + val manga = SManga.create() + manga.author = infoElement.select("div.author-content")?.text() + manga.artist = infoElement.select("div.artist-content")?.text() + + val genres = mutableListOf() + infoElement.select("div.genres-content a").forEach { element -> + val genre = element.text() + genres.add(genre) + } + manga.genre = genres.joinToString(", ") + manga.status = parseStatus(infoElement.select("div.post-status > div:nth-child(2) div").text()) + + manga.description = document.select("div.description-summary")?.text() + manga.thumbnail_url = document.select("div.summary_image > a > img").attr("data-src") + + return manga + } + + override fun chapterListSelector() = "li.wp-manga-chapter" + + + private fun parseChapterDate(date: String): Long { + if (date.contains(",")) { + return try { + SimpleDateFormat("MMM d, yyyy", Locale.US).parse(date).time + } catch (e: ParseException) { + 0 + } + } else { + val value = date.split(' ')[0].toInt() + + return when { + "mins" in date -> Calendar.getInstance().apply { + add(Calendar.MINUTE, value * -1) + }.timeInMillis + "hours" in date -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + }.timeInMillis + "days" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * -1) + }.timeInMillis + "weeks" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * 7 * -1) + }.timeInMillis + "months" in date -> Calendar.getInstance().apply { + add(Calendar.MONTH, value * -1) + }.timeInMillis + "years" in date -> Calendar.getInstance().apply { + add(Calendar.YEAR, value * -1) + }.timeInMillis + "min" in date -> Calendar.getInstance().apply { + add(Calendar.MINUTE, value * -1) + }.timeInMillis + "hour" in date -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + }.timeInMillis + "day" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * -1) + }.timeInMillis + "week" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * 7 * -1) + }.timeInMillis + "month" in date -> Calendar.getInstance().apply { + add(Calendar.MONTH, value * -1) + }.timeInMillis + "year" in date -> Calendar.getInstance().apply { + add(Calendar.YEAR, value * -1) + }.timeInMillis + else -> { + return 0 + } + } + } + } + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = parseChapterDate(element.select("span.chapter-release-date i").text()) + return chapter + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + var i = 0 + document.select("div.reading-content * img").forEach { element -> + val url = element.attr("src") + i++ + if (url.isNotEmpty()) { + pages.add(Page(i, "", url)) + } + } + return pages + } + + private class TextField(name: String, val key: String) : Filter.Text(name) + + private class SortBy : UriPartFilter("Sort by", arrayOf( + Pair("Relevance", ""), + Pair("Latest", "latest"), + Pair("A-Z", "alphabet"), + Pair("Rating", "rating"), + Pair("Trending", "trending"), + Pair("Most View", "views"), + Pair("New", "new-manga") + )) + + private class Status(name: String, val id: String = name) : Filter.TriState(name) + private class StatusList(statuses: List) : Filter.Group("Status", statuses) + + override fun getFilterList() = FilterList( + TextField("Author", "author"), + TextField("Year", "release"), + SortBy(), + StatusList(getStatusList()), + GenreListFilter(getGenreList()) + ) + + private fun getStatusList() = listOf( + Status("Completed", "end"), + Status("Ongoing", "on-going"), + Status("Canceled", "canceled"), + Status("Onhold", "on-hold") + ) + + override fun getGenreList(): List = listOf( + Genre("Adventure", "Adventure"), + Genre("Action", "action"), + Genre("Adventure", "adventure"), + Genre("Cars", "cars"), + Genre("4-Koma", "4-koma"), + Genre("Comedy", "comedy"), + Genre("Completed", "completed"), + Genre("Cooking", "cooking"), + Genre("Dementia", "dementia"), + Genre("Demons", "demons"), + Genre("Doujinshi", "doujinshi"), + Genre("Drama", "drama"), + Genre("Ecchi", "ecchi"), + Genre("Fantasy", "fantasy"), + Genre("Game", "game"), + 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("Manga", "manga"), + Genre("Manhua", "manhua"), + Genre("Manhwa", "manhwa"), + Genre("Martial Arts", "martial-arts"), + Genre("Mature", "mature"), + Genre("Mecha", "mecha"), + Genre("Military", "military"), + Genre("Music", "music"), + Genre("Mystery", "mystery"), + Genre("Old Comic", "old-comic"), + Genre("One Shot", "one-shot"), + Genre("Oneshot", "oneshot"), + Genre("Parodi", "parodi"), + Genre("Parody", "parody"), + Genre("Police", "police"), + Genre("Psychological", "psychological"), + Genre("Romance", "romance"), + Genre("Samurai", "samurai"), + Genre("School", "school"), + Genre("School Life", "school-life"), + Genre("Sci-Fi", "sci-fi"), + Genre("Seinen", "seinen"), + Genre("Shoujo", "shoujo"), + Genre("Shoujo Ai", "shoujo-ai"), + Genre("Shounen", "shounen"), + Genre("Shounen ai", "shounen-ai"), + Genre("Slice of Life", "slice-of-life"), + Genre("Sports", "sports"), + Genre("Super Power", "super-power"), + Genre("Supernatural", "supernatural"), + Genre("Thriller", "thriller"), + Genre("Tragedy", "tragedy"), + Genre("Vampire", "vampire"), + Genre("Webtoons", "webtoons"), + Genre("Yaoi", "yaoi"), + Genre("Yuri", "yuri") + ) +} +class KomikIndo : WPMangaStream("Komik Indo (WP Manga Stream)", "https://www.komikindo.web.id", "id") { + + override fun popularMangaRequest(page: Int): Request { + val url = if (page == 1) baseUrl else "$baseUrl/page/$page" + return GET(url, headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + val url = if (page == 1) "$baseUrl/manga/" else "$baseUrl/manga/page/$page" + return GET(url, headers) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + var builtUrl = if (page == 1) "$baseUrl/manga/" else "$baseUrl/manga/page/$page/" + if (query != "") { + builtUrl = if (page == 1) "$baseUrl/search/$query/" else "$baseUrl/search/$query/page/$page/" + } else if (filters.size > 0) { + filters.forEach { filter -> + when (filter) { + is GenreListFilter -> { + builtUrl = if (page == 1) "$baseUrl/genres/" + filter.toUriPart() else "$baseUrl/genres/" + filter.toUriPart() + "/page/$page/" + } + } + } + } + val url = HttpUrl.parse(builtUrl)!!.newBuilder() + return GET(url.build().toString(), headers) + } + + override fun popularMangaSelector() = "div.lchap > .lch > .ch" + override fun latestUpdatesSelector() = "div.ctf > div.lsmin > div.chl" + override fun searchMangaSelector() = latestUpdatesSelector() + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.select("div.thumbnail img").first().attr("src") + element.select("div.l > h3 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.select("div.thumbnail img").first().attr("src") + element.select("div.chlf > h2 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga = searchMangaFromElement(element) + + @SuppressLint("DefaultLocale") + override fun mangaDetailsParse(document: Document): SManga { + val infoElm = document.select(".listinfo > ul > li") + val manga = SManga.create() + infoElm.forEach { elmt -> + val infoTitle = elmt.select("b").text().toLowerCase() + val infoContent = elmt.text() + when { + infoTitle.contains("status") -> manga.status = parseStatus(infoContent) + infoTitle.contains("author") -> manga.author = infoContent + infoTitle.contains("artist") -> manga.artist = infoContent + infoTitle.contains("genres") -> { + val genres = mutableListOf() + elmt.select("a").forEach { + val genre = it.text() + genres.add(genre) + } + manga.genre = genres.joinToString(", ") + } + } + } + manga.description = document.select("div.rm > span > p:first-child").text() + manga.thumbnail_url = document.select("div.animeinfo .lm .imgdesc img:first-child").attr("src") + return manga + } + + override fun chapterListSelector() = "div.cl ul li" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select(".leftoff > a").first() + val chapter = SChapter.create() + val timeElement = element.select("span.rightoff").first() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = parseChapterDate(timeElement.text()) + return chapter + } + + @SuppressLint("SimpleDateFormat") + private fun parseChapterDate(date: String): Long { + val sdf = SimpleDateFormat("MMM dd, yyyy") + val parse = sdf.parse(date) + val cal = Calendar.getInstance() + cal.time = parse + return cal.timeInMillis + } + + private class GenreListFilter : UriPartFilter("Genre", arrayOf( + Pair("Default", ""), + Pair("4-Koma", "4-koma"), + Pair("Action", "action"), + Pair("Adventure", "adventure"), + Pair("Comedy", "comedy"), + Pair("Cooking", "cooking"), + Pair("Crime", "crime"), + Pair("Dark Fantasy", "dark-fantasy"), + Pair("Demons", "demons"), + Pair("Drama", "drama"), + Pair("Ecchi", "ecchi"), + Pair("Fantasy", "fantasy"), + Pair("Game", "game"), + Pair("Gender Bender", "gender-bender"), + Pair("Harem", "harem"), + Pair("Historical", "historical"), + Pair("Horor", "horor"), + Pair("Horror", "horror"), + Pair("Isekai", "isekai"), + Pair("Josei", "josei"), + Pair("Komik Tamat", "komik-tamat"), + Pair("Life", "life"), + Pair("Magic", "magic"), + Pair("Manhua", "manhua"), + Pair("Martial Art", "martial-art"), + Pair("Martial Arts", "martial-arts"), + Pair("Mature", "mature"), + Pair("Mecha", "mecha"), + Pair("Military", "military"), + Pair("Music", "music"), + Pair("Mystery", "mystery"), + Pair("Post-Apocalyptic", "post-apocalyptic"), + Pair("Psychological", "psychological"), + Pair("Romance", "romance"), + Pair("School", "school"), + Pair("School Life", "school-life"), + Pair("Sci-Fi", "sci-fi"), + Pair("Seinen", "seinen"), + Pair("Shonen", "shonen"), + Pair("Shoujo", "shoujo"), + Pair("Shounen", "shounen"), + Pair("Slice of Life", "slice-of-life"), + Pair("Sports", "sports"), + Pair("Super Power", "super-power"), + Pair("Superheroes", "superheroes"), + Pair("Supernatural", "supernatural"), + Pair("Survival", "survival"), + Pair("Thriller", "thriller"), + Pair("Tragedy", "tragedy"), + Pair("Zombies", "zombies") + )) + + override fun getFilterList() = FilterList( + Filter.Header("NOTE: filter will be ignored when using text search!"), + GenreListFilter() + ) + + private open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } +} +class MaidManga : WPMangaStream("Maid Manga (WP Manga Stream)", "https://www.maid.my.id", "id") { + override fun latestUpdatesSelector() = "h2:contains(Update Chapter) + div.row div.col-12" + override fun latestUpdatesRequest(page: Int): Request { + val builtUrl = if (page == 1) baseUrl else "$baseUrl/page/$page/" + return GET(builtUrl) + } + + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() + val item = element.select("h3 a") + val imgurl = element.select("div.limit img").attr("src").replace("?resize=100,140", "") + manga.url = item.attr("href") + manga.title = item.text() + manga.thumbnail_url = imgurl + return manga + } + + override fun latestUpdatesNextPageSelector() = "a:containsOwn(Berikutnya)" + + override fun popularMangaRequest(page: Int): Request { + val builtUrl = if (page == 1) "$baseUrl/advanced-search/?order=popular" else "$baseUrl/advanced-search/page/$page/?order=popular" + return GET(builtUrl) + } + + override fun popularMangaSelector() = "div.row div.col-6" + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + val imgurl = element.select("div.card img").attr("src").replace("?resize=165,225", "") + manga.url = element.select("div.card a").attr("href") + manga.title = element.select("div.card img").attr("title") + manga.thumbnail_url = imgurl + return manga + } + + override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val builtUrl = if (page == 1) "$baseUrl/advanced-search/" else "$baseUrl/advanced-search/page/$page/" + val url = HttpUrl.parse(builtUrl)!!.newBuilder() + url.addQueryParameter("title", query) + url.addQueryParameter("page", page.toString()) + filters.forEach { filter -> + when (filter) { + is AuthorFilter -> { + url.addQueryParameter("author", filter.state) + } + is YearFilter -> { + url.addQueryParameter("yearx", filter.state) + } + is StatusFilter -> { + val status = when (filter.state) { + Filter.TriState.STATE_INCLUDE -> "completed" + Filter.TriState.STATE_EXCLUDE -> "ongoing" + else -> "" + } + url.addQueryParameter("status", status) + } + is TypeFilter -> { + url.addQueryParameter("type", filter.toUriPart()) + } + is SortByFilter -> { + url.addQueryParameter("order", filter.toUriPart()) + } + is GenreListFilter -> { + filter.state + .filter { it.state != Filter.TriState.STATE_IGNORE } + .forEach { url.addQueryParameter("genre[]", it.id) } + } + } + } + return GET(url.build().toString(), headers) + } + + override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() + + override fun mangaDetailsRequest(manga: SManga): Request { + if (manga.url.startsWith("http")) { + return GET(manga.url, headers) + } + return super.mangaDetailsRequest(manga) + } + + override fun mangaDetailsParse(document: Document): SManga { + val stringBuilder = StringBuilder() + val infoElement = document.select("div.infox") + val author = document.select("span:contains(author)").text().substringAfter("Author: ").substringBefore(" (") + val manga = SManga.create() + val genres = mutableListOf() + val status = document.select("span:contains(Status)").text() + val desc = document.select("div.sinopsis p") + infoElement.select("div.gnr a").forEach { element -> + val genre = element.text() + genres.add(genre) + } + if (desc.size > 0) { + desc.forEach { + stringBuilder.append(it.text()) + if (it != desc.last()) + stringBuilder.append("\n\n") + } + manga.description = stringBuilder.toString() + } else + manga.description = document.select("div.sinopsis").text() + + manga.title = infoElement.select("h1").text() + manga.author = author + manga.artist = author + manga.status = parseStatus(status) + manga.genre = genres.joinToString(", ") + manga.description = stringBuilder.toString() + manga.thumbnail_url = document.select("div.bigcontent img").attr("src") + return manga + } + + override fun chapterListRequest(manga: SManga): Request { + if (manga.url.startsWith("http")) { + return GET(manga.url, headers) + } + return super.chapterListRequest(manga) + } + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val chapters = mutableListOf() + document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) } + // Add date for latest chapter only + document.select("script.yoast-schema-graph").html() + .let { + val date = JSONObject(it).getJSONArray("@graph").getJSONObject(3).getString("dateModified") + chapters[0].date_upload = parseDate(date) + } + return chapters + } + + @SuppressLint("SimpleDateFormat") + private fun parseDate(date: String): Long { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(date).time + } + + override fun chapterListSelector() = "ul#chapter_list li a:contains(chapter)" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a:contains(chapter)") + val chapter = SChapter.create() + chapter.url = urlElement.attr("href") + chapter.name = urlElement.text() + return chapter + } + + override fun pageListRequest(chapter: SChapter): Request { + if (chapter.url.startsWith("http")) { + return GET(chapter.url, headers) + } + return super.pageListRequest(chapter) + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + document.select("div#readerarea img").forEach { + val url = it.attr("src") + pages.add(Page(pages.size, "", url)) + } + return pages + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") + + override fun getFilterList() = FilterList( + Filter.Header("You can combine filter."), + Filter.Separator(), + AuthorFilter(), + YearFilter(), + StatusFilter(), + TypeFilter(), + SortByFilter(), + GenreListFilter(getGenreList()) + ) + + private class AuthorFilter : Filter.Text("Author") + + private class YearFilter : Filter.Text("Year") + + private class StatusFilter : Filter.TriState("Completed") + + private class TypeFilter : UriPartFilter("Type", arrayOf( + Pair("All", ""), + Pair("Manga", "Manga"), + Pair("Manhua", "Manhua"), + Pair("Manhwa", "Manhwa"), + Pair("One-Shot", "One-Shot"), + Pair("Doujin", "Doujin") + )) +}