[CuuTruyen] Fix

master
Koitharu 2 years ago
parent 955c75a99f
commit c85d17d60d
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -11,7 +11,7 @@ import org.koitharu.kotatsu.parsers.util.Paginator
public abstract class PagedMangaParser(
context: MangaLoaderContext,
source: MangaParserSource,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField internal val pageSize: Int,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
searchPageSize: Int = pageSize,
) : MangaParser(context, source) {

@ -2,22 +2,26 @@ package org.koitharu.kotatsu.parsers.site.vi
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import okhttp3.*
import okhttp3.ResponseBody.Companion.toResponseBody
import org.koitharu.kotatsu.parsers.Broken
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import okio.IOException
import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
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.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.util.*
@Broken
@MangaSourceParser("CUUTRUYEN", "CuuTruyen", "vi")
internal class CuuTruyenParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.CUUTRUYEN, 20), Interceptor {
@ -46,8 +50,6 @@ internal class CuuTruyenParser(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.KOTATSU)
.build()
private val decryptionKey = "3141592653589793".toByteArray()
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
@ -83,7 +85,15 @@ internal class CuuTruyenParser(context: MangaLoaderContext) :
append(pageSize)
}
val json = webClient.httpGet(url).parseJson()
val json = try {
webClient.httpGet(url).parseJson()
} catch (e: HttpStatusException) {
if (e.statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
return emptyList()
} else {
throw e
}
}
val data = json.getJSONArray("data")
return data.mapJSON { jo ->
@ -93,8 +103,8 @@ internal class CuuTruyenParser(context: MangaLoaderContext) :
publicUrl = "https://$domain/manga/${jo.getLong("id")}",
title = jo.getString("name"),
altTitle = null,
coverUrl = jo.getString("cover_url"),
largeCoverUrl = jo.getString("cover_mobile_url"),
coverUrl = jo.getString("cover_mobile_url"),
largeCoverUrl = jo.getString("cover_url"),
author = jo.getStringOrNull("author_name"),
tags = emptySet(),
state = null,
@ -144,19 +154,20 @@ internal class CuuTruyenParser(context: MangaLoaderContext) :
)
}
private val pageSizesMap = mutableMapOf<Long, Pair<Int, Int>>()
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val url = "https://$domain${chapter.url}"
val json = webClient.httpGet(url).parseJson().getJSONObject("data")
return json.getJSONArray("pages").mapJSON { jo ->
val imageUrl = jo.getString("image_url")
val imageUrl = jo.getString("image_url").toHttpUrl().newBuilder()
val id = jo.getLong("id")
pageSizesMap[id] = jo.getInt("width") to jo.getInt("height")
val drm = jo.getStringOrNull("drm_data")
if (!drm.isNullOrEmpty()) {
imageUrl.fragment(DRM_DATA_KEY + drm)
}
MangaPage(
id = generateUid(id),
url = imageUrl,
url = imageUrl.build().toString(),
preview = null,
source = source,
)
@ -164,99 +175,52 @@ internal class CuuTruyenParser(context: MangaLoaderContext) :
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val response = chain.proceed(chain.request())
val fragment = response.request.url.fragment
if (!request.url.host.contains(domain, ignoreCase = true)) {
if (fragment == null || !fragment.contains(DRM_DATA_KEY)) {
return response
}
val pageId = getPageIdFromUrl(request.url)
val (originalWidth, originalHeight) = pageSizesMap[pageId] ?: (0 to 0)
val decryptedResponse = response.map { body ->
val bytes = body.bytes()
val decrypted = decryptDRM(bytes, decryptionKey)
(swapSegments(decrypted, originalWidth, originalHeight) ?: decrypted).toResponseBody(body.contentType())
val drmData = fragment.substringAfter(DRM_DATA_KEY)
return context.redrawImageResponse(response) { bitmap ->
unscrambleImage(bitmap, drmData)
}
return context.redrawImageResponse(decryptedResponse) {
redrawImage(it)
}
}
private fun getPageIdFromUrl(url: HttpUrl): Long {
return url.pathSegments.lastOrNull()?.toLongOrNull() ?: 0L
}
private fun getOriginalWidthFromRequest(request: Request): Int {
val width = request.url.queryParameter("width")?.toIntOrNull() ?: 0
return width
}
private fun unscrambleImage(bitmap: Bitmap, drmData: String): Bitmap {
val data = context.decodeBase64(drmData)
.decodeXorCipher(DECRYPTION_KEY)
.toString(Charsets.UTF_8)
private fun getOriginalHeightFromRequest(request: Request): Int {
val height = request.url.queryParameter("height")?.toIntOrNull() ?: 0
return height
if (!data.startsWith("#v4|")) {
throw IOException("Invalid DRM data (does not start with expected magic bytes): $data")
}
private fun decryptDRM(drmData: ByteArray, key: ByteArray): ByteArray = runCatchingCancellable {
drmData.mapIndexed { index, byte ->
(byte.toInt() xor key[index % key.size].toInt()).toByte()
}.toByteArray()
}.getOrDefault(drmData)
val result = context.createBitmap(bitmap.width, bitmap.height)
var sy = 0
for (t in data.split('|').drop(1)) {
val (dy, height) = t.split('-').map(String::toInt)
val srcRect = Rect(0, sy, bitmap.width, sy + height)
val dstRect = Rect(0, dy, bitmap.width, dy + height)
private fun redrawImage(source: Bitmap): Bitmap {
return source
result.drawBitmap(bitmap, srcRect, dstRect)
sy += height
}
private fun swapSegments(decrypted: ByteArray, originalWidth: Int, originalHeight: Int): ByteArray? {
val delimiter = "#v".toByteArray()
val delimiterIndex = decrypted.indexOfFirst {
decrypted.sliceArray(it until (it + delimiter.size)).contentEquals(delimiter)
}
if (delimiterIndex == -1) {
return null
}
val segmentsInfoStart = delimiterIndex + delimiter.size
val segmentsData = decrypted.sliceArray(segmentsInfoStart until decrypted.size)
val segments = String(segmentsData).split("|").filter { it.contains("-") }
if (segments.isEmpty()) {
return null
}
val segmentInfo = segments.mapNotNull { seg ->
try {
val (dyStr, heightStr) = seg.split("-")
val dy = if (dyStr.startsWith("dy")) dyStr.substring(2).trim() else dyStr.trim()
val dyInt = dy.toInt()
val height = heightStr.trim().toInt()
dyInt to height
} catch (e: Exception) {
null
}
return result
}
if (segmentInfo.isEmpty()) {
return null
}
private fun ByteArray.decodeXorCipher(key: String): ByteArray {
val k = key.toByteArray(Charsets.UTF_8)
var finalSegmentInfo = segmentInfo
val totalHeight = finalSegmentInfo.sumOf { it.second }
if (totalHeight != originalHeight) {
val remainingHeight = originalHeight - totalHeight
if (remainingHeight > 0) {
finalSegmentInfo = finalSegmentInfo.toMutableList().apply { add(0 to remainingHeight) }
}
}
return decrypted
return this.mapIndexed { i, b ->
(b.toInt() xor k[i % k.size].toInt()).toByte()
}.toByteArray()
}
private fun ByteArray.indexOfFirst(predicate: (Int) -> Boolean): Int {
for (i in indices) {
if (predicate(i)) return i
}
return -1
private companion object {
const val DRM_DATA_KEY = "drm_data="
const val DECRYPTION_KEY = "3141592653589793"
}
}

@ -108,4 +108,4 @@ public fun DateFormat.tryParse(str: String?): Long = if (str.isNullOrEmpty()) {
}.getOrDefault(0L)
}
internal fun Response.requireBody(): ResponseBody = requireNotNull(body) { "Response body is null" }
public fun Response.requireBody(): ResponseBody = requireNotNull(body) { "Response body is null" }

Loading…
Cancel
Save