GocTruyenTranhVui + KuroNeko: Fixes (#2165)

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
master
Hoàng Phi Hùng 8 months ago committed by GitHub
parent b8d3124227
commit 8573921243
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -14,274 +14,264 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.json.JSONObject
import java.util.*
import org.koitharu.kotatsu.parsers.Broken
@Broken("Need to clean code + Testing")
@MangaSourceParser("GOCTRUYENTRANHVUI", "Góc Truyện Tranh Vui", "vi")
internal class GocTruyenTranhVui(context: MangaLoaderContext):
PagedMangaParser(context, MangaParserSource.GOCTRUYENTRANHVUI, 50) {
internal class GocTruyenTranhVui(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.GOCTRUYENTRANHVUI, 50) {
override val configKeyDomain = ConfigKey.Domain("goctruyentranhvui17.com")
override val configKeyDomain = ConfigKey.Domain("goctruyentranhvui17.com")
private val apiUrl by lazy { "https://$domain/api/v2" }
private val apiUrl = "https://$domain/api/v2"
private val requestMutex = Mutex()
private var lastRequestTime = 0L
private val requestMutex = Mutex()
private var lastRequestTime = 0L
override fun getRequestHeaders(): Headers = Headers.Builder()
.add("Authorization", TOKEN_KEY)
.add("Referer", "https://$domain/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
private val apiHeaders by lazy {
Headers.Builder()
.add("Authorization", TOKEN_KEY)
.add("Referer", "https://$domain/")
.add("X-Requested-With", "XMLHttpRequest")
.build()
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.RATING,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.RATING
)
override val filterCapabilities = MangaListFilterCapabilities(
isSearchSupported = true,
isMultipleTagsSupported = true,
)
override val filterCapabilities = MangaListFilterCapabilities(
isSearchSupported = true,
isMultipleTagsSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = availableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = availableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
)
init {
setFirstPage(0)
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
enforceRateLimit()
val url = buildString {
append(apiUrl)
append("/search?p=${page - 1}")
if (!filter.query.isNullOrBlank()) {
append("&searchValue=${filter.query.urlEncoded()}")
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
enforceRateLimit()
val url = buildString {
append(apiUrl)
append("/search?p=$page")
if (!filter.query.isNullOrBlank()) {
append("&searchValue=${filter.query.urlEncoded()}")
}
val sortValue = when (order) {
SortOrder.POPULARITY -> "viewCount"
SortOrder.NEWEST -> "createdAt"
SortOrder.RATING -> "evaluationScore"
else -> "recentDate" // UPDATED
}
append("&orders%5B%5D=$sortValue")
val sortValue = when (order) {
SortOrder.POPULARITY -> "viewCount"
SortOrder.NEWEST -> "createdAt"
SortOrder.RATING -> "evaluationScore"
else -> "recentDate" // UPDATED
}
append("&orders%5B%5D=$sortValue")
filter.tags.forEach { append("&categories%5B%5D=${it.key}") }
filter.tags.forEach { append("&categories%5B%5D=${it.key}") }
filter.states.forEach {
val statusKey = when (it) {
MangaState.ONGOING -> "PRG"
MangaState.FINISHED -> "END"
else -> null
}
if (statusKey != null) append("&status%5B%5D=$statusKey")
}
}
filter.states.forEach {
val statusKey = when (it) {
MangaState.ONGOING -> "PRG"
MangaState.FINISHED -> "END"
else -> null
}
if (statusKey != null) append("&status%5B%5D=$statusKey")
}
}
val json = webClient.httpGet(url, extraHeaders = apiHeaders).parseJson()
val result = json.optJSONObject("result") ?: return emptyList()
val data = result.optJSONArray("data") ?: return emptyList()
val json = webClient.httpGet(url).parseJson()
val result = json.optJSONObject("result") ?: return emptyList()
val data = result.optJSONArray("data") ?: return emptyList()
return List(data.length()) { i ->
val item = data.getJSONObject(i)
val comicId = item.getString("id")
val slug = item.getString("nameEn")
val mangaUrl = "/truyen/$slug"
val tags = item.optJSONArray("category")?.let { arr ->
(0 until arr.length()).mapNotNullTo(mutableSetOf()) { index ->
val tagName = arr.getString(index)
availableTags().find { it.title.equals(tagName, ignoreCase = true) }?.let { genrePair ->
MangaTag(key = genrePair.key, title = genrePair.title, source = source)
}
}
} ?: emptySet()
return List(data.length()) { i ->
val item = data.getJSONObject(i)
val comicId = item.getString("id")
val slug = item.getString("nameEn")
val mangaUrl = "/truyen/$slug"
val tags = item.optJSONArray("category")?.let { arr ->
(0 until arr.length()).mapNotNullTo(mutableSetOf()) { index ->
val tagName = arr.getString(index)
availableTags().find { it.title.equals(tagName, ignoreCase = true) }?.let { genrePair ->
MangaTag(key = genrePair.key, title = genrePair.title, source = source)
}
}
} ?: emptySet()
Manga(
id = generateUid(comicId),
title = item.getString("name"),
altTitles = item.optString("otherName", "").split(",").mapNotNull { it.trim().takeIf(String::isNotBlank) }.toSet(),
url = "$comicId:$slug", // Store both id and slug, separated by ':'
publicUrl = "https://$domain$mangaUrl",
rating = item.optDouble("evaluationScore", 0.0).toFloat(),
contentRating = null,
coverUrl = "https://$domain${item.getString("photo")}",
tags = tags,
state = when (item.optString("statusCode")) {
"PRG" -> MangaState.ONGOING
"END" -> MangaState.FINISHED
else -> null
},
authors = setOf(item.optString("author", "Updating")),
source = source
)
}
}
Manga(
id = generateUid(comicId),
title = item.getString("name"),
altTitles = item.optString("otherName", "").split(",").mapNotNull { it.trim().takeIf(String::isNotBlank) }.toSet(),
url = "$comicId:$slug", // Store both id and slug, separated by ':'
publicUrl = "https://$domain$mangaUrl",
rating = item.optDouble("evaluationScore", 0.0).toFloat(),
contentRating = null,
coverUrl = "https://$domain${item.getString("photo")}",
tags = tags,
state = when (item.optString("statusCode")) {
"PRG" -> MangaState.ONGOING
"END" -> MangaState.FINISHED
else -> null
},
authors = setOf(item.optString("author", "Updating")),
source = source
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val comicId = manga.url.substringBefore(':')
val slug = manga.url.substringAfter(':')
override suspend fun getDetails(manga: Manga): Manga {
val comicId = manga.url.substringBefore(':')
val slug = manga.url.substringAfter(':')
val chapters = try {
enforceRateLimit()
val chapterApiUrl = "https://$domain/api/comic/$comicId/chapter?limit=-1"
val chapterJson = webClient.httpGet(chapterApiUrl, extraHeaders = apiHeaders).parseJson()
val chaptersData = chapterJson.getJSONObject("result").getJSONArray("chapters")
val chapters = try {
enforceRateLimit()
val chapterApiUrl = "https://$domain/api/comic/$comicId/chapter?limit=-1"
val chapterJson = webClient.httpGet(chapterApiUrl).parseJson()
val chaptersData = chapterJson.getJSONObject("result").getJSONArray("chapters")
List(chaptersData.length()) { i ->
val item = chaptersData.getJSONObject(i)
val number = item.getString("numberChapter")
val name = item.getString("name")
val chapterUrl = "/truyen/$slug/chuong-$number"
MangaChapter(
id = generateUid(chapterUrl),
title = if (name != "N/A" && name.isNotBlank()) name else "Chapter $number",
number = number.toFloatOrNull() ?: -1f,
volume = 0,
url = chapterUrl,
scanlator = null,
uploadDate = item.optLong("updateTime", 0L),
branch = null,
source = source
)
}
} catch (e: Exception) {
emptyList()
}.reversed()
List(chaptersData.length()) { i ->
val item = chaptersData.getJSONObject(i)
val number = item.getString("numberChapter")
val name = item.getString("name")
val chapterUrl = "/truyen/$slug/chuong-$number"
MangaChapter(
id = generateUid(chapterUrl),
title = if (name != "N/A" && name.isNotBlank()) name else "Chapter $number",
number = number.toFloatOrNull() ?: -1f,
volume = 0,
url = chapterUrl,
scanlator = null,
uploadDate = item.optLong("updateTime", 0L),
branch = null,
source = source
)
}
} catch (e: Exception) {
emptyList()
}.reversed()
enforceRateLimit()
val doc = webClient.httpGet(manga.publicUrl).parseHtml()
enforceRateLimit()
val doc = webClient.httpGet(manga.publicUrl).parseHtml()
val detailTags = doc.select(".group-content > .v-chip-link").mapNotNullTo(mutableSetOf()) { el ->
availableTags().find { it.title.equals(el.text(), ignoreCase = true) }?.let {
MangaTag(key = it.key, title = it.title, source = source)
}
}
val detailTags = doc.select(".group-content > .v-chip-link").mapNotNullTo(mutableSetOf()) { el ->
availableTags().find { it.title.equals(el.text(), ignoreCase = true) }?.let {
MangaTag(key = it.key, title = it.title, source = source)
}
}
return manga.copy(
title = doc.selectFirst(".v-card-title")?.text().orEmpty(),
tags = manga.tags + detailTags,
coverUrl = doc.selectFirst("img.image")?.absUrl("src"),
state = when (doc.selectFirst(".mb-1:contains(Trạng thái:) span")?.text()) {
"Đang thực hiện" -> MangaState.ONGOING
"Hoàn thành" -> MangaState.FINISHED
else -> manga.state
},
authors = setOfNotNull(doc.selectFirst(".mb-1:contains(Tác giả:) span")?.text()),
description = doc.selectFirst(".v-card-text")?.text(),
chapters = chapters
)
}
return manga.copy(
title = doc.selectFirst(".v-card-title")?.text().orEmpty(),
tags = manga.tags + detailTags,
coverUrl = doc.selectFirst("img.image")?.absUrl("src"),
state = when (doc.selectFirst(".mb-1:contains(Trạng thái:) span")?.text()) {
"Đang thực hiện" -> MangaState.ONGOING
"Hoàn thành" -> MangaState.FINISHED
else -> manga.state
},
authors = setOfNotNull(doc.selectFirst(".mb-1:contains(Tác giả:) span")?.text()),
description = doc.selectFirst(".v-card-text")?.text(),
chapters = chapters
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
enforceRateLimit()
val responseBody = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).body?.string()
?: throw Exception("Response body is null for chapter page")
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
enforceRateLimit()
val responseBody = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).body?.string()
?: throw Exception("Response body is null for chapter page")
val chapterJsonRaw = responseBody.substringAfter("chapterJson: `", "").substringBefore("`", "")
val chapterJsonRaw = responseBody.substringAfter("chapterJson: `", "").substringBefore("`", "")
val imageUrls: List<String>
if (chapterJsonRaw.isNotBlank()) {
val json = JSONObject(chapterJsonRaw)
val data = json.getJSONObject("body").getJSONObject("result").getJSONArray("data")
imageUrls = List(data.length()) { i -> data.getString(i) }
} else {
// Fallback: Call the authenticated API
val comicId = responseBody.substringAfter("comic = {id:\"", "").substringBefore("\"", "")
val chapterNumber = chapter.url.substringAfterLast("chuong-")
val nameEn = chapter.url.substringAfter("/truyen/").substringBefore("/chuong-")
val imageUrls: List<String>
if (chapterJsonRaw.isNotBlank()) {
val json = JSONObject(chapterJsonRaw)
val data = json.getJSONObject("body").getJSONObject("result").getJSONArray("data")
imageUrls = List(data.length()) { i -> data.getString(i) }
} else {
// Fallback: Call the authenticated API
val comicId = responseBody.substringAfter("comic = {id:\"", "").substringBefore("\"", "")
val chapterNumber = chapter.url.substringAfterLast("chuong-")
val nameEn = chapter.url.substringAfter("/truyen/").substringBefore("/chuong-")
if (comicId.isBlank()) {
throw Exception("Cannot find comicId in HTML for fallback image request")
}
if (comicId.isBlank()) {
throw Exception("Cannot find comicId in HTML for fallback image request")
}
val formBody = mapOf(
"comicId" to comicId,
"chapterNumber" to chapterNumber,
"nameEn" to nameEn
)
val authApiUrl = "$apiUrl/chapter/auth".toHttpUrl()
val authResponse = webClient.httpPost(url = authApiUrl, form = formBody, extraHeaders = apiHeaders).parseJson()
val data = authResponse.getJSONObject("result").getJSONArray("data")
imageUrls = List(data.length()) { i -> data.getString(i) }
}
val formBody = mapOf(
"comicId" to comicId,
"chapterNumber" to chapterNumber,
"nameEn" to nameEn
)
val authApiUrl = "$apiUrl/chapter/auth".toHttpUrl()
val authResponse = webClient.httpPost(url = authApiUrl, form = formBody).parseJson()
val data = authResponse.getJSONObject("result").getJSONArray("data")
imageUrls = List(data.length()) { i -> data.getString(i) }
}
return imageUrls.map { url ->
val finalUrl = if (url.startsWith("/image/")) "https://$domain$url" else url
MangaPage(id = generateUid(finalUrl), url = finalUrl, preview = null, source = source)
}
}
return imageUrls.map { url ->
val finalUrl = if (url.startsWith("/image/")) "https://$domain$url" else url
MangaPage(
id = generateUid(finalUrl),
url = finalUrl,
preview = null,
source = source
)
}
}
private suspend fun enforceRateLimit() {
requestMutex.withLock {
val currentTime = System.currentTimeMillis()
val timeSinceLastRequest = currentTime - lastRequestTime
if (timeSinceLastRequest < REQUEST_DELAY_MS) { // Vẫn truy cập được REQUEST_DELAY_MS
delay(REQUEST_DELAY_MS - timeSinceLastRequest)
}
lastRequestTime = System.currentTimeMillis()
}
}
private suspend fun enforceRateLimit() {
requestMutex.withLock {
val currentTime = System.currentTimeMillis()
val timeSinceLastRequest = currentTime - lastRequestTime
if (timeSinceLastRequest < REQUEST_DELAY_MS) {
delay(REQUEST_DELAY_MS - timeSinceLastRequest)
}
lastRequestTime = System.currentTimeMillis()
}
}
private fun availableTags() = arraySetOf(
MangaTag("Anime", "ANI", source),
MangaTag("Drama", "DRA", source),
MangaTag("Josei", "JOS", source),
MangaTag("Manhwa", "MAW", source),
MangaTag("One Shot", "OSH", source),
MangaTag("Shounen", "SHO", source),
MangaTag("Webtoons", "WEB", source),
MangaTag("Shoujo", "SHJ", source),
MangaTag("Harem", "HAR", source),
MangaTag("Ecchi", "ECC", source),
MangaTag("Mature", "MAT", source),
MangaTag("Slice of life", "SOL", source),
MangaTag("Isekai", "ISE", source),
MangaTag("Manga", "MAG", source),
MangaTag("Manhua", "MAU", source),
MangaTag("Hành Động", "ACT", source),
MangaTag("Phiêu Lưu", "ADV", source),
MangaTag("Hài Hước", "COM", source),
MangaTag("Võ Thuật", "MAA", source),
MangaTag("Huyền Bí", "MYS", source),
MangaTag("Lãng Mạn", "ROM", source),
MangaTag("Thể Thao", "SPO", source),
MangaTag("Học Đường", "SCL", source),
MangaTag("Lịch Sử", "HIS", source),
MangaTag("Kinh Dị", "HOR", source),
MangaTag("Siêu Nhiên", "SUN", source),
MangaTag("Bi Kịch", "TRA", source),
MangaTag("Trùng Sinh", "RED", source),
MangaTag("Game", "GAM", source),
MangaTag("Viễn Tưởng", "FTS", source),
MangaTag("Khoa Học", "SCF", source),
MangaTag("Truyện Màu", "COI", source),
MangaTag("Người Lớn", "ADU", source),
MangaTag("BoyLove", "BBL", source),
MangaTag("Hầm Ngục", "DUN", source),
MangaTag("Săn Bắn", "HUNT", source),
MangaTag("Ngôn Từ Nhạy Cảm", "NTNC", source),
MangaTag("Doujinshi", "DOU", source),
MangaTag("Bạo Lực", "BLM", source),
MangaTag("Ngôn Tình", "NTT", source),
MangaTag("Nữ Cường", "NCT", source),
MangaTag("Gender Bender", "GDB", source),
MangaTag("Murim", "MRR", source),
MangaTag("Leo Tháp", "LTT", source),
MangaTag("Nấu Ăn", "COO", source)
)
private fun availableTags() = arraySetOf(
MangaTag("Anime", "ANI", source),
MangaTag("Drama", "DRA", source),
MangaTag("Josei", "JOS", source),
MangaTag("Manhwa", "MAW", source),
MangaTag("One Shot", "OSH", source),
MangaTag("Shounen", "SHO", source),
MangaTag("Webtoons", "WEB", source),
MangaTag("Shoujo", "SHJ", source),
MangaTag("Harem", "HAR", source),
MangaTag("Ecchi", "ECC", source),
MangaTag("Mature", "MAT", source),
MangaTag("Slice of life", "SOL", source),
MangaTag("Isekai", "ISE", source),
MangaTag("Manga", "MAG", source),
MangaTag("Manhua", "MAU", source),
MangaTag("Hành Động", "ACT", source),
MangaTag("Phiêu Lưu", "ADV", source),
MangaTag("Hài Hước", "COM", source),
MangaTag("Võ Thuật", "MAA", source),
MangaTag("Huyền Bí", "MYS", source),
MangaTag("Lãng Mạn", "ROM", source),
MangaTag("Thể Thao", "SPO", source),
MangaTag("Học Đường", "SCL", source),
MangaTag("Lịch Sử", "HIS", source),
MangaTag("Kinh Dị", "HOR", source),
MangaTag("Siêu Nhiên", "SUN", source),
MangaTag("Bi Kịch", "TRA", source),
MangaTag("Trùng Sinh", "RED", source),
MangaTag("Game", "GAM", source),
MangaTag("Viễn Tưởng", "FTS", source),
MangaTag("Khoa Học", "SCF", source),
MangaTag("Truyện Màu", "COI", source),
MangaTag("Người Lớn", "ADU", source),
MangaTag("BoyLove", "BBL", source),
MangaTag("Hầm Ngục", "DUN", source),
MangaTag("Săn Bắn", "HUNT", source),
MangaTag("Ngôn Từ Nhạy Cảm", "NTNC", source),
MangaTag("Doujinshi", "DOU", source),
MangaTag("Bạo Lực", "BLM", source),
MangaTag("Ngôn Tình", "NTT", source),
MangaTag("Nữ Cường", "NCT", source),
MangaTag("Gender Bender", "GDB", source),
MangaTag("Murim", "MRR", source),
MangaTag("Leo Tháp", "LTT", source),
MangaTag("Nấu Ăn", "COO", source),
)
companion object {
private const val REQUEST_DELAY_MS = 350L
private const val TOKEN_KEY = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBbG9uZSBGb3JldmVyIiwiY29taWNJZHMiOltdLCJyb2xlSWQiOm51bGwsImdyb3VwSWQiOm51bGwsImFkbWluIjpmYWxzZSwicmFuayI6MCwicGVybWlzc2lvbiI6W10sImlkIjoiMDAwMTA4NDQyNSIsInRlYW0iOmZhbHNlLCJpYXQiOjE3NTM2OTgyOTAsImVtYWlsIjoibnVsbCJ9.HT080LGjvzfh6XAPmdDZhf5vhnzUhXI4GU8U6tzwlnXWjgMO4VdYL1jsSFWd-s3NBGt-OAt89XnzaQ03iqDyA"
}
companion object {
private const val REQUEST_DELAY_MS = 350L
private const val TOKEN_KEY = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBbG9uZSBGb3JldmVyIiwiY29taWNJZHMiOltdLCJyb2xlSWQiOm51bGwsImdyb3VwSWQiOm51bGwsImFkbWluIjpmYWxzZSwicmFuayI6MCwicGVybWlzc2lvbiI6W10sImlkIjoiMDAwMTA4NDQyNSIsInRlYW0iOmZhbHNlLCJpYXQiOjE3NTM2OTgyOTAsImVtYWlsIjoibnVsbCJ9.HT080LGjvzfh6XAPmdDZhf5vhnzUhXI4GU8U6tzwlnXWjgMO4VdYL1jsSFWd-s3NBGt-OAt89XnzaQ03iqDyA"
}
}

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.parsers.site.vi
import org.jsoup.nodes.Document
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
@ -10,11 +12,13 @@ import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("KURONEKO", "Kuro Neko / vi-Hentai", "vi", type = ContentType.HENTAI)
internal class KuroNeko(context: MangaLoaderContext):
PagedMangaParser(context, MangaParserSource.KURONEKO, 30) {
internal class KuroNeko(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.KURONEKO, 30) {
override val configKeyDomain = ConfigKey.Domain("vi-hentai.moe")
private val pagesRequestMutex = Mutex()
private var lastPagesRequestTime = 0L
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -101,7 +105,7 @@ internal class KuroNeko(context: MangaLoaderContext):
append("&keyword=")
append(filter.query.urlEncoded())
}
if (page > 1) {
append("&page=")
append(page)
@ -129,33 +133,33 @@ internal class KuroNeko(context: MangaLoaderContext):
}
}
val doc = parseHttp(url) { it }
return doc.select("div.grid div.relative").map { div ->
val href = div.selectFirst("a[href^=/truyen/]")?.attrOrNull("href")
?: div.parseFailed("Không thể tìm thấy nguồn ảnh của Manga này!")
val coverUrl = div.selectFirst("div.cover")?.attr("style")
?.substringAfter("url('")?.substringBefore("')")
Manga(
id = generateUid(href),
title = div.select("div.p-2 a.text-ellipsis").text(),
altTitles = emptySet(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = coverUrl.orEmpty(),
tags = setOf(),
state = null,
authors = emptySet(),
source = source,
)
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.grid div.relative")
.map { div ->
val href = div.selectFirst("a[href^=/truyen/]")?.attrOrNull("href")
?: div.parseFailed("Không thể tìm thấy nguồn ảnh của Manga này!")
val coverUrl = div.selectFirst("div.cover")?.attr("style")
?.substringAfter("url('")?.substringBefore("')")
Manga(
id = generateUid(href),
title = div.select("div.p-2 a.text-ellipsis").text(),
altTitles = emptySet(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = coverUrl.orEmpty(),
tags = setOf(),
state = null,
authors = emptySet(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = parseHttp(manga.url.toAbsoluteUrl(domain)) { it }
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val author = root.selectFirst("div.mt-2:contains(Tác giả) span a")?.textOrNull()
return manga.copy(
@ -180,7 +184,6 @@ internal class KuroNeko(context: MangaLoaderContext):
val name = a.selectFirst("span.text-ellipsis")?.text().orEmpty()
val dateText = a.parent()?.selectFirst("span.timeago")?.attr("datetime").orEmpty()
val scanlator = root.selectFirst("div.mt-2:contains(Nhóm dịch) span a")?.textOrNull()
MangaChapter(
id = generateUid(href),
title = name,
@ -197,12 +200,18 @@ internal class KuroNeko(context: MangaLoaderContext):
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = parseHttp(chapter.url.toAbsoluteUrl(domain)) { it }
return doc.select("div.text-center img.lazy").mapNotNull { img ->
val url = img.attr("src").takeIf { it.isNotBlank() }
?: img.attr("data-src").takeIf { it.isNotBlank() }
?: return@mapNotNull null
pagesRequestMutex.withLock {
val currentTime = System.currentTimeMillis()
val timeSinceLastRequest = currentTime - lastPagesRequestTime
if (timeSinceLastRequest < PAGES_REQUEST_DELAY_MS) {
delay(PAGES_REQUEST_DELAY_MS - timeSinceLastRequest)
}
lastPagesRequestTime = System.currentTimeMillis()
}
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return doc.select("div.text-center img").mapNotNull { img ->
val url = img.requireSrc()
MangaPage(
id = generateUid(url),
url = url,
@ -243,15 +252,8 @@ internal class KuroNeko(context: MangaLoaderContext):
calendar.timeInMillis
}.getOrDefault(0L)
private suspend fun <T> parseHttp(url: String, block: (Document) -> T): T {
// 15 reqs / minute
kotlinx.coroutines.delay(4000L)
val doc = webClient.httpGet(url).parseHtml()
return block(doc)
}
companion object {
private const val PAGES_REQUEST_DELAY_MS = 5000L
const val PATH = "AxsAEQdJWk4YDUkHDgcVEwxaBQoHShIXHwYbD1seHAwHOwAKCAYFFw==\n"
}
}

Loading…
Cancel
Save