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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.mdlist.MdList 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.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter 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.MangaDexFabHeaderAdapter
import exh.md.dto.MangaDto import exh.md.dto.MangaDto
import exh.md.handlers.ApiMangaParser import exh.md.handlers.ApiMangaParser
import exh.md.handlers.ComikeyHandler
import exh.md.handlers.FollowsHandler import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler import exh.md.handlers.MangaPlusHandler
@ -113,8 +112,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
private val mangaPlusHandler by lazy { private val mangaPlusHandler by lazy {
MangaPlusHandler(network.client) MangaPlusHandler(network.client)
} }
private val comikeyHandler by lazy {
ComikeyHandler(network.cloudflareClient)
}
private val pageHandler by lazy { 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? { 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> { override fun fetchImage(page: Page): Observable<Response> {
return if (page.imageUrl?.contains("mangaplus", true) == true) { return pageHandler.fetchImage(page) {
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers)) super.fetchImage(it)
.asObservableSuccess() }
} else super.fetchImage(page)
} }
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class

View File

@ -30,10 +30,14 @@ data class ChapterAttributesDto(
val volume: String?, val volume: String?,
val chapter: String?, val chapter: String?,
val translatedLanguage: String, val translatedLanguage: String,
val publishAt: String, val hash: String,
val data: List<String>, val data: List<String>,
val dataSaver: 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 @Serializable

View File

@ -3,101 +3,64 @@ package exh.md.handlers
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import exh.md.dto.MangaPlusSerializer import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.protobuf.ProtoBuf 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.Headers
import okhttp3.Interceptor import okhttp3.HttpUrl.Companion.toHttpUrl
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 java.util.UUID
class MangaPlusHandler(currentClient: OkHttpClient) { class ComikeyHandler(cloudflareClient: OkHttpClient) {
val baseUrl = "https://jumpg-webapi.tokyo-cdn.com/api" val baseUrl = "https://comikey.com"
private val apiUrl = "$baseUrl/sapi"
val headers = Headers.Builder() val headers = Headers.Builder()
.add("Origin", WEB_URL) .add("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.add("Referer", WEB_URL)
.add("User-Agent", USER_AGENT)
.add("SESSION-TOKEN", UUID.randomUUID().toString()).build()
val client: OkHttpClient = currentClient.newBuilder()
.addInterceptor { imageIntercept(it) }
.build() .build()
suspend fun fetchPageList(chapterId: String): List<Page> { val client: OkHttpClient = cloudflareClient
val response = client.newCall(pageListRequest(chapterId)).await()
return pageListParse(response) 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 { suspend fun getMangaId(mangaUrl: String): Int {
return GET( val response = client.newCall(GET("$baseUrl/read/$mangaUrl")).await()
"$baseUrl/manga_viewer?chapter_id=$chapterId&split=yes&img_quality=super_high", val url = response.asJsoup().selectFirst("meta[property=og:url]")!!.attr("content")
headers return url.trimEnd('/').substringAfterLast('/').toInt()
)
} }
private fun pageListParse(response: Response): List<Page> { private fun pageListRequest(mangaId: Int, chapterGuid: String): Request {
val result = ProtoBuf.decodeFromByteArray(MangaPlusSerializer, response.body!!.bytes()) return GET("$apiUrl/comics/$mangaId/read?format=json&content=EPI-$chapterGuid", headers)
if (result.success == null) {
throw Exception("error getting images")
} }
return result.success.mangaViewer!!.pages private fun getActualPageList(response: Response): Request? {
.mapNotNull { it.page } val element = Json.parseToJsonElement(response.body!!.string()).jsonObject
.mapIndexed { i, page -> val ok = element["ok"]?.jsonPrimitive?.booleanOrNull ?: false
val encryptionKey = if (ok.not()) {
if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}" return null
Page(i, "", "${page.imageUrl}$encryptionKey")
} }
val url = element["href"]?.jsonPrimitive!!.content
return GET(url, headers)
} }
private fun imageIntercept(chain: Interceptor.Chain): Response { fun pageListParse(response: Response): List<Page> {
var request = chain.request() return Json.parseToJsonElement(response.body!!.string())
.jsonObject["readingOrder"]!!
if (!request.url.queryParameterNames.contains("encryptionKey")) { .jsonArray.mapIndexed { index, element ->
return chain.proceed(request) val url = element.jsonObject["href"]!!.jsonPrimitive.content
Page(index, url, url)
} }
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()
} }
} }

View File

@ -2,15 +2,20 @@ package exh.md.handlers
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.mdlist.MdList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.log.xLogD
import exh.md.dto.AtHomeDto import exh.md.dto.AtHomeDto
import exh.md.dto.ChapterDto import exh.md.dto.ChapterDto
import exh.md.service.MangaDexService import exh.md.service.MangaDexService
import exh.md.utils.MdApi import exh.md.utils.MdApi
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Response
import rx.Observable
import tachiyomi.source.Source import tachiyomi.source.Source
import kotlin.reflect.full.superclasses import kotlin.reflect.full.superclasses
import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.isAccessible
@ -19,6 +24,7 @@ class PageHandler(
private val headers: Headers, private val headers: Headers,
private val service: MangaDexService, private val service: MangaDexService,
private val mangaPlusHandler: MangaPlusHandler, private val mangaPlusHandler: MangaPlusHandler,
private val comikeyHandler: ComikeyHandler,
private val preferences: PreferencesHelper, private val preferences: PreferencesHelper,
private val mdList: MdList, private val mdList: MdList,
) { ) {
@ -27,12 +33,17 @@ class PageHandler(
return withIOContext { return withIOContext {
val chapterResponse = service.viewChapter(MdUtil.getChapterId(chapter.url)) val chapterResponse = service.viewChapter(MdUtil.getChapterId(chapter.url))
if (chapter.scanlator.equals("mangaplus", true)) { if (chapterResponse.data.attributes.externalUrl != null) {
mangaPlusHandler.fetchPageList( this@PageHandler.xLogD(chapterResponse.data.attributes.externalUrl)
chapterResponse.data.attributes.data when {
.first() chapter.scanlator.equals("mangaplus", true) -> mangaPlusHandler.fetchPageList(
.substringAfterLast("/") chapterResponse.data.attributes.externalUrl
) )
chapter.scanlator.equals("comikey", true) -> comikeyHandler.fetchPageList(
chapterResponse.data.attributes.externalUrl
)
else -> throw Exception("Chapter not supported")
}
} else { } else {
val headers = if (isLogged) { val headers = if (isLogged) {
MdUtil.getAuthHeaders(headers, preferences, mdList) MdUtil.getAuthHeaders(headers, preferences, mdList)
@ -87,4 +98,18 @@ class PageHandler(
Page(pos, "${atHomeDto.baseUrl},$atHomeRequestUrl,$now", imgUrl) 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)
}
}
} }