Support Comikey chapters

This commit is contained in:
Jobobby04 2021-09-01 15:53:23 -04:00
parent adc6398589
commit 94595a99ac
4 changed files with 88 additions and 95 deletions

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}