Merge pull request #401 from KotatsuApp/filter

Rework getList
pull/404/head
Koitharu 2 years ago committed by GitHub
commit f1e8fbec5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -33,6 +33,8 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.ALPHABETICAL,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain(
"bato.to",
"batocomic.com",
@ -61,39 +63,66 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
"zbato.org",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
return search(page, query)
}
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
return search(page, filter.query)
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=")
when (sortOrder) {
SortOrder.UPDATED,
-> append("update.za")
append("/browse?sort=")
when (filter.sortOrder) {
SortOrder.UPDATED -> append("update.za")
SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za")
SortOrder.ALPHABETICAL -> append("title.az")
SortOrder.RATING -> Unit
else -> append("update.za")
}
filter.states.oneOrThrowIfMany()?.let {
append("&release=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "cancelled"
MangaState.PAUSED -> "hiatus"
},
)
}
if (!tags.isNullOrEmpty()) {
// langs= en ...
if (filter.tags.isNotEmpty()) {
append("&genres=")
appendAll(tags, ",") { it.key }
appendAll(filter.tags, ",") { it.key }
}
append("&page=")
append(page)
}
return parseList(url, page)
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=update.za")
append("&page=")
append(page)
}
return parseList(url, page)
}
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
.requireElementById("mainer")
@ -109,9 +138,11 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
?.selectFirst(".limit-html")
?.html(),
tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(),
state = when (attrs["Release status:"]?.text()) {
state = when (attrs["Original work:"]?.text()) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Cancelled" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> manga.state
},
author = attrs["Authors:"]?.text()?.trim() ?: manga.author,

@ -114,9 +114,11 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true"
val jo = webClient.httpGet(url).parseJson()
val comic = jo.getJSONObject("comic")
var alt = ""
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
return manga.copy(
title = comic.getString("title"),
altTitle = null, // TODO
altTitle = alt,
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {

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

@ -132,27 +132,11 @@ internal abstract class LineWebtoonsParser(
}
}
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val genre = tags.oneOrThrowIfMany()?.key ?: "ALL"
val sortOrderStr = when (sortOrder) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder")
}
val manga = if (query != null) {
if (!tags.isNullOrEmpty()) {
throw IllegalArgumentException("This source does not support search with tags")
}
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val manga =
when (filter) {
is MangaListFilter.Search -> {
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
.getJSONObject("challengeSearch")
.getJSONArray("titleList")
.mapJSON { jo ->
@ -175,7 +159,19 @@ internal abstract class LineWebtoonsParser(
source = source,
)
}
} else {
}
is MangaListFilter.Advanced -> {
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
val sortOrderStr = when (filter.sortOrder) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
}
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
@ -210,6 +206,44 @@ internal abstract class LineWebtoonsParser(
}
}
null -> {
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=ALL&sortOrder=UPDATE&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
}
return manga
}
@ -309,10 +343,10 @@ internal abstract class LineWebtoonsParser(
}
fun makeEncryptUrl(urlBuilder: HttpUrl.Builder) {
val msgpad = Calendar.getInstance().timeInMillis.toString()
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgpad))
val msgPad = Calendar.getInstance().timeInMillis.toString()
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgPad))
urlBuilder
.addQueryParameter("msgpad", msgpad)
.addQueryParameter("msgpad", msgPad)
.addQueryParameter("md", digest)
// .addEncodedQueryParameter("md", digest.urlEncoded())
}

@ -30,19 +30,12 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
override val configKeyDomain = ConfigKey.Domain("mangadex.org")
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.POPULARITY,
)
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
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> {
val domain = domain
val url = buildString {
append("https://api.")
@ -52,29 +45,46 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append("&offset=")
append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist&")
tags?.forEach { tag ->
append("includedTags[]=")
append(tag.key)
when (filter) {
is MangaListFilter.Search -> {
append("title=")
append(filter.query)
append('&')
}
if (!query.isNullOrEmpty()) {
append("title=")
append(query.urlEncoded())
is MangaListFilter.Advanced -> {
filter.tags.forEach { tag ->
append("includedTags[]=")
append(tag.key)
append('&')
}
append(CONTENT_RATING)
append("&order")
append(
when (sortOrder) {
SortOrder.UPDATED,
-> "[latestUploadedChapter]=desc"
when (filter.sortOrder) {
SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
SortOrder.RATING -> "[rating]=desc"
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.NEWEST -> "[createdAt]=desc"
SortOrder.POPULARITY -> "[followedCount]=desc"
else -> "[followedCount]=desc"
},
)
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.ABANDONED -> append("cancelled")
MangaState.PAUSED -> append("hiatus")
}
}
}
null -> {
append("&order[latestUploadedChapter]=desc")
}
}
}
val json = webClient.httpGet(url).parseJson().getJSONArray("data")
return json.mapJSON { jo ->
@ -110,9 +120,11 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
source = source,
)
},
state = when (jo.getStringOrNull("status")) {
state = when (attrs.getStringOrNull("status")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"hiatus" -> MangaState.PAUSED
"cancelled" -> MangaState.ABANDONED
else -> null
},
author = (relations["author"] ?: relations["artist"])

@ -35,6 +35,11 @@ internal abstract class NineMangaParser(
SortOrder.POPULARITY,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = if (request.url.host == domain) {
@ -45,38 +50,61 @@ internal abstract class NineMangaParser(
return chain.proceed(newRequest)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?name_sel=&wd=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(page)
append(".html")
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("/search/?category_id=")
for (tag in tags) {
for (tag in filter.tags) {
append(tag.key)
append(',')
}
filter.states.oneOrThrowIfMany()?.let {
append("&completed_series=")
when (it) {
MangaState.ONGOING -> append("no")
MangaState.FINISHED -> append("yes")
else -> append("either")
}
}
append("&page=")
} else {
append("/category/")
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
when (it) {
MangaState.ONGOING -> append("updated_")
MangaState.FINISHED -> append("completed_")
else -> append("either")
}
else -> {
append("/category/index_")
}
} else {
append("index_")
}
}
append(page)
append(".html")
}
null -> {
append("/category/index_")
append(page)
append(".html")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
val root = doc.body().selectFirst("ul.direlist") ?: doc.parseFailed("Cannot find root")
val baseHost = root.baseUri().toHttpUrl().host

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

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

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

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

@ -19,33 +19,67 @@ import java.util.*
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) {
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 suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val json = if (!query.isNullOrEmpty()) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val json = when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val url = "https://api.$domain/api/v1/search/serie"
val body = JSONObject()
body.put("title", query.urlEncoded())
body.put("title", filter.query.urlEncoded())
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) {
return emptyList()
}
val tagQuery = tags.joinToString(separator = ",") { it.key }
val url = "https://api.$domain/api/v1/search/advance?=&genres=$tagQuery&serie_type=webtoon"
webClient.httpGet(url).parseJson().getJSONArray("data")
append("search/advance?=")
if (filter.tags.isNotEmpty()) {
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 {
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"
webClient.httpGet(url).parseJson().getJSONArray("data")
}
}
return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
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")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"onhold" -> MangaState.PAUSED
"droped" -> MangaState.ABANDONED
else -> null
},
author = null,
@ -77,9 +113,9 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
return tagsList.mapNotNullToSet { idTag ->
val id = idTag.toInt()
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 name = json.get(idName).toString()
val name = json.getString(idName)
MangaTag(
key = key,
title = name,

@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("MANGASTORM", "MangaStorm", "ar")
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 isMultipleTagsSupported = false
@ -21,32 +21,41 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url =
if (!tags.isNullOrEmpty()) {
buildString {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
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(tag?.key.orEmpty())
append("?page=")
append(page)
}
} else {
buildString {
append("https://")
append(domain)
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/mangas?page=")
append(page)
} else {
if (page > 1) {
return emptyList()
}
}
}
}
null -> {
append("/mangas?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 {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.selectFirstOrThrow(".card-body .col-lg-9")
return manga.copy(
altTitle = null,
state = null,

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

@ -19,35 +19,30 @@ internal class BeeToon(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
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(tag?.key.orEmpty())
append("/page-")
append(page)
append("/")
}
else -> {
when (sortOrder) {
} else {
when (filter.sortOrder) {
SortOrder.UPDATED -> append("/latest-update/")
SortOrder.POPULARITY -> append("/popular-manga/")
else -> append("/latest-update/")
@ -57,6 +52,9 @@ internal class BeeToon(context: MangaLoaderContext) :
append("/")
}
}
null -> append("/latest-update/page-$page/")
}
}
val doc = webClient.httpGet(url).parseHtml()
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")
@InternalParsersApi
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
if (query != null || offset > 0) {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val link = when (filter) {
is MangaListFilter.Search -> {
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 mangas = doc.getElementsByClass("comicPreviewContainer")
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.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat
import java.util.*
@ -17,6 +18,8 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
override val availableSortOrders: Set<SortOrder> =
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 isMultipleTagsSupported = false
@ -25,39 +28,63 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://$domain/")
if (!tags.isNullOrEmpty()) {
append(tag?.key.orEmpty())
if (page > 1) {
append("https://")
append(domain)
append("/")
append(page)
}
} else if (!query.isNullOrEmpty()) {
when (filter) {
is MangaListFilter.Search -> {
append("comic-search?key=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
if (page > 1) {
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 {
when (sortOrder) {
SortOrder.POPULARITY -> append("popular-comic/")
SortOrder.UPDATED -> append("new-comic/")
SortOrder.NEWEST -> append("recent-comic/")
else -> append("new-comic/")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("popular-comic")
SortOrder.UPDATED -> append("new-comic")
SortOrder.NEWEST -> append("recent-comic")
else -> append("new-comic")
}
}
if (page > 1) {
append("/")
append(page.toString())
}
}
null -> {
append("popular-comic")
if (page > 1) {
append(page)
append("/")
append(page.toString())
}
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.movie-list-index div.cartoon-box").map { div ->

@ -1,8 +1,13 @@
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 org.json.JSONArray
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -22,85 +27,128 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
val url = buildString {
append("https://")
append(domain)
if (!query.isNullOrEmpty()) {
append("/search?q=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&")
append("classes[]".urlEncoded())
append("=Serie&page=")
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/")
for (tag in tags) {
append(tag.key)
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
append("?view=groupings&page=")
append(page.toString())
append("?view=groupings")
} 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(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()) {
return doc.select("dl.chapter-list dd")
private fun parseMangaList(doc: Document): List<Manga> {
return doc.select("li.span2")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("a").text(),
title = div.selectFirstOrThrow("div.caption").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "",
tags = div.select("span.tags a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
} else {
return doc.select("li.span2")
}
private fun parseMangaListQuery(doc: Document): List<Manga> {
return doc.select("dl.chapter-list dd")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("div.caption").text(),
title = div.selectFirstOrThrow("a").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
tags = setOf(),
coverUrl = "",
tags = div.select("span.tags a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
state = null,
author = null,
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 {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
@ -110,19 +158,14 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
altTitle = null,
state = when (root.select("h2.tag-title small").last()?.text()) {
"— 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
},
coverUrl = root.selectFirst("img.thumbnail")?.src()
.orEmpty(), // It is needed if the manga was found via the search.
tags = root.select("div.tag-tags a").mapNotNullToSet { a ->
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
MangaTag(
key = href,
title = a.text(),
source = source,
)
},
tags = root.selectFirstOrThrow("div.tag-tags").parseTags(),
author = null,
description = null,
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)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = if (!query.isNullOrEmpty()) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
buildString {
append("https://$domain/search/?search=")
append(query.urlEncoded())
append("/search/?search=")
append(filter.query.urlEncoded())
}
} else {
buildString {
append("https://$domain/browse-comics/?results=")
is MangaListFilter.Advanced -> {
append("/browse-comics/?results=")
append(page)
append("&filter=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("Updated")
SortOrder.NEWEST -> append("New")
else -> append("Updated")
}
if (!tags.isNullOrEmpty()) {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
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()
return doc.select("li.novel-item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(

@ -22,35 +22,69 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
SortOrder.UPDATED,
)
private val regexTag = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+")
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sortKey = when (sortOrder) {
SortOrder.ALPHABETICAL -> "?name.az"
SortOrder.RATING -> "?rating.za"
SortOrder.UPDATED -> "?last_chapter_time.za"
else -> ""
}
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
)
override val isMultipleTagsSupported = false
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val page = (offset / 30) + 1
val url = when {
!query.isNullOrEmpty() -> {
if (offset != 0) {
return emptyList()
val url = buildString {
append("https://")
append(domain)
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)
tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".toAbsoluteUrl(domain)
else -> tags.joinToString(
prefix = "/search?page=$page".toAbsoluteUrl(domain),
) { tag ->
"&genres[${tag.key}]=1"
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "ongoing"
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()
@ -81,7 +115,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x ->
MangaTag(
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,
)
}.orEmpty(),
@ -106,7 +140,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
}?.select("a")?.mapNotNull { a ->
MangaTag(
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,
)
}.orEmpty(),
@ -165,10 +199,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
?.nextElementSibling() ?: doc.parseFailed("Root not found")
return root.select("li").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val key = a.attr("href").parseTagKey()
if (key.isNullOrEmpty()) {
return@mapNotNullToSet null
}
val key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-")
MangaTag(
source = MangaSource.MANGATOWN,
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.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("mangaowl.to")
@ -31,46 +32,56 @@ internal class Mangaowl(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
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"
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
append("/8-search")
append("?q=")
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append("/10-search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
append("/8-genres/")
for (tag in tags) {
append(tag.key)
}
is MangaListFilter.Advanced -> {
append("/10-comics")
append("?page=")
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(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> {
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 ->
val key = a.attr("href").substringAfterLast("/")
val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-")
MangaTag(
key = key,
title = a.text(),
@ -112,7 +123,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
manga.copy(
tags = doc.body().select("div.comic-attrs div.column.my-2:contains(Genres) a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
key = a.attr("href").removeSuffix("/").substringAfterLast('/').substringBefore("-"),
title = a.text().toTitleCase().replace(",", ""),
source = source,
)

@ -9,14 +9,26 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MANHWA18", "Manhwa18", "en", type = ContentType.HENTAI)
@MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI)
class Manhwa18Parser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net")
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)
@ -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 {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val cardInfoElement = docs.selectFirst("div.series-information")
@ -45,6 +133,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
when (it.text().lowercase()) {
"on going" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"on hold" -> MangaState.PAUSED
else -> null
}
}
@ -99,60 +188,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
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> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(chapterUrl).parseHtml()

@ -21,29 +21,42 @@ class ManhwasMen(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.POPULARITY)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/manga-list")
append("?page=")
append(page)
when {
!query.isNullOrEmpty() -> {
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
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()
@ -89,9 +102,9 @@ class ManhwasMen(context: MangaLoaderContext) :
)
},
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
"completed" -> MangaState.FINISHED
"complete" -> MangaState.FINISHED
else -> null
},
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 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) {
return emptyList()
}
val url = buildString {
append("https://$domain/series")
if (!query.isNullOrEmpty()) {
append("https://")
append(domain)
append("/series")
when (filter) {
is MangaListFilter.Search -> {
append("?search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {}
null -> {}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".series-list").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h2").text(),

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

@ -10,6 +10,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.lang.IllegalArgumentException
import java.util.*
@MangaSourceParser("TEMPLESCANESP", "TempleScanEsp", "es", ContentType.HENTAI)
@ -24,18 +25,18 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (sortOrder == SortOrder.NEWEST) {
append("/comics")
append("?page=")
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.NEWEST) {
append("/comics?page=")
append(page.toString())
} else {
if (page > 1) {
@ -44,6 +45,13 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
}
}
null -> {
append("/comics?page=")
append(page.toString())
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.grid figure").ifEmpty {
doc.requireElementById("projectsDiv").select("figure")

@ -32,41 +32,47 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.RATING,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val order =
when (sortOrder) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/library")
when (filter) {
is MangaListFilter.Search -> {
append("?title=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("?order_item=")
append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> "likes_count"
SortOrder.UPDATED -> "release_date"
SortOrder.NEWEST -> "creation"
SortOrder.ALPHABETICAL -> "alphabetically"
SortOrder.RATING -> "score"
}
val url = buildString {
append("/library")
if (query.isNullOrEmpty()) {
append("?order_item=")
append(order)
},
)
append("&order_dir=desc")
append("&filter_by=title")
if (tags != null) {
for (tag in tags) {
append("&genders[]=${tag.key}")
if (filter.tags.isNotEmpty()) {
for (tag in filter.tags) {
append("&genders[]=")
append(tag.key)
}
}
} else {
append("?title=$query")
}
append("&_pg=1")
append("&page=$page")
}.toAbsoluteUrl(domain)
null -> {
append("?order_item=release_date&order_dir=desc&filter_by=title")
}
}
append("&_pg=1&page=")
append(page.toString())
}
val doc = webClient.httpGet(url, headers).parseHtml()
val items = doc.body().select("div.element")
return items.mapNotNull { item ->
@ -95,6 +101,13 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
val contents = doc.body().selectFirstOrThrow("section.element-header-content")
return manga.copy(
description = contents.selectFirst("p.element-description")?.html(),
tags = contents.select("h6 a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").substringBefore("&").substringAfterLast("="),
title = a.text(),
source = source,
)
},
largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"),
state = parseStatus(contents.select("span.book-status").text().orEmpty()),
author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "),
@ -105,9 +118,9 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
} else {
val chapters = ChaptersListBuilder(10)
doc.select(regularChapterListSelector).reversed().forEachIndexed { i, item ->
val chaptername = item.select("div.col-10.text-truncate").text().replace("&nbsp;", " ").trim()
val scanelement = item.select("ul.chapter-list > li")
scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) }
val chapterName = item.select("div.col-10.text-truncate").text().replace("&nbsp;", " ").trim()
val scanElement = item.select("ul.chapter-list > li")
scanElement.forEach { chapters.add(regularChapterFromElement(it, chapterName, i)) }
}
chapters.toList()
},

@ -27,7 +27,11 @@ internal abstract class FmreaderParser(
SortOrder.ALPHABETICAL,
)
override val isMultipleTagsSupported = false
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
)
protected open val listUrl = "/manga-list.html"
protected open val datePattern = "MMMM d, yyyy"
@ -40,58 +44,73 @@ internal abstract class FmreaderParser(
@JvmField
protected val ongoing: Set<String> = setOf(
"On going",
"Incomplete",
"En curso",
"on going",
"incomplete",
"en curso",
)
@JvmField
protected val finished: Set<String> = setOf(
"Completed",
"Completado",
"completed",
"completado",
)
@JvmField
protected val abandoned: Set<String> = hashSetOf(
"Canceled",
"Cancelled",
"Drop",
"canceled",
"cancelled",
"drop",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("&name=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
append("&genre=")
append(tag?.key.orEmpty())
}
}
append(filter.tags.joinToString(",") { it.key })
append("&sort=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
append("&m_status=")
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "2"
MangaState.FINISHED -> "1"
MangaState.ABANDONED -> "3"
else -> ""
},
)
}
}
null -> append("&sort=last_update")
}
val doc = webClient.httpGet(url).parseHtml()
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.thumb-item-flow").map { div ->
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
Manga(
@ -99,8 +118,8 @@ internal abstract class FmreaderParser(
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('")
.substringBeforeLast("')"),
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("(")
.substringBefore(")"),
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
@ -140,7 +159,7 @@ internal abstract class FmreaderParser(
val desc = doc.selectFirst(selectDesc)?.html()
val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let {
when (it.text()) {
when (it.text().lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
in abandoned -> MangaState.ABANDONED

@ -20,79 +20,67 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
override val selectTag = "div.info-item:contains(Genre) span.info-value a"
override val datePattern = "dd/MM/yyyy"
override val selectPage = "div#chapter-content img"
override val selectBodyTag = "div.genres-menu a"
override val selectBodyTag = "div.advanced-wrapper .genre_label"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/genre/")
append(tag?.key.orEmpty())
append("?page=")
append("/tim-kiem?page=")
append(page.toString())
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
} else {
append(listUrl)
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("&q=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&accept_genres=")
if (filter.tags.isNotEmpty()) {
append(
filter.tags.joinToString(",") { it.key },
)
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
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 -> ""
},
)
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.thumb-item-flow").map { div ->
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('")
.substringBeforeLast("')"),
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
null -> append("&sort=update")
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectBodyTag).mapNotNullToSet { a ->
val href = a.attr("href").substringAfterLast("/")
return doc.select(selectBodyTag).mapNotNullToSet { label ->
val key = label.attr("data-genre-id")
MangaTag(
key = href,
title = a.text(),
key = key,
title = label.selectFirstOrThrow(".gerne-name").text(),
source = source,
)
}
@ -105,7 +93,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let {
when (it.text()) {
when (it.text().lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
else -> null

@ -14,39 +14,62 @@ internal class OlimpoScans(context: MangaLoaderContext) :
override val selectAlt = "ul.manga-info li:contains(Otros nombres)"
override val selectTag = "ul.manga-info li:contains(Género) a"
override val tagPrefix = "lista-de-comics-genero-"
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append(listUrl)
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
append("&name=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("&genre=")
append(tag?.key.orEmpty())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/lista-de-comics-genero-")
append(it.key)
append(".html")
}
} else {
append(listUrl)
append("?page=")
append(page.toString())
append("&sort=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
}
append("&m_status=")
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "2"
MangaState.FINISHED -> "1"
MangaState.ABANDONED -> "3"
else -> ""
},
)
}
}
null -> {
append(listUrl)
append("?page=")
append(page.toString())
append("&sort=last_update")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
val lastPage =
doc.selectLast(".pagination a")?.attr("href")?.substringAfterLast("page=")?.substringBeforeLast("&artist")

@ -3,13 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fmreader.ja
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@ -27,40 +21,7 @@ internal class Klz9(context: MangaLoaderContext) :
override val selectPage = "img"
override val selectBodyTag = "div.panel-body a"
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)
append("/$listUrl")
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
append("&name=")
append(query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("&genre=")
append(tag?.key.orEmpty())
}
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
}
val doc = webClient.httpGet(url).parseHtml()
override fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.thumb-item-flow").map { div ->
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@ -112,7 +73,6 @@ internal class Klz9(context: MangaLoaderContext) :
val docLoad = webClient.httpGet("https://$domain/app/manga/controllers/cont.listImg.php?cid=$cid").parseHtml()
return docLoad.select(selectPage).map { img ->
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,

@ -32,24 +32,31 @@ internal abstract class FoolSlideParser(
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val doc = if (!query.isNullOrEmpty()) {
val url = buildString {
append("https://$domain/$searchUrl")
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc =
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/")
append(searchUrl)
}
webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml()
}
val q = query.urlEncoded()
webClient.httpPost(url, "search=$q").parseHtml()
} else {
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://$domain/$listUrl")
append("https://")
append(domain)
append("/")
append(listUrl)
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
if (page > 1) {
@ -60,7 +67,28 @@ internal abstract class FoolSlideParser(
}
}
webClient.httpGet(url).parseHtml()
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/")
append(listUrl)
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
}
}
webClient.httpGet(url).parseHtml()
}
}
return doc.select("div.list div.group").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(

@ -16,40 +16,30 @@ internal class AssortedScans(context: MangaLoaderContext) :
override val pagination = false
override val selectInfo = "div.#series-info"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val doc = if (!query.isNullOrEmpty()) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append('/')
when (filter) {
is MangaListFilter.Search -> {
append(searchUrl)
append("?q=")
append(query.urlEncoded())
}
webClient.httpGet(url).parseHtml()
} else {
val url = buildString {
append("https://$domain/$listUrl")
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
if (page > 1) {
return emptyList()
append(filter.query.urlEncoded())
}
} else {
append(page.toString())
is MangaListFilter.Advanced -> {
append(listUrl)
}
null -> append(listUrl)
}
webClient.httpGet(url).parseHtml()
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("section.series, tr.result").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(

@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("POWERMANGA", "PowerManga", "it")
internal class PowerManga(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.POWERMANGA, "reader.powermanga.org") {

@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("RAMAREADER", "RamaReader", "it")
internal class Ramareader(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.RAMAREADER, "www.ramareader.it") {

@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("READNIFTEAM", "ReadNifTeam", "it")
internal class ReadNifteam(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.READNIFTEAM, "read-nifteam.info") {

@ -28,23 +28,28 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
override val configKeyDomain = ConfigKey.Domain("bentomanga.com", "www.bentomanga.com")
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
init {
paginator.firstPage = 0
searchPaginator.firstPage = 0
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = urlBuilder()
.host(domain)
.addPathSegment("manga_list")
.addQueryParameter("limit", page.toString())
.addQueryParameter(
when (filter) {
is MangaListFilter.Search -> {
url.addQueryParameter("search", filter.query)
}
is MangaListFilter.Advanced -> {
url.addQueryParameter(
"order_by",
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "views"
SortOrder.RATING -> "top"
@ -52,11 +57,26 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.ALPHABETICAL -> "name"
},
)
if (!tags.isNullOrEmpty()) {
url.addQueryParameter("withCategories", tags.joinToString(",") { it.key })
if (filter.tags.isNotEmpty()) {
url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key })
}
filter.states.oneOrThrowIfMany()?.let {
url.addQueryParameter(
"state",
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
MangaState.PAUSED -> "3"
MangaState.ABANDONED -> "5"
},
)
}
if (!query.isNullOrEmpty()) {
url.addQueryParameter("search", query)
}
null -> url.addQueryParameter("order_by", "update")
}
val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content")
return root.select(".manga[data-manga]").map { div ->
@ -109,7 +129,7 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
"En pause" -> MangaState.PAUSED
else -> null
},
author = root.selectFirst(".datas_more-authors-people")?.textOrNull().assertNotNull("author"),
author = root.selectFirst(".datas_more-authors-people")?.textOrNull(),
chapters = run {
val input = root.selectFirst("input[name=\"limit\"]") ?: return@run parseChapters(root)
val max = input.attr("max").toInt()

@ -18,51 +18,83 @@ internal class FmTeam(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.FMTEAM, 0) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("fmteam.fr")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
val jsonManga = if (!query.isNullOrEmpty()) {
//3 letters minimum
webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson().getJSONArray("comics")
} else {
webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics")
var foundTag = true
var foundState = true
val manga = ArrayList<Manga>()
when (filter) {
is MangaListFilter.Search -> {
val jsonManga = webClient.httpGet("https://$domain/api/search/${filter.query.urlEncoded()}").parseJson()
.getJSONArray("comics")
for (i in 0 until jsonManga.length()) {
val j = jsonManga.getJSONObject(i)
val href = "/api" + j.getString("url")
manga.add(addManga(href, j))
}
}
val manga = ArrayList<Manga>(jsonManga.length())
is MangaListFilter.Advanced -> {
val jsonManga = webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics")
for (i in 0 until jsonManga.length()) {
val j = jsonManga.getJSONObject(i)
val href = "/api" + j.getString("url")
when {
!tags.isNullOrEmpty() -> {
if (filter.tags.isNotEmpty() && filter.states.isEmpty()) {
val a = j.getJSONArray("genres").toString()
var found = true
tags.forEach {
if (!a.contains(it.key, ignoreCase = true)) {
found = false
foundTag = false
filter.tags.forEach {
if (a.contains(it.key, ignoreCase = true)) {
foundTag = true
}
}
if (found) {
manga.add(
addManga(href, j),
}
if (filter.states.isNotEmpty()) {
val a = j.getString("status")
foundState = false
filter.states.oneOrThrowIfMany()?.let {
if (a.contains(
when (it) {
MangaState.ONGOING -> "En cours"
MangaState.FINISHED -> "Terminé"
else -> ""
},
ignoreCase = true,
)
) {
foundState = true
}
}
}
else -> {
if (foundState && foundTag) {
manga.add(addManga(href, j))
}
}
}
null -> {
val jsonManga = webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics")
for (i in 0 until jsonManga.length()) {
val j = jsonManga.getJSONObject(i)
val href = "/api" + j.getString("url")
manga.add(
addManga(href, j),
)
}
}
}
return manga
}

@ -10,6 +10,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.lang.IllegalArgumentException
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@ -35,21 +36,27 @@ internal class FuryoSociety(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
if (page == 1) {
if (sortOrder == SortOrder.ALPHABETICAL) {
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
append("/mangas")
}
} else {
return emptyList()
}
null -> {}
}
}

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers.site.fr
import okhttp3.Headers
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -16,9 +17,7 @@ import java.util.*
internal class LegacyScansParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.LEGACY_SCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val configKeyDomain = ConfigKey.Domain("legacy-scans.com")
@ -26,42 +25,53 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val end = page * pageSize
val start = end - (pageSize - 1)
val url = if (!query.isNullOrEmpty()) {
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
buildString {
val url = buildString {
append("https://api.$domain/misc/home/search?title=")
append(query.urlEncoded())
}
} else {
buildString {
append("https://api.$domain/misc/comic/search/query?status=&order=&genreNames=")
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
append(tag.key)
append(',')
append(filter.query.urlEncoded())
}
return parseMangaListQuery(webClient.httpGet(url).parseJson())
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://api.")
append(domain)
append("/misc/comic/search/query?status=&order=&genreNames=")
append(filter.tags.joinToString(",") { it.key })
append("&type=&start=")
append(start)
append("&end=")
append(end)
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
null -> {
val url = buildString {
append("https://api.")
append(domain)
append("/misc/comic/search/query?status=&order=&genreNames=&type=&start=")
append(start)
append("&end=")
append(end)
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
}
}
val json = webClient.httpGet(url).parseJson()
return if (!query.isNullOrEmpty()) {
json.getJSONArray("results").mapJSON { j ->
private fun parseMangaList(json: JSONObject): List<Manga> {
return json.getJSONArray("comics").mapJSON { j ->
val slug = j.getString("slug")
val urlManga = "https://$domain/comics/$slug"
Manga(
@ -72,15 +82,17 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "",
coverUrl = "https://api.$domain/" + j.getString("cover"),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
} else {
json.getJSONArray("comics").mapJSON { j ->
}
private fun parseMangaListQuery(json: JSONObject): List<Manga> {
return json.getJSONArray("results").mapJSON { j ->
val slug = j.getString("slug")
val urlManga = "https://$domain/comics/$slug"
Manga(
@ -91,7 +103,7 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "https://api.$domain/" + j.getString("cover"),
coverUrl = "",
tags = setOf(),
state = null,
author = null,
@ -100,12 +112,9 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
}
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH)
return manga.copy(
altTitle = null,
tags = root.select("div.serieGenre span").mapNotNullToSet { span ->
@ -156,7 +165,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
val script = doc.requireElementById("__NUXT_DATA__").data()
.substringAfterLast("\"genres\"").substringBeforeLast("\"comics\"")
.split("\",\"").drop(1)
return script.mapNotNullToSet { tag ->
MangaTag(
key = tag.substringBeforeLast("\",{"),

@ -24,29 +24,42 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc =
if (!query.isNullOrEmpty()) { // search only works with 4 or more letters
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val q = query.urlEncoded().replace("%20", "+")
val q = filter.query.urlEncoded().replace("%20", "+")
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
} else {
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/manga/")
append(tag?.key.orEmpty())
append(it.key)
}
if (page > 1) {
append("/page/")
append(page)
append('/')
}
}
webClient.httpGet(url).parseHtml()
}
null -> {
val url = buildString {
append("https://")
append(domain)
if (page > 1) {
append("/page/")
append(page)
@ -55,6 +68,7 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
}
webClient.httpGet(url).parseHtml()
}
}
return doc.select("div.sect__content.grid-items div.item-poster").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers.site.fr
import okhttp3.Headers
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -21,6 +22,8 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
SortOrder.UPDATED,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com")
override val headers: Headers = Headers.Builder()
@ -43,48 +46,34 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
if (sortOrder == SortOrder.ALPHABETICAL) {
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/api/get/catalog?page=0&filter=all")
append("/api/get/catalog?page=0&filter=")
filter.states.oneOrThrowIfMany()?.let {
when (it) {
MangaState.ONGOING -> append("0")
MangaState.FINISHED -> append("1")
MangaState.PAUSED -> append("4")
MangaState.ABANDONED -> append("3")
}
val json = webClient.httpGet(url).parseJsonArray()
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = img,
tags = setOf(),
state = when (j.getString("status")) {
"0" -> MangaState.ONGOING
"1" -> MangaState.FINISHED
"3" -> MangaState.ABANDONED
else -> null
},
author = null,
source = source,
)
}
}
return parseMangaListAlpha(webClient.httpGet(url).parseJsonArray())
} else {
val url = buildString {
append("https://")
@ -92,7 +81,23 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
append("/api/get/homegrid/")
append(page)
}
val json = webClient.httpGet(url).parseJsonArray()
return parseMangaList(webClient.httpGet(url).parseJsonArray())
}
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/api/get/homegrid/")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseJsonArray())
}
}
}
private fun parseMangaList(json: JSONArray): List<Manga> {
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}"
@ -111,9 +116,32 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
source = source,
)
}
}
private fun parseMangaListAlpha(json: JSONArray): List<Manga> {
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = img,
tags = setOf(),
state = when (j.getString("status")) {
"0" -> MangaState.ONGOING
"1" -> MangaState.FINISHED
"3" -> MangaState.ABANDONED
else -> null
},
author = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {

@ -30,31 +30,31 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
if (page == 1) {
if (!query.isNullOrEmpty()) {
when (filter) {
is MangaListFilter.Search -> {
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&post_type=manga")
}
} else if (!tags.isNullOrEmpty()) {
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("/genres/")
for (tag in tags) {
append(tag.key)
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
append("/tous-nos-mangas/")
append("?order=")
when (sortOrder) {
append("/tous-nos-mangas/?order=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("update")
SortOrder.ALPHABETICAL -> append("title")
@ -62,14 +62,13 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
else -> append("update")
}
}
} else {
return emptyList()
}
null -> append("/tous-nos-mangas/?order=update")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.postbody .bs .bsx").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@ -90,7 +89,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/tous-nos-mangas/").parseHtml()
return doc.select("ul.genre li").mapNotNullToSet { li ->
@ -104,25 +102,18 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = getChapters(doc)
val desc = doc.selectFirstOrThrow("div.desc").html()
val state = if (doc.select("div.spe span:contains(En cours)").isNullOrEmpty()) {
MangaState.FINISHED
} else {
MangaState.ONGOING
}
val alt = doc.body().select("div.infox span.alter").text()
val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "")
manga.copy(
tags = doc.select("div.spe span:contains(Genres) a").mapNotNullToSet { a ->
MangaTag(
@ -140,7 +131,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
)
}
private fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().requireElementById("chapter_list").select("li").mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
@ -161,10 +151,8 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val script = doc.selectFirstOrThrow("script:containsData(page_image)")
val images = JSONArray(script.data().substringAfterLast("var pages = ").substringBefore(';'))
val pages = ArrayList<MangaPage>(images.length())
for (i in 0 until images.length()) {
val pageTake = images.getJSONObject(i)
@ -177,7 +165,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
),
)
}
return pages
}
}

@ -20,49 +20,54 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
SortOrder.UPDATED,
)
override val isMultipleTagsSupported = false
override val configKeyDomain = ConfigKey.Domain("scantrad-union.com")
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
for (tag in tags) {
append(tag.key)
append(',')
}
append(it.key)
append("/page/")
append(page.toString())
append("/")
}
else -> {
if (sortOrder == SortOrder.ALPHABETICAL) {
append("/manga/")
append("/page/")
} else {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
append("/manga/page/")
append(page.toString())
append("/")
}
if (filter.sortOrder == SortOrder.UPDATED && page > 1) {
return emptyList()
}
}
if (sortOrder == SortOrder.UPDATED) {
append("")
}
null -> {
append("/manga/page/")
append(page.toString())
append("/")
}
}
}
@ -180,9 +185,8 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
val root = body.select(".asp_gochosen")[1]
val list = root?.select("option").orEmpty()
return list.mapToSet { li ->
MangaTag(
key = li.text(),
key = li.text().lowercase().replace(" ", "-"),
title = li.text(),
source = source,
)

@ -24,36 +24,42 @@ internal abstract class GalleryAdultsParser(
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?q=")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
append("/?")
} else {
append("/tag/")
append(tag?.key.orEmpty())
append(it.key)
append("/?")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
append("&")
}
} else {
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -31,47 +31,60 @@ internal class Hentai3(context: MangaLoaderContext) :
"/japanese",
)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
if (tag.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.tags.size > 1) {
append("/search?q=")
append(buildQuery(filter.tags))
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
append(page.toString())
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
} else {
append("/tags/")
append(tag.key)
append(it.key)
}
}
append("/")
append(page)
if (sortOrder == SortOrder.POPULARITY) {
append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) {
append("?sort=popular")
}
} else if (!query.isNullOrEmpty()) {
append("/search?q=")
append(query.urlEncoded())
if (sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
append(page)
} else {
append("/")
append(page)
}
}
null -> {
append("/")
append(page)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -32,40 +32,45 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
"/portuguese",
)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?s_key=")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
append("/?")
} else {
append("/tag/")
append(tag?.key.orEmpty())
if (sortOrder == SortOrder.POPULARITY) {
append(it.key)
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/popular")
}
append("/?")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?s_key=")
append(query.urlEncoded())
append("&")
}
} else {
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -25,7 +25,9 @@ internal class HentaiEra(context: MangaLoaderContext) :
"/russian",
)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
@ -37,46 +39,54 @@ internal class HentaiEra(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
if (tag.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?key=")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.tags.size > 1) {
append("/search/?key=")
if (filter.sortOrder == SortOrder.POPULARITY) {
append(buildQuery(filter.tags).replace("&lt=1&dl=0&pp=0&tr=0", "&lt=0&dl=0&pp=1&tr=0"))
} else {
append(buildQuery(filter.tags))
}
append("&")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
} else {
append("/tag/")
append(tag.key)
append(it.key)
}
}
append("/")
if (sortOrder == SortOrder.POPULARITY) {
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else if (!query.isNullOrEmpty()) {
append("/search/?key=")
if (sortOrder == SortOrder.POPULARITY) {
append(query.replace("&lt=1&dl=0&pp=0&tr=0", "&lt=0&dl=0&pp=1&tr=0"))
} else {
append(query)
}
append("&")
} else {
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -35,50 +35,60 @@ internal class HentaiForce(context: MangaLoaderContext) :
"/vietnamese",
)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found")
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
if (tag.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.tags.size > 1) {
append("/search?q=")
append(buildQuery(filter.tags))
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
} else {
append("/tag/")
append(tag.key)
append(it.key)
}
if (sortOrder == SortOrder.POPULARITY) {
append("/popular")
}
append("/")
} else if (!query.isNullOrEmpty()) {
append("/search?q=")
append(query.urlEncoded())
if (sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("&page=")
append("?")
} else {
append("/page/")
}
append(page)
}
null -> append("/page/")
}
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -32,59 +32,83 @@ internal class HentaiFox(context: MangaLoaderContext) :
"/vietnamese",
)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
if (tag.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?q=")
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
append(page.toString())
}
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.tags.size > 1) {
append("/search/?q=")
append(buildQuery(filter.tags))
if (page > 1) {
append("&page=")
append(page.toString())
}
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
} else {
append("/tag/")
append(tag.key)
append(it.key)
}
if (sortOrder == SortOrder.POPULARITY) {
append("/popular")
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
if (page > 1) {
append("/pag/")
append(page)
append(page.toString())
append("/")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
if (sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
} else {
if (page > 2) {
append("/pag/")
append(page.toString())
append("/")
} else if (page > 1) {
append("/page/")
append(page.toString())
append("/")
}
if (page > 1) {
append("&page=")
append(page)
}
} else {
}
null -> {
if (page > 2) {
append("/pag/")
append(page)
append(page.toString())
append("/")
} else if (page > 1) {
append("/page/")
append(page)
append(page.toString())
append("/")
}
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -27,50 +27,60 @@ internal class NHentaiParser(context: MangaLoaderContext) :
"/chinese",
)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
if (tag.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?q=pages:>0 ")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.tags.size > 1) {
append("/search/?q=")
append(buildQuery(filter.tags))
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append(it.title)
} else {
append("/tag/")
append(tag.key)
append(it.key)
}
}
append("/")
if (sortOrder == SortOrder.POPULARITY) {
append("popular")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
if (sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&")
} else {
if (sortOrder == SortOrder.POPULARITY) {
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/?sort=popular&")
} else {
append("/?")
}
}
}
null -> append("/?")
}
append("page=")
append(page)
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -33,33 +33,39 @@ internal class NHentaiUk(context: MangaLoaderContext) :
"/turkish",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
if (it.key == "languageKey") {
append("/language")
append(tag.title)
append("/?p=")
append(it.title)
} else {
append("/tag/")
append(tag?.key)
append("/?p=")
append(it.key)
}
} else if (!query.isNullOrEmpty()) {
throw IllegalArgumentException("Search is not supported by this source")
}
append("/?p=")
} else {
append("/home?p=")
}
append(page)
}
null -> append("/?")
}
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -27,6 +27,8 @@ internal abstract class HeanCms(
SortOrder.POPULARITY,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
@ -34,50 +36,54 @@ internal abstract class HeanCms(
protected open val pathManga = "series"
//For some sources, you need to send a json. For the moment, this part only works in Get. ( ex source need json gloriousscan.com , omegascans.org )
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domain)
append("/query?query_string=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
append("&series_status=All&order=desc&orderBy=")
when (sortOrder) {
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
},
)
}
append("&order=desc")
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("created_at")
SortOrder.ALPHABETICAL -> append("title")
else -> append("latest")
}
append("&series_type=Comic&page=")
append(page)
append("&perPage=12&tags_ids=")
append("&series_type=Comic&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
// Just to make it fit [1,2,44] ect
if (!firstTag) {
firstTag = true
} else {
append(",")
}
append(tag.key)
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> {}
}
append("]".urlEncoded())
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug"
@ -107,8 +113,10 @@ internal abstract class HeanCms(
source = source,
)
}
}
protected open val datePattern = "yyyy-MM-dd"
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()

@ -2,19 +2,10 @@ package org.koitharu.kotatsu.parsers.site.heancms.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.urlEncoded
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI)
internal class YugenMangasEs(context: MangaLoaderContext) :
@ -22,46 +13,55 @@ internal class YugenMangasEs(context: MangaLoaderContext) :
private val domainAlt = "yugenmangas.net"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domainAlt)
append("/query?query_string=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
append("&series_status=All&order=desc&orderBy=")
when (sortOrder) {
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
},
)
}
append("&order=desc")
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("created_at")
SortOrder.ALPHABETICAL -> append("title")
else -> append("latest")
}
append("&series_type=Comic&page=")
append(page)
append("&perPage=12&tags_ids=")
append("&series_type=Comic&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
// Just to make it fit [1,2,44] ect
if (!firstTag) {
firstTag = true
} else {
append(",")
}
append(tag.key)
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> {}
}
append("]".urlEncoded())
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/series/$slug"

@ -5,6 +5,7 @@ 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 java.lang.IllegalArgumentException
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@ -33,23 +34,24 @@ internal abstract class HeanCmsAlt(
protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)"
protected open val selectMangaTitle = "h5"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
// No search or tag
if (!query.isNullOrEmpty()) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
is MangaListFilter.Advanced -> {
}
null -> {}
}
if (page > 1) {
append("?page=")
append(page)
append(page.toString())
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -18,6 +18,78 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.POPULARITY)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = urlBuilder().apply {
addPathSegment("manga")
addPathSegment("page")
addPathSegment("$page/")
when (filter) {
is MangaListFilter.Search -> {
addQueryParameter("title", filter.query)
}
is MangaListFilter.Advanced -> {
addQueryParameter("title", "")
addQueryParameter(
"order",
when (filter.sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "popular"
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
else -> "latest"
},
)
filter.tags.forEach {
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
}
filter.states.oneOrThrowIfMany()?.let {
addEncodedQueryParameter(
"statusx",
when (it) {
MangaState.ONGOING -> "Publishing"
MangaState.FINISHED -> "Finished"
else -> ""
},
)
}
}
null -> addQueryParameter("order", "update")
}
}.build()
return webClient.httpGet(url).parseHtml()
.requireElementById("archives")
.selectFirstOrThrow("div.entries")
.select(".entry")
.map {
val titleTag = it.selectFirstOrThrow(".metadata > a")
val relativeUrl = titleTag.attrAsRelativeUrl("href")
Manga(
id = generateUid(relativeUrl),
title = titleTag.attr("title"),
altTitle = null,
url = relativeUrl,
publicUrl = relativeUrl.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive")
val chapterDateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", sourceLocale)
@ -58,56 +130,6 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = urlBuilder().apply {
addPathSegment("manga")
addPathSegment("page")
addPathSegment("$page/")
val order = when (sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "popular"
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
else -> throw IllegalArgumentException("Sort order not supported")
}
addQueryParameter("order", order)
addQueryParameter("title", query.orEmpty())
tags?.forEach {
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
}
}.build()
return webClient.httpGet(url).parseHtml()
.requireElementById("archives")
.selectFirstOrThrow("div.entries")
.select(".entry")
.map {
val titleTag = it.selectFirstOrThrow(".metadata > a")
val relativeUrl = titleTag.attrAsRelativeUrl("href")
Manga(
id = generateUid(relativeUrl),
title = titleTag.attr("title"),
altTitle = null,
url = relativeUrl,
publicUrl = relativeUrl.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
.requireElementById("reader")

@ -39,26 +39,37 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
SortOrder.POPULARITY,
)
override val isMultipleTagsSupported = false
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nicovideo.jp")
@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> {
val page = (offset / 20f).toIntUp().inc()
val domain = getDomain("seiga")
val url = when {
!query.isNullOrEmpty() -> return if (offset == 0) getSearchList(query, page) else emptyList()
tags.isNullOrEmpty() -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
tags.size == 1 -> "https://$domain/manga/list?category=${tags.first().key}&page=$page" +
"&sort=${getSortKey(sortOrder)}"
val url =
when (filter) {
is MangaListFilter.Search -> {
return if (offset == 0) getSearchList(filter.query, page) else emptyList()
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany().let {
"https://$domain/manga/list?category=${it?.key}&page=$page&sort=${getSortKey(filter.sortOrder)}"
}
tags.size > 1 -> throw IllegalArgumentException("This source supports only 1 category")
else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
} else {
"https://$domain/manga/list?page=$page&sort=${getSortKey(filter.sortOrder)}"
}
}
null -> "https://$domain/manga/list?page=$page"
}
val doc = webClient.httpGet(url).parseHtml()
val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found")
val items = comicList.select("div > .description > div > div")
@ -145,12 +156,12 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml()
val root = doc.body().selectOrThrow("#mg_category_list > ul > li")
val root = doc.body().selectOrThrow("#mg_category_list > ul > li").drop(1)
return root.mapToSet { li ->
val a = li.selectFirstOrThrow("a")
MangaTag(
title = a.text(),
key = a.attrAsRelativeUrlOrNull("href").orEmpty(),
key = a.attrAsRelativeUrl("href").substringAfter("category=").substringBefore("&"),
source = source,
)
}

@ -23,44 +23,75 @@ internal abstract class LikeMangaParser(
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/?act=search&f")
append("/?act=search")
when (filter) {
is MangaListFilter.Search -> {
append("&f")
append("[keyword]".urlEncoded())
append("=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&f")
append("[sortby]".urlEncoded())
append("=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("hot")
SortOrder.UPDATED -> append("lastest-chap")
SortOrder.NEWEST -> append("lastest-manga")
else -> append("lastest-chap")
}
if (page > 1) {
append("&pageNum=")
append(page)
}
if (!tags.isNullOrEmpty()) {
if (filter.tags.isNotEmpty()) {
append("&f")
append("[genres]".urlEncoded())
append("=")
append(tag?.key.orEmpty())
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
}
if (!query.isNullOrEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&f")
append("[keyword]".urlEncoded())
append("[status]".urlEncoded())
append("=")
append(query.urlEncoded())
append(
when (it) {
MangaState.ONGOING -> "in-process"
MangaState.FINISHED -> "complete"
MangaState.PAUSED -> "pause"
else -> "all"
},
)
}
}
null -> {
append("&f")
append("[sortby]".urlEncoded())
append("=lastest-chap")
}
}
if (page > 1) {
append("&pageNum=")
append(page)
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.card-body div.video").map { div ->

@ -33,6 +33,8 @@ internal abstract class MadaraParser(
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
protected open val tagPrefix = "manga-genre/"
protected open val datePattern = "MMMM d, yyyy"
protected open val stylePage = "?style=list"
@ -155,38 +157,60 @@ internal abstract class MadaraParser(
// can be changed to retrieve tags see getTags
protected open val listUrl = "manga/"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc = if (withoutAjax) {
val pages = page + 1
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
if (!tags.isNullOrEmpty()) {
when (filter) {
is MangaListFilter.Search -> {
if (pages > 1) {
append("/page/")
append(pages.toString())
}
append("/?s=")
append(filter.query.urlEncoded())
append("&post_type=wp-manga")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append(it.key)
if (pages > 1) {
append("/page/")
append(pages.toString())
}
append("?")
append("/?")
}
} else {
if (pages > 1) {
append("/page/")
append(pages)
append("/?s=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
append(pages.toString())
}
append("/?s=&post_type=wp-manga")
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("on-going")
MangaState.FINISHED -> append("end")
MangaState.ABANDONED -> append("canceled")
MangaState.PAUSED -> append("on-hold")
}
append("&post_type=wp-manga&")
/// &status[]= ( on-going - end - canceled - on-hold - upcoming )
}
append("&")
}
append("m_orderby=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
@ -194,14 +218,35 @@ internal abstract class MadaraParser(
SortOrder.RATING -> append("rating")
}
}
null -> {
append("?s&post_type=wp-manga&m_orderby=latest")
}
}
}
webClient.httpGet(url).parseHtml()
} else {
val payload = if (sortOrder == SortOrder.RATING) {
val payload = if (filter?.sortOrder == SortOrder.RATING) {
createRequestTemplate(ratingRequest)
} else {
createRequestTemplate(defaultRequest)
}
when (sortOrder) {
payload["page"] = page.toString()
when (filter) {
is MangaListFilter.Search -> {
payload["vars[s]"] = filter.query.urlEncoded()
}
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
payload["vars[wp-manga-genre]"] = it.key
}
when (filter.sortOrder) {
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"
SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update"
SortOrder.NEWEST -> payload["vars[meta_key]"] = ""
@ -212,10 +257,23 @@ internal abstract class MadaraParser(
SortOrder.RATING -> {}
}
payload["page"] = page.toString()
payload["vars[wp-manga-genre]"] = tag?.key.orEmpty()
payload["vars[s]"] = query?.urlEncoded().orEmpty()
/// payload["vars[meta_query][0][0][value][]"] = ( on-going - end - canceled - on-hold - upcoming )
filter.states.forEach {
payload["vars[meta_query][0][0][value][]"] =
when (it) {
MangaState.ONGOING -> "on-going"
MangaState.FINISHED -> "end"
MangaState.ABANDONED -> "canceled"
MangaState.PAUSED -> "on-hold"
}
}
}
null -> {
payload["vars[meta_key]"] = "_latest_update"
}
}
webClient.httpPost(
"https://$domain/wp-admin/admin-ajax.php",
payload,

@ -19,39 +19,49 @@ internal class Manga18Fx(context: MangaLoaderContext) :
override val selectDate = "span.chapter-time"
override val selectChapter = "li.a-h"
override val selectBodyPage = "div.read-content"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(pages)
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
if (pages > 1) {
if (page > 1) {
append("/")
append(pages)
append(page.toString())
}
} else {
if (page > 1) {
append("/page/")
append(page)
}
}
}
else -> {
if (pages > 1) {
null -> {
if (page > 1) {
append("/page/")
append(pages)
append(page)
}
}
}

@ -26,43 +26,37 @@ internal class Manhwa18Cc(context: MangaLoaderContext) :
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
if (page > 1) {
append(page.toString())
}
append("?")
} else {
append("/$listUrl")
}
else -> {
append("/$listUrl")
if (page > 1) {
append(page)
}
append("?")
}
append(page.toString())
}
append("m_orderby=")
when (sortOrder) {
append("?orderby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("trending")
SortOrder.UPDATED -> append("latest")
SortOrder.ALPHABETICAL -> append("alphabet")
@ -70,6 +64,12 @@ internal class Manhwa18Cc(context: MangaLoaderContext) :
else -> append("latest")
}
}
null -> {
append("?s&post_type=wp-manga&m_orderby=latest")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.manga-lists div.manga-item").map { div ->
val href = div.selectFirst("a")?.attrAsRelativeUrlOrNull("href") ?: div.parseFailed("Link not found")

@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
@ -20,48 +21,48 @@ internal class Hentai4Free(context: MangaLoaderContext) :
override val listUrl = ""
override val withoutAjax = true
override val datePattern = "MMMM dd, yyyy"
override val selectGenre = "div.tags-content a"
override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(pages.toString())
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append("&post_type=wp-manga&")
append(filter.query.urlEncoded())
append("&post_type=wp-manga")
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("/")
if (pages > 1) {
if (page > 1) {
append("page/")
append(pages.toString())
}
append(page.toString())
}
else -> {
if (pages > 1) {
} else {
if (page > 1) {
append("/page/")
append(pages.toString())
append(page.toString())
}
}
append("/?m_orderby=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
@ -69,9 +70,17 @@ internal class Hentai4Free(context: MangaLoaderContext) :
SortOrder.RATING -> append("rating")
}
}
}
null -> {
if (page > 1) {
append("/page/")
append(page.toString())
}
append("/?m_orderby=latest")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.row.c-tabs-item__content").ifEmpty {

@ -23,46 +23,48 @@ internal class InstaManhwa(context: MangaLoaderContext) :
SortOrder.NEWEST,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> get() = emptySet()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/?search=")
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append("&post_type=wp-manga&post_type=wp-manga")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/genre/")
append(tag?.key.orEmpty())
append("?page=")
append(pages.toString())
}
else -> {
when (sortOrder) {
append(page.toString())
} else {
when (filter.sortOrder) {
SortOrder.UPDATED -> append("/latest")
SortOrder.NEWEST -> append("/new")
SortOrder.ALPHABETICAL -> append("/alphabet")
else -> append("/latest")
}
append("?page=")
append(pages.toString())
append(page.toString())
}
}
null -> {
append("/latest?page=")
append(page.toString())
}
}
}

@ -22,50 +22,58 @@ internal class IsekaiScan(context: MangaLoaderContext) :
SortOrder.UPDATED,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> get() = emptySet()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/?search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append(page.toString())
append("&post_type=wp-manga")
}
!tags.isNullOrEmpty() -> {
append("/mangas/")
append(tag?.key.orEmpty())
append("?orderby=2&page=")
append(pages.toString())
}
else -> {
is MangaListFilter.Advanced -> {
if (sortOrder == SortOrder.POPULARITY) {
append("/popular-manga")
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("?orderby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("2")
SortOrder.UPDATED -> append("3")
else -> append("3")
}
if (sortOrder == SortOrder.UPDATED) {
append("/latest-manga")
append("&page=")
append(page.toString())
} else {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("/popular-manga")
SortOrder.UPDATED -> append("/latest-manga")
else -> append("/latest-manga")
}
append("?page=")
append(pages.toString())
}
append(page.toString())
}
}
null -> {
append("/latest-manga?page=")
append(page.toString())
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.row.c-tabs-item__content").ifEmpty {
@ -91,8 +99,8 @@ internal class IsekaiScan(context: MangaLoaderContext) :
author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(),
state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText()?.trim()
?.lowercase()) {
"Ongoing" -> MangaState.ONGOING
"Completed " -> MangaState.FINISHED
"ongoing" -> MangaState.ONGOING
"completed " -> MangaState.FINISHED
else -> null
},
source = source,
@ -133,8 +141,8 @@ internal class IsekaiScan(context: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val urlarray = doc.select("p#arraydata").text().split(",").toTypedArray()
return urlarray.map { url ->
val urlArray = doc.select("p#arraydata").text().split(",").toTypedArray()
return urlArray.map { url ->
MangaPage(
id = generateUid(url),
url = url,

@ -14,45 +14,60 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
override val withoutAjax = true
override val listUrl = "mangax/"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
append("/page/")
append(pages.toString())
append(page.toString())
}
append("/?s=")
append(query.urlEncoded())
append("&post_type=wp-manga&")
append(filter.query.urlEncoded())
append("&post_type=wp-manga")
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
if (page > 1) {
append("/page/")
append(pages.toString())
append("?")
append(page.toString())
}
else -> {
append("/$listUrl")
append("/?")
} else {
if (page > 1) {
append("/page/")
append(pages.toString())
append("?")
append(page.toString())
}
append("/?s=&post_type=wp-manga")
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("on-going")
MangaState.FINISHED -> append("end")
MangaState.ABANDONED -> append("canceled")
MangaState.PAUSED -> append("on-hold")
}
}
append("&")
}
append("m_orderby=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
@ -60,6 +75,12 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
SortOrder.RATING -> append("rating")
}
}
null -> {
append("/?s&post_type=wp-manga&m_orderby=latest")
}
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -19,45 +19,44 @@ internal class MangaDass(context: MangaLoaderContext) :
override val selectChapter = "li.a-h"
override val selectDesc = "div.ss-manga"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> get() = emptySet()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/?s=")
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("/")
append(pages.toString())
append(page.toString())
append("?")
}
else -> {
} else {
append("/$listUrl")
append("/")
append(pages.toString())
append(page.toString())
append("?")
}
}
append("orderby=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
@ -65,9 +64,16 @@ internal class MangaDass(context: MangaLoaderContext) :
SortOrder.RATING -> append("rating")
}
}
val doc = webClient.httpGet(url).parseHtml()
null -> {
append("/$listUrl")
append("/")
append(page.toString())
append("?orderby=latest")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.row.c-tabs-item__content").ifEmpty {
doc.select("div.page-item-detail")
}.map { div ->

@ -18,53 +18,53 @@ internal class MangaDna(context: MangaLoaderContext) :
override val withoutAjax = true
override val selectDesc = "div.dsct"
override val selectChapter = "li.a-h"
override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/page/")
append(pages.toString())
append("/?s=")
append(query.urlEncoded())
append("&post_type=wp-manga&")
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("/")
append(page.toString())
} else {
append("/$listUrl")
append("/page/")
append(pages.toString())
append("?")
append(page.toString())
}
else -> {
append("?orderby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("trending")
SortOrder.UPDATED -> append("latest")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
else -> append("latest")
}
}
null -> {
append("/$listUrl")
append("/page/")
append(pages.toString())
append("?")
append(page.toString())
append("?orderby=latest")
}
}
append("m_orderby=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -21,46 +21,54 @@ internal class MangaPure(context: MangaLoaderContext) :
SortOrder.UPDATED,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> get() = emptySet()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/?search=")
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append("/search?s=")
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append(page.toString())
append("&post_type=wp-manga")
}
!tags.isNullOrEmpty() -> {
append("/mangas/")
append(tag?.key.orEmpty())
append("?orderby=2&page=")
append(pages.toString())
}
else -> {
is MangaListFilter.Advanced -> {
if (sortOrder == SortOrder.POPULARITY) {
append("/popular-manga")
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("?orderby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("2")
SortOrder.UPDATED -> append("3")
else -> append("3")
}
if (sortOrder == SortOrder.UPDATED) {
append("/latest-manga")
append("&page=")
append(page.toString())
} else {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("/popular-manga")
SortOrder.UPDATED -> append("/latest-manga")
else -> append("/latest-manga")
}
append("?page=")
append(pages.toString())
append(page.toString())
}
}
null -> {
append("/latest-manga?page=")
append(page.toString())
}
}
}

@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
@MangaSourceParser("MANHWAZ", "ManhwaZ", "en")
internal class Manhwaz(context: MangaLoaderContext) :
@ -15,50 +16,64 @@ internal class Manhwaz(context: MangaLoaderContext) :
override val withoutAjax = true
override val selectTestAsync = "div.list-chapter"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> get() = emptySet()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.RATING,
)
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/search?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("?page=")
append(pages.toString())
append(page.toString())
append("&")
}
else -> {
} else {
append("/$listUrl")
append("?page=")
append(pages.toString())
append(page.toString())
append("&")
}
}
append("m_orderby=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.NEWEST -> append("new")
SortOrder.RATING -> append("rating")
else -> append("latest")
}
}
null -> {
append("/$listUrl")
append("?page=")
append(page.toString())
append("&m_orderby=latest")
}
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -3,8 +3,9 @@ package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
@ -13,48 +14,46 @@ import java.util.*
@MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es")
internal class DragonTranslationParser(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.DRAGONTRANSLATION, "dragontranslation.net", 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val selectPage = "div#chapter_imgs img"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/mangas?buscar=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append(page.toString())
}
!tags.isNullOrEmpty() -> {
append("/mangas?tag=")
is MangaListFilter.Advanced -> {
append("/mangas?page=")
append(page.toString())
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("&tag=")
append(tag?.key.orEmpty())
append("&page=")
append(pages.toString())
}
}
else -> {
append("/mangas")
append("?page=")
append(pages.toString())
null -> {
append("/mangas?page=")
append(page.toString())
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.video-bg div.col-6 ").map { div ->

@ -4,93 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("MONARCAMANGA", "MonarcaManga", "es")
internal class MonarcaManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MONARCAMANGA, "monarcamanga.com") {
override val tagPrefix = "manga-generos/"
override val withoutAjax = true
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)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/page/")
append(pages.toString())
append("/?s=")
append(query.urlEncoded())
append("&post_type=wp-manga&")
}
!tags.isNullOrEmpty() -> {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("/page/")
append(pages.toString())
append("?")
}
else -> {
append("/$listUrl")
append("/page/")
append(pages.toString())
append("?")
}
}
append("m_orderby=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.row.c-tabs-item__content").ifEmpty {
doc.select("div.page-item-detail")
}.map { div ->
val href = div.selectFirst("a")?.attrAsRelativeUrlOrNull("href") ?: div.parseFailed("Link not found")
val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.src().orEmpty(),
title = (div?.selectFirst("h3") ?: div?.selectFirst("h4"))?.text().orEmpty(),
altTitle = null,
rating = div.selectFirst("span.total_votes")?.ownText()?.toFloatOrNull()?.div(5f) ?: -1f,
tags = summary?.selectFirst(".mg_genres")?.select("a")?.mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text().ifEmpty { return@mapNotNullToSet null }.toTitleCase(),
source = source,
)
}.orEmpty(),
author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(),
state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText()
?.lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
else -> null
},
source = source,
isNsfw = isNsfwSource,
)
}
}
}

@ -16,44 +16,49 @@ internal class TmoManga(context: MangaLoaderContext) :
override val listUrl = "biblioteca/"
override val selectGenre = "div.summary-content a.tags_manga"
override val withoutAjax = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val availableStates: Set<MangaState> get() = emptySet()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/$listUrl")
append("?search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
}
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
if (page > 1) {
append("?page=")
append(page)
}
} else {
append("/$listUrl")
if (page > 1) {
append("?page=")
append(page)
}
}
}
else -> {
null -> {
append("/$listUrl")
if (page > 1) {
append("?page=")
@ -62,6 +67,7 @@ internal class TmoManga(context: MangaLoaderContext) :
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.page-item-detail").map { div ->

@ -17,56 +17,47 @@ internal class ManhwaHub(context: MangaLoaderContext) :
override val withoutAjax = true
override val listUrl = "genre/manhwa"
override val selectTestAsync = "ul.box-list-chapter"
override val availableStates: Set<MangaState> get() = emptySet()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/search?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(page)
append("&")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("?page=")
append(page)
append("&")
append(page.toString())
append("&m_orderby=latest")
} else {
append("/?page=")
append(page.toString())
}
else -> {
append("/$listUrl")
append("?page=")
append(page)
append("&")
}
null -> {
append("/?page=")
append(page.toString())
}
append("m_orderby=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -7,6 +7,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
@MangaSourceParser("SAYTRUYENHAY", "Saytruyenhay", "vi")
internal class Saytruyenhay(context: MangaLoaderContext) :
@ -15,50 +16,57 @@ internal class Saytruyenhay(context: MangaLoaderContext) :
override val tagPrefix = "genre/"
override val withoutAjax = true
override val listUrl = "public/genre/manga/"
override val availableStates: Set<MangaState> get() = emptySet()
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.RATING, SortOrder.NEWEST)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/public/search?s=")
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append("/search?s=")
append(filter.query.urlEncoded())
append("&page=")
append(pages.toString())
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
} else {
append("/$listUrl")
}
append("?page=")
append(pages.toString())
append(page.toString())
append("&m_orderby=")
when (filter.sortOrder) {
SortOrder.UPDATED -> append("latest")
SortOrder.RATING -> append("rating")
SortOrder.POPULARITY -> append("views")
SortOrder.NEWEST -> append("new")
else -> append("latest")
}
}
else -> {
null -> {
append("/$listUrl")
append("?page=")
append(pages.toString())
}
append(page.toString())
}
append("&m_orderby=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -29,6 +29,8 @@ internal abstract class MadthemeParser(
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
protected open val listUrl = "search/"
protected open val datePattern = "MMM dd, yyyy"
@ -52,35 +54,52 @@ internal abstract class MadthemeParser(
"COMPLETED",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/$listUrl?sort=")
when (sortOrder) {
append('/')
append(listUrl)
when (filter) {
is MangaListFilter.Search -> {
append("?sort=updated_at&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("?sort=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("updated_at")
SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty.
SortOrder.NEWEST -> append("created_at")
SortOrder.RATING -> append("rating")
}
if (!query.isNullOrEmpty()) {
append("&q=")
append(query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
if (filter.tags.isNotEmpty()) {
filter.tags.forEach {
append("&")
append("genre[]".urlEncoded())
append("=")
append(tag.key)
append(it.key)
}
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "all"
},
)
}
}
null -> append("?sort=updated_at")
}
append("&page=")
@ -117,7 +136,7 @@ internal abstract class MadthemeParser(
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select("div.genres label.checkbox").mapNotNullToSet { checkbox ->
return doc.select("div.genres .checkbox").mapNotNullToSet { checkbox ->
val key = checkbox.selectFirstOrThrow("input").attr("value") ?: return@mapNotNullToSet null
val name = checkbox.selectFirstOrThrow("span.radio__label").text()
MangaTag(

@ -7,44 +7,58 @@ import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.Locale
@MangaSourceParser("MANHUASCAN", "ManhuaScan", "")
@MangaSourceParser("MANHUASCAN", "ManhuaScan.io", "")
internal class ManhuaScan(context: MangaLoaderContext) :
MadthemeParser(context, MangaSource.MANHUASCAN, "manhuascan.io") {
override val sourceLocale: Locale = Locale.ENGLISH
override val listUrl = "search"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append('/')
append(listUrl)
when (filter) {
is MangaListFilter.Search -> {
append("?sort=updated_at&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("?sort=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("updated_at")
SortOrder.ALPHABETICAL -> append("name")
SortOrder.NEWEST -> append("created_at")
SortOrder.RATING -> append("rating")
}
if (!query.isNullOrEmpty()) {
append("&q=")
append(query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
if (filter.tags.isNotEmpty()) {
filter.tags.forEach {
append("&")
append("include[]".urlEncoded())
append("=")
append(tag.key)
append(it.key)
}
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "all"
},
)
}
}
null -> append("?sort=updated_at")
}
append("&page=")

@ -34,6 +34,5 @@ internal class MangaBuddy(context: MangaLoaderContext) :
)
}
return pages
}
}

@ -9,6 +9,5 @@ import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser
@MangaSourceParser("TOONITUBE", "TooniTube", "en", ContentType.HENTAI)
internal class TooniTube(context: MangaLoaderContext) :
MadthemeParser(context, MangaSource.TOONITUBE, "toonitube.com") {
override val selectDesc = "div.summary div.section-body p.content"
}

@ -9,6 +9,5 @@ import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser
@MangaSourceParser("TOONILY_ME", "Toonily.Me", "en", ContentType.HENTAI)
internal class ToonilyMe(context: MangaLoaderContext) :
MadthemeParser(context, MangaSource.TOONILY_ME, "toonily.me") {
override val selectDesc = "div.summary div.section-body p.content"
}

@ -26,6 +26,8 @@ internal abstract class Manga18Parser(
SortOrder.ALPHABETICAL,
)
override val isMultipleTagsSupported = false
protected open val listUrl = "list-manga/"
protected open val tagUrl = "manga-list/"
protected open val datePattern = "dd-MM-yyyy"
@ -47,49 +49,53 @@ internal abstract class Manga18Parser(
"Completed",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
append("/$listUrl")
append('/')
when (filter) {
is MangaListFilter.Search -> {
append(listUrl)
append(page.toString())
append("?search=")
append(query.urlEncoded())
append("&")
append(filter.query.urlEncoded())
append("&order_by=latest")
}
!tags.isNullOrEmpty() -> {
append("/$tagUrl")
append(tag?.key.orEmpty())
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(tagUrl)
append(it.key)
append("/")
append(page.toString())
append("?")
}
} else {
append(listUrl)
}
else -> {
append("/$listUrl")
append(page.toString())
append("?")
}
}
append("order_by=")
when (sortOrder) {
append("?order_by=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("lastest")
SortOrder.ALPHABETICAL -> append("name")
else -> append("latest")
}
}
val doc = webClient.httpGet(url).parseHtml()
null -> {
append(listUrl)
append(page.toString())
append("?order_by=latest")
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.story_item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.manga18.en
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
@ -10,51 +11,7 @@ import org.koitharu.kotatsu.parsers.util.*
internal class Hentai3zCc(context: MangaLoaderContext) :
Manga18Parser(context, MangaSource.HENTAI3ZCC, "hentai3z.cc") {
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)
val pages = page + 1
when {
!query.isNullOrEmpty() -> {
append("/$listUrl")
append(pages.toString())
append("?search=")
append(query.urlEncoded())
append("&")
}
!tags.isNullOrEmpty() -> {
append("/$tagUrl")
append(tag?.key.orEmpty())
append("/")
append(pages.toString())
append("?")
}
else -> {
append("/$listUrl")
append(pages.toString())
append("?")
}
}
append("order_by=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("lastest")
SortOrder.ALPHABETICAL -> append("name")
else -> append("latest")
}
}
val doc = webClient.httpGet(url).parseHtml()
override fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.story_item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(

@ -9,7 +9,6 @@ import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser
@MangaSourceParser("TUMANHWAS", "Tumanhwas", "es", ContentType.HENTAI)
internal class Tumanhwas(context: MangaLoaderContext) :
Manga18Parser(context, MangaSource.TUMANHWAS, "tumanhwas.club") {
override val selectTag = "div.item:contains(Géneros) div.info_value a"
override val selectAlt = "div.item:contains(Títulos alternativos) div.info_value"
}

@ -21,9 +21,12 @@ internal abstract class MangaboxParser(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL,
)
protected open val listUrl = "/genre-all"
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
protected open val listUrl = "/advanced_search"
protected open val searchUrl = "/search/story/"
protected open val datePattern = "MMM dd,yy"
@ -36,50 +39,64 @@ internal abstract class MangaboxParser(
@JvmField
protected val ongoing: Set<String> = setOf(
"Ongoing",
"ongoing",
)
@JvmField
protected val finished: Set<String> = setOf(
"Completed",
"completed",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?s=all")
when (filter) {
if (!query.isNullOrEmpty()) {
append(searchUrl)
append(query.urlEncoded())
append("?page=")
append(page.toString())
is MangaListFilter.Search -> {
append("&keyw=")
append(filter.query.urlEncoded())
}
} else if (!tags.isNullOrEmpty()) {
append("/")
append(tag?.key.orEmpty())
append("/")
append(page.toString())
} else {
append("$listUrl/")
if (page > 1) {
append(page.toString())
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("&g_i=")
filter.tags.forEach {
append("_")
append(it.key)
append("_")
}
}
filter.states.oneOrThrowIfMany()?.let {
append("&sts=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> ""
},
)
}
when (sortOrder) {
SortOrder.POPULARITY -> append("?type=topview")
append("&orby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("?type=newest")
SortOrder.NEWEST -> append("newest")
SortOrder.ALPHABETICAL -> append("az")
else -> append("")
}
}
null -> {}
}
append("&page=")
append(page.toString())
}
val doc = webClient.httpGet(url).parseHtml()
@ -109,7 +126,8 @@ internal abstract class MangaboxParser(
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectTagMap).mapNotNullToSet { a ->
val tags = doc.select(selectTagMap).drop(1) // remove all tags
return tags.mapNotNullToSet { a ->
val key = a.attr("href").removeSuffix('/').substringAfterLast('/')
val name = a.attr("title").replace(" Manga", "")
MangaTag(
@ -129,25 +147,18 @@ internal abstract class MangaboxParser(
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.select(selectState).text()
val state = stateDiv.let {
when (it) {
when (it.lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
else -> null
}
}
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "")
val aut = doc.body().select(selectAut).eachText().joinToString()
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag(

@ -9,13 +9,7 @@ import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
@MangaSourceParser("HMANGABAT", "MangaBat", "en")
internal class Mangabat(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.HMANGABAT) {
override val configKeyDomain = ConfigKey.Domain("h.mangabat.com", "readmangabat.com")
override val otherDomain = "readmangabat.com"
override val searchUrl = "/search/manga/"
override val listUrl = "/manga-list-all"
override val selectTagMap = "div.panel-category p.pn-category-row:not(.pn-category-row-border) a"
}

@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
@ -13,73 +14,87 @@ import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
@MangaSourceParser("MANGAIRO", "MangaIro", "en")
internal class Mangairo(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.MANGAIRO) {
override val configKeyDomain = ConfigKey.Domain("w.mangairo.com", "chap.mangairo.com")
override val otherDomain = "chap.mangairo.com"
override val datePattern = "MMM-dd-yy"
override val listUrl = "/manga-list"
override val searchUrl = "/list/search/"
override val selectDesc = "div#story_discription p"
override val selectState = "ul.story_info_right li:contains(Status) a"
override val selectAlt = "ul.story_info_right li:contains(Alter) h2"
override val selectAut = "ul.story_info_right li:contains(Author) a"
override val selectTag = "ul.story_info_right li:contains(Genres) a"
override val selectChapter = "div.chapter_list li"
override val selectDate = "p"
override val selectPage = "div.panel-read-story img"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
)
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
if (!query.isNullOrEmpty()) {
is MangaListFilter.Search -> {
append(searchUrl)
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("?page=")
append(page.toString())
} else {
append("$listUrl/")
}
is MangaListFilter.Advanced -> {
append(listUrl)
append("/type-")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("newest")
else -> append("latest")
}
if (!tags.isNullOrEmpty()) {
append("/ctg-")
append(tag?.key.orEmpty())
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
append("/ctg-all")
append("all")
}
append("/state-all/page-")
append(page.toString())
append("/state-")
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "all"
},
)
}
} else {
append("all")
}
val doc = webClient.httpGet(url).parseHtml()
append("/page-")
}
null -> {
append(listUrl)
append("/type-latest/ctg-all/state-all/page-")
}
}
append(page.toString())
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.story-item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@ -115,13 +130,9 @@ internal class Mangairo(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.select(selectState).text()
val state = stateDiv.let {
when (it) {
in ongoing -> MangaState.ONGOING
@ -131,9 +142,7 @@ internal class Mangairo(context: MangaLoaderContext) :
}
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "")
val aut = doc.body().select(selectAut).eachText().joinToString()
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag(
@ -151,6 +160,4 @@ internal class Mangairo(context: MangaLoaderContext) :
isNsfw = manga.isNsfw,
)
}
}

@ -8,54 +8,69 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.EnumSet
@MangaSourceParser("MANGAKAKALOT", "Mangakakalot", "en")
@MangaSourceParser("MANGAKAKALOT", "Mangakakalot.com", "en")
internal class Mangakakalot(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.MANGAKAKALOT) {
override val configKeyDomain = ConfigKey.Domain("mangakakalot.com", "chapmanganato.com")
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
)
override val isMultipleTagsSupported = false
override val otherDomain = "chapmanganato.com"
override val listUrl = "/manga_list"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
if (!query.isNullOrEmpty()) {
is MangaListFilter.Search -> {
append(searchUrl)
append(query.replace(" ", "_").urlEncoded())
append(filter.query.urlEncoded())
append("?page=")
append(page.toString())
}
} else {
append("$listUrl/")
when (sortOrder) {
SortOrder.POPULARITY -> append("?type=topview")
SortOrder.UPDATED -> append("?type=latest")
SortOrder.NEWEST -> append("?type=newest")
else -> append("?type=latest")
}
if (!tags.isNullOrEmpty()) {
is MangaListFilter.Advanced -> {
append(listUrl)
append("?type=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("newest")
else -> append("latest")
}
if (filter.tags.isNotEmpty()) {
append("&category=")
append(tag?.key.orEmpty())
} else {
append("&category=all")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
}
append("&state=all&page=")
append(page)
filter.states.oneOrThrowIfMany()?.let {
append("&state=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "all"
},
)
}
append("&page=")
}
null -> {
append(listUrl)
append("?type=latest&page=")
}
}
append(page.toString())
}
val doc = webClient.httpGet(url).parseHtml()
@ -81,8 +96,21 @@ internal class Mangakakalot(context: MangaLoaderContext) :
}
}
override suspend fun getChapters(doc: Document): List<MangaChapter> {
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
val tags = doc.select("ul.tag li a").drop(1)
return tags.mapNotNullToSet { a ->
val key = a.attr("href").substringAfterLast("category=").substringBefore("&")
val name = a.attr("title").replace(" Manga", "")
MangaTag(
key = key,
title = name,
source = source,
)
}
}
override suspend fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")

@ -1,11 +1,14 @@
package org.koitharu.kotatsu.parsers.site.mangabox.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.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
@MangaSourceParser("MANGAKAKALOTTV", "Mangakakalot.tv", "en")
internal class MangakakalotTv(context: MangaLoaderContext) :
@ -14,41 +17,62 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("ww6.mangakakalot.tv")
override val searchUrl = "/search/"
override val listUrl = "/manga_list"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
)
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!query.isNullOrEmpty()) {
when (filter) {
is MangaListFilter.Search -> {
append(searchUrl)
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("?page=")
append(page.toString())
} else {
append("$listUrl/")
}
is MangaListFilter.Advanced -> {
append(listUrl)
append("?type=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("newest")
else -> append("latest")
}
if (!tags.isNullOrEmpty()) {
if (filter.tags.isNotEmpty()) {
append("&category=")
append(tag?.key.orEmpty())
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
}
if (page > 1) {
filter.states.oneOrThrowIfMany()?.let {
append("&state=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
else -> "all"
},
)
}
append("&page=")
append(page.toString())
}
null -> {
append(listUrl)
append("?type=latest&page=")
}
}
append(page.toString())
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.list-truyen-item-wrap").ifEmpty {
doc.select("div.story_item")
@ -71,6 +95,38 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.select(selectState).text().replace("Status : ", "")
val state = stateDiv.let {
when (it.lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
else -> null
}
}
val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "")
val aut = doc.body().select(selectAut).eachText().joinToString()
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("category=").substringBefore("&"),
title = a.text().toTitleCase(),
source = source,
)
},
description = desc,
altTitle = alt,
author = aut,
state = state,
chapters = chaptersDeferred.await(),
isNsfw = manga.isNsfw,
)
}
override val selectTagMap = "ul.tag li a"
override suspend fun getAvailableTags(): Set<MangaTag> {

@ -9,9 +9,6 @@ import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
@MangaSourceParser("MANGANATO", "Manganato", "en")
internal class Manganato(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.MANGANATO) {
override val configKeyDomain = ConfigKey.Domain("chapmanganato.com", "manganato.com")
override val otherDomain = "chapmanganato.com"
}

@ -32,60 +32,72 @@ internal abstract class MangaReaderParser(
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST)
override val availableStates: Set<MangaState>
get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
protected open val listUrl = "/manga"
protected open val datePattern = "MMMM d, yyyy"
protected open val isNetShieldProtected = false
private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex()
protected open var lastSearchPage = 1
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page)
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
is MangaListFilter.Advanced -> {
append(listUrl)
val sortQuery = when (sortOrder) {
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""
}
},
)
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?order=")
append(sortQuery)
if (filter.tags.isEmpty()) ""
else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
append(tagQuery)
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.PAUSED -> append("hiatus")
else -> append("")
}
}
}
append("&page=")
append(page)
append(page.toString())
}
null -> {
append(listUrl)
append("/?order=update&page=")
append(page.toString())
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -17,52 +17,58 @@ internal class SwaTeam(context: MangaLoaderContext) :
override val selectMangaListImg = "img"
override val isNetShieldProtected = true
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/?s=")
append(query.urlEncoded())
append("&page=")
append(page)
append(filter.query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
is MangaListFilter.Advanced -> {
append(listUrl)
val sortQuery = when (sortOrder) {
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "a-z"
SortOrder.NEWEST -> "added"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""
}
},
)
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?order=")
append(sortQuery)
if (filter.tags.isEmpty()) ""
else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
append(tagQuery)
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.PAUSED -> append("hiatus")
else -> append("")
}
}
}
}
null -> {
append(listUrl)
append("/?order=update")
}
}
append("&page=")
append(page)
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())

@ -17,60 +17,52 @@ internal class ManhwaFreak(context: MangaLoaderContext) :
override val selectMangaList = ".listupd .lastest-serie"
override val selectMangaListImg = "img"
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page)
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
if (!tags.isNullOrEmpty()) {
is MangaListFilter.Advanced -> {
if (page > 1) {
return emptyList()
}
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/genres/?genre=")
append(tag?.key.orEmpty())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
if (page > 1) {
return emptyList()
append(it.key)
}
val sortQuery = when (sortOrder) {
} else {
append(listUrl)
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.NEWEST -> "new"
SortOrder.POPULARITY -> "views"
SortOrder.UPDATED -> ""
else -> ""
},
)
}
val url = buildString {
append("https://")
append(domain)
}
null -> {
append(listUrl)
append("/?order=")
append(sortQuery)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -3,8 +3,9 @@ package org.koitharu.kotatsu.parsers.site.mangareader.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain
@ -19,31 +20,40 @@ internal class RizzComic(context: MangaLoaderContext) :
override val datePattern = "dd MMM yyyy"
override val listUrl = "/series"
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.ALPHABETICAL)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
// TODO Query created in json
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
if (!query.isNullOrEmpty()) {
throw IllegalArgumentException("Search is not supported by this source")
}
val url = if (!tags.isNullOrEmpty()) {
buildString {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("/genre/")
append(tag?.key.orEmpty())
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
"https://$domain$listUrl"
append(listUrl)
}
}
null -> {
append(listUrl)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -3,11 +3,13 @@ package org.koitharu.kotatsu.parsers.site.mangareader.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.util.*
@ -19,53 +21,34 @@ internal class Zahard(context: MangaLoaderContext) :
override val listUrl = "/library"
override val selectChapter = "#chapterlist > ul > a"
override val selectPage = "div#chapter_imgs img"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST)
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.NEWEST)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?search=")
append(query.urlEncoded())
append("&page=")
append(page)
}
append("?page=")
append(page.toString())
when (filter) {
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst("a[rel=next]")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
is MangaListFilter.Search -> {
append("&search=")
append(filter.query.urlEncoded())
}
val tagKey = "tag".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?page=")
append(page)
append(tagQuery)
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("tag=")
append(it.key)
}
}
null -> {}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
}

@ -12,41 +12,41 @@ import java.util.ArrayList
import java.util.Calendar
import java.util.EnumSet
@MangaSourceParser("TU_MANHWAS", "TuManhwas", "es")
@MangaSourceParser("TU_MANHWAS", "TuManhwas.com", "es")
internal class TuManhwas(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.TU_MANHWAS, "tumanhwas.com", 20, 20) {
override val listUrl = "/biblioteca"
override val selectPage = "div#readerarea img"
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.NEWEST)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST)
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?page=")
append(page)
if (!tags.isNullOrEmpty()) {
append("&genero=")
append(tag?.key.orEmpty())
}
if (!query.isNullOrEmpty()) {
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&genero=")
append(it.key)
}
}
null -> {}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -18,61 +18,52 @@ internal class ManhwaFreakFr(context: MangaLoaderContext) :
override val selectMangaList = ".listupd .lastest-serie"
override val selectMangaListImg = "img"
override val sourceLocale: Locale = Locale.ENGLISH
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page)
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
if (!tags.isNullOrEmpty()) {
is MangaListFilter.Advanced -> {
if (page > 1) {
return emptyList()
}
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/genres/?genre=")
append(tag?.key.orEmpty())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
if (page > 1) {
return emptyList()
append(it.key)
}
val sortQuery = when (sortOrder) {
} else {
append(listUrl)
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.NEWEST -> "new"
SortOrder.POPULARITY -> "views"
SortOrder.UPDATED -> ""
else -> ""
},
)
}
val url = buildString {
append("https://")
append(domain)
}
null -> {
append(listUrl)
append("/?order=")
append(sortQuery)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -3,13 +3,16 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.util.EnumSet
import java.util.Locale
@MangaSourceParser("COSMIC_SCANS", "CosmicScans.id", "id")
@ -17,54 +20,50 @@ internal class CosmicScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.COSMIC_SCANS, "cosmicscans.id", pageSize = 30, searchPageSize = 30) {
override val sourceLocale: Locale = Locale.ENGLISH
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override val listUrl = "/semua-komik"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page)
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/genres/")
append(it.key)
append("/page/")
append(page.toString())
append('/')
}
} else {
if (page > 1) {
return emptyList()
}
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""
append(listUrl)
}
}
null -> {
if (page > 1) {
return emptyList()
}
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?order=")
append(sortQuery)
append(tagQuery)
append("&page=")
append(page)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -3,11 +3,13 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded
@ -19,50 +21,61 @@ internal class KomikSan(context: MangaLoaderContext) :
override val listUrl = "/list"
override val datePattern = "MMM d, yyyy"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/search?search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(page)
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
append(page.toString())
}
val sortQuery = when (sortOrder) {
is MangaListFilter.Advanced -> {
append(listUrl)
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""
}
},
)
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?order=")
append(sortQuery)
if (filter.tags.isEmpty()) ""
else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
append(tagQuery)
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.PAUSED -> append("hiatus")
else -> append("")
}
}
}
append("&page=")
append(page)
append(page.toString())
}
null -> {
append(listUrl)
append("/?order=update&page=")
append(page.toString())
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -13,55 +13,70 @@ import java.util.*
@MangaSourceParser("KOMIKCAST", "KomikCast", "id")
internal class Komikcast(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.KOMIKCAST, "komikcast.ch", pageSize = 60, searchPageSize = 28) {
MangaReaderParser(context, MangaSource.KOMIKCAST, "komikcast.lol", pageSize = 60, searchPageSize = 28) {
override val listUrl = "/daftar-komik"
override val datePattern = "MMM d, yyyy"
override val sourceLocale: Locale = Locale.ENGLISH
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page)
append(page.toString())
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append(listUrl)
append("/page/")
append(page.toString())
append("/?orderby=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> append("titleasc")
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("update")
else -> append("update")
},
)
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
if (filter.tags.isEmpty()) ""
else filter.tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
append(tagQuery)
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
when (it) {
MangaState.ONGOING -> append("Ongoing")
MangaState.FINISHED -> append("Completed")
else -> append("")
}
}
}
}
null -> {
append(listUrl)
append("/page/")
append(page)
when (sortOrder) {
SortOrder.ALPHABETICAL -> append("/?orderby=titleasc")
SortOrder.POPULARITY -> append("/?orderby=popular")
SortOrder.UPDATED -> append("/?sortby=update")
else -> append("/?sortby=update")
append("/?orderby=update&page=")
append(page.toString())
}
}
append(tagQuery)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}

@ -55,49 +55,76 @@ internal abstract class MmrcmsParser(
"مكتملة",
)
override val isMultipleTagsSupported = false
protected open val imgUpdated = "/cover/cover_250x350.jpg"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = if (!query.isNullOrEmpty() || !tags.isNullOrEmpty() || sortOrder != SortOrder.UPDATED) {
buildString {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
val url = buildString {
append("https://")
append(domain)
append("/$listUrl/")
append("?page=")
append('/')
append(listUrl)
append("/?page=")
append(page.toString())
append("&asc=true&author=&tag=")
append("&alpha=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
append("&asc=true&author=&tag=&alpha=")
append(filter.query.urlEncoded())
append("&cat=&sortBy=views")
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
append("&cat=")
if (!tags.isNullOrEmpty()) {
append(tag?.key.orEmpty())
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.UPDATED) {
val url = buildString {
append("https://")
append(domain)
append("/latest-release?page=")
append(page.toString())
}
return parseMangaListUpdated(webClient.httpGet(url).parseHtml())
} else {
val url = buildString {
append("https://")
append(domain)
append('/')
append(listUrl)
append("/?page=")
append(page.toString())
append("&asc=true&author=&tag=&alpha=&cat=")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
append("&sortBy=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL -> append("name")
else -> append("name")
}
}
} else {
buildString {
return parseMangaList(webClient.httpGet(url).parseHtml())
}
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/latest-release")
append("?page=")
append("/latest-release?page=")
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
}
}
val doc = webClient.httpGet(url).parseHtml()
if (!query.isNullOrEmpty() || !tags.isNullOrEmpty() || sortOrder != SortOrder.UPDATED) {
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.media").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@ -115,7 +142,9 @@ internal abstract class MmrcmsParser(
isNsfw = isNsfwSource,
)
}
} else {
}
protected open fun parseMangaListUpdated(doc: Document): List<Manga> {
return doc.select("div.manga-item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val deeplink = href.substringAfterLast("/")
@ -136,8 +165,6 @@ internal abstract class MmrcmsParser(
}
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$tagUrl/").parseHtml()
return doc.select("ul.list-category li").mapNotNullToSet { li ->

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save