[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
This commit is contained in:
Arif Ramadan 2019-10-20 05:17:37 +07:00 committed by arkon
parent bd92939583
commit 32de7342cb
9 changed files with 1330 additions and 0 deletions

View File

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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<Application>().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<String>()
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<Page> {
val pages = mutableListOf<Page>()
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<Genre>) : Filter.Group<Genre>("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<Genre> = 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<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
}

View File

@ -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<Source> = 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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<String>()
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<Page> {
val pages = mutableListOf<Page>()
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<Status>) : Filter.Group<Status>("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<Genre> = 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<String>()
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<Pair<String, String>>) :
Filter.Select<String>(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<String>()
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<SChapter> {
val document = response.asJsoup()
val chapters = mutableListOf<SChapter>()
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<Page> {
val pages = mutableListOf<Page>()
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")
))
}