From 73dd8c468822e3935afbd8ddb595696714a004ef Mon Sep 17 00:00:00 2001 From: Draken Date: Sun, 9 Feb 2025 20:21:28 +0000 Subject: [PATCH] [TruyenHentaiVN] Add source --- .github/summary.yaml | 2 +- .../kotatsu/parsers/site/vi/TruyenHentaiVN.kt | 176 ++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt diff --git a/.github/summary.yaml b/.github/summary.yaml index 58f3065c..4b34760a 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1180 +total: 1181 diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt new file mode 100644 index 00000000..25b12481 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyenHentaiVN.kt @@ -0,0 +1,176 @@ +package org.koitharu.kotatsu.parsers.site.vi + +import androidx.collection.arraySetOf +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 java.util.* +import java.text.SimpleDateFormat + +@MangaSourceParser("TRUYENHENTAIVN", "TruyenHentaiVN", "vi", type = ContentType.HENTAI) +internal class TruyenHentaiVN(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.TRUYENHENTAIVN, 30) { + + override val configKeyDomain = ConfigKey.Domain("truyenhentaivn.live") + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + isSearchWithFiltersSupported = false + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions(availableTags = getAvailableTags()) + + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + + when { + !filter.tags.isNullOrEmpty() -> { + val tag = filter.tags.first() + append("/") + append(tag.key) + if (page > 1) { + append("?page=") + append(page) + } + } + + !filter.query.isNullOrEmpty() -> { + append("/tim-kiem-truyen/?q=") + append(filter.query.urlEncoded()) + if (page > 1) { + append("&page=") + append(page) + } + } + + else -> { + append("/chap-moi") + if (page > 1) { + append("?page=") + append(page) + } + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + + return doc.select("div.entry").map { element -> + val href = element.selectFirst("a")?.attrAsRelativeUrl("href") ?: "" + val title = element.selectFirst("a.name")?.text() ?: "" + val cover = element.selectFirst("img")?.src() + val dateText = element.selectFirst("span.date-time")?.text() + + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + title = title, + altTitle = null, + author = null, + tags = emptySet(), + rating = RATING_UNKNOWN, + state = null, + coverUrl = cover, + isNsfw = true, + source = source + ) + } + } + + private suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain").parseHtml() + return doc.select("a.py-2[href^=/the-loai-]").mapNotNull { element -> + val href = element.attr("href") + val key = href.removePrefix("/") + val title = element.text() + + if (key.isNotEmpty() && title.isNotEmpty()) { + MangaTag( + key = key, + title = title, + source = source + ) + } else null + }.toSet() + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + return manga.copy( + author = doc.selectFirst("div.author i")?.text(), + tags = doc.select("div.genre.mb-3.mgen a").mapNotNull { a -> + val key = a.attr("href").substringAfterLast("-") + val title = a.text().trim() + if (key.isNotEmpty() && title.isNotEmpty()) { + MangaTag( + key = key, + title = title, + source = source + ) + } else null + }.toSet(), + description = doc.selectFirst("div.inner.mb-1.full")?.let { div -> + div.select("p").joinToString("\n") { it.wholeText() } + }, + coverUrl = doc.selectFirst("div.book img")?.src(), + state = when(doc.selectFirst("div.tsinfo .imptdt i")?.text()?.trim()) { + "Đã hoàn thành" -> MangaState.FINISHED + "Đang tiến hành" -> MangaState.ONGOING + else -> null + }, + chapters = doc.select("div.chap-list .d-flex").mapChapters(reversed = true) { i, div -> + val url = div.selectFirst("a")?.attrAsRelativeUrl("href") ?: "" + val name = div.selectFirst("a .name")?.text() ?: "" + val dateStr = div.selectFirst("a span:last-child")?.text() + + val uploadDate = dateStr?.let { + try { + SimpleDateFormat("dd-MM-yyyy", Locale.US).parse(it)?.time ?: 0L + } catch (e: Exception) { + 0L + } + } ?: 0L + + MangaChapter( + id = generateUid(url), + name = name, + number = i + 1f, + url = url, + scanlator = null, + uploadDate = uploadDate, + branch = null, + source = source, + volume = 0 + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + return doc.select("div.content-text img").mapNotNull { img -> + val url = img.requireSrc().toAbsoluteUrl(domain) + if (url.isNotEmpty()) { + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source + ) + } else null + } + } +} \ No newline at end of file