Support Comikey chapters
This commit is contained in:
parent
adc6398589
commit
94595a99ac
@ -7,8 +7,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
@ -27,6 +25,7 @@ import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.md.dto.MangaDto
|
||||
import exh.md.handlers.ApiMangaParser
|
||||
import exh.md.handlers.ComikeyHandler
|
||||
import exh.md.handlers.FollowsHandler
|
||||
import exh.md.handlers.MangaHandler
|
||||
import exh.md.handlers.MangaPlusHandler
|
||||
@ -113,8 +112,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
private val mangaPlusHandler by lazy {
|
||||
MangaPlusHandler(network.client)
|
||||
}
|
||||
private val comikeyHandler by lazy {
|
||||
ComikeyHandler(network.cloudflareClient)
|
||||
}
|
||||
private val pageHandler by lazy {
|
||||
PageHandler(headers, mangadexService, mangaPlusHandler, preferences, mdList)
|
||||
PageHandler(headers, mangadexService, mangaPlusHandler, comikeyHandler, preferences, mdList)
|
||||
}
|
||||
|
||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
@ -159,10 +161,9 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
|
||||
override fun fetchImage(page: Page): Observable<Response> {
|
||||
return if (page.imageUrl?.contains("mangaplus", true) == true) {
|
||||
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers))
|
||||
.asObservableSuccess()
|
||||
} else super.fetchImage(page)
|
||||
return pageHandler.fetchImage(page) {
|
||||
super.fetchImage(it)
|
||||
}
|
||||
}
|
||||
|
||||
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
|
||||
|
@ -30,10 +30,14 @@ data class ChapterAttributesDto(
|
||||
val volume: String?,
|
||||
val chapter: String?,
|
||||
val translatedLanguage: String,
|
||||
val publishAt: String,
|
||||
val hash: String,
|
||||
val data: List<String>,
|
||||
val dataSaver: List<String>,
|
||||
val hash: String,
|
||||
val externalUrl: String?,
|
||||
val version: Int,
|
||||
val createdAt: String,
|
||||
val updatedAt: String,
|
||||
val publishAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -3,101 +3,64 @@ package exh.md.handlers
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import exh.md.dto.MangaPlusSerializer
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.util.UUID
|
||||
|
||||
class MangaPlusHandler(currentClient: OkHttpClient) {
|
||||
val baseUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
|
||||
class ComikeyHandler(cloudflareClient: OkHttpClient) {
|
||||
val baseUrl = "https://comikey.com"
|
||||
private val apiUrl = "$baseUrl/sapi"
|
||||
val headers = Headers.Builder()
|
||||
.add("Origin", WEB_URL)
|
||||
.add("Referer", WEB_URL)
|
||||
.add("User-Agent", USER_AGENT)
|
||||
.add("SESSION-TOKEN", UUID.randomUUID().toString()).build()
|
||||
|
||||
val client: OkHttpClient = currentClient.newBuilder()
|
||||
.addInterceptor { imageIntercept(it) }
|
||||
.add("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
||||
.build()
|
||||
|
||||
suspend fun fetchPageList(chapterId: String): List<Page> {
|
||||
val response = client.newCall(pageListRequest(chapterId)).await()
|
||||
return pageListParse(response)
|
||||
val client: OkHttpClient = cloudflareClient
|
||||
|
||||
private val urlForbidden = "https://fakeimg.pl/1800x2252/FFFFFF/000000/?font_size=120&text=This%20chapter%20is%20not%20available%20for%20free.%0A%0AIf%20you%20have%20purchased%20this%20chapter%2C%20please%20%0Aopen%20the%20website%20in%20web%20view%20and%20log%20in."
|
||||
|
||||
suspend fun fetchPageList(externalUrl: String): List<Page> {
|
||||
val httpUrl = externalUrl.toHttpUrl()
|
||||
val mangaId = getMangaId(httpUrl.pathSegments[1])
|
||||
val response = client.newCall(pageListRequest(mangaId, httpUrl.pathSegments[2])).await()
|
||||
val request = getActualPageList(response) ?: return listOf(Page(0, urlForbidden, urlForbidden))
|
||||
return pageListParse(client.newCall(request).await())
|
||||
}
|
||||
|
||||
private fun pageListRequest(chapterId: String): Request {
|
||||
return GET(
|
||||
"$baseUrl/manga_viewer?chapter_id=$chapterId&split=yes&img_quality=super_high",
|
||||
headers
|
||||
)
|
||||
suspend fun getMangaId(mangaUrl: String): Int {
|
||||
val response = client.newCall(GET("$baseUrl/read/$mangaUrl")).await()
|
||||
val url = response.asJsoup().selectFirst("meta[property=og:url]")!!.attr("content")
|
||||
return url.trimEnd('/').substringAfterLast('/').toInt()
|
||||
}
|
||||
|
||||
private fun pageListParse(response: Response): List<Page> {
|
||||
val result = ProtoBuf.decodeFromByteArray(MangaPlusSerializer, response.body!!.bytes())
|
||||
|
||||
if (result.success == null) {
|
||||
throw Exception("error getting images")
|
||||
private fun pageListRequest(mangaId: Int, chapterGuid: String): Request {
|
||||
return GET("$apiUrl/comics/$mangaId/read?format=json&content=EPI-$chapterGuid", headers)
|
||||
}
|
||||
|
||||
return result.success.mangaViewer!!.pages
|
||||
.mapNotNull { it.page }
|
||||
.mapIndexed { i, page ->
|
||||
val encryptionKey =
|
||||
if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}"
|
||||
Page(i, "", "${page.imageUrl}$encryptionKey")
|
||||
private fun getActualPageList(response: Response): Request? {
|
||||
val element = Json.parseToJsonElement(response.body!!.string()).jsonObject
|
||||
val ok = element["ok"]?.jsonPrimitive?.booleanOrNull ?: false
|
||||
if (ok.not()) {
|
||||
return null
|
||||
}
|
||||
val url = element["href"]?.jsonPrimitive!!.content
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
private fun imageIntercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
|
||||
if (!request.url.queryParameterNames.contains("encryptionKey")) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
val encryptionKey = request.url.queryParameter("encryptionKey")!!
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray {
|
||||
val keyStream = HEX_GROUP
|
||||
.findAll(encryptionKey)
|
||||
.map { it.groupValues[1].toInt(16) }
|
||||
.toList()
|
||||
|
||||
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() }
|
||||
}
|
||||
|
||||
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()
|
||||
fun pageListParse(response: Response): List<Page> {
|
||||
return Json.parseToJsonElement(response.body!!.string())
|
||||
.jsonObject["readingOrder"]!!
|
||||
.jsonArray.mapIndexed { index, element ->
|
||||
val url = element.jsonObject["href"]!!.jsonPrimitive.content
|
||||
Page(index, url, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,20 @@ package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.log.xLogD
|
||||
import exh.md.dto.AtHomeDto
|
||||
import exh.md.dto.ChapterDto
|
||||
import exh.md.service.MangaDexService
|
||||
import exh.md.utils.MdApi
|
||||
import exh.md.utils.MdUtil
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import tachiyomi.source.Source
|
||||
import kotlin.reflect.full.superclasses
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
@ -19,6 +24,7 @@ class PageHandler(
|
||||
private val headers: Headers,
|
||||
private val service: MangaDexService,
|
||||
private val mangaPlusHandler: MangaPlusHandler,
|
||||
private val comikeyHandler: ComikeyHandler,
|
||||
private val preferences: PreferencesHelper,
|
||||
private val mdList: MdList,
|
||||
) {
|
||||
@ -27,12 +33,17 @@ class PageHandler(
|
||||
return withIOContext {
|
||||
val chapterResponse = service.viewChapter(MdUtil.getChapterId(chapter.url))
|
||||
|
||||
if (chapter.scanlator.equals("mangaplus", true)) {
|
||||
mangaPlusHandler.fetchPageList(
|
||||
chapterResponse.data.attributes.data
|
||||
.first()
|
||||
.substringAfterLast("/")
|
||||
if (chapterResponse.data.attributes.externalUrl != null) {
|
||||
this@PageHandler.xLogD(chapterResponse.data.attributes.externalUrl)
|
||||
when {
|
||||
chapter.scanlator.equals("mangaplus", true) -> mangaPlusHandler.fetchPageList(
|
||||
chapterResponse.data.attributes.externalUrl
|
||||
)
|
||||
chapter.scanlator.equals("comikey", true) -> comikeyHandler.fetchPageList(
|
||||
chapterResponse.data.attributes.externalUrl
|
||||
)
|
||||
else -> throw Exception("Chapter not supported")
|
||||
}
|
||||
} else {
|
||||
val headers = if (isLogged) {
|
||||
MdUtil.getAuthHeaders(headers, preferences, mdList)
|
||||
@ -87,4 +98,18 @@ class PageHandler(
|
||||
Page(pos, "${atHomeDto.baseUrl},$atHomeRequestUrl,$now", imgUrl)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchImage(page: Page, superMethod: (Page) -> Observable<Response>): Observable<Response> {
|
||||
return when {
|
||||
page.imageUrl?.contains("mangaplus", true) == true -> {
|
||||
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers))
|
||||
.asObservableSuccess()
|
||||
}
|
||||
page.imageUrl?.contains("comikey", true) == true -> {
|
||||
comikeyHandler.client.newCall(GET(page.imageUrl!!, comikeyHandler.headers))
|
||||
.asObservableSuccess()
|
||||
}
|
||||
else -> superMethod(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user