From 01cc33e30971954c3e2eb0a5e891e5d4c55e1053 Mon Sep 17 00:00:00 2001 From: vianh Date: Fri, 2 Sep 2022 00:44:54 +0700 Subject: [PATCH] [NetTruyen] New source --- .../kotatsu/parsers/site/NetTruyenParser.kt | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/NetTruyenParser.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/NetTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/NetTruyenParser.kt new file mode 100644 index 000000000..ff2891992 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/NetTruyenParser.kt @@ -0,0 +1,217 @@ +package org.koitharu.kotatsu.parsers.site + +import androidx.collection.ArrayMap +import androidx.collection.ArraySet +import org.koitharu.kotatsu.parsers.InternalParsersApi +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.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(override val context: MangaLoaderContext) : MangaParser(MangaSource.NETTRUYEN) { + override val configKeyDomain: ConfigKey.Domain + get() = ConfigKey.Domain("www.nettruyenme.com", null) + + override val sortOrders: Set + get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.RATING) + + private val dateFormat = SimpleDateFormat("dd/MM/yy", Locale.US) + private var tagCache: ArrayMap? = null + + override suspend fun getDetails(manga: Manga): Manga { + val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).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.asReversed().mapChapters { 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 = listOf("giây", "phút", "giờ", "ngày") + val calendar = Calendar.getInstance() + val timeArr = timeText.split(' ') + if (timeText.containsAny(timeWords)) { + 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 + } + + + @InternalParsersApi + override suspend fun getList(offset: Int, query: String?, tags: Set?, sortOrder: SortOrder): List { + val page = (offset / 36f).toIntUp() + 1 + val isSearching = !query.isNullOrEmpty() + val url = buildString { + append("https://") + append(getDomain()) + if (isSearching) { + append("/tim-truyen?keyword=$query&page=$page") + } else { + val tagQuery = tags.orEmpty().joinToString(",") { it.key } + append("/tim-truyen-nang-cao?genres=$tagQuery") + append("¬genres=&gender=-1&status=-1&minchapter=1&sort=${getSortOrderKey(sortOrder)}") + append("&page=$page") + } + } + + val response = if (isSearching) { + val result = runCatching { context.httpGet(url) } + val exception = result.exceptionOrNull() + if (exception is NotFoundException) { + return emptyList() + } + + result.getOrThrow() + } else { + context.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")?.attr("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(getDomain()), + 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 { + val pageElements = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml() + .select("div.reading-detail.box_doc > div img") + return pageElements.map { element -> + val url = element.absUrl("data-original") + MangaPage( + id = generateUid(url), + url = url, + referer = getDomain(), + preview = null, + source = source, + ) + } + } + + override suspend fun getTags(): Set { + val map = getOrCreateTagMap() + val tagSet = ArraySet(map.size) + for (entry in map) { + tagSet.add(entry.value) + } + + return tagSet + } + + private suspend fun getOrCreateTagMap(): ArrayMap { + tagCache?.let { return it } + val doc = context.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(getDomain())).parseHtml() + val tagItems = doc.select("div.genre-item") + val result = ArrayMap(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 + return 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") + } + + private fun String.containsAny(items: List) = items.any { this.contains(it, ignoreCase = true) } + +} \ No newline at end of file