Dâm Cô Nương: Remove redundant escape character (#2160)

master
Draken 8 months ago committed by GitHub
parent b3e7d8e8d5
commit f64ce0f4a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -187,7 +187,7 @@ internal class DamCoNuong(context: MangaLoaderContext) :
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
doc.selectFirst("script:containsData(window.encryptionConfig)")?.data()?.let { scriptContent -> doc.selectFirst("script:containsData(window.encryptionConfig)")?.data()?.let { scriptContent ->
val fallbackUrlsRegex = Regex(""""fallbackUrls"\s*:\s*(\[.*?\])""") val fallbackUrlsRegex = Regex(""""fallbackUrls"\s*:\s*(\[.*?])""")
val arrayString = fallbackUrlsRegex.find(scriptContent)?.groupValues?.get(1) ?: return@let val arrayString = fallbackUrlsRegex.find(scriptContent)?.groupValues?.get(1) ?: return@let
val urlRegex = Regex("""(https?:\\?/\\?[^"]+\.(?:jpg|jpeg|png|webp|gif))""") val urlRegex = Regex("""(https?:\\?/\\?[^"]+\.(?:jpg|jpeg|png|webp|gif))""")
val scriptImages = urlRegex.findAll(arrayString).map { val scriptImages = urlRegex.findAll(arrayString).map {

@ -238,4 +238,8 @@ internal class KuroNeko(context: MangaLoaderContext) : PagedMangaParser(context,
) )
calendar.timeInMillis calendar.timeInMillis
}.getOrDefault(0L) }.getOrDefault(0L)
companion object {
const val PATH = "AxsAEQdJWk4YDUkHDgcVEwxaBQoHShIXHwYbD1seHAwHOwAKCAYFFw==\n"
}
} }

@ -1,9 +1,14 @@
package org.koitharu.kotatsu.parsers.site.vi package org.koitharu.kotatsu.parsers.site.vi
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import org.json.JSONArray import org.json.JSONArray
import org.koitharu.kotatsu.parsers.Broken 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.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.bitmap.Rect
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
@ -13,7 +18,6 @@ import org.koitharu.kotatsu.parsers.util.json.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@Broken("Request from site owner: Open webview to read")
@MangaSourceParser("MIMIHENTAI", "MimiHentai", "vi", type = ContentType.HENTAI) @MangaSourceParser("MIMIHENTAI", "MimiHentai", "vi", type = ContentType.HENTAI)
internal class MimiHentai(context: MangaLoaderContext) : internal class MimiHentai(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MIMIHENTAI, 18) { PagedMangaParser(context, MangaParserSource.MIMIHENTAI, 18) {
@ -40,14 +44,14 @@ internal class MimiHentai(context: MangaLoaderContext) :
} }
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.POPULARITY, SortOrder.POPULARITY,
SortOrder.POPULARITY_TODAY, SortOrder.POPULARITY_TODAY,
SortOrder.POPULARITY_WEEK, SortOrder.POPULARITY_WEEK,
SortOrder.POPULARITY_MONTH, SortOrder.POPULARITY_MONTH,
SortOrder.RATING, SortOrder.RATING,
) )
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities( get() = MangaListFilterCapabilities(
@ -65,31 +69,31 @@ internal class MimiHentai(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions(availableTags = fetchTags()) override suspend fun getFilterOptions() = MangaListFilterOptions(availableTags = fetchTags())
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append("$domain/$apiSuffix") append("$domain/$apiSuffix")
if (!filter.query.isNullOrEmpty() || if (!filter.query.isNullOrEmpty() ||
!filter.author.isNullOrEmpty() || !filter.author.isNullOrEmpty() ||
filter.tags.isNotEmpty() filter.tags.isNotEmpty()
) { ) {
append("/advance-search?page=") append("/advance-search?page=")
append(page) append(page)
append("&max=18") // page size, avoid rate limit append("&max=18") // page size, avoid rate limit
if (!filter.query.isNullOrEmpty()) { if (!filter.query.isNullOrEmpty()) {
append("&name=") append("&name=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
if (!filter.author.isNullOrEmpty()) { if (!filter.author.isNullOrEmpty()) {
append("&author=") append("&author=")
append(filter.author.urlEncoded()) append(filter.author.urlEncoded())
} }
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&genre=") append("&genre=")
append(filter.tags.joinToString(",") { it.key }) append(filter.tags.joinToString(",") { it.key })
} }
if (filter.tagsExclude.isNotEmpty()) { if (filter.tagsExclude.isNotEmpty()) {
@ -97,43 +101,43 @@ internal class MimiHentai(context: MangaLoaderContext) :
append(filter.tagsExclude.joinToString(",") { it.key }) append(filter.tagsExclude.joinToString(",") { it.key })
} }
append("&sort=") append("&sort=")
append( append(
when (order) { when (order) {
SortOrder.UPDATED -> "updated_at" SortOrder.UPDATED -> "updated_at"
SortOrder.ALPHABETICAL -> "title" SortOrder.ALPHABETICAL -> "title"
SortOrder.POPULARITY -> "follows" SortOrder.POPULARITY -> "follows"
SortOrder.POPULARITY_TODAY, SortOrder.POPULARITY_TODAY,
SortOrder.POPULARITY_WEEK, SortOrder.POPULARITY_WEEK,
SortOrder.POPULARITY_MONTH -> "views" SortOrder.POPULARITY_MONTH -> "views"
SortOrder.RATING -> "likes" SortOrder.RATING -> "likes"
else -> "" else -> ""
} }
) )
} }
else { else {
append( append(
when (order) { when (order) {
SortOrder.UPDATED -> "/tatcatruyen?page=$page&sort=updated_at" SortOrder.UPDATED -> "/tatcatruyen?page=$page&sort=updated_at"
SortOrder.ALPHABETICAL -> "/tatcatruyen?page=$page&sort=title" SortOrder.ALPHABETICAL -> "/tatcatruyen?page=$page&sort=title"
SortOrder.POPULARITY -> "/tatcatruyen?page=$page&sort=follows" SortOrder.POPULARITY -> "/tatcatruyen?page=$page&sort=follows"
SortOrder.POPULARITY_TODAY -> "/tatcatruyen?page=$page&sort=views" SortOrder.POPULARITY_TODAY -> "/tatcatruyen?page=$page&sort=views"
SortOrder.POPULARITY_WEEK -> "/top-manga?page=$page&timeType=1&limit=18" SortOrder.POPULARITY_WEEK -> "/top-manga?page=$page&timeType=1&limit=18"
SortOrder.POPULARITY_MONTH -> "/top-manga?page=$page&timeType=2&limit=18" SortOrder.POPULARITY_MONTH -> "/top-manga?page=$page&timeType=2&limit=18"
SortOrder.RATING -> "/tatcatruyen?page=$page&sort=likes" SortOrder.RATING -> "/tatcatruyen?page=$page&sort=likes"
else -> "/tatcatruyen?page=$page&sort=updated_at" // default else -> "/tatcatruyen?page=$page&sort=updated_at" // default
} }
) )
if (filter.tagsExclude.isNotEmpty()) { if (filter.tagsExclude.isNotEmpty()) {
append("&ex=") append("&ex=")
append(filter.tagsExclude.joinToString(",") { it.key }) append(filter.tagsExclude.joinToString(",") { it.key })
} }
} }
} }
val raw = webClient.httpGet(url) val raw = webClient.httpGet(url)
return if (url.contains("/top-manga")) { return if (url.contains("/top-manga")) {
val data = raw.parseJsonArray() val data = raw.parseJsonArray()
parseTopMangaList(data) parseTopMangaList(data)
@ -195,19 +199,19 @@ internal class MimiHentai(context: MangaLoaderContext) :
val title = jo.getString("title").takeIf { it.isNotEmpty() } ?: "Web chưa đặt tên" val title = jo.getString("title").takeIf { it.isNotEmpty() } ?: "Web chưa đặt tên"
val description = jo.getStringOrNull("description") val description = jo.getStringOrNull("description")
val differentNames = mutableSetOf<String>().apply { val differentNames = mutableSetOf<String>().apply {
jo.optJSONArray("differentNames")?.let { namesArray -> jo.optJSONArray("differentNames")?.let { namesArray ->
for (i in 0 until namesArray.length()) { for (i in 0 until namesArray.length()) {
namesArray.optString(i)?.takeIf { it.isNotEmpty() }?.let { name -> namesArray.optString(i)?.takeIf { it.isNotEmpty() }?.let { name ->
add(name) add(name)
} }
} }
} }
} }
val authors = jo.getJSONArray("authors").mapJSON { val authors = jo.getJSONArray("authors").mapJSON {
it.getString("name") it.getString("name")
}.toSet() }.toSet()
val tags = jo.getJSONArray("genres").mapJSON { genre -> val tags = jo.getJSONArray("genres").mapJSON { genre ->
MangaTag( MangaTag(
@ -238,7 +242,7 @@ internal class MimiHentai(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val url = manga.url.toAbsoluteUrl(domain) val url = manga.url.toAbsoluteUrl(domain)
val json = webClient.httpGet(url).parseJson() val json = webClient.httpGet(url).parseJson()
val id = json.getLong("id") val id = json.getLong("id")
val description = json.getStringOrNull("description") val description = json.getStringOrNull("description")
val uploaderName = json.getJSONObject("uploader").getString("displayName") val uploaderName = json.getJSONObject("uploader").getString("displayName")
@ -258,7 +262,7 @@ internal class MimiHentai(context: MangaLoaderContext) :
id = generateUid(jo.getLong("id")), id = generateUid(jo.getLong("id")),
title = jo.getStringOrNull("title"), title = jo.getStringOrNull("title"),
number = jo.getFloatOrDefault("order", 0f), number = jo.getFloatOrDefault("order", 0f),
url = "/$apiSuffix/chapter?id=${jo.getLong("id")}", url = "${jo.getLong("id")}",
uploadDate = dateFormat.parse(jo.getString("createdAt"))?.time ?: 0L, uploadDate = dateFormat.parse(jo.getString("createdAt"))?.time ?: 0L,
source = source, source = source,
scanlator = uploaderName, scanlator = uploaderName,
@ -275,13 +279,160 @@ internal class MimiHentai(context: MangaLoaderContext) :
} }
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val json = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseJson() val url = context.decodeBase64(KuroNeko.PATH)
.decodeXorCipher()
.toString(Charsets.UTF_8) + "/" + chapter.url
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("pages").mapJSON { jo ->
val imageUrl = jo.getString("imageUrl")
val gt = jo.getStringOrNull("drm")
MangaPage(
id = generateUid(imageUrl),
url = if (gt != null) "$imageUrl#$GT$gt" else imageUrl,
preview = null,
source = source,
)
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val fragment = response.request.url.fragment
if (fragment == null || !fragment.contains(GT)) {
return response
}
return context.redrawImageResponse(response) { bitmap ->
val gt = fragment.substringAfter(GT)
runBlocking {
extractMetadata(bitmap, gt)
}
}
}
private fun extractMetadata(bitmap: Bitmap, gt: String): Bitmap {
val metadata = JSONObject().apply {
var sw = 0
var sh = 0
val pos = JSONObject()
val dims = JSONObject()
for (t in gt.split("|")) {
when {
t.startsWith("sw:") -> sw = t.substring(3).toInt()
t.startsWith("sh:") -> sh = t.substring(3).toInt()
t.contains("@") && t.contains(">") -> {
val (left, right) = t.split(">")
val (n, rectStr) = left.split("@")
val (x, y, w, h) = rectStr.split(",").map { it.toInt() }
dims.put(n, JSONObject().apply {
put("x", x)
put("y", y)
put("width", w)
put("height", h)
})
pos.put(n, right)
}
}
}
put("sw", sw)
put("sh", sh)
put("dims", dims)
put("pos", pos)
}
val sw = metadata.optInt("sw")
val sh = metadata.optInt("sh")
if (sw <= 0 || sh <= 0) return bitmap
val fullW = bitmap.width
val fullH = bitmap.height
val working = context.createBitmap(sw, sh).also { k ->
k.drawBitmap(bitmap, Rect(0, 0, sw, sh), Rect(0, 0, sw, sh))
}
val info = json.getJSONObject("info") val keys = arrayOf("00","01","02","10","11","12","20","21","22")
val manga = info.getJSONObject("manga") val baseW = sw / 3
val baseH = sh / 3
val rw = sw % 3
val rh = sh % 3
val defaultDims = HashMap<String, IntArray>().apply {
for (k in keys) {
val i = k[0].digitToInt()
val j = k[1].digitToInt()
val w = baseW + if (j == 2) rw else 0
val h = baseH + if (i == 2) rh else 0
put(k, intArrayOf(j * baseW, i * baseH, w, h))
}
}
val dimsJson = metadata.optJSONObject("dims") ?: JSONObject()
val dims = HashMap<String, IntArray>().apply {
for (k in keys) {
val jo = dimsJson.optJSONObject(k)
if (jo != null) {
put(k, intArrayOf(
jo.getInt("x"),
jo.getInt("y"),
jo.getInt("width"),
jo.getInt("height"),
))
} else {
put(k, defaultDims.getValue(k))
}
}
}
val chapterUrl = "https://$domain/g/${manga.getInt("id")}/chapter/${info.getString("title")}-${info.getInt("id")}" val pos = metadata.optJSONObject("pos") ?: JSONObject()
context.requestBrowserAction(this, chapterUrl) val inv = HashMap<String, String>().apply {
val it = pos.keys()
while (it.hasNext()) {
val a = it.next()
val b = pos.getString(a)
put(b, a)
}
}
val result = context.createBitmap(fullW, fullH)
for (k in keys) {
val srcKey = inv[k] ?: continue
val s = dims.getValue(k)
val d = dims.getValue(srcKey)
result.drawBitmap(
working,
Rect(s[0], s[1], s[0] + s[2], s[1] + s[3]),
Rect(d[0], d[1], d[0] + d[2], d[1] + d[3]),
)
}
if (sh < fullH) {
result.drawBitmap(
bitmap,
Rect(0, sh, fullW, fullH),
Rect(0, sh, fullW, fullH),
)
}
if (sw < fullW) {
result.drawBitmap(
bitmap,
Rect(sw, 0, fullW, sh),
Rect(sw, 0, fullW, sh),
)
}
return result
}
private fun ByteArray.decodeXorCipher(): ByteArray {
val k = "kotatsuanddokiarethebest"
.toByteArray(Charsets.UTF_8)
return this.mapIndexed { i, b ->
(b.toInt() xor k[i % k.size].toInt()).toByte()
}.toByteArray()
} }
private suspend fun fetchTags(): Set<MangaTag> { private suspend fun fetchTags(): Set<MangaTag> {
@ -295,4 +446,8 @@ internal class MimiHentai(context: MangaLoaderContext) :
) )
} }
} }
companion object {
private const val GT = "gt="
}
} }

Loading…
Cancel
Save