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) }
|
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)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user