Fix manga plus chapters
This commit is contained in:
parent
81283dc5cf
commit
d013578fb5
@ -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<Page> {
|
||||
return pageHandler.fetchPageList(chapter, usePort443Only(), dataSaver(), delegate)
|
||||
}
|
||||
|
||||
override fun fetchImage(page: Page): Observable<Response> {
|
||||
val call = pageHandler.getImageCall(page)
|
||||
return call?.asObservableSuccess() ?: super.fetchImage(page)
|
||||
|
@ -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<MangaPlusPage> = emptyList())
|
||||
data class MangaViewer(val pages: List<MangaPlusPage> = 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,
|
||||
)
|
||||
|
@ -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<Page> {
|
||||
@ -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<Page> {
|
||||
val result = ProtoBuf.decodeFromByteArray<MangaPlusResponse>(response.body.bytes())
|
||||
val result = json.decodeFromString<MangaPlusResponse>(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"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user