From c976e0e4e0b05cab49f1aa5933094683913fcfb8 Mon Sep 17 00:00:00 2001 From: vianh Date: Thu, 1 Sep 2022 22:17:31 +0700 Subject: [PATCH] [BlogTruyen] New source --- .gitignore | 3 +- .../kotatsu/parsers/site/BlogTruyenParser.kt | 246 ++++++++++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/BlogTruyenParser.kt diff --git a/.gitignore b/.gitignore index 49187094b..04c031559 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,5 @@ build/ .idea/**/ktlint.xml .idea/codeStyles/ -src/test/resources/cookies.txt \ No newline at end of file +src/test/resources/cookies.txt +local.properties \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/BlogTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/BlogTruyenParser.kt new file mode 100644 index 000000000..dcf6fae73 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/BlogTruyenParser.kt @@ -0,0 +1,246 @@ +package org.koitharu.kotatsu.parsers.site + +import androidx.collection.ArrayMap +import org.json.JSONArray +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.* +import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat +import java.util.* +import kotlin.collections.HashSet + +@MangaSourceParser("BLOGTRUYEN", "BlogTruyen", "vi") +class BlogTruyenParser(override val context: MangaLoaderContext): MangaParser(MangaSource.BLOGTRUYEN) { + override val configKeyDomain: ConfigKey.Domain + get() = ConfigKey.Domain("blogtruyen.vn", null) + + override val sortOrders: Set + get() = EnumSet.of(SortOrder.UPDATED) + + private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US) + private var cacheTags: ArrayMap? = null + + override fun getFaviconUrl() = "https://${getDomain()}/Content/themes/img/favicon.ico" + + override suspend fun getDetails(manga: Manga): Manga { + val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).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 tagMap = getOrCreateTagMap() + val tags = descriptionElement.select("p > span.category").mapNotNullToSet { + val tagName = it.selectFirst("a")?.text()?.trim() ?: return@mapNotNullToSet null + tagMap[tagName] + } + + return manga.copy( + tags = tags, + 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")?.attrAsAbsoluteUrlOrNull("src"), + state = state, + rating = rating ?: RATING_UNKNOWN, + isNsfw = doc.getElementById("warningCategory") != null + ) + } + + private fun parseChapterList(doc: Document): List { + val chapterList = doc.select("#list-chapters > p") + return chapterList.asReversed().mapChapters { index, element -> + val titleElement = element.selectFirst("span.title > a") ?: return@mapChapters null + val name = titleElement.text() + val relativeUrl = titleElement.attr("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 getList( + offset: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + val page = (offset / 20f).toIntUp() + 1 + return when { + !query.isNullOrEmpty() -> { + val searchUrl = "https://${getDomain()}/timkiem/nangcao/1/0/-1/-1?txt=$query&p=$page" + val searchContent = context.httpGet(searchUrl).parseHtml() + .selectFirst("section.list-manga-bycate > div.list") + parseMangaList(searchContent) + } + + !tags.isNullOrEmpty() -> { + val tag = tags.oneOrThrowIfMany()!! + val categoryAjax = "https://${getDomain()}/ajax/Category/AjaxLoadMangaByCategory?id=${tag.key}&orderBy=5&p=$page" + val listContent = context.httpGet(categoryAjax).parseHtml().selectFirst("div.list") + parseMangaList(listContent) + } + else -> getNormalList(page) + } + } + + private suspend fun getNormalList(page: Int): List { + val pageLink = "https://${getDomain()}/page-$page" + val doc = context.httpGet(pageLink).parseHtml() + val listElements = doc.selectFirstOrThrow("section.list-mainpage.listview") + .select("div.bg-white.storyitem") + + return listElements.mapNotNull { + val linkTag = it.selectFirst("div.fl-l > a") ?: return@mapNotNull null + val relativeUrl = linkTag.attr("href") + val tagMap = getOrCreateTagMap() + val tags = it.select("footer > div.category > a").mapNotNullToSet { a -> + tagMap[a.text()] + } + + Manga( + id = generateUid(relativeUrl), + title = linkTag.attr("title"), + altTitle = null, + description = it.selectFirst("p.al-j.break.line-height-15")?.text(), + url = relativeUrl, + publicUrl = relativeUrl.toAbsoluteUrl(getDomain()), + coverUrl = linkTag.selectLast("img")?.attr("src").orEmpty(), + source = source, + tags = tags, + isNsfw = false, + rating = RATING_UNKNOWN, + author = null, + state = null, + ) + } + } + + private fun parseMangaList(listElement: Element?): List { + listElement ?: return emptyList() + + return listElement.select("span.tiptip[data-tiptip]").mapNotNull { + val mangaInfo = listElement.getElementById(it.attr("data-tiptip")) ?: return@mapNotNull null + val a = it.selectFirst("a") ?: return@mapNotNull null + val relativeUrl = a.attr("href") + Manga( + id = generateUid(relativeUrl), + title = a.text(), + altTitle = null, + description = mangaInfo.select("div.al-j.fs-12").text(), + url = relativeUrl, + publicUrl = relativeUrl.toAbsoluteUrl(getDomain()), + coverUrl = mangaInfo.selectFirst("div > img.img")?.attr("src").orEmpty(), + isNsfw = false, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + ) + } + } + + override suspend fun getPages(chapter: MangaChapter): List { + fun generateImageId(index: Int) = generateUid("${chapter.url}/$index") + + val doc = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml() + val pages = ArrayList() + val referer = chapter.url.toAbsoluteUrl(getDomain()) + doc.select("#content > img").forEach { img -> + val url = img.attr("src") + pages.add( + MangaPage( + id = generateImageId(pages.lastIndex), + url = url, + referer = referer, + preview = null, + source = source, + ) + ) + } + + // Some chapters use js script to render images + val script = doc.selectLast("#content > script") + if (script != null && script.data().contains("listImageCaption")) { + val imagesStr = script.data().substringBefore(';').substringAfterLast('=').trim() + val imageArr = JSONArray(imagesStr) + for (i in 0 until imageArr.length()) { + val imageUrl = imageArr.getJSONObject(i).getString("url") + pages.add( + MangaPage( + id = generateImageId(pages.lastIndex), + url = imageUrl, + referer = referer, + preview = null, + source = source + ) + ) + } + } + + return pages + } + + override suspend fun getTags(): Set { + val map = getOrCreateTagMap() + val tags = HashSet(map.size) + for (entry in map) { + tags.add(entry.value) + } + + return tags + } + + + private suspend fun getOrCreateTagMap(): ArrayMap { + cacheTags?.let { return it } + val doc = context.httpGet("https://${getDomain()}/timkiem/nangcao").parseHtml() + val tagItems = doc.select("li[data-id]") + val tagMap = ArrayMap(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 + ) + } + + cacheTags = tagMap + return tagMap + } +} \ No newline at end of file