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, SortOrder.ALPHABETICAL,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain( override val configKeyDomain = ConfigKey.Domain(
"bato.to", "bato.to",
"batocomic.com", "batocomic.com",
@ -61,37 +63,64 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
"zbato.org", "zbato.org",
) )
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
return search(page, query)
}
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=")
when (sortOrder) {
SortOrder.UPDATED,
-> append("update.za")
SortOrder.POPULARITY -> append("views_a.za") when (filter) {
SortOrder.NEWEST -> append("create.za") is MangaListFilter.Search -> {
SortOrder.ALPHABETICAL -> append("title.az") return search(page, filter.query)
SortOrder.RATING -> Unit
} }
if (!tags.isNullOrEmpty()) {
append("&genres=") is MangaListFilter.Advanced -> {
appendAll(tags, ",") { it.key }
val url = buildString {
append("https://")
append(domain)
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")
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"
},
)
}
// langs= en ...
if (filter.tags.isNotEmpty()) {
append("&genres=")
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)
} }
append("&page=")
append(page)
} }
return parseList(url, page)
} }
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
@ -109,9 +138,11 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
?.selectFirst(".limit-html") ?.selectFirst(".limit-html")
?.html(), ?.html(),
tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(), tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(),
state = when (attrs["Release status:"]?.text()) { state = when (attrs["Original work:"]?.text()) {
"Ongoing" -> MangaState.ONGOING "Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED "Completed" -> MangaState.FINISHED
"Cancelled" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> manga.state else -> manga.state
}, },
author = attrs["Authors:"]?.text()?.trim() ?: manga.author, 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 url = "https://api.$domain/comic/${manga.url}?tachiyomi=true"
val jo = webClient.httpGet(url).parseJson() val jo = webClient.httpGet(url).parseJson()
val comic = jo.getJSONObject("comic") val comic = jo.getJSONObject("comic")
var alt = ""
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
return manga.copy( return manga.copy(
title = comic.getString("title"), title = comic.getString("title"),
altTitle = null, // TODO altTitle = alt,
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"), isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"), description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet { tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -27,7 +27,11 @@ internal abstract class FmreaderParser(
SortOrder.ALPHABETICAL, 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 listUrl = "/manga-list.html"
protected open val datePattern = "MMMM d, yyyy" protected open val datePattern = "MMMM d, yyyy"
@ -40,58 +44,73 @@ internal abstract class FmreaderParser(
@JvmField @JvmField
protected val ongoing: Set<String> = setOf( protected val ongoing: Set<String> = setOf(
"On going", "on going",
"Incomplete", "incomplete",
"En curso", "en curso",
) )
@JvmField @JvmField
protected val finished: Set<String> = setOf( protected val finished: Set<String> = setOf(
"Completed", "completed",
"Completado", "completado",
) )
@JvmField @JvmField
protected val abandoned: Set<String> = hashSetOf( protected val abandoned: Set<String> = hashSetOf(
"Canceled", "canceled",
"Cancelled", "cancelled",
"Drop", "drop",
) )
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) append(listUrl)
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
when { when (filter) {
!query.isNullOrEmpty() -> { is MangaListFilter.Search -> {
append("&name=") append("&name=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("&genre=") append("&genre=")
append(tag?.key.orEmpty()) append(filter.tags.joinToString(",") { it.key })
append("&sort=")
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 -> ""
},
)
}
} }
}
append("&sort=") null -> append("&sort=last_update")
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() return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.thumb-item-flow").map { div -> return doc.select("div.thumb-item-flow").map { div ->
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
Manga( Manga(
@ -99,8 +118,8 @@ internal abstract class FmreaderParser(
url = href, url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain), publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg") coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('") ?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("(")
.substringBeforeLast("')"), .substringBefore(")"),
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(), title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
altTitle = null, altTitle = null,
rating = RATING_UNKNOWN, rating = RATING_UNKNOWN,
@ -140,7 +159,7 @@ internal abstract class FmreaderParser(
val desc = doc.selectFirst(selectDesc)?.html() val desc = doc.selectFirst(selectDesc)?.html()
val stateDiv = doc.selectFirst(selectState) val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let { val state = stateDiv?.let {
when (it.text()) { when (it.text().lowercase()) {
in ongoing -> MangaState.ONGOING in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED in finished -> MangaState.FINISHED
in abandoned -> MangaState.ABANDONED 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 selectTag = "div.info-item:contains(Genre) span.info-value a"
override val datePattern = "dd/MM/yyyy" override val datePattern = "dd/MM/yyyy"
override val selectPage = "div#chapter-content img" 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( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
if (!tags.isNullOrEmpty()) { append("/tim-kiem?page=")
append("/genre/") append(page.toString())
append(tag?.key.orEmpty())
append("?page=") when (filter) {
append(page.toString()) is MangaListFilter.Search -> {
append("&sort=") append("&q=")
when (sortOrder) { append(filter.query.urlEncoded())
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
} }
} else {
append(listUrl) is MangaListFilter.Advanced -> {
append("?page=")
append(page.toString()) append("&accept_genres=")
when { if (filter.tags.isNotEmpty()) {
!query.isNullOrEmpty() -> { append(
append("&q=") filter.tags.joinToString(",") { it.key },
append(query.urlEncoded()) )
}
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 -> ""
},
)
} }
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
} }
null -> append("&sort=update")
} }
} }
val doc = webClient.httpGet(url).parseHtml() return parseMangaList(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,
)
}
} }
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectBodyTag).mapNotNullToSet { a -> return doc.select(selectBodyTag).mapNotNullToSet { label ->
val href = a.attr("href").substringAfterLast("/") val key = label.attr("data-genre-id")
MangaTag( MangaTag(
key = href, key = key,
title = a.text(), title = label.selectFirstOrThrow(".gerne-name").text(),
source = source, source = source,
) )
} }
@ -105,7 +93,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
val desc = doc.selectFirstOrThrow(selectDesc).html() val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.selectFirst(selectState) val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let { val state = stateDiv?.let {
when (it.text()) { when (it.text().lowercase()) {
in ongoing -> MangaState.ONGOING in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED in finished -> MangaState.FINISHED
else -> null else -> null

@ -14,37 +14,60 @@ internal class OlimpoScans(context: MangaLoaderContext) :
override val selectAlt = "ul.manga-info li:contains(Otros nombres)" override val selectAlt = "ul.manga-info li:contains(Otros nombres)"
override val selectTag = "ul.manga-info li:contains(Género) a" override val selectTag = "ul.manga-info li:contains(Género) a"
override val tagPrefix = "lista-de-comics-genero-" override val tagPrefix = "lista-de-comics-genero-"
override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) when (filter) {
append("?page=") is MangaListFilter.Search -> {
append(page.toString()) append(listUrl)
when { append("?page=")
!query.isNullOrEmpty() -> { append(page.toString())
append("&name=") append("&name=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("&genre=") if (filter.tags.isNotEmpty()) {
append(tag?.key.orEmpty()) 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 (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")
} }
}
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() val doc = webClient.httpGet(url).parseHtml()

@ -3,13 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fmreader.ja
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.*
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.site.fmreader.FmreaderParser import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -27,40 +21,7 @@ internal class Klz9(context: MangaLoaderContext) :
override val selectPage = "img" override val selectPage = "img"
override val selectBodyTag = "div.panel-body a" override val selectBodyTag = "div.panel-body a"
override suspend fun getListPage( override fun parseMangaList(doc: Document): List<Manga> {
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()
return doc.select("div.thumb-item-flow").map { div -> return doc.select("div.thumb-item-flow").map { div ->
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( 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() val docLoad = webClient.httpGet("https://$domain/app/manga/controllers/cont.listImg.php?cid=$cid").parseHtml()
return docLoad.select(selectPage).map { img -> return docLoad.select(selectPage).map { img ->
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found") val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,

@ -32,35 +32,63 @@ internal abstract class FoolSlideParser(
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int, val doc =
query: String?, when (filter) {
tags: Set<MangaTag>?, is MangaListFilter.Search -> {
sortOrder: SortOrder,
): List<Manga> {
val doc = if (!query.isNullOrEmpty()) {
val url = buildString {
append("https://$domain/$searchUrl")
if (page > 1) {
return emptyList()
}
}
val q = query.urlEncoded()
webClient.httpPost(url, "search=$q").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) { if (page > 1) {
return emptyList() return emptyList()
} }
} else {
append(page.toString()) val url = buildString {
append("https://")
append(domain)
append("/")
append(searchUrl)
}
webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml()
}
is MangaListFilter.Advanced -> {
val url = buildString {
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) {
return emptyList()
}
} else {
append(page.toString())
}
}
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()
} }
} }
webClient.httpGet(url).parseHtml()
}
return doc.select("div.list div.group").map { div -> return doc.select("div.list div.group").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(

@ -16,40 +16,30 @@ internal class AssortedScans(context: MangaLoaderContext) :
override val pagination = false override val pagination = false
override val selectInfo = "div.#series-info" override val selectInfo = "div.#series-info"
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int, if (page > 1) {
query: String?, return emptyList()
tags: Set<MangaTag>?, }
sortOrder: SortOrder,
): List<Manga> {
val doc = if (!query.isNullOrEmpty()) { val url = buildString {
if (page > 1) { append("https://")
return emptyList() append(domain)
} append('/')
val url = buildString { when (filter) {
append("https://") is MangaListFilter.Search -> {
append(domain) append(searchUrl)
append('/') append("?q=")
append(searchUrl) append(filter.query.urlEncoded())
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()
}
} 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 -> return doc.select("section.series, tr.result").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(

@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("HNISCANTRAD", "Hni Scantrad", "fr") @MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr")
internal class HniScantrad(context: MangaLoaderContext) : internal class HniScantrad(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.HNISCANTRAD, "hni-scantrad.com") { FoolSlideParser(context, MangaSource.HNISCANTRAD, "hni-scantrad.com") {
override val pagination = false override val pagination = false

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -27,50 +27,60 @@ internal class NHentaiParser(context: MangaLoaderContext) :
"/chinese", "/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 { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
if (!tags.isNullOrEmpty()) { when (filter) {
val tag = tags.single()
if (tag.key == "languageKey") { is MangaListFilter.Search -> {
append("/language") append("/search/?q=pages:>0 ")
append(tag.title) append(filter.query.urlEncoded())
} else { append("&")
append("/tag/")
append(tag.key)
}
append("/")
if (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 { is MangaListFilter.Advanced -> {
if (sortOrder == SortOrder.POPULARITY) { if (filter.tags.isNotEmpty() && filter.tags.size > 1) {
append("/?sort=popular&") append("/search/?q=")
} else { append(buildQuery(filter.tags))
append("/?") 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(it.title)
} else {
append("/tag/")
append(it.key)
}
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else {
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/?sort=popular&")
} else {
append("/?")
}
}
} }
null -> append("/?")
} }
append("page=") append("page=")
append(page) append(page.toString())
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
} }

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

@ -27,6 +27,8 @@ internal abstract class HeanCms(
SortOrder.POPULARITY, SortOrder.POPULARITY,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val headers: Headers = Headers.Builder() override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
@ -34,50 +36,54 @@ internal abstract class HeanCms(
protected open val pathManga = "series" 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 ) //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( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
val url = buildString { val url = buildString {
append("https://api.") append("https://api.")
append(domain) append(domain)
append("/query?query_string=") append("/query?query_string=")
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
if (!query.isNullOrEmpty()) { is MangaListFilter.Advanced -> {
append(query.urlEncoded())
}
append("&series_status=All&order=desc&orderBy=") filter.states.oneOrThrowIfMany()?.let {
when (sortOrder) { append("&series_status=")
SortOrder.POPULARITY -> append("total_views") append(
SortOrder.UPDATED -> append("latest") when (it) {
SortOrder.NEWEST -> append("created_at") MangaState.ONGOING -> "Ongoing"
SortOrder.ALPHABETICAL -> append("title") MangaState.FINISHED -> "Completed"
else -> append("latest") MangaState.ABANDONED -> "Dropped"
} MangaState.PAUSED -> "Hiatus"
},
)
append("&series_type=Comic&page=")
append(page)
append("&perPage=12&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("&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&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
} }
null -> {}
} }
append("]".urlEncoded()) append("&page=")
append(page.toString())
} }
val json = webClient.httpGet(url).parseJson() val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j -> return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug") val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug" val urlManga = "https://$domain/$pathManga/$slug"
@ -107,8 +113,10 @@ internal abstract class HeanCms(
source = source, source = source,
) )
} }
} }
protected open val datePattern = "yyyy-MM-dd" protected open val datePattern = "yyyy-MM-dd"
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.*
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.site.heancms.HeanCms 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.json.mapJSON
import org.koitharu.kotatsu.parsers.util.parseJson import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.urlEncoded
@MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI) @MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI)
internal class YugenMangasEs(context: MangaLoaderContext) : internal class YugenMangasEs(context: MangaLoaderContext) :
@ -22,46 +13,55 @@ internal class YugenMangasEs(context: MangaLoaderContext) :
private val domainAlt = "yugenmangas.net" private val domainAlt = "yugenmangas.net"
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
val url = buildString { val url = buildString {
append("https://api.") append("https://api.")
append(domainAlt) append(domainAlt)
append("/query?query_string=") append("/query?query_string=")
if (!query.isNullOrEmpty()) { when (filter) {
append(query.urlEncoded()) is MangaListFilter.Search -> {
} append(filter.query.urlEncoded())
append("&series_status=All&order=desc&orderBy=") }
when (sortOrder) {
SortOrder.POPULARITY -> append("total_views") is MangaListFilter.Advanced -> {
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("created_at") filter.states.oneOrThrowIfMany()?.let {
SortOrder.ALPHABETICAL -> append("title") append("&series_status=")
else -> append("latest") append(
} when (it) {
append("&series_type=Comic&page=") MangaState.ONGOING -> "Ongoing"
append(page) MangaState.FINISHED -> "Completed"
append("&perPage=12&tags_ids=") MangaState.ABANDONED -> "Dropped"
append("[".urlEncoded()) MangaState.PAUSED -> "Hiatus"
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("&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&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
} }
null -> {}
} }
append("]".urlEncoded())
append("&page=")
append(page.toString())
} }
val json = webClient.httpGet(url).parseJson() val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j -> return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug") val slug = j.getString("series_slug")
val urlManga = "https://$domain/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.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.lang.IllegalArgumentException
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* 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 selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)"
protected open val selectMangaTitle = "h5" protected open val selectMangaTitle = "h5"
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
// No search or tag
if (!query.isNullOrEmpty()) {
return emptyList()
}
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) append(listUrl)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException("Search is not supported by this source")
}
is MangaListFilter.Advanced -> {
}
null -> {}
}
if (page > 1) { if (page > 1) {
append("?page=") append("?page=")
append(page) append(page.toString())
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -18,6 +18,78 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.POPULARITY) 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 { override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive") val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive")
val chapterDateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", sourceLocale) 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> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
.requireElementById("reader") .requireElementById("reader")

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

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

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

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

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

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

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

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

@ -14,50 +14,71 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
override val withoutAjax = true override val withoutAjax = true
override val listUrl = "mangax/" override val listUrl = "mangax/"
override suspend fun getListPage( init {
page: Int, paginator.firstPage = 1
query: String?, searchPaginator.firstPage = 1
tags: Set<MangaTag>?, }
sortOrder: SortOrder,
): List<Manga> { override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
val pages = page + 1
when { when (filter) {
!query.isNullOrEmpty() -> {
append("/page/") is MangaListFilter.Search -> {
append(pages.toString()) if (page > 1) {
append("/page/")
append(page.toString())
}
append("/?s=") append("/?s=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
append("&post_type=wp-manga&") append("&post_type=wp-manga")
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("/page/")
append(pages.toString())
append("?")
}
else -> { val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
if (page > 1) {
append("/page/")
append(page.toString())
}
append("/?")
} else {
if (page > 1) {
append("/page/")
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("/$listUrl") append("m_orderby=")
append("/page/") when (filter.sortOrder) {
append(pages.toString()) SortOrder.POPULARITY -> append("views")
append("?") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
}
}
null -> {
append("/?s&post_type=wp-manga&m_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() val doc = webClient.httpGet(url).parseHtml()

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

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

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

@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
@MangaSourceParser("MANHWAZ", "ManhwaZ", "en") @MangaSourceParser("MANHWAZ", "ManhwaZ", "en")
internal class Manhwaz(context: MangaLoaderContext) : internal class Manhwaz(context: MangaLoaderContext) :
@ -15,51 +16,65 @@ internal class Manhwaz(context: MangaLoaderContext) :
override val withoutAjax = true override val withoutAjax = true
override val selectTestAsync = "div.list-chapter" override val selectTestAsync = "div.list-chapter"
override suspend fun getListPage( override val availableStates: Set<MangaState> get() = emptySet()
page: Int,
query: String?, override val availableSortOrders: Set<SortOrder> = EnumSet.of(
tags: Set<MangaTag>?, SortOrder.UPDATED,
sortOrder: SortOrder, SortOrder.POPULARITY,
): List<Manga> { SortOrder.NEWEST,
val tag = tags.oneOrThrowIfMany() SortOrder.RATING,
)
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
val pages = page + 1 when (filter) {
is MangaListFilter.Search -> {
when {
!query.isNullOrEmpty() -> {
append("/search?s=") append("/search?s=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(pages.toString()) append(page.toString())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("?page=")
append(pages.toString())
append("&")
}
else -> { val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append("?page=")
append(page.toString())
append("&")
} else {
append("/$listUrl")
append("?page=")
append(page.toString())
append("&")
}
append("m_orderby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new")
SortOrder.RATING -> append("rating")
else -> append("latest")
}
}
null -> {
append("/$listUrl") append("/$listUrl")
append("?page=") append("?page=")
append(pages.toString()) append(page.toString())
append("&") append("&m_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() 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga 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.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.model.SortOrder
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
@ -13,48 +14,46 @@ import java.util.*
@MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es") @MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es")
internal class DragonTranslationParser(context: MangaLoaderContext) : internal class DragonTranslationParser(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.DRAGONTRANSLATION, "dragontranslation.net", 30) { 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 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( init {
page: Int, paginator.firstPage = 1
query: String?, searchPaginator.firstPage = 1
tags: Set<MangaTag>?, }
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
val pages = page + 1 when (filter) {
is MangaListFilter.Search -> {
when {
!query.isNullOrEmpty() -> {
append("/mangas?buscar=") append("/mangas?buscar=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(pages.toString()) append(page.toString())
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("/mangas?tag=")
append(tag?.key.orEmpty())
append("&page=")
append(pages.toString())
}
else -> { append("/mangas?page=")
append(page.toString())
append("/mangas") val tag = filter.tags.oneOrThrowIfMany()
append("?page=") if (filter.tags.isNotEmpty()) {
append(pages.toString()) append("&tag=")
append(tag?.key.orEmpty())
}
}
null -> {
append("/mangas?page=")
append(page.toString())
} }
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.video-bg div.col-6 ").map { div -> 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.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("MONARCAMANGA", "MonarcaManga", "es") @MangaSourceParser("MONARCAMANGA", "MonarcaManga", "es")
internal class MonarcaManga(context: MangaLoaderContext) : internal class MonarcaManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MONARCAMANGA, "monarcamanga.com") { MadaraParser(context, MangaSource.MONARCAMANGA, "monarcamanga.com") {
override val tagPrefix = "manga-generos/" 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 listUrl = "biblioteca/"
override val selectGenre = "div.summary-content a.tags_manga" override val selectGenre = "div.summary-content a.tags_manga"
override val withoutAjax = true override val withoutAjax = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val availableStates: Set<MangaState> get() = emptySet()
init { init {
paginator.firstPage = 1 paginator.firstPage = 1
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY) 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> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when { when (filter) {
!query.isNullOrEmpty() -> { is MangaListFilter.Search -> {
append("/$listUrl") append("/$listUrl")
append("?search=") append("?search=")
append(query.urlEncoded()) append(filter.query.urlEncoded())
if (page > 1) { if (page > 1) {
append("&page=") append("&page=")
append(page) append(page)
} }
} }
!tags.isNullOrEmpty() -> { is MangaListFilter.Advanced -> {
append("/$tagPrefix")
append(tag?.key.orEmpty()) val tag = filter.tags.oneOrThrowIfMany()
if (page > 1) { if (filter.tags.isNotEmpty()) {
append("?page=") append("/$tagPrefix")
append(page) 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") append("/$listUrl")
if (page > 1) { if (page > 1) {
append("?page=") append("?page=")
@ -62,6 +67,7 @@ internal class TmoManga(context: MangaLoaderContext) :
} }
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.page-item-detail").map { div -> return doc.select("div.page-item-detail").map { div ->

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

@ -29,6 +29,8 @@ internal abstract class MadthemeParser(
SortOrder.RATING, SortOrder.RATING,
) )
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
protected open val listUrl = "search/" protected open val listUrl = "search/"
protected open val datePattern = "MMM dd, yyyy" protected open val datePattern = "MMM dd, yyyy"
@ -52,35 +54,52 @@ internal abstract class MadthemeParser(
"COMPLETED", "COMPLETED",
) )
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/$listUrl?sort=") append('/')
when (sortOrder) { append(listUrl)
SortOrder.POPULARITY -> append("views") when (filter) {
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()) { is MangaListFilter.Search -> {
for (tag in tags) { append("?sort=updated_at&q=")
append("&") append(filter.query.urlEncoded())
append("genre[]".urlEncoded())
append("=")
append(tag.key)
} }
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 (filter.tags.isNotEmpty()) {
filter.tags.forEach {
append("&")
append("genre[]".urlEncoded())
append("=")
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=") append("&page=")
@ -117,7 +136,7 @@ internal abstract class MadthemeParser(
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() 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 key = checkbox.selectFirstOrThrow("input").attr("value") ?: return@mapNotNullToSet null
val name = checkbox.selectFirstOrThrow("span.radio__label").text() val name = checkbox.selectFirstOrThrow("span.radio__label").text()
MangaTag( MangaTag(

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

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

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

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

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.manga18.en package org.koitharu.kotatsu.parsers.site.manga18.en
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
@ -10,51 +11,7 @@ import org.koitharu.kotatsu.parsers.util.*
internal class Hentai3zCc(context: MangaLoaderContext) : internal class Hentai3zCc(context: MangaLoaderContext) :
Manga18Parser(context, MangaSource.HENTAI3ZCC, "hentai3z.cc") { Manga18Parser(context, MangaSource.HENTAI3ZCC, "hentai3z.cc") {
override suspend fun getListPage( override fun parseMangaList(doc: Document): List<Manga> {
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()
return doc.select("div.story_item").map { div -> return doc.select("div.story_item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga( Manga(

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

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

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

@ -1,11 +1,14 @@
package org.koitharu.kotatsu.parsers.site.mangabox.en 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
@MangaSourceParser("MANGAKAKALOTTV", "Mangakakalot.tv", "en") @MangaSourceParser("MANGAKAKALOTTV", "Mangakakalot.tv", "en")
internal class MangakakalotTv(context: MangaLoaderContext) : internal class MangakakalotTv(context: MangaLoaderContext) :
@ -14,40 +17,61 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("ww6.mangakakalot.tv") override val configKeyDomain = ConfigKey.Domain("ww6.mangakakalot.tv")
override val searchUrl = "/search/" override val searchUrl = "/search/"
override val listUrl = "/manga_list" 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( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
if (!query.isNullOrEmpty()) { when (filter) {
append(searchUrl)
append(query.urlEncoded()) is MangaListFilter.Search -> {
append("?page=") append(searchUrl)
append(page.toString()) append(filter.query.urlEncoded())
} else { append("?page=")
append("$listUrl/")
append("?type=")
when (sortOrder) {
SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("newest")
else -> append("latest")
}
if (!tags.isNullOrEmpty()) {
append("&category=")
append(tag?.key.orEmpty())
} }
if (page > 1) {
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=")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
}
filter.states.oneOrThrowIfMany()?.let {
append("&state=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
else -> "all"
},
)
}
append("&page=") append("&page=")
append(page.toString()) }
null -> {
append(listUrl)
append("?type=latest&page=")
} }
} }
append(page.toString())
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.list-truyen-item-wrap").ifEmpty { return doc.select("div.list-truyen-item-wrap").ifEmpty {
@ -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 val selectTagMap = "ul.tag li a"
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {

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

@ -32,60 +32,72 @@ internal abstract class MangaReaderParser(
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST) get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST)
override val availableStates: Set<MangaState>
get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
protected open val listUrl = "/manga" protected open val listUrl = "/manga"
protected open val datePattern = "MMMM d, yyyy" protected open val datePattern = "MMMM d, yyyy"
protected open val isNetShieldProtected = false protected open val isNetShieldProtected = false
private var tagCache: ArrayMap<String, MangaTag>? = null private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex() 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()
}
val url = buildString { override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
append("https://")
append(domain)
append("/page/")
append(page)
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
val sortQuery = when (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 { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl)
append("/?order=") when (filter) {
append(sortQuery)
append(tagQuery) is MangaListFilter.Search -> {
append("&page=") append("/page/")
append(page) append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
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 (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.toString())
}
null -> {
append(listUrl)
append("/?order=update&page=")
append(page.toString())
}
}
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
} }

@ -17,52 +17,58 @@ internal class SwaTeam(context: MangaLoaderContext) :
override val selectMangaListImg = "img" override val selectMangaListImg = "img"
override val isNetShieldProtected = true override val isNetShieldProtected = true
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/?s=")
append(query.urlEncoded())
append("&page=")
append(page)
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
val sortQuery = when (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 { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl)
append("/?order=") when (filter) {
append(sortQuery)
append(tagQuery) is MangaListFilter.Search -> {
append("/?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append(listUrl)
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 (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) append(page.toString())
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -17,60 +17,52 @@ internal class ManhwaFreak(context: MangaLoaderContext) :
override val selectMangaList = ".listupd .lastest-serie" override val selectMangaList = ".listupd .lastest-serie"
override val selectMangaListImg = "img" override val selectMangaListImg = "img"
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/page/")
append(page)
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
if (!tags.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
}
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
append("/genres/?genre=")
append(tag?.key.orEmpty())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
if (page > 1) {
return emptyList()
}
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.NEWEST -> "new"
SortOrder.POPULARITY -> "views"
SortOrder.UPDATED -> ""
else -> ""
}
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl)
append("/?order=") when (filter) {
append(sortQuery)
is MangaListFilter.Search -> {
append("/page/")
append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (page > 1) {
return emptyList()
}
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/genres/?genre=")
append(it.key)
}
} else {
append(listUrl)
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.NEWEST -> "new"
SortOrder.POPULARITY -> "views"
SortOrder.UPDATED -> ""
else -> ""
},
)
}
}
null -> {
append(listUrl)
}
}
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga 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.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.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.domain
@ -19,31 +20,40 @@ internal class RizzComic(context: MangaLoaderContext) :
override val datePattern = "dd MMM yyyy" override val datePattern = "dd MMM yyyy"
override val listUrl = "/series" override val listUrl = "/series"
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
get() = EnumSet.of(SortOrder.ALPHABETICAL) override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getListPage( // TODO Query created in json
page: Int, override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
if (!query.isNullOrEmpty()) { val url = buildString {
throw IllegalArgumentException("Search is not supported by this source") append("https://")
} append(domain)
val url = if (!tags.isNullOrEmpty()) { when (filter) {
buildString {
append("https://") is MangaListFilter.Search -> {
append(domain) throw IllegalArgumentException("Search is not supported by this source")
append("/genre/") }
append(tag?.key.orEmpty())
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("/genre/")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
append(listUrl)
}
}
null -> {
append(listUrl)
}
} }
} else {
"https://$domain$listUrl"
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga 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.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.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain 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.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.util.* import java.util.*
@ -19,53 +21,34 @@ internal class Zahard(context: MangaLoaderContext) :
override val listUrl = "/library" override val listUrl = "/library"
override val selectChapter = "#chapterlist > ul > a" override val selectChapter = "#chapterlist > ul > a"
override val selectPage = "div#chapter_imgs img" 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 suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
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()
}
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?search=")
append(query.urlEncoded())
append("&page=")
append(page)
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst("a[rel=next]")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
val tagKey = "tag".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) append(listUrl)
append("?page=") append("?page=")
append(page) append(page.toString())
append(tagQuery) when (filter) {
is MangaListFilter.Search -> {
append("&search=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("tag=")
append(it.key)
}
}
null -> {}
}
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
} }
} }

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

@ -18,61 +18,52 @@ internal class ManhwaFreakFr(context: MangaLoaderContext) :
override val selectMangaList = ".listupd .lastest-serie" override val selectMangaList = ".listupd .lastest-serie"
override val selectMangaListImg = "img" override val selectMangaListImg = "img"
override val sourceLocale: Locale = Locale.ENGLISH override val sourceLocale: Locale = Locale.ENGLISH
override val availableStates: Set<MangaState> = emptySet()
override val isMultipleTagsSupported = false
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/page/")
append(page)
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
if (!tags.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
}
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
append("/genres/?genre=")
append(tag?.key.orEmpty())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
if (page > 1) {
return emptyList()
}
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.NEWEST -> "new"
SortOrder.POPULARITY -> "views"
SortOrder.UPDATED -> ""
else -> ""
}
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl)
append("/?order=") when (filter) {
append(sortQuery)
is MangaListFilter.Search -> {
append("/page/")
append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (page > 1) {
return emptyList()
}
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/genres/?genre=")
append(it.key)
}
} else {
append(listUrl)
append("/?order=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.NEWEST -> "new"
SortOrder.POPULARITY -> "views"
SortOrder.UPDATED -> ""
else -> ""
},
)
}
}
null -> {
append(listUrl)
}
}
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga 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.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.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain 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.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.util.EnumSet
import java.util.Locale import java.util.Locale
@MangaSourceParser("COSMIC_SCANS", "CosmicScans.id", "id") @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) { MangaReaderParser(context, MangaSource.COSMIC_SCANS, "cosmicscans.id", pageSize = 30, searchPageSize = 30) {
override val sourceLocale: Locale = Locale.ENGLISH 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 val listUrl = "/semua-komik"
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/page/")
append(page)
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
if (page > 1) {
return emptyList()
}
val sortQuery = when (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 { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl)
append("/?order=") when (filter) {
append(sortQuery)
append(tagQuery) is MangaListFilter.Search -> {
append("&page=") append("/page/")
append(page) append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
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()
}
append(listUrl)
}
}
null -> {
if (page > 1) {
return emptyList()
}
append(listUrl)
}
}
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga 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.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.model.SortOrder
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.domain 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.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded import org.koitharu.kotatsu.parsers.util.urlEncoded
@ -19,50 +21,61 @@ internal class KomikSan(context: MangaLoaderContext) :
override val listUrl = "/list" override val listUrl = "/list"
override val datePattern = "MMM d, yyyy" override val datePattern = "MMM d, yyyy"
override suspend fun getListPage( override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/search?search=")
append(query.urlEncoded())
append("&page=")
append(page)
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
val sortQuery = when (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 { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) when (filter) {
append("/?order=")
append(sortQuery) is MangaListFilter.Search -> {
append(tagQuery) append("/search?search=")
append("&page=") append(filter.query.urlEncoded())
append(page) append("&page=")
append(page.toString())
}
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 (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.toString())
}
null -> {
append(listUrl)
append("/?order=update&page=")
append(page.toString())
}
}
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
} }

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

Loading…
Cancel
Save