[CuuTruyen] Fix attempt 1

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

@ -1 +1 @@
total: 1110 total: 1111

@ -2,286 +2,261 @@ 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.Headers import okhttp3.*
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.HttpUrl
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import org.koitharu.kotatsu.parsers.Broken
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.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.awt.Color
import java.awt.Graphics2D
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.imageio.ImageIO
@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 {
override val configKeyDomain = override val configKeyDomain = ConfigKey.Domain(
ConfigKey.Domain("cuutruyen.net", "nettrom.com", "hetcuutruyen.net", "cuutruyent9sv7.xyz") "cuutruyen.net",
"nettrom.com",
override val availableSortOrders: Set<SortOrder> = EnumSet.of( "hetcuutruyen.net",
SortOrder.UPDATED, "cuutruyent9sv7.xyz",
SortOrder.POPULARITY, )
SortOrder.NEWEST,
) override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
override val filterCapabilities: MangaListFilterCapabilities SortOrder.POPULARITY,
get() = MangaListFilterCapabilities( SortOrder.NEWEST,
isSearchSupported = true, )
)
override val filterCapabilities: MangaListFilterCapabilities
override suspend fun getFilterOptions() = MangaListFilterOptions() get() = MangaListFilterCapabilities(
isSearchSupported = true,
override fun getRequestHeaders(): Headers = Headers.Builder() )
.add("User-Agent", UserAgents.KOTATSU)
.build() override suspend fun getFilterOptions() = MangaListFilterOptions()
private val decryptionKey = "3141592653589793".toByteArray() override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", UserAgents.KOTATSU)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { .build()
val url = buildString {
append("https://") private val decryptionKey = "3141592653589793".toByteArray()
append(domain)
when { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
!filter.query.isNullOrEmpty() -> { val url = buildString {
append("/api/v2/mangas/search?q=") append("https://")
append(filter.query.urlEncoded()) append(domain)
append("&page=") when {
append(page.toString()) !filter.query.isNullOrEmpty() -> {
} append("/api/v2/mangas/search?q=")
append(filter.query.urlEncoded())
else -> { append("&page=")
val tag = filter.tags.oneOrThrowIfMany() append(page.toString())
if (tag != null) { }
append("/api/v2/tags/")
append(tag.key) else -> {
} else { val tag = filter.tags.oneOrThrowIfMany()
append("/api/v2/mangas") if (tag != null) {
when (order) { append("/api/v2/tags/")
SortOrder.UPDATED -> append("/recently_updated") append(tag.key)
SortOrder.POPULARITY -> append("/top") } else {
SortOrder.NEWEST -> append("/recently_updated") append("/api/v2/mangas")
else -> append("/recently_updated") when (order) {
} SortOrder.UPDATED -> append("/recently_updated")
} SortOrder.POPULARITY -> append("/top")
append("?page=") SortOrder.NEWEST -> append("/recently_updated")
append(page.toString()) else -> append("/recently_updated")
} }
} }
append("?page=")
append("&per_page=") append(page.toString())
append(pageSize) }
} }
val json = webClient.httpGet(url).parseJson() append("&per_page=")
val data = json.getJSONArray("data") append(pageSize)
}
return data.mapJSON { jo ->
Manga( val json = webClient.httpGet(url).parseJson()
id = generateUid(jo.getLong("id")), val data = json.getJSONArray("data")
url = "/api/v2/mangas/${jo.getLong("id")}",
publicUrl = "https://$domain/manga/${jo.getLong("id")}", return data.mapJSON { jo ->
title = jo.getString("name"), Manga(
altTitle = null, id = generateUid(jo.getLong("id")),
coverUrl = jo.getString("cover_url"), url = "/api/v2/mangas/${jo.getLong("id")}",
largeCoverUrl = jo.getString("cover_mobile_url"), publicUrl = "https://$domain/manga/${jo.getLong("id")}",
author = jo.getStringOrNull("author_name"), title = jo.getString("name"),
tags = emptySet(), altTitle = null,
state = null, coverUrl = jo.getString("cover_url"),
description = null, largeCoverUrl = jo.getString("cover_mobile_url"),
isNsfw = isNsfwSource, author = jo.getStringOrNull("author_name"),
source = source, tags = emptySet(),
rating = RATING_UNKNOWN, state = null,
) description = null,
} isNsfw = isNsfwSource,
} source = source,
rating = RATING_UNKNOWN,
override suspend fun getDetails(manga: Manga): Manga = coroutineScope { )
val url = "https://" + domain + manga.url }
val chapters = async { }
webClient.httpGet("$url/chapters").parseJson().getJSONArray("data")
} override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val json = webClient.httpGet(url).parseJson().getJSONObject("data") val url = "https://" + domain + manga.url
val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) val chapters = async {
webClient.httpGet("$url/chapters").parseJson().getJSONArray("data")
manga.copy( }
title = json.getStringOrNull("name") ?: manga.title, val json = webClient.httpGet(url).parseJson().getJSONObject("data")
isNsfw = json.getBooleanOrDefault("is_nsfw", manga.isNsfw), val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
author = json.optJSONObject("author")?.getStringOrNull("name")?.substringBefore(','),
description = json.getString("full_description"), manga.copy(
tags = json.optJSONArray("tags")?.mapJSONToSet { jo -> title = json.getStringOrNull("name") ?: manga.title,
MangaTag( isNsfw = json.getBooleanOrDefault("is_nsfw", manga.isNsfw),
title = jo.getString("name").toTitleCase(sourceLocale), author = json.optJSONObject("author")?.getStringOrNull("name")?.substringBefore(','),
key = jo.getString("slug"), description = json.getString("full_description"),
source = source, tags = json.optJSONArray("tags")?.mapJSONToSet { jo ->
) MangaTag(
}.orEmpty(), title = jo.getString("name").toTitleCase(sourceLocale),
chapters = chapters.await().mapJSON { jo -> key = jo.getString("slug"),
val chapterId = jo.getLong("id") source = source,
val number = jo.getFloatOrDefault("number", 0f) )
MangaChapter( }.orEmpty(),
id = generateUid(chapterId), chapters = chapters.await().mapJSON { jo ->
name = jo.getStringOrNull("name") ?: number.formatSimple(), val chapterId = jo.getLong("id")
number = number, val number = jo.getFloatOrDefault("number", 0f)
volume = 0, MangaChapter(
url = "/api/v2/chapters/$chapterId", id = generateUid(chapterId),
scanlator = jo.optString("group_name"), name = jo.getStringOrNull("name") ?: number.formatSimple(),
uploadDate = chapterDateFormat.tryParse(jo.getStringOrNull("created_at")), number = number,
branch = null, volume = 0,
source = source, url = "/api/v2/chapters/$chapterId",
) scanlator = jo.optString("group_name"),
}.reversed(), uploadDate = chapterDateFormat.tryParse(jo.getStringOrNull("created_at")),
) branch = null,
} source = source,
)
private val pageSizesMap = mutableMapOf<Long, Pair<Int, Int>>() }.reversed(),
)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { }
val url = "https://$domain${chapter.url}"
val json = webClient.httpGet(url).parseJson().getJSONObject("data") private val pageSizesMap = mutableMapOf<Long, Pair<Int, Int>>()
return json.getJSONArray("pages").mapJSON { jo -> override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val imageUrl = jo.getString("image_url") val url = "https://$domain${chapter.url}"
val id = jo.getLong("id") val json = webClient.httpGet(url).parseJson().getJSONObject("data")
pageSizesMap[id] = jo.getInt("width") to jo.getInt("height")
MangaPage( return json.getJSONArray("pages").mapJSON { jo ->
id = generateUid(id), val imageUrl = jo.getString("image_url")
url = imageUrl, val id = jo.getLong("id")
preview = null, pageSizesMap[id] = jo.getInt("width") to jo.getInt("height")
source = source, MangaPage(
) id = generateUid(id),
} url = imageUrl,
} preview = null,
source = source,
override fun intercept(chain: Interceptor.Chain): Response { )
val request = chain.request() }
val response = chain.proceed(request) }
if (!request.url.host.contains(domain, ignoreCase = true)) { override fun intercept(chain: Interceptor.Chain): Response {
return response val request = chain.request()
} val response = chain.proceed(request)
val body = response.body ?: return response if (!request.url.host.contains(domain, ignoreCase = true)) {
val contentType = body.contentType() return response
val bytes = body.bytes() }
val pageId = getPageIdFromUrl(request.url)
val (originalWidth, originalHeight) = pageSizesMap[pageId] ?: (0 to 0) val pageId = getPageIdFromUrl(request.url)
val (originalWidth, originalHeight) = pageSizesMap[pageId] ?: (0 to 0)
val decrypted = decryptDRM(bytes, decryptionKey) val decryptedResponse = response.map { body ->
val reconstructed = decrypted?.let { val bytes = body.bytes()
reconstructImage(it, originalWidth, originalHeight) val decrypted = decryptDRM(bytes, decryptionKey)
} ?: bytes (swapSegments(decrypted, originalWidth, originalHeight) ?: decrypted).toResponseBody(body.contentType())
}
val newBody = reconstructed.toResponseBody(contentType)
return response.newBuilder().body(newBody).build() return context.redrawImageResponse(decryptedResponse) {
} redrawImage(it)
}
private fun getPageIdFromUrl(url: HttpUrl): Long { }
return url.pathSegments.lastOrNull()?.toLongOrNull() ?: 0L
} private fun getPageIdFromUrl(url: HttpUrl): Long {
return url.pathSegments.lastOrNull()?.toLongOrNull() ?: 0L
private fun getOriginalWidthFromRequest(request: okhttp3.Request): Int { }
val width = request.url.queryParameter("width")?.toIntOrNull() ?: 0
return width private fun getOriginalWidthFromRequest(request: Request): Int {
} val width = request.url.queryParameter("width")?.toIntOrNull() ?: 0
return width
private fun getOriginalHeightFromRequest(request: okhttp3.Request): Int { }
val height = request.url.queryParameter("height")?.toIntOrNull() ?: 0
return height private fun getOriginalHeightFromRequest(request: Request): Int {
} val height = request.url.queryParameter("height")?.toIntOrNull() ?: 0
return height
private fun decryptDRM(drmData: ByteArray, key: ByteArray): ByteArray? { }
return try {
drmData.mapIndexed { index, byte -> private fun decryptDRM(drmData: ByteArray, key: ByteArray): ByteArray = runCatchingCancellable {
(byte.toInt() xor key[index % key.size].toInt()).toByte() drmData.mapIndexed { index, byte ->
}.toByteArray() (byte.toInt() xor key[index % key.size].toInt()).toByte()
} catch (e: Exception) { }.toByteArray()
null }.getOrDefault(drmData)
}
} private fun redrawImage(source: Bitmap): Bitmap {
return source
private fun reconstructImage(decrypted: ByteArray, originalWidth: Int, originalHeight: Int): ByteArray? { }
return try {
val delimiter = "#v".toByteArray() private fun swapSegments(decrypted: ByteArray, originalWidth: Int, originalHeight: Int): ByteArray? {
val delimiterIndex = decrypted.indexOfFirst { val delimiter = "#v".toByteArray()
decrypted.sliceArray(it until (it + delimiter.size)).contentEquals(delimiter) val delimiterIndex = decrypted.indexOfFirst {
} decrypted.sliceArray(it until (it + delimiter.size)).contentEquals(delimiter)
if (delimiterIndex == -1) { }
return null if (delimiterIndex == -1) {
} return null
}
val segmentsInfoStart = delimiterIndex + delimiter.size
val segmentsData = decrypted.sliceArray(segmentsInfoStart until decrypted.size) val segmentsInfoStart = delimiterIndex + delimiter.size
val segments = String(segmentsData).split("|").filter { it.contains("-") } val segmentsData = decrypted.sliceArray(segmentsInfoStart until decrypted.size)
val segments = String(segmentsData).split("|").filter { it.contains("-") }
if (segments.isEmpty()) {
return null if (segments.isEmpty()) {
} return null
}
val segmentInfo = segments.mapNotNull { seg ->
try { val segmentInfo = segments.mapNotNull { seg ->
val (dyStr, heightStr) = seg.split("-") try {
val dy = if (dyStr.startsWith("dy")) dyStr.substring(2).trim() else dyStr.trim() val (dyStr, heightStr) = seg.split("-")
val dyInt = dy.toInt() val dy = if (dyStr.startsWith("dy")) dyStr.substring(2).trim() else dyStr.trim()
val height = heightStr.trim().toInt() val dyInt = dy.toInt()
dyInt to height val height = heightStr.trim().toInt()
} catch (e: Exception) { dyInt to height
null } catch (e: Exception) {
} null
} }
}
if (segmentInfo.isEmpty()) {
return null if (segmentInfo.isEmpty()) {
} return null
}
var finalSegmentInfo = segmentInfo
val totalHeight = finalSegmentInfo.sumOf { it.second } var finalSegmentInfo = segmentInfo
if (totalHeight != originalHeight) { val totalHeight = finalSegmentInfo.sumOf { it.second }
val remainingHeight = originalHeight - totalHeight if (totalHeight != originalHeight) {
if (remainingHeight > 0) { val remainingHeight = originalHeight - totalHeight
finalSegmentInfo = finalSegmentInfo.toMutableList().apply { add(0 to remainingHeight) } if (remainingHeight > 0) {
} finalSegmentInfo = finalSegmentInfo.toMutableList().apply { add(0 to remainingHeight) }
} }
}
val originalImage = ImageIO.read(decrypted.inputStream()) ?: return null return decrypted
val newImage = BufferedImage(originalWidth, originalHeight, BufferedImage.TYPE_INT_RGB) }
val graphics: Graphics2D = newImage.createGraphics()
private fun ByteArray.indexOfFirst(predicate: (Int) -> Boolean): Int {
var sy = 0 for (i in indices) {
for ((dy, segHeight) in finalSegmentInfo) { if (predicate(i)) return i
if (sy + segHeight > originalHeight) { }
break return -1
} }
val subImage = originalImage.getSubimage(0, sy, originalWidth, segHeight)
graphics.drawImage(subImage, 0, dy, null)
sy += segHeight
}
graphics.dispose()
val outputStream = ByteArrayOutputStream()
ImageIO.write(newImage, "JPEG", outputStream)
outputStream.toByteArray()
} catch (e: Exception) {
null
}
}
private fun ByteArray.indexOfFirst(predicate: (Int) -> Boolean): Int {
for (i in indices) {
if (predicate(i)) return i
}
return -1
}
} }

@ -6,6 +6,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody
public suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation -> public suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
val callback = ContinuationCallCallback(this, continuation) val callback = ContinuationCallCallback(this, continuation)
@ -37,3 +38,9 @@ public fun Response.Builder.setHeader(name: String, value: String?): Response.Bu
} else { } else {
header(name, value) header(name, value)
} }
public inline fun Response.map(mapper: (ResponseBody) -> ResponseBody): Response = body?.use { responseBody ->
newBuilder()
.body(mapper(responseBody))
.build()
} ?: this

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

@ -1,17 +1,23 @@
package org.koitharu.kotatsu.parsers package org.koitharu.kotatsu.parsers
import com.koushikdutta.quack.QuackContext import com.koushikdutta.quack.QuackContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.koitharu.kotatsu.parsers.bitmap.Bitmap import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.test_util.BitmapTestImpl
import java.awt.image.BufferedImage
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.imageio.ImageIO
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
@ -46,12 +52,17 @@ internal object MangaLoaderContextMock : MangaLoaderContext() {
override fun getDefaultUserAgent(): String = UserAgents.FIREFOX_MOBILE override fun getDefaultUserAgent(): String = UserAgents.FIREFOX_MOBILE
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response { override fun redrawImageResponse(response: Response, redraw: (Bitmap) -> Bitmap): Response {
return response val srcImage = response.requireBody().byteStream().use(ImageIO::read)
checkNotNull(srcImage) { "Cannot decode image" }
val resImage = (redraw(BitmapTestImpl(srcImage)) as BitmapTestImpl)
return response.newBuilder()
.body(resImage.compress("png").toResponseBody("image/png".toMediaTypeOrNull()))
.build()
} }
override fun createBitmap(width: Int, height: Int): Bitmap { override fun createBitmap(width: Int, height: Int): Bitmap {
throw UnsupportedOperationException() return BitmapTestImpl(BufferedImage(width, height, BufferedImage.TYPE_INT_RGB))
} }
suspend fun doRequest(url: String, source: MangaSource?): Response { suspend fun doRequest(url: String, source: MangaSource?): Response {

@ -0,0 +1,35 @@
package org.koitharu.kotatsu.test_util
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.bitmap.Rect
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO
class BitmapTestImpl(
val image: BufferedImage,
) : Bitmap {
override val width: Int
get() = image.width
override val height: Int
get() = image.height
override fun drawBitmap(
sourceBitmap: Bitmap,
src: Rect,
dst: Rect,
) {
val graphics = image.createGraphics()
val subImage = (sourceBitmap as BitmapTestImpl).image.getSubimage(
src.left, src.top, src.width, src.height,
)
graphics.drawImage(subImage, dst.left, dst.top, dst.width, dst.height, null)
graphics.dispose()
}
fun compress(format: String): ByteArray = ByteArrayOutputStream().also { stream ->
ImageIO.write(image, format, stream)
}.toByteArray()
}
Loading…
Cancel
Save