Fix manga plus chapters

This commit is contained in:
Jobobby04 2023-04-09 22:07:24 -04:00
parent 81283dc5cf
commit d013578fb5
3 changed files with 61 additions and 55 deletions

View File

@ -194,6 +194,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return runAsObservable { pageHandler.fetchPageList(chapter, usePort443Only(), dataSaver(), delegate) } return runAsObservable { pageHandler.fetchPageList(chapter, usePort443Only(), dataSaver(), delegate) }
} }
override suspend fun getPageList(chapter: SChapter): List<Page> {
return pageHandler.fetchPageList(chapter, usePort443Only(), dataSaver(), delegate)
}
override fun fetchImage(page: Page): Observable<Response> { override fun fetchImage(page: Page): Observable<Response> {
val call = pageHandler.getImageCall(page) val call = pageHandler.getImageCall(page)
return call?.asObservableSuccess() ?: super.fetchImage(page) return call?.asObservableSuccess() ?: super.fetchImage(page)

View File

@ -1,28 +1,27 @@
package exh.md.dto package exh.md.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class MangaPlusResponse( data class MangaPlusResponse(
@ProtoNumber(1) val success: SuccessResult? = null, val success: SuccessResult? = null,
) )
@Serializable @Serializable
data class SuccessResult( data class SuccessResult(
@ProtoNumber(10) val mangaViewer: MangaViewer? = null, val mangaViewer: MangaViewer? = null,
) )
@Serializable @Serializable
data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList()) data class MangaViewer(val pages: List<MangaPlusPage> = emptyList())
@Serializable @Serializable
data class MangaPlusPage(@ProtoNumber(1) val page: MangaPage? = null) data class MangaPlusPage(val mangaPage: MangaPage? = null)
@Serializable @Serializable
data class MangaPage( data class MangaPage(
@ProtoNumber(1) val imageUrl: String, val imageUrl: String,
@ProtoNumber(2) val width: Int, val width: Int,
@ProtoNumber(3) val height: Int, val height: Int,
@ProtoNumber(5) val encryptionKey: String? = null, val encryptionKey: String? = null,
) )

View File

@ -2,21 +2,26 @@ package exh.md.handlers
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import exh.md.dto.MangaPlusPage
import exh.md.dto.MangaPlusResponse import exh.md.dto.MangaPlusResponse
import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.decodeFromString
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull 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 okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.injectLazy
import java.util.UUID import java.util.UUID
class MangaPlusHandler(currentClient: OkHttpClient) { class MangaPlusHandler(currentClient: OkHttpClient) {
val baseUrl = "https://jumpg-webapi.tokyo-cdn.com/api" val json: Json by injectLazy()
val headers = Headers.Builder() val headers = Headers.Builder()
.add("Origin", WEB_URL) .add("Origin", WEB_URL)
.add("Referer", WEB_URL) .add("Referer", WEB_URL)
@ -24,7 +29,9 @@ class MangaPlusHandler(currentClient: OkHttpClient) {
.add("SESSION-TOKEN", UUID.randomUUID().toString()).build() .add("SESSION-TOKEN", UUID.randomUUID().toString()).build()
val client: OkHttpClient = currentClient.newBuilder() val client: OkHttpClient = currentClient.newBuilder()
.addInterceptor { imageIntercept(it) } .addInterceptor(::imageIntercept)
.rateLimitHost(API_URL.toHttpUrl(), 1)
.rateLimitHost(WEB_URL.toHttpUrl(), 2)
.build() .build()
suspend fun fetchPageList(chapterId: String): List<Page> { suspend fun fetchPageList(chapterId: String): List<Page> {
@ -33,72 +40,68 @@ class MangaPlusHandler(currentClient: OkHttpClient) {
} }
private fun pageListRequest(chapterId: String): Request { private fun pageListRequest(chapterId: String): Request {
return GET( val newHeaders = headers.newBuilder()
"$baseUrl/manga_viewer?chapter_id=$chapterId&split=yes&img_quality=super_high", .set("Referer", "$WEB_URL/viewer/$chapterId")
headers, .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<Page> { private fun pageListParse(response: Response): List<Page> {
val result = ProtoBuf.decodeFromByteArray<MangaPlusResponse>(response.body.bytes()) val result = json.decodeFromString<MangaPlusResponse>(response.body.string())
if (result.success == null) { if (result.success == null) {
throw Exception("error getting images") throw Exception("error getting images")
} }
val referer = response.request.header("Referer")!!
return result.success.mangaViewer!!.pages return result.success.mangaViewer!!.pages
.mapNotNull { it.page } .mapNotNull(MangaPlusPage::mangaPage)
.mapIndexed { i, page -> .mapIndexed { i, page ->
val encryptionKey = val encryptionKey = if (page.encryptionKey == null) "" else "#${page.encryptionKey}"
if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}" Page(i, referer, page.imageUrl + encryptionKey)
Page(i, "", "${page.imageUrl}$encryptionKey")
} }
} }
private fun imageIntercept(chain: Interceptor.Chain): Response { 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")) { if (encryptionKey.isNullOrEmpty()) {
return chain.proceed(request) 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. return response.newBuilder()
val newUrl = request.url.newBuilder().removeAllQueryParameters("encryptionKey").build() .body(body)
request = request.newBuilder().url(newUrl).build() .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()
} }
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray { private fun ByteArray.decodeXorCipher(key: String): ByteArray {
val keyStream = HEX_GROUP val keyStream = key.chunked(2)
.findAll(encryptionKey) .map { it.toInt(16) }
.map { it.groupValues[1].toInt(16) }
.toList()
val content = image return mapIndexed { i, byte -> byte.toInt() xor keyStream[i % keyStream.size] }
.map { it.toInt() } .map(Int::toByte)
.toIntArray() .toByteArray()
val blockSizeInBytes = keyStream.size
content.forEachIndexed { i, value ->
content[i] = value xor keyStream[i % blockSizeInBytes]
}
return ByteArray(content.size) { pos -> content[pos].toByte() }
} }
companion object { companion object {
private const val WEB_URL = "https://mangaplus.shueisha.co.jp" private const val WEB_URL = "https://mangaplus.shueisha.co.jp"
private const val USER_AGENT = private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36" "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
private val HEX_GROUP = "(.{1,2})".toRegex() private const val API_URL = "https://jumpg-webapi.tokyo-cdn.com/api"
} }
} }