fix getlist VmpParser, LineWebtoonsParser, BlogTruyenParser, HentaiVNParser, YurinekoParser

add filter on WpComicsParser, ZMangaParser, LxManga, Truyenqq, TruyentranhLHParser

Move NetTruyen to template WpComicsParser and add multitags on WpComicsParser
pull/401/head
devi 2 years ago
parent 6ad78f1cba
commit 56eb059bcd

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

@ -180,15 +180,16 @@ internal abstract class MadaraParser(
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/$tagPrefix")
append(tag?.key.orEmpty())
append(it.key)
if (pages > 1) {
append("/page/")
append(pages.toString())
}
append("/?")
}
} else {
if (pages > 1) {
@ -241,8 +242,9 @@ internal abstract class MadaraParser(
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
payload["vars[wp-manga-genre]"] = tag?.key.orEmpty()
filter.tags.oneOrThrowIfMany()?.let {
payload["vars[wp-manga-genre]"] = it.key
}
when (filter.sortOrder) {
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"

@ -29,98 +29,38 @@ class BlogTruyenParser(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override val isMultipleTagsSupported = false
private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US)
private var cacheTags = SuspendLazy(::fetchTags)
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val descriptionElement = doc.selectFirstOrThrow("div.description")
val statusText = descriptionElement
.selectFirst("p:contains(Trạng thái) > span.color-red")
?.text()
val state = when (statusText) {
"Đang tiến hành" -> MangaState.ONGOING
"Đã hoàn thành" -> MangaState.FINISHED
else -> null
}
val rating = doc.selectFirst("span.total-vote")?.attr("ng-init")?.let { text ->
val like = text.substringAfter("TotalLike=")
.substringBefore(';')
.toIntOrNull() ?: return@let RATING_UNKNOWN
val dislike = text.substringAfter("TotalDisLike=")
.toIntOrNull() ?: return@let RATING_UNKNOWN
when {
like == 0 && dislike == 0 -> RATING_UNKNOWN
else -> like.toFloat() / (like + dislike)
}
}
val tags = cacheTags.tryGet().getOrNull()?.let { tagMap ->
descriptionElement.select("p > span.category").mapNotNullToSet {
val tagName = it.selectFirst("a")?.text()?.trim() ?: return@mapNotNullToSet null
tagMap[tagName]
}
}
return manga.copy(
tags = tags ?: emptySet(),
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(),
description = doc.selectFirst(".detail .content")?.html(),
chapters = parseChapterList(doc),
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.imageUrl(),
state = state,
rating = rating ?: RATING_UNKNOWN,
isNsfw = doc.getElementById("warningCategory") != null,
)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
private fun parseChapterList(doc: Document): List<MangaChapter> {
val chapterList = doc.select("#list-chapters > p")
return chapterList.mapChapters(reversed = true) { index, element ->
val titleElement = element.selectFirst("span.title > a") ?: return@mapChapters null
val name = titleElement.text()
val relativeUrl = titleElement.attrAsRelativeUrl("href")
val id = relativeUrl.substringAfter('/').substringBefore('/')
val uploadDate = dateFormat.tryParse(element.select("span.publishedDate").text())
MangaChapter(
id = generateUid(id),
name = name,
number = index + 1,
url = relativeUrl,
scanlator = null,
uploadDate = uploadDate,
branch = null,
source = source,
)
}
}
return when (filter) {
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
return when {
!query.isNullOrEmpty() -> {
val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page"
is MangaListFilter.Search -> {
val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page"
val searchContent = webClient.httpGet(searchUrl).parseHtml()
.selectFirst("section.list-manga-bycate > div.list")
parseMangaList(searchContent)
}
!tags.isNullOrEmpty() -> {
val tag = tags.oneOrThrowIfMany()!!
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany().let {
val categoryAjax =
"https://${domain}/ajax/Category/AjaxLoadMangaByCategory?id=${tag.key}&orderBy=5&p=$page"
"https://${domain}/ajax/Category/AjaxLoadMangaByCategory?id=${it?.key}&orderBy=5&p=$page"
val listContent = webClient.httpGet(categoryAjax).parseHtml().selectFirst("div.list")
parseMangaList(listContent)
}
} else {
getNormalList(page)
}
}
else -> getNormalList(page)
null -> getNormalList(page)
}
}
@ -146,7 +86,7 @@ class BlogTruyenParser(context: MangaLoaderContext) :
description = el.selectFirst("p.al-j.break.line-height-15")?.text(),
url = relativeUrl,
publicUrl = relativeUrl.toAbsoluteUrl(domain),
coverUrl = linkTag.selectLast("img")?.imageUrl().orEmpty(),
coverUrl = linkTag.selectLast("img")?.src().orEmpty(),
source = source,
tags = tags ?: emptySet(),
isNsfw = false,
@ -171,7 +111,7 @@ class BlogTruyenParser(context: MangaLoaderContext) :
description = mangaInfo.select("div.al-j.fs-12").text(),
url = relativeUrl,
publicUrl = relativeUrl.toAbsoluteUrl(domain),
coverUrl = mangaInfo.selectFirst("div > img.img")?.imageUrl().orEmpty(),
coverUrl = mangaInfo.selectFirst("div > img.img")?.src().orEmpty(),
isNsfw = false,
rating = RATING_UNKNOWN,
tags = emptySet(),
@ -182,6 +122,91 @@ class BlogTruyenParser(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return cacheTags.get().values.toSet()
}
private suspend fun fetchTags(): Map<String, MangaTag> {
val doc = webClient.httpGet("/timkiem/nangcao".toAbsoluteUrl(domain)).parseHtml()
val tagItems = doc.select("li[data-id]")
val tagMap = ArrayMap<String, MangaTag>(tagItems.size)
for (tag in tagItems) {
val title = tag.text().trim()
tagMap[tag.text().trim()] = MangaTag(
title = title,
key = tag.attr("data-id"),
source = source,
)
}
return tagMap
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val descriptionElement = doc.selectFirstOrThrow("div.description")
val statusText = descriptionElement
.selectFirst("p:contains(Trạng thái) > span.color-red")
?.text()
val state = when (statusText) {
"Đang tiến hành" -> MangaState.ONGOING
"Đã hoàn thành" -> MangaState.FINISHED
else -> null
}
val rating = doc.selectFirst("span.total-vote")?.attr("ng-init")?.let { text ->
val like = text.substringAfter("TotalLike=")
.substringBefore(';')
.toIntOrNull() ?: return@let RATING_UNKNOWN
val dislike = text.substringAfter("TotalDisLike=")
.toIntOrNull() ?: return@let RATING_UNKNOWN
when {
like == 0 && dislike == 0 -> RATING_UNKNOWN
else -> like.toFloat() / (like + dislike)
}
}
val tags = cacheTags.tryGet().getOrNull()?.let { tagMap ->
descriptionElement.select("p > span.category").mapNotNullToSet {
val tagName = it.selectFirst("a")?.text()?.trim() ?: return@mapNotNullToSet null
tagMap[tagName]
}
}
return manga.copy(
tags = tags ?: emptySet(),
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(),
description = doc.selectFirst(".detail .content")?.html(),
chapters = parseChapterList(doc),
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src().orEmpty(),
state = state,
rating = rating ?: RATING_UNKNOWN,
isNsfw = doc.getElementById("warningCategory") != null,
)
}
private fun parseChapterList(doc: Document): List<MangaChapter> {
val chapterList = doc.select("#list-chapters > p")
return chapterList.mapChapters(reversed = true) { index, element ->
val titleElement = element.selectFirst("span.title > a") ?: return@mapChapters null
val name = titleElement.text()
val relativeUrl = titleElement.attrAsRelativeUrl("href")
val id = relativeUrl.substringAfter('/').substringBefore('/')
val uploadDate = dateFormat.tryParse(element.select("span.publishedDate").text())
MangaChapter(
id = generateUid(id),
name = name,
number = index + 1,
url = relativeUrl,
scanlator = null,
uploadDate = uploadDate,
branch = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
fun generateImageId(index: Int) = generateUid("${chapter.url}/$index")
@ -191,7 +216,7 @@ class BlogTruyenParser(context: MangaLoaderContext) :
pages.add(
MangaPage(
id = generateImageId(pages.size),
url = img.imageUrl(),
url = img.src().orEmpty(),
preview = null,
source = source,
),
@ -218,30 +243,4 @@ class BlogTruyenParser(context: MangaLoaderContext) :
return pages
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return cacheTags.get().values.toSet()
}
private suspend fun fetchTags(): Map<String, MangaTag> {
val doc = webClient.httpGet("/timkiem/nangcao".toAbsoluteUrl(domain)).parseHtml()
val tagItems = doc.select("li[data-id]")
val tagMap = ArrayMap<String, MangaTag>(tagItems.size)
for (tag in tagItems) {
val title = tag.text().trim()
tagMap[tag.text().trim()] = MangaTag(
title = title,
key = tag.attr("data-id"),
source = source,
)
}
return tagMap
}
private fun Element.imageUrl(): String {
return attrAsAbsoluteUrlOrNull("src")
?: attrAsAbsoluteUrlOrNull("data-cfsrc")
?: ""
}
}

@ -35,6 +35,57 @@ class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
SortOrder.NEWEST,
)
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Search -> {
val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1
urlBuilder()
val searchUrl =
"/tim-kiem-truyen.html?key=${filter.query.urlEncoded()}&page=$page".toAbsoluteUrl(domain)
val docs = webClient.httpGet(searchUrl).parseHtml()
parseMainList(docs, page)
}
is MangaListFilter.Advanced -> {
val pageSize = if (filter.tags.isEmpty()) PAGE_SIZE else SEARCH_PAGE_SIZE
val page = (offset / pageSize.toFloat()).toIntUp() + 1
if (filter.tags.isNotEmpty()) {
val url = buildString {
val tagKey = "tag[]".urlEncoded()
append("/forum/search-plus.php?name=")
append("&dou=&char=")
filter.tags.forEach { tag ->
append("&")
append(tagKey)
append("=")
append(tag.key)
}
append("&search=")
append("&page=")
append(page)
}.toAbsoluteUrl(domain)
val docs = webClient.httpGet(url).parseHtml()
return parseAdvanceSearch(docs, page)
} else {
val site = if (filter.sortOrder == SortOrder.UPDATED) "/chap-moi" else "/danh-sach"
val url = "$site.html?page=$page".toAbsoluteUrl(domain)
context.cookieJar.insertCookies(domain, *getSortCookies(filter.sortOrder))
val docs = webClient.httpGet(url).parseHtml()
parseMainList(docs, page)
}
}
null -> {
val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1
val url = "/chap-moi.html?page=$page".toAbsoluteUrl(domain)
parseMainList(webClient.httpGet(url).parseHtml(), page)
}
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val chapterDeferred = async { fetchChapters(manga.url) }
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
@ -79,48 +130,6 @@ class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
)
}
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
val pageSize = if (tags.isNullOrEmpty()) PAGE_SIZE else SEARCH_PAGE_SIZE
val page = (offset / pageSize.toFloat()).toIntUp() + 1
return when {
!tags.isNullOrEmpty() -> {
val url = buildString {
val tagKey = "tag[]".urlEncoded()
append("/forum/search-plus.php?name=")
append(query?.urlEncoded().orEmpty())
append("&dou=&char=")
tags.forEach { tag ->
append("&")
append(tagKey)
append("=")
append(tag.key)
}
append("&search=")
append("&page=")
append(page)
}.toAbsoluteUrl(domain)
val docs = webClient.httpGet(url).parseHtml()
return parseAdvanceSearch(docs, page)
}
!query.isNullOrEmpty() -> {
urlBuilder()
val searchUrl = "/tim-kiem-truyen.html?key=${query.urlEncoded()}&page=$page".toAbsoluteUrl(domain)
val docs = webClient.httpGet(searchUrl).parseHtml()
parseMainList(docs, page)
}
else -> {
val site = if (sortOrder == SortOrder.UPDATED) "/chap-moi" else "/danh-sach"
val url = "$site.html?page=$page".toAbsoluteUrl(domain)
context.cookieJar.insertCookies(domain, *getSortCookies(sortOrder))
val docs = webClient.httpGet(url).parseHtml()
parseMainList(docs, page)
}
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val docs = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return docs.select("#image > img").map {

@ -20,56 +20,75 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context,
SortOrder.NEWEST,
SortOrder.POPULARITY,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("lxmanga.net")
override val isMultipleTagsSupported = false
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
val skey = "filter[name]=".urlEncoded()
append("/tim-kiem?$skey")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/the-loai/")
for (tag in tags) {
append(tag.key)
}
append(it.key)
}
else -> {
} else {
append("/danh-sach")
}
}
append("?page=")
append(page.toString())
if (filter.states.isNotEmpty()) {
append("&filter[status]=")
filter.states.forEach {
append(
when (it) {
MangaState.ONGOING -> "2,"
MangaState.FINISHED -> "1,"
else -> "1,2"
},
)
}
}
append("&sort=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("-views")
SortOrder.UPDATED -> append("-updated_at")
SortOrder.NEWEST -> append("-created_at")
SortOrder.ALPHABETICAL -> append("name")
else -> append("-updated_at")
}
}
null -> {
append("/danh-sach?sort=-updated_at&page=")
append(page.toString())
}
}
val doc = webClient.httpGet(url).parseHtml()
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.grid div.manga-vertical")
.map { div ->
@ -95,7 +114,6 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context,
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
return manga.copy(
altTitle = root.select(".divider2:contains(Noms associés :)").firstOrNull()?.text(),
state = when (root.select("div.grow div.mt-2:contains(Tình trạng) a").first()!!.text()) {
@ -135,9 +153,7 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context,
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select("div.text-center img.lazy").map { img ->
val url = img.attrAsRelativeUrlOrNull("data-src") ?: img.attrAsRelativeUrlOrNull("src")
?: img.parseFailed("Image src not found")

@ -1,228 +0,0 @@
package org.koitharu.kotatsu.parsers.site.vi
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
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.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi")
class NetTruyenParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.NETTRUYEN, pageSize = 36) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain(
"www.nettruyenlive.com",
"www.nettruyenio.com",
"www.nettruyento.com",
"nettruyento.com",
"nettruyenin.com",
)
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.RATING)
private val mutex = Mutex()
private val dateFormat = SimpleDateFormat("dd/MM/yy", Locale.US)
private var tagCache: ArrayMap<String, MangaTag>? = null
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val rating = doc.selectFirst("span[itemprop=ratingValue]")
?.ownText()
?.toFloatOrNull() ?: 0f
val chapterElements = doc.getElementById("nt_listchapter")?.select("ul > li") ?: doc.parseFailed()
val chapters = chapterElements.mapChapters(reversed = true) { index, element ->
val a = element.selectFirst("div.chapter > a") ?: return@mapChapters null
val relativeUrl = a.attrAsRelativeUrlOrNull("href") ?: return@mapChapters null
val timeText = element.selectFirst("div.col-xs-4.text-center.no-wrap.small")?.text()
MangaChapter(
id = generateUid(relativeUrl),
name = a.text(),
number = index + 1,
url = relativeUrl,
scanlator = null,
uploadDate = parseChapterTime(timeText),
branch = null,
source = source,
)
}
return manga.copy(
rating = rating / 5,
chapters = chapters,
description = doc.selectFirst("div.detail-content > p")?.html(),
isNsfw = doc.selectFirst("div.alert.alert-danger > strong:contains(Cảnh báo độ tuổi)") != null,
)
}
// 20 giây trước
// 52 phút trước
// 6 giờ trước
// 2 ngày trước
// 19:09 30/07
// 23/12/21
private fun parseChapterTime(timeText: String?): Long {
if (timeText.isNullOrEmpty()) {
return 0L
}
val timeWords = arrayOf("giây", "phút", "giờ", "ngày")
val calendar = Calendar.getInstance()
val timeArr = timeText.split(' ')
if (WordSet(*timeWords).anyWordIn(timeText)) {
val timeSuffix = timeArr.getOrNull(1)
val timeDiff = timeArr.getOrNull(0)?.toIntOrNull() ?: return 0L
when (timeSuffix) {
timeWords[0] -> calendar.add(Calendar.SECOND, -timeDiff)
timeWords[1] -> calendar.add(Calendar.MINUTE, -timeDiff)
timeWords[2] -> calendar.add(Calendar.HOUR, -timeDiff)
timeWords[3] -> calendar.add(Calendar.DATE, -timeDiff)
else -> return 0L
}
} else {
val relativeDate = timeArr.lastOrNull() ?: return 0L
val dateString = when (relativeDate.split('/').size) {
2 -> {
val currentYear = calendar.get(Calendar.YEAR).toString().takeLast(2)
"$relativeDate/$currentYear"
}
3 -> relativeDate
else -> return 0L
}
calendar.timeInMillis = dateFormat.tryParse(dateString)
}
return calendar.time.time
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val isSearching = !query.isNullOrEmpty()
val url = buildString {
append("https://")
append(domain)
if (isSearching) {
append("/tim-truyen?keyword=")
append(query!!.urlEncoded())
append("&page=")
append(page)
} else {
val tagQuery = tags.orEmpty().joinToString(",") { it.key }
append("/tim-truyen-nang-cao?genres=$tagQuery")
append("&notgenres=&gender=-1&status=-1&minchapter=1&sort=${getSortOrderKey(sortOrder)}")
append("&page=$page")
}
}
val response = if (isSearching) {
val result = runCatchingCancellable { webClient.httpGet(url) }
val exception = result.exceptionOrNull()
if (exception is NotFoundException) {
return emptyList()
}
result.getOrThrow()
} else {
webClient.httpGet(url)
}
val itemsElements = response.parseHtml()
.select("div.ModuleContent > div.items")
.select("div.item")
return itemsElements.mapNotNull { item ->
val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null
val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null
val slug = absUrl.substringAfterLast('/')
val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) {
"Đang tiến hành" -> MangaState.ONGOING
"Hoàn thành" -> MangaState.FINISHED
else -> null
}
val tagMap = getOrCreateTagMap()
val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty()
val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] }
Manga(
id = generateUid(slug),
title = tooltipElement.selectFirst("div.title")?.text().orEmpty(),
altTitle = null,
url = absUrl.toRelativeUrl(domain),
publicUrl = absUrl,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(),
largeCoverUrl = null,
tags = mangaTags,
state = mangaState,
author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(),
description = tooltipElement.selectFirst("div.box_text")?.text(),
chapters = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val pageElements = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
.select("div.reading-detail.box_doc > div img")
return pageElements.map { element ->
val url = element.attrAsAbsoluteUrl("data-original")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val map = getOrCreateTagMap()
val tagSet = ArraySet<MangaTag>(map.size)
for (entry in map) {
tagSet.add(entry.value)
}
return tagSet
}
private suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val doc = webClient.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(domain)).parseHtml()
val tagItems = doc.select("div.genre-item")
val result = ArrayMap<String, MangaTag>(tagItems.size)
for (item in tagItems) {
val title = item.text().trim()
val key = item.select("span[data-id]").attr("data-id")
result[title] = MangaTag(title = title, key = key, source = source)
}
tagCache = result
result
}
private fun getSortOrderKey(sortOrder: SortOrder) = when (sortOrder) {
SortOrder.UPDATED -> 0
SortOrder.POPULARITY -> 10
SortOrder.NEWEST -> 15
SortOrder.RATING -> 20
else -> throw IllegalArgumentException("Sort order ${sortOrder.name} not supported")
}
}

@ -14,40 +14,70 @@ internal class Truyenqq(context: MangaLoaderContext) : PagedMangaParser(context,
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("truyenqqvn.com")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tagQuery = if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = ",") { it.key }
val url = if (!query.isNullOrEmpty()) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url =
when (filter) {
is MangaListFilter.Search -> {
buildString {
append("https://")
append(domain)
append("/tim-kiem/trang-$page.html")
append("?q=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
} else {
}
is MangaListFilter.Advanced -> {
buildString {
append("https://")
append(domain)
append("/tim-kiem-nang-cao/trang-$page.html")
append("?status=-1&country=0&sort=")
when (sortOrder) {
append("/tim-kiem-nang-cao/trang-")
append(page.toString())
append(".html?country=0&sort=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("4")
SortOrder.UPDATED -> append("2")
SortOrder.NEWEST -> append("0")
else -> append("2")
}
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "0"
MangaState.FINISHED -> "1"
else -> "-1"
},
)
}
} else {
append("&status=-1")
}
append("&category=")
append(tagQuery)
append(filter.tags.joinToString(separator = ",") { it.key })
append("&notcategory=&minchapter=0")
}
}
null -> {
buildString {
append("https://")
append(domain)
append("/tim-kiem-nang-cao/trang-")
append(page.toString())
append(".html?status=-1&country=0&sort=2&category=&notcategory=&minchapter=0")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.requireElementById("main_homepage").select("li").map { li ->
val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href")

@ -19,10 +19,84 @@ class TruyentranhLHParser(context: MangaLoaderContext) :
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("truyentranhlh.net")
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
private val mutex = Mutex()
private var tagCache: Map<String, MangaTag>? = null
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = urlBuilder().apply {
addPathSegment("tim-kiem")
addQueryParameter("page", page.toString())
when (filter) {
is MangaListFilter.Search -> {
addQueryParameter("q", filter.query)
}
is MangaListFilter.Advanced -> {
addQueryParameter(
"sort",
when (filter.sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
SortOrder.RATING -> "like"
SortOrder.POPULARITY -> "top"
SortOrder.ALPHABETICAL -> "az"
},
)
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
addQueryParameter(
"status",
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "3"
MangaState.PAUSED -> "2"
else -> "0"
},
)
}
}
if (filter.tags.isNotEmpty()) {
val tagsQuery = filter.tags.joinToString(separator = ",") { it.key }
addEncodedQueryParameter("accept_genres", tagsQuery)
}
}
null -> {
addQueryParameter("sort", "update")
}
}
}.build()
return webClient.httpGet(url).parseHtml()
.select(".container .card.card-dark .row > .thumb-item-flow")
.mapNotNull {
val a = it.selectFirstOrThrow(".thumb-wrapper a")
Manga(
id = generateUid(a.attrAsRelativeUrl("href")),
url = a.attrAsRelativeUrl("href"),
publicUrl = a.attrAsAbsoluteUrl("href"),
title = it.select(".thumb_attr.series-title").text(),
altTitle = null,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = a.selectFirst("div[data-bg]")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val infoHeaderEl = docs.selectFirst("main.section-body")
@ -33,6 +107,7 @@ class TruyentranhLHParser(context: MangaLoaderContext) :
val state = when (infoEl?.selectFirst(".info-item:contains(Tình trạng) > .info-value")?.text()) {
"Đang tiến hành" -> MangaState.ONGOING
"Đã hoàn thành" -> MangaState.FINISHED
"Tạm ngưng" -> MangaState.PAUSED
else -> null
}
val rating = infoHeaderEl?.let {
@ -68,53 +143,6 @@ class TruyentranhLHParser(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sortQuery = when (sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
SortOrder.RATING -> "like"
SortOrder.POPULARITY -> "top"
SortOrder.ALPHABETICAL -> "az"
}
val url = urlBuilder().apply {
addPathSegment("tim-kiem")
addQueryParameter("sort", sortQuery)
addQueryParameter("page", page.toString())
if (!query.isNullOrEmpty()) {
addQueryParameter("q", query)
}
if (!tags.isNullOrEmpty()) {
val tagsQuery = tags.joinToString(separator = ",") { it.key }
addEncodedQueryParameter("accept_genres", tagsQuery)
}
}.build()
return webClient.httpGet(url).parseHtml()
.select(".container .card.card-dark .row > .thumb-item-flow")
.mapNotNull {
val a = it.selectFirstOrThrow(".thumb-wrapper a")
Manga(
id = generateUid(a.attrAsRelativeUrl("href")),
url = a.attrAsRelativeUrl("href"),
publicUrl = a.attrAsAbsoluteUrl("href"),
title = it.select(".thumb_attr.series-title").text(),
altTitle = null,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = a.selectFirst("div[data-bg]")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val url = chapter.url.toAbsoluteUrl(domain)
return webClient.httpGet(url).parseHtml().select("#chapter-content > img").mapNotNull {

@ -5,30 +5,12 @@ 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.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.json.asIterable
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
import org.koitharu.kotatsu.parsers.util.json.toJSONList
import org.koitharu.kotatsu.parsers.util.json.*
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.parsers.util.requireElementById
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.tryParse
import org.koitharu.kotatsu.parsers.util.urlEncoded
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
@ -44,44 +26,24 @@ class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, Ma
private val apiDomain
get() = "api.$domain"
override suspend fun getDetails(manga: Manga): Manga {
val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson()
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
return manga.copy(
chapters = response.getJSONArray("chapters")
.toJSONList()
.mapChapters(true) { i, jo ->
val mangaId = jo.getInt("mangaID")
val chapterId = jo.getInt("id")
MangaChapter(
id = generateUid(chapterId.toLong()),
name = jo.getString("name"),
number = i + 1,
scanlator = null,
url = "/read/$mangaId/$chapterId",
uploadDate = df.tryParse(jo.getString("date")),
branch = null,
source = source,
)
},
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val listUrl =
when (filter) {
is MangaListFilter.Search -> {
"/search?query=${filter.query.urlEncoded()}&page=$page"
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val listUrl = when {
!query.isNullOrEmpty() -> "/search?query=${query.urlEncoded()}&page=$page"
tags.isNullOrEmpty() -> "/lastest2?page=$page"
tags.size == 1 -> "/searchType?type=tag&id=${tags.first().key}&page=$page"
else -> {
// Sort order is different when filter with multiple tags
val tagKeys = tags.joinToString(separator = ",") { it.key }
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
val tagKeys = filter.tags.joinToString(separator = ",") { it.key }
"/advancedSearch?genre=$tagKeys&notGenre=&sort=7&minChapter=1&status=0&page=$page"
} else {
"/lastest2?page=$page"
}
}
null -> "/lastest2?page=$page"
}
val jsonResponse = webClient.httpGet(listUrl.toAbsoluteUrl(apiDomain)).parseJson()
return jsonResponse.getJSONArray("result")
@ -119,6 +81,29 @@ class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, Ma
}
}
override suspend fun getDetails(manga: Manga): Manga {
val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson()
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
return manga.copy(
chapters = response.getJSONArray("chapters")
.toJSONList()
.mapChapters(true) { i, jo ->
val mangaId = jo.getInt("mangaID")
val chapterId = jo.getInt("id")
MangaChapter(
id = generateUid(chapterId.toLong()),
name = jo.getString("name"),
number = i + 1,
scanlator = null,
url = "/read/$mangaId/$chapterId",
uploadDate = df.tryParse(jo.getString("date")),
branch = null,
source = source,
)
},
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val jsonData = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
.requireElementById("__NEXT_DATA__")

@ -16,8 +16,11 @@ internal abstract class VmpParser(
) : PagedMangaParser(context, source, pageSize) {
override val configKeyDomain = ConfigKey.Domain(domain)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val isMultipleTagsSupported = false
protected open val listUrl = "xxx/"
protected open val geneUrl = "genero/"
@ -26,27 +29,42 @@ internal abstract class VmpParser(
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://$domain/")
if (!tags.isNullOrEmpty()) {
append("https://")
append(domain)
append('/')
when (filter) {
is MangaListFilter.Search -> {
append(listUrl)
append("/page/")
append(page.toString())
append("?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(geneUrl)
append(tag?.key.orEmpty())
append(it.key)
append("/page/")
append(page.toString())
}
} else {
append(listUrl)
append("/page/")
append(page.toString())
if (!query.isNullOrEmpty()) {
append("?s=")
append(query.urlEncoded())
}
}
null -> {
append(listUrl)
append("/page/")
append(page.toString())
}
}
}

@ -1,11 +1,16 @@
package org.koitharu.kotatsu.parsers.site.wpcomics
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
@ -21,15 +26,16 @@ internal abstract class WpComicsParser(
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.NEWEST,
SortOrder.POPULARITY,
SortOrder.RATING,
)
protected open val listUrl = "/the-loai"
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
protected open val listUrl = "/tim-truyen-nang-cao"
protected open val datePattern = "dd/MM/yy"
@ -48,75 +54,136 @@ internal abstract class WpComicsParser(
@JvmField
protected val finished: Set<String> = setOf(
"Hoàn thành",
"Completed ",
"Completed",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val response =
when (filter) {
is MangaListFilter.Search -> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/tim-truyen?keyword=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
if (!tags.isNullOrEmpty()) {
append("/")
append(tag?.key.orEmpty())
val result = runCatchingCancellable { webClient.httpGet(url) }
val exception = result.exceptionOrNull()
if (exception is NotFoundException) {
return emptyList()
}
result.getOrThrow()
}
append("?page=")
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
val tagQuery = filter.tags.joinToString(",") { it.key }
append("/tim-truyen-nang-cao?genres=")
append(tagQuery)
append("&notgenres=&gender=-1&minchapter=1&sort=")
append(
when (filter.sortOrder) {
SortOrder.UPDATED -> 0
SortOrder.POPULARITY -> 10
SortOrder.NEWEST -> 15
SortOrder.RATING -> 20
else -> throw IllegalArgumentException("Sort order ${filter.sortOrder.name} not supported")
},
)
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
else -> "-1"
},
)
}
append("&page=")
append(page.toString())
if (!query.isNullOrEmpty()) {
append("&keyword=")
append(query.urlEncoded())
}
webClient.httpGet(url)
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("10")
SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("15")
else -> append("")
null -> {
val url = buildString {
append("https://")
append(domain)
append("/tim-truyen-nang-cao?genres=&notgenres=&gender=-1&status=-1&minchapter=1&sort=0&page=")
append(page.toString())
}
webClient.httpGet(url)
}
}
val itemsElements = response.parseHtml()
.select("div.ModuleContent > div.items")
.select("div.item")
return itemsElements.mapNotNull { item ->
val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null
val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null
val slug = absUrl.substringAfterLast('/')
val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) {
"Đang tiến hành" -> MangaState.ONGOING
"Hoàn thành" -> MangaState.FINISHED
else -> null
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val tagMap = getOrCreateTagMap()
val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty()
val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] }
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.src().orEmpty(),
title = div.selectFirstOrThrow("h3").text().orEmpty(),
id = generateUid(slug),
title = tooltipElement.selectFirst("div.title")?.text().orEmpty(),
altTitle = null,
url = absUrl.toRelativeUrl(domain),
publicUrl = absUrl,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
isNsfw = false,
coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(),
largeCoverUrl = null,
tags = mangaTags,
state = mangaState,
author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(),
description = tooltipElement.selectFirst("div.box_text")?.text(),
chapters = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("div.genres ul li:not(.active)").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
MangaTag(
key = href,
title = a.text(),
source = source,
)
val map = getOrCreateTagMap()
val tagSet = ArraySet<MangaTag>(map.size)
for (entry in map) {
tagSet.add(entry.value)
}
return tagSet
}
private val mutex = Mutex()
private var tagCache: ArrayMap<String, MangaTag>? = null
private suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val doc = webClient.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(domain)).parseHtml()
val tagItems = doc.select("div.genre-item")
val result = ArrayMap<String, MangaTag>(tagItems.size)
for (item in tagItems) {
val title = item.text().trim()
val key = item.select("span[data-id]").attr("data-id")
result[title] = MangaTag(title = title, key = key, source = source)
}
tagCache = result
result
}
protected open val selectDesc = "div.detail-content p"
@ -127,13 +194,9 @@ internal abstract class WpComicsParser(
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.selectFirst(selectState)
val state = stateDiv?.let {
when (it.text()) {
in ongoing -> MangaState.ONGOING
@ -141,17 +204,8 @@ internal abstract class WpComicsParser(
else -> null
}
}
val aut = doc.body().select(selectAut).text()
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text().toTitleCase(),
source = source,
)
},
description = desc,
altTitle = null,
author = aut,
@ -165,19 +219,16 @@ internal abstract class WpComicsParser(
protected open val selectChapter = "div#nt_listchapter li:not(.heading)"
protected open suspend fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
val dateText = li.selectFirst(selectDate)?.text()
val findHours = dateText?.contains(":")
val dateFormat = if (findHours == true) {
SimpleDateFormat("HH:mm dd/MM", sourceLocale)
} else {
SimpleDateFormat(datePattern, sourceLocale)
}
MangaChapter(
id = generateUid(href),
name = a.text(),
@ -295,5 +346,4 @@ internal abstract class WpComicsParser(
else -> 0
}
}
}

@ -1,5 +1,7 @@
package org.koitharu.kotatsu.parsers.site.wpcomics.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.model.*
@ -9,11 +11,13 @@ import java.util.EnumSet
@MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS)
internal class XoxoComics(context: MangaLoaderContext) :
WpComicsParser(context, MangaSource.XOXOCOMICS, "xoxocomics.net", 50) {
WpComicsParser(context, MangaSource.XOXOCOMICS, "xoxocomic.com", 50) {
override val listUrl = "/genre"
override val listUrl = "/comic-list"
override val datePattern = "MM/dd/yyyy"
override val isMultipleTagsSupported = false
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.NEWEST,
@ -21,46 +25,66 @@ internal class XoxoComics(context: MangaLoaderContext) :
SortOrder.ALPHABETICAL,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
if (!query.isNullOrEmpty()) {
append("/search?keyword=")
append(query.urlEncoded())
is MangaListFilter.Search -> {
append("/search-comic?keyword=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
} else {
append(listUrl)
if (!tags.isNullOrEmpty()) {
append("/")
append(tag?.key.orEmpty())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/")
when (sortOrder) {
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("newest")
SortOrder.ALPHABETICAL -> append("alphabet")
else -> append("")
append(it.key)
}
}
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "/ongoing"
MangaState.FINISHED -> "/completed"
else -> ""
},
)
if (filter.tags.isEmpty()) {
append("-comic")
}
}
if (filter.states.isEmpty() && filter.tags.isEmpty()) {
append(listUrl)
}
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("/popular")
SortOrder.UPDATED -> append("/latest")
SortOrder.NEWEST -> append("/newest")
SortOrder.ALPHABETICAL -> append("")
else -> append("/latest")
}
append("?page=")
append(page.toString())
}
null -> {
append(listUrl)
append("/?page=")
append(page.toString())
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.item").map { div ->
return doc.select("div.item, #nt_listchapter nav ul li").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
@ -79,12 +103,53 @@ internal class XoxoComics(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("div.genres ul li:not(.active)").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
MangaTag(
key = href,
title = a.text(),
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all"
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.selectFirst(selectState)
val state = stateDiv?.let {
when (it.text()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
else -> null
}
}
val aut = doc.body().select(selectAut).text()
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text().toTitleCase(),
source = source,
)
},
description = desc,
altTitle = null,
author = aut,
state = state,
chapters = chaptersDeferred.await(),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all"
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select(selectPage).map { url ->
val img = url.src()?.toRelativeUrl(domain) ?: url.parseFailed("Image src not found")
MangaPage(

@ -0,0 +1,19 @@
package org.koitharu.kotatsu.parsers.site.wpcomics.vi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi")
internal class NetTruyen(context: MangaLoaderContext) :
WpComicsParser(context, MangaSource.NETTRUYEN, "www.nettruyenlive.com", 36) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain(
"www.nettruyenlive.com",
"www.nettruyenio.com",
"www.nettruyento.com",
"nettruyento.com",
"nettruyenin.com",
)
}

@ -7,6 +7,4 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
@MangaSourceParser("NETTRUYENMAX", "NettruyenMax", "vi")
internal class Nettruyenmax(context: MangaLoaderContext) :
WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenus.com", 36) {
override val listUrl = "/tim-truyen"
}
WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenus.com", 36)

@ -21,13 +21,9 @@ internal abstract class ZMangaParser(
override val configKeyDomain = ConfigKey.Domain(domain)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.RATING,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
protected open val listUrl = "advanced-search/"
protected open val datePattern = "MMMM d, yyyy"
@ -50,42 +46,56 @@ internal abstract class ZMangaParser(
"Completed",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/$listUrl")
append('/')
append(listUrl)
if (page > 1) {
append("page/")
append(page.toString())
append("/")
append('/')
}
when (filter) {
is MangaListFilter.Search -> {
append("&title=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("?order=")
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("update")
SortOrder.ALPHABETICAL -> append("title")
SortOrder.NEWEST -> append("latest")
SortOrder.RATING -> append("rating")
}
if (!query.isNullOrEmpty()) {
append("&title=")
append(query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
filter.tags.forEach {
append("&")
append("genre[]".urlEncoded())
append("=")
append(tag.key)
append(it.key)
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> ""
},
)
}
}
null -> append("?order=update")
}
}

Loading…
Cancel
Save