From d013578fb5d5ef6e796810869a90a7989caf51b2 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sun, 9 Apr 2023 22:07:24 -0400 Subject: [PATCH] Fix manga plus chapters --- .../tachiyomi/source/online/all/MangaDex.kt | 4 + app/src/main/java/exh/md/dto/MangaPlusDto.kt | 17 ++-- .../java/exh/md/handlers/MangaPlusHandler.kt | 95 ++++++++++--------- 3 files changed, 61 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 6e144ebb2..2639197ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -194,6 +194,10 @@ class MangaDex(delegate: HttpSource, val context: Context) : return runAsObservable { pageHandler.fetchPageList(chapter, usePort443Only(), dataSaver(), delegate) } } + override suspend fun getPageList(chapter: SChapter): List { + return pageHandler.fetchPageList(chapter, usePort443Only(), dataSaver(), delegate) + } + override fun fetchImage(page: Page): Observable { val call = pageHandler.getImageCall(page) return call?.asObservableSuccess() ?: super.fetchImage(page) diff --git a/app/src/main/java/exh/md/dto/MangaPlusDto.kt b/app/src/main/java/exh/md/dto/MangaPlusDto.kt index 41b5ace44..aded0e6da 100644 --- a/app/src/main/java/exh/md/dto/MangaPlusDto.kt +++ b/app/src/main/java/exh/md/dto/MangaPlusDto.kt @@ -1,28 +1,27 @@ package exh.md.dto import kotlinx.serialization.Serializable -import kotlinx.serialization.protobuf.ProtoNumber @Serializable data class MangaPlusResponse( - @ProtoNumber(1) val success: SuccessResult? = null, + val success: SuccessResult? = null, ) @Serializable data class SuccessResult( - @ProtoNumber(10) val mangaViewer: MangaViewer? = null, + val mangaViewer: MangaViewer? = null, ) @Serializable -data class MangaViewer(@ProtoNumber(1) val pages: List = emptyList()) +data class MangaViewer(val pages: List = emptyList()) @Serializable -data class MangaPlusPage(@ProtoNumber(1) val page: MangaPage? = null) +data class MangaPlusPage(val mangaPage: MangaPage? = null) @Serializable data class MangaPage( - @ProtoNumber(1) val imageUrl: String, - @ProtoNumber(2) val width: Int, - @ProtoNumber(3) val height: Int, - @ProtoNumber(5) val encryptionKey: String? = null, + val imageUrl: String, + val width: Int, + val height: Int, + val encryptionKey: String? = null, ) diff --git a/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt b/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt index 644c8e624..4c9a8f8ac 100644 --- a/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt @@ -2,21 +2,26 @@ package exh.md.handlers import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.source.model.Page +import exh.md.dto.MangaPlusPage import exh.md.dto.MangaPlusResponse -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.protobuf.ProtoBuf +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody +import uy.kohesive.injekt.injectLazy import java.util.UUID class MangaPlusHandler(currentClient: OkHttpClient) { - val baseUrl = "https://jumpg-webapi.tokyo-cdn.com/api" + val json: Json by injectLazy() + val headers = Headers.Builder() .add("Origin", WEB_URL) .add("Referer", WEB_URL) @@ -24,7 +29,9 @@ class MangaPlusHandler(currentClient: OkHttpClient) { .add("SESSION-TOKEN", UUID.randomUUID().toString()).build() val client: OkHttpClient = currentClient.newBuilder() - .addInterceptor { imageIntercept(it) } + .addInterceptor(::imageIntercept) + .rateLimitHost(API_URL.toHttpUrl(), 1) + .rateLimitHost(WEB_URL.toHttpUrl(), 2) .build() suspend fun fetchPageList(chapterId: String): List { @@ -33,72 +40,68 @@ class MangaPlusHandler(currentClient: OkHttpClient) { } private fun pageListRequest(chapterId: String): Request { - return GET( - "$baseUrl/manga_viewer?chapter_id=$chapterId&split=yes&img_quality=super_high", - headers, - ) + val newHeaders = headers.newBuilder() + .set("Referer", "$WEB_URL/viewer/$chapterId") + .build() + + val url = "$API_URL/manga_viewer".toHttpUrl().newBuilder() + .addQueryParameter("chapter_id", chapterId) + .addQueryParameter("split", "yes") + .addQueryParameter("img_quality", "super_high") + .addQueryParameter("format", "json") + .toString() + + return GET(url, newHeaders) } private fun pageListParse(response: Response): List { - val result = ProtoBuf.decodeFromByteArray(response.body.bytes()) + val result = json.decodeFromString(response.body.string()) if (result.success == null) { throw Exception("error getting images") } + val referer = response.request.header("Referer")!! + return result.success.mangaViewer!!.pages - .mapNotNull { it.page } + .mapNotNull(MangaPlusPage::mangaPage) .mapIndexed { i, page -> - val encryptionKey = - if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}" - Page(i, "", "${page.imageUrl}$encryptionKey") + val encryptionKey = if (page.encryptionKey == null) "" else "#${page.encryptionKey}" + Page(i, referer, page.imageUrl + encryptionKey) } } private fun imageIntercept(chain: Interceptor.Chain): Response { - var request = chain.request() + val request = chain.request() + val response = chain.proceed(request) + val encryptionKey = request.url.fragment - if (!request.url.queryParameterNames.contains("encryptionKey")) { - return chain.proceed(request) + if (encryptionKey.isNullOrEmpty()) { + return response } - val encryptionKey = request.url.queryParameter("encryptionKey")!! + val contentType = response.headers["Content-Type"] ?: "image/jpeg" + val image = response.body.bytes().decodeXorCipher(encryptionKey) + val body = image.toResponseBody(contentType.toMediaTypeOrNull()) - // Change the url and remove the encryptionKey to avoid detection. - val newUrl = request.url.newBuilder().removeAllQueryParameters("encryptionKey").build() - request = request.newBuilder().url(newUrl).build() - - val response = chain.proceed(request) - - val image = decodeImage(encryptionKey, response.body.bytes()) - - val body = image.toResponseBody("image/jpeg".toMediaTypeOrNull()) - return response.newBuilder().body(body).build() + return response.newBuilder() + .body(body) + .build() } - private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray { - val keyStream = HEX_GROUP - .findAll(encryptionKey) - .map { it.groupValues[1].toInt(16) } - .toList() + private fun ByteArray.decodeXorCipher(key: String): ByteArray { + val keyStream = key.chunked(2) + .map { it.toInt(16) } - val content = image - .map { it.toInt() } - .toIntArray() - - val blockSizeInBytes = keyStream.size - - content.forEachIndexed { i, value -> - content[i] = value xor keyStream[i % blockSizeInBytes] - } - - return ByteArray(content.size) { pos -> content[pos].toByte() } + return mapIndexed { i, byte -> byte.toInt() xor keyStream[i % keyStream.size] } + .map(Int::toByte) + .toByteArray() } companion object { private const val WEB_URL = "https://mangaplus.shueisha.co.jp" - private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36" - private val HEX_GROUP = "(.{1,2})".toRegex() + private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36" + private const val API_URL = "https://jumpg-webapi.tokyo-cdn.com/api" } }