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 000000000..fa71abc5d Binary files /dev/null and b/src/all/wpmangastream/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/wpmangastream/res/mipmap-mdpi/ic_launcher.png b/src/all/wpmangastream/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 000000000..fbc7bb18b Binary files /dev/null and b/src/all/wpmangastream/res/mipmap-mdpi/ic_launcher.png differ 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 000000000..c994c12cc Binary files /dev/null and b/src/all/wpmangastream/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/wpmangastream/res/mipmap-xxhdpi/ic_launcher.png b/src/all/wpmangastream/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..fddb54206 Binary files /dev/null and b/src/all/wpmangastream/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/wpmangastream/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/wpmangastream/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 000000000..154b3ae47 Binary files /dev/null and b/src/all/wpmangastream/res/mipmap-xxxhdpi/ic_launcher.png differ 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 000000000..047e5438b Binary files /dev/null and b/src/all/wpmangastream/res/web_hi_res_512.png differ 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") + )) +}