|
|
|
@ -1,8 +1,8 @@
|
|
|
|
package org.koitharu.kotatsu.parsers.site.vi
|
|
|
|
package org.koitharu.kotatsu.parsers.site.vi
|
|
|
|
|
|
|
|
|
|
|
|
import okhttp3.Headers
|
|
|
|
import okhttp3.Headers
|
|
|
|
|
|
|
|
import okio.ByteString.Companion.encode
|
|
|
|
import org.json.JSONArray
|
|
|
|
import org.json.JSONArray
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
|
|
|
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
|
|
|
|
@ -105,70 +105,71 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
val root = doc.selectFirst("div.flex-1.bg-neutral-900") ?: return manga
|
|
|
|
val root = doc.selectFirst("div.flex-1.bg-neutral-900") ?: return manga
|
|
|
|
val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
|
|
|
|
val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
|
|
|
|
timeZone = TimeZone.getTimeZone("GMT+7")
|
|
|
|
timeZone = TimeZone.getTimeZone("GMT+7")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val tags = root.select("div.flex.flex-wrap.gap-2 button").mapNotNullToSet { button ->
|
|
|
|
val tags = root.select("div.flex.flex-wrap.gap-2 button").mapNotNullToSet { button ->
|
|
|
|
val tagName = button.text().toTitleCase(sourceLocale)
|
|
|
|
val tagName = button.text().toTitleCase(sourceLocale)
|
|
|
|
val tagUrl = button.parent()?.attrOrNull("href")?.substringAfterLast('/')
|
|
|
|
val tagUrl = button.parent()?.attrOrNull("href")?.substringAfterLast('/')
|
|
|
|
if (tagUrl != null) {
|
|
|
|
if (tagUrl != null) {
|
|
|
|
MangaTag(title = tagName, key = tagUrl, source = source)
|
|
|
|
MangaTag(title = tagName, key = tagUrl, source = source)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val state = when {
|
|
|
|
val state = when {
|
|
|
|
root.selectFirst("a[href*='status=completed']") != null -> MangaState.FINISHED
|
|
|
|
root.selectFirst("a[href*='status=completed']") != null -> MangaState.FINISHED
|
|
|
|
root.selectFirst("a[href*='status=progress']") != null -> MangaState.ONGOING
|
|
|
|
root.selectFirst("a[href*='status=progress']") != null -> MangaState.ONGOING
|
|
|
|
else -> null
|
|
|
|
else -> null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val description = root.selectFirst("div#introduction-wrap p.font-light")?.html()?.nullIfEmpty()
|
|
|
|
val description = root.selectFirst("div#introduction-wrap p.font-light")?.html()?.nullIfEmpty()
|
|
|
|
|
|
|
|
|
|
|
|
val altTitles = description?.split("\n")?.mapNotNullToSet { line ->
|
|
|
|
val altTitles = description?.split("\n")?.mapNotNullToSet { line ->
|
|
|
|
when {
|
|
|
|
when {
|
|
|
|
line.startsWith("Tên tiếng anh:", ignoreCase = true) ->
|
|
|
|
line.startsWith("Tên tiếng anh:", ignoreCase = true) ->
|
|
|
|
line.substringAfter(':').substringBefore("Tên gốc:").trim()
|
|
|
|
line.substringAfter(':').substringBefore("Tên gốc:").trim()
|
|
|
|
|
|
|
|
|
|
|
|
line.startsWith("Tên gốc:", ignoreCase = true) ->
|
|
|
|
line.startsWith("Tên gốc:", ignoreCase = true) ->
|
|
|
|
line.substringAfter(':').trim().substringBefore(' ')
|
|
|
|
line.substringAfter(':').trim().substringBefore(' ')
|
|
|
|
|
|
|
|
|
|
|
|
else -> null
|
|
|
|
else -> null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val scriptTag = doc.select("script").firstOrNull { script ->
|
|
|
|
val scriptTag = doc.select("script").firstOrNull { script ->
|
|
|
|
val data = script.data()
|
|
|
|
val data = script.data()
|
|
|
|
data.contains("data") && data.contains("chapterListEn")
|
|
|
|
data.contains("data") && data.contains("chapterListEn")
|
|
|
|
}?.data()
|
|
|
|
}?.data()
|
|
|
|
val chapters = parseChapterList(scriptTag, manga, chapterDateFormat)
|
|
|
|
val chapters = parseChapterList(scriptTag, manga, chapterDateFormat)
|
|
|
|
|
|
|
|
|
|
|
|
return manga.copy(
|
|
|
|
return manga.copy(
|
|
|
|
tags = tags,
|
|
|
|
title = doc.selectFirst("h1")!!.text(),
|
|
|
|
state = state,
|
|
|
|
tags = tags,
|
|
|
|
description = description,
|
|
|
|
state = state,
|
|
|
|
altTitles = altTitles.orEmpty(),
|
|
|
|
description = description,
|
|
|
|
chapters = chapters.reversed(),
|
|
|
|
altTitles = altTitles.orEmpty(),
|
|
|
|
)
|
|
|
|
chapters = chapters.reversed(),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
private suspend fun parseChapterList(scriptTag: String?, manga: Manga, chapterDateFormat: SimpleDateFormat): List<MangaChapter> {
|
|
|
|
|
|
|
|
|
|
|
|
private fun parseChapterList(scriptTag: String?, manga: Manga, chapterDateFormat: SimpleDateFormat): List<MangaChapter> {
|
|
|
|
val idManga = manga.url.substringAfter("detail/").toIntOrNull() ?: return emptyList()
|
|
|
|
val idManga = manga.url.substringAfter("detail/").toIntOrNull() ?: return emptyList()
|
|
|
|
|
|
|
|
|
|
|
|
val chapters = ArrayList<MangaChapter>()
|
|
|
|
val chapters = ArrayList<MangaChapter>()
|
|
|
|
if (scriptTag.isNullOrEmpty()) return chapters
|
|
|
|
if (scriptTag.isNullOrEmpty()) return chapters
|
|
|
|
|
|
|
|
|
|
|
|
val cleanedScript = scriptTag.replace("\\", "")
|
|
|
|
val cleanedScript = scriptTag.replace("\\", "")
|
|
|
|
|
|
|
|
|
|
|
|
val cutScript = "null,{\"data\""
|
|
|
|
val cutScript = "null,{\"data\""
|
|
|
|
val needScript = cleanedScript.indexOf(cutScript)
|
|
|
|
val needScript = cleanedScript.indexOf(cutScript)
|
|
|
|
if (needScript == -1) return chapters
|
|
|
|
if (needScript == -1) return chapters
|
|
|
|
val finalScript = cleanedScript.substring(needScript)
|
|
|
|
val finalScript = cleanedScript.substring(needScript)
|
|
|
|
|
|
|
|
|
|
|
|
val vnPrefix = "null,{\"data\":"
|
|
|
|
val vnPrefix = "null,{\"data\":"
|
|
|
|
val vnStart = finalScript.indexOf(vnPrefix)
|
|
|
|
val vnStart = finalScript.indexOf(vnPrefix)
|
|
|
|
if (vnStart == -1) return chapters
|
|
|
|
if (vnStart == -1) return chapters
|
|
|
|
@ -176,13 +177,13 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
val vnEnd = finalScript.indexOf(beforeEn, vnStart)
|
|
|
|
val vnEnd = finalScript.indexOf(beforeEn, vnStart)
|
|
|
|
if (vnEnd == -1) return chapters
|
|
|
|
if (vnEnd == -1) return chapters
|
|
|
|
val vnChapterStr = finalScript.substring(vnStart + vnPrefix.length, vnEnd)
|
|
|
|
val vnChapterStr = finalScript.substring(vnStart + vnPrefix.length, vnEnd)
|
|
|
|
|
|
|
|
|
|
|
|
val vnArray = try {
|
|
|
|
val vnArray = try {
|
|
|
|
JSONArray(vnChapterStr)
|
|
|
|
JSONArray(vnChapterStr)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
} catch (e: Exception) {
|
|
|
|
JSONArray()
|
|
|
|
JSONArray()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (i in 0 until vnArray.length()) {
|
|
|
|
for (i in 0 until vnArray.length()) {
|
|
|
|
val chapter = vnArray.getJSONObject(i)
|
|
|
|
val chapter = vnArray.getJSONObject(i)
|
|
|
|
val name = chapter.optString("name", null) ?: continue
|
|
|
|
val name = chapter.optString("name", null) ?: continue
|
|
|
|
@ -212,13 +213,13 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
val enEnd = finalScript.indexOf(beforeId, enStart)
|
|
|
|
val enEnd = finalScript.indexOf(beforeId, enStart)
|
|
|
|
if (enEnd == -1) return chapters
|
|
|
|
if (enEnd == -1) return chapters
|
|
|
|
val enChapterStr = finalScript.substring(enStart + enPrefix.length, enEnd)
|
|
|
|
val enChapterStr = finalScript.substring(enStart + enPrefix.length, enEnd)
|
|
|
|
|
|
|
|
|
|
|
|
val enArray = try {
|
|
|
|
val enArray = try {
|
|
|
|
JSONArray(enChapterStr)
|
|
|
|
JSONArray(enChapterStr)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
} catch (e: Exception) {
|
|
|
|
JSONArray()
|
|
|
|
JSONArray()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (i in 0 until enArray.length()) {
|
|
|
|
for (i in 0 until enArray.length()) {
|
|
|
|
val chapter = enArray.getJSONObject(i)
|
|
|
|
val chapter = enArray.getJSONObject(i)
|
|
|
|
val name = chapter.optString("name", null) ?: continue
|
|
|
|
val name = chapter.optString("name", null) ?: continue
|
|
|
|
@ -239,7 +240,7 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return chapters
|
|
|
|
return chapters
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -247,11 +248,11 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
val url = chapter.url.toAbsoluteUrl(domain)
|
|
|
|
val url = chapter.url.toAbsoluteUrl(domain)
|
|
|
|
val doc = webClient.httpGet(url).parseHtml()
|
|
|
|
val doc = webClient.httpGet(url).parseHtml()
|
|
|
|
val root = doc.select("img.m-auto.read-image.w-auto.h-auto.md\\:min-h-\\[800px\\].min-h-\\[300px\\]")
|
|
|
|
val root = doc.select("img.m-auto.read-image.w-auto.h-auto.md\\:min-h-\\[800px\\].min-h-\\[300px\\]")
|
|
|
|
|
|
|
|
|
|
|
|
if (root.isEmpty()) { // for Debug #1604
|
|
|
|
if (root.isEmpty()) { // for Debug #1604
|
|
|
|
throw ParseException("Root not found!", url)
|
|
|
|
throw ParseException("Root not found!", url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return root.map { img ->
|
|
|
|
return root.map { img ->
|
|
|
|
val imgUrl = img.requireSrc()
|
|
|
|
val imgUrl = img.requireSrc()
|
|
|
|
MangaPage(
|
|
|
|
MangaPage(
|
|
|
|
@ -264,40 +265,30 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun fetchTags(): Set<MangaTag> {
|
|
|
|
private suspend fun fetchTags(): Set<MangaTag> {
|
|
|
|
val doc = webClient.httpGet(
|
|
|
|
val doc = webClient.httpGet("https://$domain").parseHtml()
|
|
|
|
urlBuilder()
|
|
|
|
val scriptSrc = doc.select("script")[7].src()!!
|
|
|
|
.addPathSegment("genre")
|
|
|
|
val docJS = webClient.httpGet(scriptSrc).parseRaw()
|
|
|
|
.addPathSegment("all")
|
|
|
|
|
|
|
|
.build(),
|
|
|
|
val optionsStart = docJS.indexOf("genres:[{")
|
|
|
|
).parseHtml()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val scriptTag = doc.select("script").firstOrNull { script ->
|
|
|
|
|
|
|
|
val data = script.data()
|
|
|
|
|
|
|
|
data.contains("buildId") && data.contains("options")
|
|
|
|
|
|
|
|
}?.data() ?: return emptySet()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val cleanedScript = scriptTag.replace("\\", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val optionsPrefix = "\"options\":"
|
|
|
|
|
|
|
|
val optionsStart = cleanedScript.indexOf(optionsPrefix)
|
|
|
|
|
|
|
|
if (optionsStart == -1) return emptySet()
|
|
|
|
if (optionsStart == -1) return emptySet()
|
|
|
|
|
|
|
|
|
|
|
|
val optionsEnd = cleanedScript.indexOf("\"zombie\"}]", optionsStart) + "\"zombie\"}]".length
|
|
|
|
val optionsEnd = docJS.indexOf("}]", optionsStart)
|
|
|
|
if (optionsEnd == -1) return emptySet()
|
|
|
|
if (optionsEnd == -1) return emptySet()
|
|
|
|
|
|
|
|
|
|
|
|
val optionsStr = cleanedScript.substring(optionsStart + optionsPrefix.length, optionsEnd)
|
|
|
|
val optionsStr = docJS.substring(optionsStart + 7, optionsEnd + 2)
|
|
|
|
|
|
|
|
|
|
|
|
val optionsArray = try {
|
|
|
|
val optionsArray = JSONArray(
|
|
|
|
JSONArray(optionsStr)
|
|
|
|
optionsStr
|
|
|
|
} catch (e: Exception) {
|
|
|
|
.replace(Regex(",description:\\s*\"[^\"]*\"(,?)"), "")
|
|
|
|
return emptySet()
|
|
|
|
.replace(Regex("(\\w+):"), "\"$1\":")
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return buildSet {
|
|
|
|
return buildSet {
|
|
|
|
for (i in 0 until optionsArray.length()) {
|
|
|
|
for (i in 0 until optionsArray.length()) {
|
|
|
|
|
|
|
|
// {"label":"Ahegao","href":"/genre/ahegao"}
|
|
|
|
val option = optionsArray.getJSONObject(i)
|
|
|
|
val option = optionsArray.getJSONObject(i)
|
|
|
|
val title = option.getStringOrNull("label")?.toTitleCase(sourceLocale) ?: continue
|
|
|
|
val title = option.getStringOrNull("label")!!.toTitleCase(sourceLocale)
|
|
|
|
val key = option.getStringOrNull("value") ?: continue
|
|
|
|
val key = option.getStringOrNull("href")!!.split("/")[2]
|
|
|
|
if (title.isNotEmpty() && key.isNotEmpty()) {
|
|
|
|
if (title.isNotEmpty() && key.isNotEmpty()) {
|
|
|
|
if (title != "Tất cả" || key != "all") { // remove "All" tags, default list = all
|
|
|
|
if (title != "Tất cả" || key != "all") { // remove "All" tags, default list = all
|
|
|
|
add(MangaTag(title = title, key = key, source = source))
|
|
|
|
add(MangaTag(title = title, key = key, source = source))
|
|
|
|
@ -306,4 +297,4 @@ internal class NhentaiWorld(context: MangaLoaderContext) :
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|