commit
f4b678083d
@ -1 +1 @@
|
|||||||
total: 1177
|
total: 1178
|
||||||
@ -0,0 +1,241 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.vi
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
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.ContentRating
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
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.attrAsRelativeUrl
|
||||||
|
import org.koitharu.kotatsu.parsers.util.attrOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.attrOrThrow
|
||||||
|
import org.koitharu.kotatsu.parsers.util.domain
|
||||||
|
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||||
|
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
|
||||||
|
import org.koitharu.kotatsu.parsers.util.src
|
||||||
|
import org.koitharu.kotatsu.parsers.util.textOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toRelativeUrl
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||||
|
import java.util.EnumSet
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.SecretKeyFactory
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@MangaSourceParser("VCOMYCS", "Vcomycs", "vi", ContentType.MANGA)
|
||||||
|
internal class VcomycsParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.VCOMYCS, 36) {
|
||||||
|
|
||||||
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
|
get() = ConfigKey.Domain("vivicomi.store")
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder>
|
||||||
|
get() = EnumSet.of(SortOrder.UPDATED)
|
||||||
|
|
||||||
|
override val filterCapabilities: MangaListFilterCapabilities
|
||||||
|
get() = MangaListFilterCapabilities(
|
||||||
|
isSearchSupported = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions(
|
||||||
|
availableTags = fetchTags(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun fetchTags(): Set<MangaTag> {
|
||||||
|
return webClient.httpGet("/so-do-trang".toAbsoluteUrl(domain)).parseHtml()
|
||||||
|
.selectFirstOrThrow(".sitemap-content .tags")
|
||||||
|
.select("a")
|
||||||
|
.mapToSet(::parseTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTag(tagEl: Element): MangaTag {
|
||||||
|
return MangaTag(
|
||||||
|
title = tagEl.text().toTitleCase(),
|
||||||
|
key = tagEl.attrAsRelativeUrl("href"),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
if (!filter.query.isNullOrEmpty()) {
|
||||||
|
if (page > 1) return emptyList()
|
||||||
|
|
||||||
|
val payload = "action=searchtax&keyword=${filter.query.urlEncoded()}"
|
||||||
|
return webClient.httpPost("/wp-admin/admin-ajax.php".toAbsoluteUrl(domain), payload)
|
||||||
|
.parseJson().getJSONArray("data")
|
||||||
|
.mapJSONNotNull { jo ->
|
||||||
|
val status = jo.getString("cstatus")
|
||||||
|
if (status == "Nhóm dịch" || status == "Tin tức") return@mapJSONNotNull null
|
||||||
|
|
||||||
|
val relativeUrl = jo.getString("link").toRelativeUrl(domain)
|
||||||
|
Manga(
|
||||||
|
id = generateUid(relativeUrl),
|
||||||
|
title = jo.getString("title"),
|
||||||
|
altTitle = null,
|
||||||
|
url = relativeUrl,
|
||||||
|
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
contentRating = null,
|
||||||
|
coverUrl = jo.getString("img"),
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
author = null,
|
||||||
|
largeCoverUrl = null,
|
||||||
|
description = null,
|
||||||
|
chapters = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = filter.tags.oneOrThrowIfMany()?.let { "${it.key}?page=$page" } ?: "/page/$page"
|
||||||
|
val pageContent = webClient.httpGet(url.toAbsoluteUrl(domain)).parseHtml()
|
||||||
|
if (pageContent.selectFirst(".pnf-404")?.text() == "Hết trang rồi!") {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseMangaList(pageContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMangaList(page: Document): List<Manga> {
|
||||||
|
return page.selectFirstOrThrow("div.comic-list")
|
||||||
|
.select(".comic-img").map { item ->
|
||||||
|
val linkEl = item.selectFirstOrThrow("a")
|
||||||
|
val relativeUrl = linkEl.attrAsRelativeUrl("href")
|
||||||
|
Manga(
|
||||||
|
id = generateUid(relativeUrl),
|
||||||
|
title = linkEl.attrOrThrow("title"),
|
||||||
|
altTitle = null,
|
||||||
|
url = relativeUrl,
|
||||||
|
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
contentRating = null,
|
||||||
|
coverUrl = linkEl.selectFirstOrThrow(".img-thumbnail").src(),
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
author = null,
|
||||||
|
largeCoverUrl = null,
|
||||||
|
description = null,
|
||||||
|
chapters = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val content = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||||
|
val info = content.selectFirstOrThrow(".comic-info")
|
||||||
|
return manga.copy(
|
||||||
|
rating = info.getElementById("cate-rating")?.let {
|
||||||
|
val score = it.attrOrNull("data-score")?.toIntOrNull()
|
||||||
|
val vote = it.attrOrNull("data-votes")?.toIntOrNull()
|
||||||
|
if (score == null || vote == null || vote == 0) return@let null
|
||||||
|
score / (vote * 10f)
|
||||||
|
} ?: RATING_UNKNOWN,
|
||||||
|
altTitle = info.selectFirst(".comic-intro-text > strong:contains(Tên khác:)")?.nextElementSibling()
|
||||||
|
?.textOrNull(),
|
||||||
|
author = info.selectFirst(".comic-intro-text > strong:contains(Tác giả:)")?.nextElementSibling()
|
||||||
|
?.textOrNull(),
|
||||||
|
state = when (info.selectFirst(".comic-stt")?.text()) {
|
||||||
|
"Đang tiến hành" -> MangaState.ONGOING
|
||||||
|
"Trọn bộ" -> MangaState.FINISHED
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
tags = info.select("div.tags > a").mapToSet(::parseTag),
|
||||||
|
description = content.selectFirst(".intro-container > div.text-justify")?.let {
|
||||||
|
it.selectFirst(".hide-long-text-shadow")?.remove()
|
||||||
|
it.html()
|
||||||
|
},
|
||||||
|
contentRating = if (content.getElementById("adult-modal") != null) {
|
||||||
|
ContentRating.ADULT
|
||||||
|
} else {
|
||||||
|
ContentRating.SAFE
|
||||||
|
},
|
||||||
|
chapters = content.select(".chapter-table .table-scroll tbody > tr a")
|
||||||
|
.mapChapters(reversed = true) { index, element ->
|
||||||
|
val url = element.attrAsRelativeUrl("href")
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid(url),
|
||||||
|
name = element.selectFirst("span")?.text().orEmpty().trim(),
|
||||||
|
number = index + 1f,
|
||||||
|
volume = 0,
|
||||||
|
url = url,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = 0L,
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val encryptedContent = webClient.httpGet(chapter.url.toAbsoluteUrl(domain))
|
||||||
|
.parseHtml()
|
||||||
|
.selectFirstOrThrow("#view-chapter script").data()
|
||||||
|
.substringAfter('\"')
|
||||||
|
.substringBeforeLast('\"')
|
||||||
|
.replace("\\\"", "\"")
|
||||||
|
|
||||||
|
val images = decryptImages(encryptedContent)
|
||||||
|
return Jsoup.parse(images).select("img").map { img ->
|
||||||
|
val url = img.attrOrThrow("data-ehwufp")
|
||||||
|
.replace("EhwuFp", ".")
|
||||||
|
.replace("SJkhMV", ":")
|
||||||
|
.replace("uUPzrw", "/")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptImages(secret: String): String {
|
||||||
|
val json = JSONObject(secret)
|
||||||
|
val salt = json.getString("salt").decodeHex()
|
||||||
|
val iv = json.getString("iv").decodeHex()
|
||||||
|
val cipherText = context.decodeBase64(json.getString("ciphertext"))
|
||||||
|
|
||||||
|
val keySpec = PBEKeySpec("EhwuFpSJkhMVuUPzrw".toCharArray(), salt, 999, 256)
|
||||||
|
val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512").generateSecret(keySpec).encoded
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(secretKey, "AES"), IvParameterSpec(iv))
|
||||||
|
return cipher.doFinal(cipherText).toString(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||||
|
val pageDocument = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml()
|
||||||
|
return parseMangaList(pageDocument)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.decodeHex(): ByteArray {
|
||||||
|
check(length % 2 == 0) { "Must have an even length" }
|
||||||
|
|
||||||
|
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue