Comikey: Fix page list (#1161)

This commit is contained in:
beerpsi 2024-02-09 22:50:09 +07:00 committed by Draff
parent 712c3a75be
commit f71938e357
3 changed files with 111 additions and 21 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = "Comikey" extName = "Comikey"
extClass = ".ComikeyFactory" extClass = ".ComikeyFactory"
extVersionCode = 1 extVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -7,6 +7,8 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.View import android.view.View
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@ -24,6 +26,7 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -267,17 +270,80 @@ open class Comikey(
innerWv.settings.domStorageEnabled = true innerWv.settings.domStorageEnabled = true
innerWv.settings.javaScriptEnabled = true innerWv.settings.javaScriptEnabled = true
innerWv.settings.blockNetworkImage = true innerWv.settings.blockNetworkImage = true
innerWv.settings.userAgentString = headers["User-Agent"]
innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null) innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
innerWv.addJavascriptInterface(jsInterface, interfaceName) innerWv.addJavascriptInterface(jsInterface, interfaceName)
// Somewhat useful if you need to debug WebView issues. Don't delete.
//
/*innerWv.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
if (consoleMessage == null) { return false }
val logContent = "wv: ${consoleMessage.message()} (${consoleMessage.sourceId()}, line ${consoleMessage.lineNumber()})"
when (consoleMessage.messageLevel()) {
ConsoleMessage.MessageLevel.DEBUG -> Log.d("comikey", logContent)
ConsoleMessage.MessageLevel.ERROR -> Log.e("comikey", logContent)
ConsoleMessage.MessageLevel.LOG -> Log.i("comikey", logContent)
ConsoleMessage.MessageLevel.TIP -> Log.i("comikey", logContent)
ConsoleMessage.MessageLevel.WARNING -> Log.w("comikey", logContent)
else -> Log.d("comikey", logContent)
}
return true
}
}*/
innerWv.webViewClient = object : WebViewClient() { innerWv.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon) super.onPageStarted(view, url, favicon)
view?.evaluateJavascript(webviewScript.replace("__interface__", interfaceName)) {} view?.evaluateJavascript(webviewScript.replace("__interface__", interfaceName)) {}
} }
// If you're logged in, the manifest URL sent to the client is not a direct link;
// it only redirects to the real one when you call it.
//
// In order to avoid a later call and remove an avenue for sniffing out users,
// we intercept said request so we can grab the real manifest URL.
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
val url = request?.url
?: return super.shouldInterceptRequest(view, request)
if (url.host != "relay-us.epub.rocks" || url.path?.endsWith("/manifest") != true) {
return super.shouldInterceptRequest(view, request)
} }
innerWv.loadUrl("$baseUrl${chapter.url}") val requestHeaders = headers.newBuilder().apply {
request.requestHeaders.entries.forEach {
set(it.key, it.value)
}
removeAll("X-Requested-With")
}.build()
val response = client.newCall(GET(url.toString(), requestHeaders)).execute()
jsInterface.manifestUrl = response.request.url
return WebResourceResponse(
response.headers["Content-Type"] ?: "application/divina+json+vnd.e4p.drm",
null,
response.code,
response.message,
response.headers.toMap(),
response.body.byteStream(),
)
}
}
innerWv.loadUrl(
"$baseUrl${chapter.url}",
buildMap {
putAll(headers.toMap())
put("X-Requested-With", randomString())
},
)
} }
latch.await(30, TimeUnit.SECONDS) latch.await(30, TimeUnit.SECONDS)
@ -291,18 +357,30 @@ open class Comikey(
throw Exception(jsInterface.error) throw Exception(jsInterface.error)
} }
val manifestUrl = jsInterface.manifestUrl.toHttpUrl() val manifestUrl = jsInterface.manifestUrl!!
val manifest = jsInterface.manifest!!
val webtoon = manifest.metadata.readingProgression == "ttb"
return jsInterface.images.mapIndexed { i, it -> return manifest.readingOrder.mapIndexed { i, it ->
val href = it.alternate.firstOrNull { it.type == "image/webp" }?.href
?: it.href
val url = manifestUrl.newBuilder().apply { val url = manifestUrl.newBuilder().apply {
removePathSegment(manifestUrl.pathSegments.size - 1) removePathSegment(manifestUrl.pathSize - 1)
addPathSegments(href)
addQueryParameter("act", jsInterface.act)
}.build()
Page(i, imageUrl = url.toString()) if (it.alternate.isNotEmpty() && it.height == 2048 && it.type == "image/jpeg") {
addPathSegments(
it.alternate.first {
val dimension = if (webtoon) it.width else it.height
dimension <= 1536 && it.type == "image/webp"
}.href,
)
} else {
addPathSegments(it.href)
}
addQueryParameter("act", jsInterface.act)
}.toString()
Page(i, imageUrl = url)
} }
} }
@ -332,13 +410,17 @@ open class Comikey(
?: throw Exception(intl["error_webview_script_not_found"]) ?: throw Exception(intl["error_webview_script_not_found"])
} }
private fun randomString() = buildString(15) { private fun randomString(): String {
val length = (10..20).random()
return buildString(length) {
val charPool = ('a'..'z') + ('A'..'Z') val charPool = ('a'..'z') + ('A'..'Z')
for (i in 0 until 15) { for (i in 0 until length) {
append(charPool.random()) append(charPool.random())
} }
} }
}
private fun makeEpisodeSlug(episode: ComikeyEpisode, defaultChapterPrefix: String): String { private fun makeEpisodeSlug(episode: ComikeyEpisode, defaultChapterPrefix: String): String {
val e4pid = episode.id.split("-", limit = 2).last() val e4pid = episode.id.split("-", limit = 2).last()
@ -354,7 +436,7 @@ open class Comikey(
defaultChapterPrefix defaultChapterPrefix
} }
return "$e4pid/$chapterPrefix-${episode.number.toString().replace(".", "-")}" return "$e4pid/$chapterPrefix-${episode.number.toString().removeSuffix(".0").replace(".", "-")}"
} }
private class JsInterface( private class JsInterface(
@ -362,11 +444,10 @@ open class Comikey(
private val json: Json, private val json: Json,
private val intl: Intl, private val intl: Intl,
) { ) {
var images: List<ComikeyPage> = emptyList() var manifest: ComikeyEpisodeManifest? = null
private set private set
var manifestUrl: String = "" var manifestUrl: HttpUrl? = null
private set
var act: String = "" var act: String = ""
private set private set
@ -390,9 +471,12 @@ open class Comikey(
@JavascriptInterface @JavascriptInterface
@Suppress("UNUSED") @Suppress("UNUSED")
fun passPayload(manifestUrl: String, act: String, rawData: String) { fun passPayload(manifestUrl: String, act: String, rawData: String) {
this.manifestUrl = manifestUrl if (this.manifestUrl == null) {
this.manifestUrl = manifestUrl.toHttpUrl()
}
this.act = act this.act = act
images = json.decodeFromString<ComikeyEpisodeManifest>(rawData).readingOrder manifest = json.decodeFromString<ComikeyEpisodeManifest>(rawData)
latch.countDown() latch.countDown()
} }

View File

@ -43,9 +43,15 @@ data class ComikeyEpisode(
@Serializable @Serializable
data class ComikeyEpisodeManifest( data class ComikeyEpisodeManifest(
val metadata: ComikeyEpisodeManifestMetadata,
val readingOrder: List<ComikeyPage>, val readingOrder: List<ComikeyPage>,
) )
@Serializable
data class ComikeyEpisodeManifestMetadata(
val readingProgression: String,
)
@Serializable @Serializable
data class ComikeyPage( data class ComikeyPage(
val href: String, val href: String,