[Vcomycs] New source

master
ViAnh 1 year ago
parent 677fd51e91
commit 0b6af2549f

@ -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.DOUJINSHI)
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…
Cancel
Save