fix getlist on AnimeBootstrapParser, ImHentai, beetoon, CloneManga, MangaGeko, Po2Scans, Pururin

add filter FlixScans, TeamXNovel, ComicExtra, MangaOwl.to, MangaTown, Manhwa18.net, ManhwasMen

fix getlist and add tag list on DynastyScans
fix getlist and add SortOrder.UPDATED on MangaStorm
devi 2 years ago
parent ab2aad6bd0
commit f62ea43de8

@ -24,27 +24,26 @@ internal class ImHentai(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) {
if (!query.isNullOrEmpty()) { is MangaListFilter.Search -> {
append("/search/?key=") append("/search/?key=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page) append(page)
} else if (!tags.isNullOrEmpty()) { }
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/tag/") append("/tag/")
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
append("/") append("/")
when (sortOrder) { when (filter.sortOrder) {
SortOrder.UPDATED -> append("") SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("popular/") SortOrder.POPULARITY -> append("popular/")
else -> append("") else -> append("")
@ -54,7 +53,7 @@ internal class ImHentai(context: MangaLoaderContext) :
} else { } else {
append("/search/?page=") append("/search/?page=")
append(page) append(page)
when (sortOrder) { when (filter.sortOrder) {
SortOrder.UPDATED -> append("&lt=1&pp=0") SortOrder.UPDATED -> append("&lt=1&pp=0")
SortOrder.POPULARITY -> append("&lt=0&pp=1") SortOrder.POPULARITY -> append("&lt=0&pp=1")
SortOrder.RATING -> append("&lt=0&pp=0") SortOrder.RATING -> append("&lt=0&pp=0")
@ -62,6 +61,14 @@ internal class ImHentai(context: MangaLoaderContext) :
} }
} }
} }
null -> {
append("/search/?lt=1&pp=0&page=")
append(page)
}
}
}
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.galleries div.thumb").map { div -> return doc.select("div.galleries div.thumb").map { div ->
val a = div.selectFirstOrThrow(".inner_thumb a") val a = div.selectFirstOrThrow(".inner_thumb a")

@ -11,8 +11,6 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.* import java.util.*
// see https://themewagon.com/themes/free-bootstrap-4-html5-gaming-anime-website-template-anime/
internal abstract class AnimeBootstrapParser( internal abstract class AnimeBootstrapParser(
context: MangaLoaderContext, context: MangaLoaderContext,
source: MangaSource, source: MangaSource,
@ -40,13 +38,7 @@ internal abstract class AnimeBootstrapParser(
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
@ -55,24 +47,32 @@ internal abstract class AnimeBootstrapParser(
append(page.toString()) append(page.toString())
append("&type=all") append("&type=all")
if (!query.isNullOrEmpty()) { when (filter) {
is MangaListFilter.Search -> {
append("&search=") append("&search=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
} }
if (!tags.isNullOrEmpty()) { is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&categorie=") append("&categorie=")
append(tag?.key.orEmpty()) append(it.key)
} }
append("&sort=") append("&sort=")
when (sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("view") SortOrder.POPULARITY -> append("view")
SortOrder.UPDATED -> append("updated") SortOrder.UPDATED -> append("updated")
SortOrder.ALPHABETICAL -> append("default") SortOrder.ALPHABETICAL -> append("default")
SortOrder.NEWEST -> append("published") SortOrder.NEWEST -> append("published")
else -> append("updated") else -> append("updated")
} }
}
null -> append("&sort=updated")
}
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -115,11 +115,8 @@ internal abstract class AnimeBootstrapParser(
override suspend fun getDetails(manga: Manga): Manga = coroutineScope { override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain) val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) } val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html() val desc = doc.selectFirstOrThrow(selectDesc).html()
val state = if (doc.select(selectState).isNullOrEmpty()) { val state = if (doc.select(selectState).isNullOrEmpty()) {
MangaState.FINISHED MangaState.FINISHED
} else { } else {

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.fr package org.koitharu.kotatsu.parsers.site.animebootstrap.fr
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -13,20 +12,14 @@ import java.text.SimpleDateFormat
import java.util.EnumSet import java.util.EnumSet
import java.util.Locale import java.util.Locale
@MangaSourceParser("PAPSCAN", "PapScan", "fr") @MangaSourceParser("PAPSCAN", "PapScan", "fr")
internal class PapScan(context: MangaLoaderContext) : internal class PapScan(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") { AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") {
override val sourceLocale: Locale = Locale.ENGLISH override val sourceLocale: Locale = Locale.ENGLISH
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override val listUrl = "/liste-manga" override val listUrl = "/liste-manga"
override val selectState = "div.anime__details__widget li:contains(En cours)" override val selectState = "div.anime__details__widget li:contains(En cours)"
override val selectTag = "div.anime__details__widget li:contains(Genre) a" override val selectTag = "div.anime__details__widget li:contains(Genre) a"
override val selectChapter = "ul.chapters li" override val selectChapter = "ul.chapters li"
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
@ -34,40 +27,39 @@ internal class PapScan(context: MangaLoaderContext) :
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
) )
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/filterList") append("/filterList")
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
when (filter) {
if (!query.isNullOrEmpty()) { is MangaListFilter.Search -> {
append("&alpha=") append("&alpha=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
} }
if (!tags.isNullOrEmpty()) { is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&cat=") append("&cat=")
append(tag?.key.orEmpty()) append(it.key)
} }
append("&sortBy=") append("&sortBy=")
when (sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL -> append("name")
else -> append("updated") else -> append("updated")
} }
} }
val doc = webClient.httpGet(url).parseHtml()
null -> append("&sortBy=updated")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.product__item").map { div -> return doc.select("div.product__item").map { div ->
val href = div.selectFirstOrThrow("h5 a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("h5 a").attrAsRelativeUrl("href")
Manga( Manga(
@ -103,17 +95,13 @@ internal class PapScan(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga = coroutineScope { override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain) val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) } val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html() val desc = doc.selectFirstOrThrow(selectDesc).html()
val state = if (doc.select(selectState).isNullOrEmpty()) { val state = if (doc.select(selectState).isNullOrEmpty()) {
MangaState.FINISHED MangaState.FINISHED
} else { } else {
MangaState.ONGOING MangaState.ONGOING
} }
manga.copy( manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a -> tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag( MangaTag(
@ -145,5 +133,4 @@ internal class PapScan(context: MangaLoaderContext) :
) )
} }
} }
} }

@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("KOMIKZOID", "Komikzo Id", "id") @MangaSourceParser("KOMIKZOID", "KomikzoId", "id")
internal class KomikzoId(context: MangaLoaderContext) : internal class KomikzoId(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.xyz") AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.xyz")

@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.id package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("NEUMANGA", "NeuManga.xyz", "id")
@MangaSourceParser("NEUMANGA", "NeuManga", "id")
internal class NeuManga(context: MangaLoaderContext) : internal class NeuManga(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.NEUMANGA, "neumanga.xyz") AnimeBootstrapParser(context, MangaSource.NEUMANGA, "neumanga.xyz")

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.id package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource

@ -19,33 +19,67 @@ import java.util.*
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) { internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain("flixscans.com") override val configKeyDomain = ConfigKey.Domain("flixscans.com")
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?, val json = when (filter) {
tags: Set<MangaTag>?, is MangaListFilter.Search -> {
sortOrder: SortOrder,
): List<Manga> {
val json = if (!query.isNullOrEmpty()) {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
val url = "https://api.$domain/api/v1/search/serie" val url = "https://api.$domain/api/v1/search/serie"
val body = JSONObject() val body = JSONObject()
body.put("title", query.urlEncoded()) body.put("title", filter.query.urlEncoded())
webClient.httpPost(url, body).parseJson().getJSONArray("data") webClient.httpPost(url, body).parseJson().getJSONArray("data")
} else if (!tags.isNullOrEmpty()) { }
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://api.")
append(domain)
append("/api/v1/")
if (filter.tags.isNotEmpty() || filter.states.isNotEmpty()) {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
val tagQuery = tags.joinToString(separator = ",") { it.key } append("search/advance?=")
val url = "https://api.$domain/api/v1/search/advance?=&genres=$tagQuery&serie_type=webtoon" if (filter.tags.isNotEmpty()) {
webClient.httpGet(url).parseJson().getJSONArray("data") val tagQuery = filter.tags.joinToString(separator = ",") { it.key }
append("&genres=")
append(tagQuery)
}
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "droped"
MangaState.PAUSED -> "onhold"
},
)
}
}
append("&serie_type=webtoon")
} else { } else {
append("webtoon/homepage/latest/home?page=")
append(page.toString())
}
}
webClient.httpGet(url).parseJson().getJSONArray("data")
}
null -> {
val url = "https://api.$domain/api/v1/webtoon/homepage/latest/home?page=$page" val url = "https://api.$domain/api/v1/webtoon/homepage/latest/home?page=$page"
webClient.httpGet(url).parseJson().getJSONArray("data") webClient.httpGet(url).parseJson().getJSONArray("data")
} }
}
return json.mapJSON { j -> return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}" val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
val cover = "https://api.$domain/storage/" + j.getString("thumbnail") val cover = "https://api.$domain/storage/" + j.getString("thumbnail")
@ -62,6 +96,8 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
state = when (j.getString("status")) { state = when (j.getString("status")) {
"ongoing" -> MangaState.ONGOING "ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED "completed" -> MangaState.FINISHED
"onhold" -> MangaState.PAUSED
"droped" -> MangaState.ABANDONED
else -> null else -> null
}, },
author = null, author = null,
@ -77,9 +113,9 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
return tagsList.mapNotNullToSet { idTag -> return tagsList.mapNotNullToSet { idTag ->
val id = idTag.toInt() val id = idTag.toInt()
val idKey = json.getJSONObject(id).getInt("id") val idKey = json.getJSONObject(id).getInt("id")
val key = json.get(idKey).toString() val key = json.getInt(idKey).toString()
val idName = json.getJSONObject(id).getInt("name") val idName = json.getJSONObject(id).getInt("name")
val name = json.get(idName).toString() val name = json.getString(idName)
MangaTag( MangaTag(
key = key, key = key,
title = name, title = name,

@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("MANGASTORM", "MangaStorm", "ar") @MangaSourceParser("MANGASTORM", "MangaStorm", "ar")
internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGASTORM, 30) { internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGASTORM, 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("mangastorm.org") override val configKeyDomain = ConfigKey.Domain("mangastorm.org")
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
@ -21,32 +21,41 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int, val url = buildString {
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url =
if (!tags.isNullOrEmpty()) {
buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/mangas?page=")
append(page)
append("&query=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/categories/") append("/categories/")
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
append("?page=") append("?page=")
append(page) append(page)
}
} else { } else {
buildString { if (filter.sortOrder == SortOrder.POPULARITY) {
append("https://") append("/mangas?page=")
append(domain) append(page)
} else {
if (page > 1) {
return emptyList()
}
}
}
}
null -> {
append("/mangas?page=") append("/mangas?page=")
append(page) append(page)
if (!query.isNullOrEmpty()) {
append("&query=")
append(query.urlEncoded())
} }
} }
} }
@ -74,9 +83,7 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.selectFirstOrThrow(".card-body .col-lg-9") val root = doc.selectFirstOrThrow(".card-body .col-lg-9")
return manga.copy( return manga.copy(
altTitle = null, altTitle = null,
state = null, state = null,

@ -17,46 +17,71 @@ import java.util.*
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) { internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("team11x11.com") override val configKeyDomain = ConfigKey.Domain("team11x11.com")
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
if (!tags.isNullOrEmpty()) { when (filter) {
append("/series?genre=")
append(tag?.key.orEmpty()) is MangaListFilter.Search -> {
append("/series?search=")
append(filter.query.urlEncoded())
if (page > 1) { if (page > 1) {
append("&page=") append("&page=")
append(page) append(page.toString())
} }
} else if (!query.isNullOrEmpty()) { }
append("/series?search=")
append(query.urlEncoded()) is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/series?genre=")
append(tag?.key.orEmpty())
if (page > 1) { if (page > 1) {
append("&page=") append("&page=")
append(page) append(page.toString())
} }
append("&")
} else { } else {
when (sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("/series") SortOrder.POPULARITY -> append("/series")
SortOrder.UPDATED -> append("/") SortOrder.UPDATED -> append("/")
else -> append("/") else -> append("/")
} }
if (page > 1) { if (page > 1) {
append("?page=") append("?page=")
append(page) append(page.toString())
append("&")
} else {
append("?")
}
}
if (filter.sortOrder == SortOrder.POPULARITY || filter.tags.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("status=")
append(
when (it) {
MangaState.ONGOING -> "مستمرة"
MangaState.FINISHED -> "مكتمل"
MangaState.ABANDONED -> "متوقف"
else -> "مستمرة"
},
)
} }
} }
} }
null -> append("/?page=$page")
}
}
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.listupd .bs .bsx").ifEmpty { return doc.select("div.listupd .bs .bsx").ifEmpty {
doc.select("div.post-body .box") doc.select("div.post-body .box")
@ -74,7 +99,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
tags = emptySet(), tags = emptySet(),
state = when (div.selectFirst(".status")?.text()) { state = when (div.selectFirst(".status")?.text()) {
"مستمرة" -> MangaState.ONGOING "مستمرة" -> MangaState.ONGOING
"متوقف", "مكتمل" -> MangaState.FINISHED "مكتمل" -> MangaState.FINISHED
"متوقف" -> MangaState.ABANDONED
else -> null else -> null
}, },
author = null, author = null,
@ -111,7 +137,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
altTitle = null, altTitle = null,
state = when (doc.selectFirstOrThrow(".full-list-info:contains(الحالة:) a").text()) { state = when (doc.selectFirstOrThrow(".full-list-info:contains(الحالة:) a").text()) {
"مستمرة" -> MangaState.ONGOING "مستمرة" -> MangaState.ONGOING
"متوقف", "مكتمل" -> MangaState.FINISHED "مكتمل" -> MangaState.FINISHED
"متوقف" -> MangaState.ABANDONED
else -> null else -> null
}, },
tags = doc.select(".review-author-info a").mapNotNullToSet { a -> tags = doc.select(".review-author-info a").mapNotNullToSet { a ->

@ -19,35 +19,30 @@ internal class BeeToon(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when { when (filter) {
!query.isNullOrEmpty() -> { is MangaListFilter.Search -> {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
append("/?s=") append("/?s=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/genre/") append("/genre/")
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
append("/page-") append("/page-")
append(page) append(page)
append("/") append("/")
} } else {
when (filter.sortOrder) {
else -> {
when (sortOrder) {
SortOrder.UPDATED -> append("/latest-update/") SortOrder.UPDATED -> append("/latest-update/")
SortOrder.POPULARITY -> append("/popular-manga/") SortOrder.POPULARITY -> append("/popular-manga/")
else -> append("/latest-update/") else -> append("/latest-update/")
@ -57,6 +52,9 @@ internal class BeeToon(context: MangaLoaderContext) :
append("/") append("/")
} }
} }
null -> append("/latest-update/page-$page/")
}
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select(".comics-grid .entry").map { div -> return doc.select(".comics-grid .entry").map { div ->

@ -19,11 +19,25 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org") override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org")
@InternalParsersApi @InternalParsersApi
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (query != null || offset > 0) {
val link = when (filter) {
is MangaListFilter.Search -> {
return emptyList() return emptyList()
} }
val link = "https://$domain/viewer_landing.php"
is MangaListFilter.Advanced -> {
if (offset > 0) {
return emptyList()
}
"https://$domain/viewer_landing.php"
}
null -> "https://$domain/viewer_landing.php"
}
val doc = webClient.httpGet(link).parseHtml() val doc = webClient.httpGet(link).parseHtml()
val mangas = doc.getElementsByClass("comicPreviewContainer") val mangas = doc.getElementsByClass("comicPreviewContainer")
return mangas.mapNotNull { item -> return mangas.mapNotNull { item ->

@ -8,6 +8,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -17,6 +18,8 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
override val availableSortOrders: Set<SortOrder> = override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST) EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("comicextra.me") override val configKeyDomain = ConfigKey.Domain("comicextra.me")
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
@ -25,39 +28,63 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://$domain/") append("https://")
if (!tags.isNullOrEmpty()) { append(domain)
append(tag?.key.orEmpty())
if (page > 1) {
append("/") append("/")
append(page) when (filter) {
} is MangaListFilter.Search -> {
} else if (!query.isNullOrEmpty()) {
append("comic-search?key=") append("comic-search?key=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
if (page > 1) { if (page > 1) {
append("&page=") append("&page=")
append(page) append(page.toString())
}
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.states.isEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
} }
} else if (filter.tags.isEmpty() && filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "/ongoing-comic"
MangaState.FINISHED -> "/completed-comic"
else -> "/ongoing-comic"
},
)
}
} else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) {
throw IllegalArgumentException("Source does not support tag + states filters")
} else { } else {
when (sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("popular-comic/") SortOrder.POPULARITY -> append("popular-comic")
SortOrder.UPDATED -> append("new-comic/") SortOrder.UPDATED -> append("new-comic")
SortOrder.NEWEST -> append("recent-comic/") SortOrder.NEWEST -> append("recent-comic")
else -> append("new-comic/") else -> append("new-comic")
} }
}
if (page > 1) {
append("/")
append(page.toString())
}
}
null -> {
append("popular-comic")
if (page > 1) { if (page > 1) {
append(page) append("/")
append(page.toString())
} }
} }
}
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.movie-list-index div.cartoon-box").map { div -> return doc.select("div.movie-list-index div.cartoon-box").map { div ->

@ -1,8 +1,13 @@
package org.koitharu.kotatsu.parsers.site.en package org.koitharu.kotatsu.parsers.site.en
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers import okhttp3.Headers
import org.json.JSONArray import org.json.JSONArray
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -22,85 +27,128 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
override suspend fun getListPage( override val isMultipleTagsSupported = false
page: Int,
query: String?, override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
tags: Set<MangaTag>?, when (filter) {
sortOrder: SortOrder, is MangaListFilter.Search -> {
): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
if (!query.isNullOrEmpty()) {
append("/search?q=") append("/search?q=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
append("&") append("&")
append("classes[]".urlEncoded()) append("classes[]".urlEncoded())
append("=Serie&page=") append("=Serie&page=")
append(page.toString()) append(page.toString())
} else if (!tags.isNullOrEmpty()) { }
return parseMangaListQuery(webClient.httpGet(url).parseHtml())
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
if (filter.tags.isNotEmpty()) {
append("/tags/") append("/tags/")
for (tag in tags) { filter.tags.oneOrThrowIfMany()?.let {
append(tag.key) append(it.key)
} }
append("?view=groupings&page=") append("?view=groupings")
append(page.toString())
} else { } else {
append("/series?view=cover")
}
append("&page=")
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/series?view=cover&page=") append("/series?view=cover&page=")
append(page.toString()) append(page.toString())
} }
return parseMangaList(webClient.httpGet(url).parseHtml())
}
}
} }
val doc = webClient.httpGet(url).parseHtml()
// There are no images on the search page
if (!query.isNullOrEmpty()) { private fun parseMangaList(doc: Document): List<Manga> {
return doc.select("dl.chapter-list dd") return doc.select("li.span2")
.map { div -> .map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(
id = generateUid(href), id = generateUid(href),
title = div.selectFirstOrThrow("a").text(), title = div.selectFirstOrThrow("div.caption").text(),
altTitle = null, altTitle = null,
url = href, url = href,
publicUrl = href.toAbsoluteUrl(domain), publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN, rating = RATING_UNKNOWN,
isNsfw = false, isNsfw = false,
coverUrl = "", coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
tags = div.select("span.tags a").mapNotNullToSet { a -> tags = setOf(),
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
state = null, state = null,
author = null, author = null,
source = source, source = source,
) )
} }
} else { }
return doc.select("li.span2")
private fun parseMangaListQuery(doc: Document): List<Manga> {
return doc.select("dl.chapter-list dd")
.map { div -> .map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(
id = generateUid(href), id = generateUid(href),
title = div.selectFirstOrThrow("div.caption").text(), title = div.selectFirstOrThrow("a").text(),
altTitle = null, altTitle = null,
url = href, url = href,
publicUrl = href.toAbsoluteUrl(domain), publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN, rating = RATING_UNKNOWN,
isNsfw = false, isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"), coverUrl = "",
tags = setOf(), tags = div.select("span.tags a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
state = null, state = null,
author = null, author = null,
source = source, source = source,
) )
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..3).map { page ->
async { getTags(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = "https://$domain/tags?page=$page"
val root = webClient.httpGet(url).parseHtml()
return root.selectFirstOrThrow(".tag-list ").parseTags()
} }
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet() private fun Element.parseTags() = select("a").mapToSet {
MangaTag(
key = it.attr("href").removeSuffix('/').substringAfterLast('/'),
title = it.text(),
source = source,
)
}
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
@ -110,19 +158,14 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
altTitle = null, altTitle = null,
state = when (root.select("h2.tag-title small").last()?.text()) { state = when (root.select("h2.tag-title small").last()?.text()) {
"— Ongoing" -> MangaState.ONGOING "— Ongoing" -> MangaState.ONGOING
"— Completed" -> MangaState.FINISHED "— Completed", "— Completed and Licensed" -> MangaState.FINISHED
"— Dropped", "— Licensed and Removed", "— Abandoned" -> MangaState.ABANDONED
"— On Hiatus" -> MangaState.PAUSED
else -> null else -> null
}, },
coverUrl = root.selectFirst("img.thumbnail")?.src() coverUrl = root.selectFirst("img.thumbnail")?.src()
.orEmpty(), // It is needed if the manga was found via the search. .orEmpty(), // It is needed if the manga was found via the search.
tags = root.select("div.tag-tags a").mapNotNullToSet { a -> tags = root.selectFirstOrThrow("div.tag-tags").parseTags(),
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
MangaTag(
key = href,
title = a.text(),
source = source,
)
},
author = null, author = null,
description = null, description = null,
chapters = chapters, chapters = chapters,

@ -1,148 +0,0 @@
package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
import java.util.*
@MangaSourceParser("FAKKU", "Fakku", "en", ContentType.HENTAI)
internal class Fakku(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.FAKKU, pageSize = 25) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.NEWEST, SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("fakku.cc")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
append("/search?q=")
append(query.urlEncoded())
append("&")
}
!tags.isNullOrEmpty() -> {
append("/tags/")
append(tag?.key.orEmpty())
append("?")
}
else -> {
append("?")
}
}
append("page=")
append(page)
append("&sort=")
when (sortOrder) {
SortOrder.ALPHABETICAL -> append("title")
SortOrder.NEWEST -> append("created_at")
SortOrder.UPDATED -> append("published_at")
else -> append("published_at")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.entries .entry a").map { a ->
val href = a.attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = a.selectFirst("img")?.src().orEmpty(),
title = a.selectFirst(".title")?.text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val root = webClient.httpGet("https://$domain/tags").parseHtml()
return root.select("div.entries .entry a").mapToSet {
MangaTag(
key = it.attr("href").substringAfterLast("/"),
title = it.selectFirstOrThrow(".name").text(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val genreDeferred = async {
webClient.httpGet(manga.url.toAbsoluteUrl(domain) + ".json").parseJson()
}
val genre = genreDeferred.await()
manga.copy(
author = doc.selectFirst("tr.artists a")?.text(),
tags = if (genre.toString().contains("tags")) {
genre.getJSONArray("tags").mapJSONToSet {
MangaTag(
key = it.getString("slug"),
title = it.getString("name"),
source = source,
)
}
} else {
emptySet()
},
chapters = listOf(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
url = manga.url + "/1",
scanlator = null,
uploadDate = 0,
branch = null,
source = source,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val totalPages = doc.selectFirstOrThrow(".total").text().toInt()
val rawUrl = chapter.url.substringBeforeLast("/")
return (1..totalPages).map {
val url = "$rawUrl/$it"
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.selectFirstOrThrow(".page img").attrAsAbsoluteUrl("src")
}
}

@ -1,187 +0,0 @@
package org.koitharu.kotatsu.parsers.site.en
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.json.JSONArray
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("KSKMOE", "Ksk.moe", "en", ContentType.HENTAI)
internal class KskMoe(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.KSKMOE, 35) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("ksk.moe")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/tags/")
append(tag?.key.orEmpty())
} else {
append("/browse")
}
if (page > 1) {
append("/page/")
append(page)
}
when (sortOrder) {
SortOrder.POPULARITY -> append("?sort=32")
SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("?sort=16")
SortOrder.ALPHABETICAL -> append("?sort=1")
else -> append("")
}
if (!query.isNullOrEmpty()) {
append("?s=")
append(query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
if (!doc.html().contains("pagination") && page > 1) {
return emptyList()
}
return doc.requireElementById("galleries").select("article").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectLastOrThrow("h3 span").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(),
tags = div.select("footer span").mapNotNullToSet { span ->
MangaTag(
key = span.text().urlEncoded(),
title = span.text(),
source = source,
)
},
state = null,
author = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..2).map { page ->
async { getTags(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = if (page == 1) {
"https://$domain/tags"
} else {
"https://$domain/tags/page/$page"
}
val root = webClient.httpGet(url).parseHtml().body().getElementById("tags")
return root?.parseTags().orEmpty()
}
private fun Element.parseTags() = select("section.tags div a").mapToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("/tags/"),
title = a.selectFirstOrThrow("span").text(),
source = source,
)
}
private val date = SimpleDateFormat("dd.MM.yyyy hh:mm 'UTC'", Locale.US)
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
return manga.copy(
tags = doc.requireElementById("metadata").select("main div:contains(Tag) a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("/tags/"),
title = a.selectFirstOrThrow("span").text(),
source = source,
)
},
author = doc.requireElementById("metadata").selectFirstOrThrow("main div:contains(Artist) a span").text(),
chapters = listOf(
MangaChapter(
id = generateUid(manga.id),
name = manga.title,
number = 1,
url = manga.url,
scanlator = null,
uploadDate = date.tryParse(doc.selectFirstOrThrow("time.updated").text()),
branch = null,
source = source,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url
.replace("/view/", "/read/")
.let { "$it/1" }
.toAbsoluteUrl(domain)
val document = webClient.httpGet(fullUrl).parseHtml()
val id = fullUrl
.substringAfter("/read/")
.substringBeforeLast("/")
val cdnUrl = document.selectFirst("meta[itemprop=image]")
?.attr("content")
?.toHttpUrlOrNull()
?.host
.let { "https://" + (it ?: domain) }
val script = document.select("script:containsData(window.metadata)").html()
val rawJson = script
.substringAfter("original:")
.substringBefore("resampled:")
.substringBeforeLast(",")
return JSONArray(rawJson).mapJSON {
val fileName = it.getString("n")
val url = "$cdnUrl/original/$id/$fileName"
val preview = "$cdnUrl/t/$id/320/$fileName"
MangaPage(
id = generateUid(url),
url = url,
preview = preview,
source = source,
)
}
}
}

@ -25,42 +25,49 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?, val url = buildString {
tags: Set<MangaTag>?, append("https://")
sortOrder: SortOrder, append(domain)
): List<Manga> { when (filter) {
val tag = tags.oneOrThrowIfMany() is MangaListFilter.Search -> {
val url = if (!query.isNullOrEmpty()) {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
buildString { append("/search/?search=")
append("https://$domain/search/?search=") append(filter.query.urlEncoded())
append(query.urlEncoded())
} }
} else {
buildString { is MangaListFilter.Advanced -> {
append("https://$domain/browse-comics/?results=")
append("/browse-comics/?results=")
append(page) append(page)
append("&filter=") append("&filter=")
when (sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("Updated") SortOrder.UPDATED -> append("Updated")
SortOrder.NEWEST -> append("New") SortOrder.NEWEST -> append("New")
else -> append("Updated") else -> append("Updated")
} }
if (!tags.isNullOrEmpty()) {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("&genre=") append("&genre=")
append(tag?.key.orEmpty()) append(it.key)
} }
} }
} }
null -> {
append("/browse-comics/?results=")
append(page)
append("&filter=Updated")
}
}
}
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("li.novel-item").map { div -> return doc.select("li.novel-item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(

@ -22,35 +22,69 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
SortOrder.UPDATED, SortOrder.UPDATED,
) )
private val regexTag = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+") override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
override suspend fun getList( MangaState.FINISHED,
offset: Int, )
query: String?,
tags: Set<MangaTag>?, override val isMultipleTagsSupported = false
sortOrder: SortOrder,
): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val sortKey = when (sortOrder) {
SortOrder.ALPHABETICAL -> "?name.az"
SortOrder.RATING -> "?rating.za"
SortOrder.UPDATED -> "?last_chapter_time.za"
else -> ""
}
val page = (offset / 30) + 1 val page = (offset / 30) + 1
val url = when { val url = buildString {
!query.isNullOrEmpty() -> { append("https://")
if (offset != 0) { append(domain)
return emptyList() when (filter) {
is MangaListFilter.Search -> {
append("/search?name=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
} }
"/search?name=${query.urlEncoded()}".toAbsoluteUrl(domain)
is MangaListFilter.Advanced -> {
append("/directory/")
append("0-")
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
append("0")
} }
append("-0-")
tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".toAbsoluteUrl(domain) if (filter.states.isNotEmpty()) {
tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".toAbsoluteUrl(domain) filter.states.oneOrThrowIfMany()?.let {
else -> tags.joinToString( append(
prefix = "/search?page=$page".toAbsoluteUrl(domain), when (it) {
) { tag -> MangaState.ONGOING -> "ongoing"
"&genres[${tag.key}]=1" MangaState.FINISHED -> "completed"
else -> "0"
},
)
}
} else {
append("0")
}
append("-0-0/")
append(page.toString())
append(".htm")
append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> ""
SortOrder.UPDATED -> "?last_chapter_time.za"
SortOrder.ALPHABETICAL -> "?name.az"
SortOrder.RATING -> "?rating.za"
else -> "?last_chapter_time.za"
},
)
}
null -> append("/directory/$page.htm?last_chapter_time.za")
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -81,7 +115,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x -> tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x ->
MangaTag( MangaTag(
title = x.attr("title").toTitleCase(), title = x.attr("title").toTitleCase(),
key = x.attr("href").parseTagKey() ?: return@tags null, key = x.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
source = MangaSource.MANGATOWN, source = MangaSource.MANGATOWN,
) )
}.orEmpty(), }.orEmpty(),
@ -106,7 +140,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
}?.select("a")?.mapNotNull { a -> }?.select("a")?.mapNotNull { a ->
MangaTag( MangaTag(
title = a.attr("title").toTitleCase(), title = a.attr("title").toTitleCase(),
key = a.attr("href").parseTagKey() ?: return@mapNotNull null, key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
source = MangaSource.MANGATOWN, source = MangaSource.MANGATOWN,
) )
}.orEmpty(), }.orEmpty(),
@ -165,10 +199,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
?.nextElementSibling() ?: doc.parseFailed("Root not found") ?.nextElementSibling() ?: doc.parseFailed("Root not found")
return root.select("li").mapNotNullToSet { li -> return root.select("li").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val key = a.attr("href").parseTagKey() val key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-")
if (key.isNullOrEmpty()) {
return@mapNotNullToSet null
}
MangaTag( MangaTag(
source = MangaSource.MANGATOWN, source = MangaSource.MANGATOWN,
key = key, key = key,
@ -211,6 +242,4 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
) )
} }
} }
private fun String.parseTagKey() = split('/').findLast { regexTag matches it }
} }

@ -24,6 +24,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.RATING, SortOrder.RATING,
) )
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("mangaowl.to") override val configKeyDomain = ConfigKey.Domain("mangaowl.to")
@ -31,46 +32,56 @@ internal class Mangaowl(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sort = when (sortOrder) {
SortOrder.POPULARITY -> "view_count"
SortOrder.UPDATED -> "-modified_at"
SortOrder.NEWEST -> "created_at"
SortOrder.RATING -> "rating"
else -> "modified_at"
}
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when { when (filter) {
!query.isNullOrEmpty() -> { is MangaListFilter.Search -> {
append("/8-search") append("/10-search?q=")
append("?q=") append(filter.query.urlEncoded())
append(query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("/8-genres/")
for (tag in tags) { append("/10-comics")
append(tag.key)
}
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
filter.tags.forEach { tag ->
append("&genres=")
append(tag.key)
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> ""
},
)
} }
else -> {
append("/8-comics")
append("?page=")
append(page.toString())
append("&ordering=") append("&ordering=")
append(sort) append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> "view_count"
SortOrder.UPDATED -> "-modified_at"
SortOrder.NEWEST -> "created_at"
SortOrder.RATING -> "rating"
else -> "modified_at"
},
)
}
null -> {
append("/10-comics?ordering=-modified_at&page=")
append(page.toString())
} }
} }
} }
@ -95,9 +106,9 @@ internal class Mangaowl(context: MangaLoaderContext) :
} }
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/8-genres").parseHtml() val doc = webClient.httpGet("https://$domain/10-genres").parseHtml()
return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a -> return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a ->
val key = a.attr("href").substringAfterLast("/") val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-")
MangaTag( MangaTag(
key = key, key = key,
title = a.text(), title = a.text(),
@ -112,7 +123,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
manga.copy( manga.copy(
tags = doc.body().select("div.comic-attrs div.column.my-2:contains(Genres) a").mapNotNullToSet { a -> tags = doc.body().select("div.comic-attrs div.column.my-2:contains(Genres) a").mapNotNullToSet { a ->
MangaTag( MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'), key = a.attr("href").removeSuffix("/").substringAfterLast('/').substringBefore("-"),
title = a.text().toTitleCase().replace(",", ""), title = a.text().toTitleCase().replace(",", ""),
source = source, source = source,
) )

@ -9,14 +9,26 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.* import java.util.*
@MangaSourceParser("MANHWA18", "Manhwa18", "en", type = ContentType.HENTAI) @MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI)
class Manhwa18Parser(context: MangaLoaderContext) : class Manhwa18Parser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) { PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net") override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net")
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST) get() = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
)
private val tagsMap = SuspendLazy(::parseTags) private val tagsMap = SuspendLazy(::parseTags)
@ -29,6 +41,82 @@ class Manhwa18Parser(context: MangaLoaderContext) :
) )
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/tim-kiem?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&accept_genres=")
if (filter.tags.isNotEmpty()) {
append(
filter.tags.joinToString(",") { it.key },
)
}
append("&sort=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
SortOrder.RATING -> "like"
},
)
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "3"
MangaState.PAUSED -> "2"
else -> ""
},
)
}
}
null -> append("&sort=update")
}
}
val docs = webClient.httpGet(url).parseHtml()
return docs.select(".card-body .thumb-item-flow")
.map {
val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a")
val absUrl = titleElement.attrAsAbsoluteUrl("href")
Manga(
id = generateUid(absUrl.toRelativeUrl(domain)),
title = titleElement.text(),
altTitle = null,
url = absUrl.toRelativeUrl(domain),
publicUrl = absUrl,
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = MangaSource.MANHWA18,
)
}
}
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val cardInfoElement = docs.selectFirst("div.series-information") val cardInfoElement = docs.selectFirst("div.series-information")
@ -45,6 +133,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
when (it.text().lowercase()) { when (it.text().lowercase()) {
"on going" -> MangaState.ONGOING "on going" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED "completed" -> MangaState.FINISHED
"on hold" -> MangaState.PAUSED
else -> null else -> null
} }
} }
@ -99,60 +188,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
return cal.time.time return cal.time.time
} }
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
else -> ""
}
val tagQuery = tags?.joinToString(",") { it.key }.orEmpty()
val url = buildString {
append("https://")
append(domain)
append("/tim-kiem?page=")
append(page)
if (!query.isNullOrEmpty()) {
append("&q=")
append(query.urlEncoded())
}
append("&accept_genres=$tagQuery")
append("&sort=")
append(sortQuery)
}
val docs = webClient.httpGet(url).parseHtml()
return docs.select(".card-body .thumb-item-flow")
.map {
val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a")
val absUrl = titleElement.attrAsAbsoluteUrl("href")
Manga(
id = generateUid(absUrl.toRelativeUrl(domain)),
title = titleElement.text(),
altTitle = null,
url = absUrl.toRelativeUrl(domain),
publicUrl = absUrl,
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = MangaSource.MANHWA18,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain) val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(chapterUrl).parseHtml() val doc = webClient.httpGet(chapterUrl).parseHtml()

@ -21,29 +21,42 @@ class ManhwasMen(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.POPULARITY) get() = EnumSet.of(SortOrder.POPULARITY)
override suspend fun getListPage( override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
page: Int,
query: String?, override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/manga-list") append("/manga-list")
append("?page=") append("?page=")
append(page) append(page.toString())
when { when (filter) {
!query.isNullOrEmpty() -> { is MangaListFilter.Search -> {
append("&search=") append("&search=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&genero=") append("&genero=")
append(tag?.key.orEmpty()) append(it.key)
} }
filter.states.oneOrThrowIfMany()?.let {
append("&estado=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "complete"
else -> ""
},
)
}
}
null -> {}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -89,9 +102,9 @@ class ManhwasMen(context: MangaLoaderContext) :
) )
}, },
description = doc.select(".sinopsis").html(), description = doc.select(".sinopsis").html(),
state = when (doc.selectLast(".anime-type-peli")?.text()?.lowercase()) { state = when (doc.selectLast("span.anime-type-peli")?.text()?.lowercase()) {
"ongoing" -> MangaState.ONGOING "ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED "complete" -> MangaState.FINISHED
else -> null else -> null
}, },
chapters = doc.select(".episodes-list li").mapChapters(reversed = true) { i, li -> chapters = doc.select(".episodes-list li").mapChapters(reversed = true) { i, li ->

@ -15,20 +15,28 @@ internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, Mang
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("po2scans.com") override val configKeyDomain = ConfigKey.Domain("po2scans.com")
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) { if (offset > 0) {
return emptyList() return emptyList()
} }
val url = buildString { val url = buildString {
append("https://$domain/series") append("https://")
if (!query.isNullOrEmpty()) { append(domain)
append("/series")
when (filter) {
is MangaListFilter.Search -> {
append("?search=") append("?search=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {}
null -> {}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select(".series-list").map { div -> return doc.select(".series-list").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(
id = generateUid(href), id = generateUid(href),
title = div.selectFirstOrThrow("h2").text(), title = div.selectFirstOrThrow("h2").text(),

@ -24,32 +24,32 @@ internal class Pururin(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
if (!query.isNullOrEmpty()) { when (filter) {
is MangaListFilter.Search -> {
append("/search?q=") append("/search?q=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page) append(page.toString())
} else { }
is MangaListFilter.Advanced -> {
append("/browse") append("/browse")
if (!tags.isNullOrEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tags/content/") append("/tags/content/")
append(tag?.key.orEmpty()) append(it.key)
append("/") append("/")
} }
append("?page=") append("?page=")
append(page) append(page)
append("&sort=") append("&sort=")
when (sortOrder) { when (filter.sortOrder) {
SortOrder.UPDATED -> append("") SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("most-viewed") SortOrder.POPULARITY -> append("most-viewed")
SortOrder.RATING -> append("highest-rated") SortOrder.RATING -> append("highest-rated")
@ -57,6 +57,12 @@ internal class Pururin(context: MangaLoaderContext) :
else -> append("") else -> append("")
} }
} }
null -> {
append("/browse?page=")
append(page)
}
}
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select(".row-gallery a.card-gallery").map { a -> return doc.select(".row-gallery a.card-gallery").map { a ->

Loading…
Cancel
Save