diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 57dac775d..62ebb2f90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -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 { - 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::class diff --git a/app/src/main/java/exh/md/dto/ChapterDto.kt b/app/src/main/java/exh/md/dto/ChapterDto.kt index be0b3938c..a2597758f 100644 --- a/app/src/main/java/exh/md/dto/ChapterDto.kt +++ b/app/src/main/java/exh/md/dto/ChapterDto.kt @@ -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, val dataSaver: List, - val hash: String, + val externalUrl: String?, + val version: Int, + val createdAt: String, + val updatedAt: String, + val publishAt: String, ) @Serializable diff --git a/app/src/main/java/exh/md/handlers/ComikeyHandler.kt b/app/src/main/java/exh/md/handlers/ComikeyHandler.kt index fbef3f8b9..fbf5862a5 100644 --- a/app/src/main/java/exh/md/handlers/ComikeyHandler.kt +++ b/app/src/main/java/exh/md/handlers/ComikeyHandler.kt @@ -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 { - 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 { + 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 { - val result = ProtoBuf.decodeFromByteArray(MangaPlusSerializer, response.body!!.bytes()) + private fun pageListRequest(mangaId: Int, chapterGuid: String): Request { + return GET("$apiUrl/comics/$mangaId/read?format=json&content=EPI-$chapterGuid", headers) + } - if (result.success == null) { - throw Exception("error getting images") + 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) + } - 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") + fun pageListParse(response: Response): List { + return Json.parseToJsonElement(response.body!!.string()) + .jsonObject["readingOrder"]!! + .jsonArray.mapIndexed { index, element -> + val url = element.jsonObject["href"]!!.jsonPrimitive.content + Page(index, url, url) } } - - 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() - } } diff --git a/app/src/main/java/exh/md/handlers/PageHandler.kt b/app/src/main/java/exh/md/handlers/PageHandler.kt index d17e58955..6e196c226 100644 --- a/app/src/main/java/exh/md/handlers/PageHandler.kt +++ b/app/src/main/java/exh/md/handlers/PageHandler.kt @@ -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): Observable { + 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) + } + } }