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.koitharu.kotatsu.parsers.util.*
import org.json.JSONObject import org.json.JSONObject
import java.util.* import java.util.*
import org.koitharu.kotatsu.parsers.Broken
@Broken("Need to clean code + Testing")
@MangaSourceParser("GOCTRUYENTRANHVUI", "Góc Truyện Tranh Vui", "vi") @MangaSourceParser("GOCTRUYENTRANHVUI", "Góc Truyện Tranh Vui", "vi")
internal class GocTruyenTranhVui(context: MangaLoaderContext): internal class GocTruyenTranhVui(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.GOCTRUYENTRANHVUI, 50) {
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 val requestMutex = Mutex() private var lastRequestTime = 0L
private var lastRequestTime = 0L
override fun getRequestHeaders(): Headers = Headers.Builder() private val apiHeaders by lazy {
.add("Authorization", TOKEN_KEY) Headers.Builder()
.add("Referer", "https://$domain/") .add("Authorization", TOKEN_KEY)
.add("X-Requested-With", "XMLHttpRequest") .add("Referer", "https://$domain/")
.build() .add("X-Requested-With", "XMLHttpRequest")
.build()
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.POPULARITY, SortOrder.POPULARITY,
SortOrder.NEWEST, SortOrder.NEWEST,
SortOrder.RATING, SortOrder.RATING
) )
override val filterCapabilities = MangaListFilterCapabilities( override val filterCapabilities = MangaListFilterCapabilities(
isSearchSupported = true, isSearchSupported = true,
isMultipleTagsSupported = true, isMultipleTagsSupported = true,
) )
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = availableTags(), availableTags = availableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
) )
init { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
setFirstPage(0) 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> { val sortValue = when (order) {
enforceRateLimit() SortOrder.POPULARITY -> "viewCount"
val url = buildString { SortOrder.NEWEST -> "createdAt"
append(apiUrl) SortOrder.RATING -> "evaluationScore"
append("/search?p=$page") else -> "recentDate" // UPDATED
if (!filter.query.isNullOrBlank()) { }
append("&searchValue=${filter.query.urlEncoded()}") append("&orders%5B%5D=$sortValue")
}
val sortValue = when (order) { filter.tags.forEach { append("&categories%5B%5D=${it.key}") }
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.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 json = webClient.httpGet(url, extraHeaders = apiHeaders).parseJson()
val statusKey = when (it) { val result = json.optJSONObject("result") ?: return emptyList()
MangaState.ONGOING -> "PRG" val data = result.optJSONArray("data") ?: return emptyList()
MangaState.FINISHED -> "END"
else -> null
}
if (statusKey != null) append("&status%5B%5D=$statusKey")
}
}
val json = webClient.httpGet(url).parseJson() return List(data.length()) { i ->
val result = json.optJSONObject("result") ?: return emptyList() val item = data.getJSONObject(i)
val data = result.optJSONArray("data") ?: return emptyList() 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 -> Manga(
val item = data.getJSONObject(i) id = generateUid(comicId),
val comicId = item.getString("id") title = item.getString("name"),
val slug = item.getString("nameEn") altTitles = item.optString("otherName", "").split(",").mapNotNull { it.trim().takeIf(String::isNotBlank) }.toSet(),
val mangaUrl = "/truyen/$slug" url = "$comicId:$slug", // Store both id and slug, separated by ':'
val tags = item.optJSONArray("category")?.let { arr -> publicUrl = "https://$domain$mangaUrl",
(0 until arr.length()).mapNotNullTo(mutableSetOf()) { index -> rating = item.optDouble("evaluationScore", 0.0).toFloat(),
val tagName = arr.getString(index) contentRating = null,
availableTags().find { it.title.equals(tagName, ignoreCase = true) }?.let { genrePair -> coverUrl = "https://$domain${item.getString("photo")}",
MangaTag(key = genrePair.key, title = genrePair.title, source = source) tags = tags,
} state = when (item.optString("statusCode")) {
} "PRG" -> MangaState.ONGOING
} ?: emptySet() "END" -> MangaState.FINISHED
else -> null
},
authors = setOf(item.optString("author", "Updating")),
source = source
)
}
}
Manga( override suspend fun getDetails(manga: Manga): Manga {
id = generateUid(comicId), val comicId = manga.url.substringBefore(':')
title = item.getString("name"), val slug = manga.url.substringAfter(':')
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 chapters = try {
val comicId = manga.url.substringBefore(':') enforceRateLimit()
val slug = manga.url.substringAfter(':') 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 { List(chaptersData.length()) { i ->
enforceRateLimit() val item = chaptersData.getJSONObject(i)
val chapterApiUrl = "https://$domain/api/comic/$comicId/chapter?limit=-1" val number = item.getString("numberChapter")
val chapterJson = webClient.httpGet(chapterApiUrl).parseJson() val name = item.getString("name")
val chaptersData = chapterJson.getJSONObject("result").getJSONArray("chapters") 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 -> enforceRateLimit()
val item = chaptersData.getJSONObject(i) val doc = webClient.httpGet(manga.publicUrl).parseHtml()
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 detailTags = doc.select(".group-content > .v-chip-link").mapNotNullTo(mutableSetOf()) { el ->
val doc = webClient.httpGet(manga.publicUrl).parseHtml() 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 -> return manga.copy(
availableTags().find { it.title.equals(el.text(), ignoreCase = true) }?.let { title = doc.selectFirst(".v-card-title")?.text().orEmpty(),
MangaTag(key = it.key, title = it.title, source = source) 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( override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
title = doc.selectFirst(".v-card-title")?.text().orEmpty(), enforceRateLimit()
tags = manga.tags + detailTags, val responseBody = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).body?.string()
coverUrl = doc.selectFirst("img.image")?.absUrl("src"), ?: throw Exception("Response body is null for chapter page")
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> { val chapterJsonRaw = responseBody.substringAfter("chapterJson: `", "").substringBefore("`", "")
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 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 (comicId.isBlank()) {
if (chapterJsonRaw.isNotBlank()) { throw Exception("Cannot find comicId in HTML for fallback image request")
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()) { val formBody = mapOf(
throw Exception("Cannot find comicId in HTML for fallback image request") "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( return imageUrls.map { url ->
"comicId" to comicId, val finalUrl = if (url.startsWith("/image/")) "https://$domain$url" else url
"chapterNumber" to chapterNumber, MangaPage(id = generateUid(finalUrl), url = finalUrl, preview = null, source = source)
"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 -> private suspend fun enforceRateLimit() {
val finalUrl = if (url.startsWith("/image/")) "https://$domain$url" else url requestMutex.withLock {
MangaPage( val currentTime = System.currentTimeMillis()
id = generateUid(finalUrl), val timeSinceLastRequest = currentTime - lastRequestTime
url = finalUrl, if (timeSinceLastRequest < REQUEST_DELAY_MS) { // Vẫn truy cập được REQUEST_DELAY_MS
preview = null, delay(REQUEST_DELAY_MS - timeSinceLastRequest)
source = source }
) lastRequestTime = System.currentTimeMillis()
} }
} }
private suspend fun enforceRateLimit() { private fun availableTags() = arraySetOf(
requestMutex.withLock { MangaTag("Anime", "ANI", source),
val currentTime = System.currentTimeMillis() MangaTag("Drama", "DRA", source),
val timeSinceLastRequest = currentTime - lastRequestTime MangaTag("Josei", "JOS", source),
if (timeSinceLastRequest < REQUEST_DELAY_MS) { MangaTag("Manhwa", "MAW", source),
delay(REQUEST_DELAY_MS - timeSinceLastRequest) MangaTag("One Shot", "OSH", source),
} MangaTag("Shounen", "SHO", source),
lastRequestTime = System.currentTimeMillis() 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( companion object {
MangaTag("Anime", "ANI", source), private const val REQUEST_DELAY_MS = 350L
MangaTag("Drama", "DRA", source), private const val TOKEN_KEY = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBbG9uZSBGb3JldmVyIiwiY29taWNJZHMiOltdLCJyb2xlSWQiOm51bGwsImdyb3VwSWQiOm51bGwsImFkbWluIjpmYWxzZSwicmFuayI6MCwicGVybWlzc2lvbiI6W10sImlkIjoiMDAwMTA4NDQyNSIsInRlYW0iOmZhbHNlLCJpYXQiOjE3NTM2OTgyOTAsImVtYWlsIjoibnVsbCJ9.HT080LGjvzfh6XAPmdDZhf5vhnzUhXI4GU8U6tzwlnXWjgMO4VdYL1jsSFWd-s3NBGt-OAt89XnzaQ03iqDyA"
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"
}
} }

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

Loading…
Cancel
Save