Merge pull request #219 from VietAnh14/source/hentaivn
Add new source HentaiVN
commit
81dd8b58c8
@ -0,0 +1,285 @@
|
||||
package org.koitharu.kotatsu.parsers.site.vi
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.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.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrl
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrlOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.insertCookies
|
||||
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.requireElementById
|
||||
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
|
||||
import org.koitharu.kotatsu.parsers.util.selectLast
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import org.koitharu.kotatsu.parsers.util.tryParse
|
||||
import org.koitharu.kotatsu.parsers.util.urlBuilder
|
||||
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.EnumSet
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
private const val SEARCH_PAGE_SIZE = 10
|
||||
|
||||
@MangaSourceParser("HENTAIVN", "HentaiVN", "vi")
|
||||
class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.HENTAIVN) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("hentaivn.autos", "hentaivn.tv")
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.RATING,
|
||||
SortOrder.NEWEST
|
||||
)
|
||||
|
||||
private val tagCache = SuspendLazy(this::fetchTags)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val chapterDeferred = async { fetchChapters(manga.url) }
|
||||
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val infoEl = docs.selectFirstOrThrow("div.container")
|
||||
.selectFirstOrThrow("div.page-info")
|
||||
|
||||
manga.copy(
|
||||
altTitle = infoEl.infoText("Tên Khác:"),
|
||||
author = infoEl.infoText("Tác giả"),
|
||||
description = infoEl.selectFirst("p:contains(Nội dung:)")
|
||||
?.nextElementSibling()
|
||||
?.outerHtml(),
|
||||
tags = tagCache.tryGet().getOrNull()?.let { tagMap ->
|
||||
infoEl.selectFirst("p:contains(Thể Loại:)")
|
||||
?.select("span > a")
|
||||
?.mapNotNullToSet {
|
||||
tagMap[it.text()]
|
||||
}
|
||||
}.orEmpty(),
|
||||
state = infoEl.infoText("Tình Trạng:")?.let {
|
||||
when (it) {
|
||||
"Đã hoàn thành" -> MangaState.FINISHED
|
||||
"Đang tiến hành" -> MangaState.ONGOING
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
rating = docs.selectFirst("div.page_like")?.let {
|
||||
val like = it.selectFirst("div.but_like")?.text()?.trim()?.toIntOrNull() ?: return@let null
|
||||
val dislike = it.selectFirst("div.but_unlike")?.text()?.trim()?.toIntOrNull() ?: return@let null
|
||||
when {
|
||||
like == 0 && dislike == 0 -> RATING_UNKNOWN
|
||||
else -> like.toFloat() / (like + dislike)
|
||||
}
|
||||
} ?: RATING_UNKNOWN,
|
||||
chapters = chapterDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
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 ids = chapter.url.removePrefix("/").split('-').take(2)
|
||||
val mangaId = ids[0].toInt()
|
||||
val chapterId = ids[1].toInt()
|
||||
val contentUrl = "/list-loadchapter.php?id_episode=$chapterId&idchapshowz=$mangaId".toAbsoluteUrl(domain)
|
||||
val docs = webClient.httpGet(contentUrl).parseHtml()
|
||||
return docs.select("img").map {
|
||||
val pageUrl = it.attrAsAbsoluteUrl("src")
|
||||
MangaPage(
|
||||
id = generateUid(pageUrl),
|
||||
url = pageUrl,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
return tagCache.get().values.toSet()
|
||||
}
|
||||
|
||||
private suspend fun fetchTags(): Map<String, MangaTag> {
|
||||
val url = "/forum/search-plus.php".toAbsoluteUrl(domain)
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
return docs.selectFirstOrThrow("ul.ul-search")
|
||||
.select("li")
|
||||
.mapNotNull { el ->
|
||||
MangaTag(
|
||||
title = el.text(),
|
||||
key = el.selectFirst("input")?.attr("value") ?: return@mapNotNull null,
|
||||
source = source,
|
||||
)
|
||||
}.associateBy { it.title }
|
||||
}
|
||||
|
||||
private fun getSortCookies(sortOrder: SortOrder): Array<String> {
|
||||
return mapOf(
|
||||
SortOrder.POPULARITY to "view4",
|
||||
SortOrder.RATING to "view",
|
||||
SortOrder.NEWEST to "view0",
|
||||
).map { (order, cookieKey) ->
|
||||
val data = if (order == sortOrder) "1" else "0"
|
||||
"${cookieKey}=$data"
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private fun parseMainList(docs: Document, page: Int): List<Manga> {
|
||||
val realPage = docs.selectFirst("ul.pagination > li > b")?.text()?.toIntOrNull() ?: 1
|
||||
if (page > realPage) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return docs.selectFirstOrThrow("div.main")
|
||||
.selectFirstOrThrow("div.block-item")
|
||||
.select("ul > li.item")
|
||||
.map { el ->
|
||||
val relativeUrl = el.selectFirstOrThrow("div.box-cover > a").attrAsRelativeUrl("href")
|
||||
val descriptionsEl = el.selectFirstOrThrow("div.box-description")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = descriptionsEl.selectFirst("p > a")?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = el.selectFirst("div.box-cover img").imageUrl(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseAdvanceSearch(docs: Document, page: Int): List<Manga> {
|
||||
val realPage = docs.selectFirst("ul.pagination > li > b")?.text()?.toIntOrNull() ?: 1
|
||||
if (page > realPage) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return docs.requireElementById("main")
|
||||
.selectFirstOrThrow("ul.search-ul")
|
||||
.select("li.search-li")
|
||||
.mapNotNull { el ->
|
||||
val titleEl = el.selectFirst("div.search-des > a") ?: return@mapNotNull null
|
||||
val relativeUrl = titleEl.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = titleEl.text(),
|
||||
altTitle = null,
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = el.selectFirst("div.search-img img")
|
||||
?.attrAsAbsoluteUrlOrNull("data-cfsrc")
|
||||
.orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchChapters(mangaUrl: String): List<MangaChapter> {
|
||||
val slug = mangaUrl.substringAfterLast("/")
|
||||
.removeSuffix(".html")
|
||||
val name = slug.substringAfter("-")
|
||||
val id = slug.substringBefore("-").toInt()
|
||||
val chaptersAjax = "/list-showchapter.php?idchapshow=$id&idlinkanime=$name".toAbsoluteUrl(domain)
|
||||
val chaptersEl = webClient.httpGet(chaptersAjax).parseHtml()
|
||||
val chapterDateFormat = SimpleDateFormat("dd/MM/yyyy")
|
||||
return chaptersEl.select("tbody > tr")
|
||||
.mapChapters(reversed = true) { index, element ->
|
||||
val titleEl = element.selectFirst("td > a") ?: return@mapChapters null
|
||||
val dateStr = element.selectLast("td")?.text()
|
||||
MangaChapter(
|
||||
id = generateUid(titleEl.attrAsRelativeUrl("href")),
|
||||
name = titleEl.text(),
|
||||
number = index + 1,
|
||||
url = titleEl.attrAsRelativeUrl("href"),
|
||||
scanlator = null,
|
||||
uploadDate = chapterDateFormat.tryParse(dateStr),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element?.imageUrl(): String {
|
||||
if (this == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return attrAsRelativeUrlOrNull("data-src")
|
||||
?: attrAsRelativeUrlOrNull("data-srcset")
|
||||
?: attrAsRelativeUrlOrNull("src")
|
||||
?: ""
|
||||
}
|
||||
|
||||
private fun Element.infoText(title: String) = selectFirst("span.info:contains($title)")
|
||||
?.parent()
|
||||
?.select("span:not(.info) > a")
|
||||
?.joinToString { it.text() }
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue