[CuuTruyen] Fix

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( public abstract class PagedMangaParser(
context: MangaLoaderContext, context: MangaLoaderContext,
source: MangaParserSource, source: MangaParserSource,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField internal val pageSize: Int, @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
searchPageSize: Int = pageSize, searchPageSize: Int = pageSize,
) : MangaParser(context, source) { ) : MangaParser(context, source) {

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

@ -108,4 +108,4 @@ public fun DateFormat.tryParse(str: String?): Long = if (str.isNullOrEmpty()) {
}.getOrDefault(0L) }.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