diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt index a8550d25..584a5a05 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/Hentai18VN.kt @@ -1,17 +1,21 @@ package org.koitharu.kotatsu.parsers.site.vi import androidx.collection.arraySetOf +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.json.JSONObject +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser -import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat import java.util.* @MangaSourceParser("HENTAI18VN", "Hentai18VN", "vi", type = ContentType.HENTAI) -internal class Hentai18VN(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.HENTAI18VN, 60) { +internal class Hentai18VN(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.HENTAI18VN, 30) { override val configKeyDomain = ConfigKey.Domain("hentai18vn.art") @@ -20,18 +24,220 @@ internal class Hentai18VN(context: MangaLoaderContext) : PagedMangaParser(contex keys.add(userAgentKey) } - override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) - override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) - override val filterCapabilities: MangaListFilterCapabilities get() = MangaListFilterCapabilities( isSearchSupported = true, - isSearchWithFiltersSupported = false, + isSearchWithFiltersSupported = false ) - - override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchAvailableTags(), - ) + override suspend fun getFilterOptions() = MangaListFilterOptions(availableTags = fetchAvailableTags()) + override val availableSortOrders: Set = EnumSet.of( + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.UPDATED + ) + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + return when { + !filter.query.isNullOrEmpty() -> { + if (page > 1) { + return emptyList() + } + val url = "https://$domain/search/html/1".toHttpUrl() + val body = JSONObject().apply { + put("keyword", filter.query.urlEncoded()) + } + + val headers = Headers.Builder() + .add("X-Requested-With", "XMLHttpRequest") + .build() + + val response = webClient.httpPost(url, body, headers).parseHtml() + parseMangaSearch(response) + } + + !filter.tags.isNullOrEmpty() -> { + val tag = filter.tags.first() + val url = buildString { + append("https://") + append(domain) + append("/the-loai/") + append(tag.key) + if (page > 1) { + append("?page=") + append(page) + } + } + val response = webClient.httpGet(url).parseHtml() + parseMangaList(response) + } + + else -> { + val url = buildString { + append("https://") + append(domain) + append("/") + append( + when (order) { + SortOrder.NEWEST -> "danh-sach/truyen-hentai-moi" + SortOrder.POPULARITY -> "danh-sach/truyen-hentai-hot" + SortOrder.UPDATED -> "danh-sach/truyen-hentai-hoan-thanh" + else -> "danh-sach/truyen-hentai-hay" + } + ) + if (page > 1) { + append("?page=") + append(page) + } + } + + val response = webClient.httpGet(url).parseHtml() + parseMangaList(response) + } + } + } + + private fun parseMangaSearch(doc: Document): List { + return doc.select("ul li a.item").map { a -> + val href = a.attr("href") + val img = a.selectFirst("img")!! + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + title = img.attr("alt"), + altTitle = null, + author = null, + tags = emptySet(), + rating = RATING_UNKNOWN, + state = null, + coverUrl = img.attr("src"), + isNsfw = isNsfwSource, + source = source + ) + } + } + + private fun parseMangaList(doc: Document): List { + return doc.select("div.visual").map { div -> + val a = div.selectFirst("div.main_text h3.title a")!! + val img = div.selectFirst("div.hentai-cover img")!! + val mangaUrl = a.attr("href") + Manga( + id = generateUid(mangaUrl), + publicUrl = mangaUrl, + url = mangaUrl.removePrefix("https://$domain"), + title = a.text(), + altTitle = null, + author = null, + description = null, + tags = emptySet(), + rating = RATING_UNKNOWN, + state = null, + coverUrl = img.attr("data-original").takeIf { it.isNotEmpty() } ?: img.attr("src"), + isNsfw = isNsfwSource, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val tags = doc.select("div.hentai-info .line-content a.item-tag") + .mapNotNull { a -> + MangaTag( + title = a.text(), + key = a.attr("href").substringAfterLast("/"), + source = source + ) + }.toSet() + + val chapters = doc.select("ul#chapter-list li.citem").map { li -> + val a = li.selectFirst("a")!! + val number = a.text().substringAfter("Chap ").toFloatOrNull() ?: 0f + MangaChapter( + id = generateUid(a.attr("href")), + name = a.text(), + number = number, + url = a.attr("href").removePrefix("https://$domain"), + uploadDate = parseChapterDate(li.selectFirst(".time")?.text()), + source = source, + scanlator = null, + branch = null, + volume = 0 + ) + } + + val altTitle = doc.selectFirst("h2.alternative")?.text() + val author = doc.selectFirst("div.hentai-info .line:contains(Tác giả) .line-content")?.text() + val state = when(doc.selectFirst("div.hentai-info .line:contains(Tình trạng) .line-content")?.text()) { + "Đang cập nhật" -> MangaState.ONGOING + "Hoàn thành" -> MangaState.FINISHED + else -> null + } + + return manga.copy( + tags = tags, + author = author?.takeUnless { it == "Đang cập nhật" }, + altTitle = altTitle, + state = state, + chapters = chapters, + description = doc.select("div.about").text() + ) + } + + private fun parseChapterDate(date: String?): Long { + if (date == null) return 0 + return try { + val now = SimpleDateFormat("dd/MM/yyyy", Locale.US) + now.parse(date)?.time ?: 0L + } catch (e: Exception) { + 0L + } + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + return doc.select("div.chapter-content div.item-photo img").mapNotNull { img -> + val url = img.requireSrc() + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source + ) + } + } + + private suspend fun fetchAvailableTags(): Set { + val tags = arraySetOf() + val firstPage = webClient.httpGet("https://$domain/tim-the-loai").parseHtml() + val lastPage = firstPage.selectFirst("a[aria-label=Last]") + ?.attr("href") + ?.substringAfter("page=") + ?.toIntOrNull() ?: 1 + + for (page in 1..lastPage) { + val doc = if (page == 1) { + firstPage + } else { + webClient.httpGet("https://$domain/tim-the-loai?page=$page").parseHtml() + } + + doc.select("ul.list-tags li").forEach { li -> + val a = li.selectFirst("a") ?: return@forEach + val title = a.selectFirst("h3.tag-name")?.text()?.trim() ?: return@forEach + val url = a.attr("href") + tags.add( + MangaTag( + title = title, + key = url.substringAfterLast("/"), + source = source + ) + ) + } + } + + return tags + } } \ No newline at end of file