Bring back ktor CIO for image downloading

This commit is contained in:
tarsin committed 2023-02-28 17:52:16 +08:00
1 parent 3f4efc51c9
commit 5c18dad1d8
9 files changed
+159 -219

No files matched your search

@@ -1,66 +0,0 @@
@file:JvmName("GifDecodeUtils")
package coil.decode
import kotlin.experimental.and
import okio.BufferedSource
import okio.ByteString.Companion.encodeUtf8
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
// https://developers.google.com/speed/webp/docs/riff_container
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
// https://nokiatech.github.io/heif/technical.html
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
/**
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
*/
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
return source.rangeEquals(0, GIF_HEADER_89A) ||
source.rangeEquals(0, GIF_HEADER_87A)
}
/**
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
*/
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
source.rangeEquals(8, WEBP_HEADER_WEBP)
}
/**
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
*/
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
return isWebP(source) &&
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
source.request(17) &&
(source.buffer[16] and 0b00000010) > 0
}
/**
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
*/
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
return source.rangeEquals(4, HEIF_HEADER_FTYP)
}
/**
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
* consumed.
*/
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
return isHeif(source) &&
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
source.rangeEquals(8, HEIF_HEADER_HEVX))
}
@@ -1,88 +0,0 @@
@file:Suppress("SameParameterValue")
package coil.decode
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.ForwardingSource
import okio.Source
/**
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
* below a threshold.
*/
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
// An intermediary buffer so we can read and alter the data before it's written to the destination.
private val buffer = Buffer()
override fun read(sink: Buffer, byteCount: Long): Long {
// Ensure our buffer has enough bytes to satisfy this read.
request(byteCount)
// Short circuit if there are no bytes in the buffer.
if (buffer.size == 0L) {
return if (byteCount == 0L) 0L else -1L
}
// Search through the buffer and rewrite any frame delays below the threshold.
var bytesWritten = 0L
while (true) {
val index = indexOf(FRAME_DELAY_START_MARKER)
if (index == -1L) break
// Write up until the end of the frame delay start marker.
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
// Check for the end of the graphics control extension block.
if (!request(5) || buffer[4] != 0.toByte()) continue
// Rewrite the frame delay if it is below the threshold.
// The frame delay is stored as two unsigned bytes in reverse order
// (i.e. the most significant bits are in the byte that's read second).
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
if (frameDelay < MINIMUM_FRAME_DELAY) {
sink.writeByte(buffer[0].toInt())
sink.writeByte(DEFAULT_FRAME_DELAY)
sink.writeByte(0)
buffer.skip(3)
}
}
// Write anything left in the source.
if (bytesWritten < byteCount) {
bytesWritten += write(sink, byteCount - bytesWritten)
}
return if (bytesWritten == 0L) -1 else bytesWritten
}
private fun indexOf(bytes: ByteString): Long {
var index = -1L
while (true) {
index = buffer.indexOf(bytes[0], index + 1)
if (index == -1L) break
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
}
return index
}
private fun write(sink: Buffer, byteCount: Long): Long {
return buffer.read(sink, byteCount).coerceAtLeast(0)
}
private fun request(byteCount: Long): Boolean {
if (buffer.size >= byteCount) return true
val toRead = byteCount - buffer.size
return super.read(buffer, toRead) == toRead
}
private companion object {
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
// See: "Graphics Control Extension"
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
private const val MINIMUM_FRAME_DELAY = 2
private const val DEFAULT_FRAME_DELAY = 10
}
}
@@ -46,10 +46,16 @@ import com.hippo.yorozuya.IntIdGenerator
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.launchIO
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.cookies.HttpCookies
import kotlinx.coroutines.DelicateCoroutinesApi
import okhttp3.OkHttpClient
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.security.KeyStore
import java.security.cert.X509Certificate
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
@@ -231,6 +237,53 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
builder.build()
}
val ktorClient by lazy {
HttpClient(CIO) {
engine {
proxy = when (Settings.proxyType) {
EhProxySelector.TYPE_DIRECT -> Proxy.NO_PROXY
EhProxySelector.TYPE_SYSTEM -> null
EhProxySelector.TYPE_HTTP -> {
val ip = Settings.proxyIp
val port = Settings.proxyPort
val iNetAddress = InetAddress.getByName(ip)
val socketAddress = InetSocketAddress(iNetAddress, port)
Proxy(Proxy.Type.HTTP, socketAddress)
}
// CIO does not support Socks proxy yet
else -> null
}
https {
serverName = "0.0.0.0".takeIf { Settings.dF }
trustManager = @SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(
p0: Array<out X509Certificate>?,
p1: String?
) {
}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(
p0: Array<out X509Certificate>?,
p1: String?
) {
}
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
}
}
pipelining = true
}
install(HttpCookies) {
storage = EhCookieStore
}
}
}
@JvmStatic
val galleryDetailCache by lazy {
LruCache<Long, GalleryDetail>(25).also {
@@ -18,6 +18,8 @@ package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication
import com.hippo.network.CookieDatabase
import com.hippo.network.CookieSet
import io.ktor.client.plugins.cookies.CookiesStorage
import io.ktor.http.Url
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -25,7 +27,9 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
typealias KtorCookie = io.ktor.http.Cookie
object EhCookieStore : CookieJar, CookiesStorage {
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
private val map: MutableMap<String, CookieSet> = db.allCookies
@@ -178,9 +182,19 @@ object EhCookieStore : CookieJar {
db.clear()
}
@Synchronized
fun close() {
db.close()
override suspend fun addCookie(requestUrl: Url, cookie: io.ktor.http.Cookie) {}
// This should never be closed
override fun close() {}
private val okhttpCookieToKtorCookieMap = hashMapOf<Cookie, KtorCookie>()
override suspend fun get(requestUrl: Url): List<io.ktor.http.Cookie> {
return loadForRequest(requestUrl.toString().toHttpUrl()).map {
okhttpCookieToKtorCookieMap[it] ?: KtorCookie(
it.name,
it.value
).apply { okhttpCookieToKtorCookieMap[it] = this }
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
@@ -16,6 +16,7 @@
package com.hippo.ehviewer.client
import com.hippo.okhttp.ChromeRequestBuilder
import io.ktor.http.HttpMessageBuilder
class EhRequestBuilder @JvmOverloads constructor(
url: String,
@@ -27,3 +28,6 @@ class EhRequestBuilder @JvmOverloads constructor(
origin?.let { addHeader("Origin", it) }
}
}
fun HttpMessageBuilder.referer(value: String?): Unit =
value?.let { headers.append("Referer", it) } ?: Unit
@@ -18,39 +18,40 @@ package com.hippo.ehviewer.spider
import android.graphics.ImageDecoder
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
import coil.decode.DecodeUtils
import coil.decode.FrameDelayRewritingSource
import coil.decode.isGif
import coil.disk.DiskCache
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.referer
import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS
import com.hippo.image.Image
import com.hippo.image.Image.CloseableSource
import com.hippo.sendTo
import com.hippo.unifile.UniFile
import com.hippo.unifile.openOutputStream
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.MathUtils
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType
import io.ktor.http.contentLength
import io.ktor.http.contentType
import io.ktor.utils.io.jvm.nio.copyTo
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import okio.buffer
import java.io.File
import java.io.IOException
import java.nio.channels.FileChannel
import java.util.Locale
import kotlin.io.path.readText
private val client = EhApplication.okHttpClient
private val client = EhApplication.ktorClient
class SpiderDen(private val mGalleryInfo: GalleryInfo) {
private val mGid: Long = mGalleryInfo.gid
@@ -168,61 +169,62 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
referer: String?,
notifyProgress: (Long, Long, Int) -> Unit
): Boolean {
val call = client.newCall(EhRequestBuilder(url, referer).build())
call.executeAsync().use { response ->
if (response.code >= 400) return false
val body = response.body
val extension = body.contentType()?.subtype ?: "jpg"
val length = body.contentLength()
suspend fun doSave(chan: FileChannel): Long {
var receivedSize: Long = 0
body.source().use { responseSource ->
val source = if (DecodeUtils.isGif(responseSource)) FrameDelayRewritingSource(
responseSource
).buffer() else responseSource
source.use {
while (true) {
currentCoroutineContext().ensureActive()
val bytesRead = chan.transferFrom(it, receivedSize, TRANSFER_BLOCK)
receivedSize += bytesRead
notifyProgress(body.contentLength(), receivedSize, bytesRead.toInt())
if (bytesRead.toInt() == 0) break
}
}
}
return receivedSize
return client.prepareGet(url) {
var state: Long = 0
referer(referer)
onDownload { bytesSentTotal, contentLength ->
notifyProgress(contentLength, bytesSentTotal, (bytesSentTotal - state).toInt())
state = bytesSentTotal
}
}.execute {
if (it.status.value >= 400) return@execute false
saveFromHttpResponse(index, it)
}
}
findDownloadFileForIndex(index, extension)?.runSuspendCatching {
openOutputStream().use { outputStream ->
outputStream.channel.use { return doSave(it) == length }
}
}?.onFailure {
it.printStackTrace()
return false
}
private suspend fun saveFromHttpResponse(index: Int, body: HttpResponse): Boolean {
val contentType = body.contentType()
val extension = contentType?.contentSubtype ?: "jpg"
val length = body.contentLength() ?: return false
// Read Mode, allow save to cache
if (mMode == SpiderQueen.MODE_READ) {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
var received: Long = 0
runSuspendCatching {
sCache.edit(key) {
metadata.toFile().writeText(extension)
data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
}
}
}.onFailure {
it.printStackTrace()
}.onSuccess {
return received == length
suspend fun doSave(outFile: UniFile): Long {
val ret: Long
outFile.openOutputStream().use { outputStream ->
outputStream.channel.use {
ret = body.bodyAsChannel().copyTo(it)
}
}
if (contentType == ContentType.Image.GIF)
outFile.openFileDescriptor("rw").use {
Image.rewriteGifSource2(it.fd)
}
return ret
}
findDownloadFileForIndex(index, extension)?.runSuspendCatching {
return doSave(this) == length
}?.onFailure {
it.printStackTrace()
return false
}
// Read Mode, allow save to cache
if (mMode == SpiderQueen.MODE_READ) {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
var received: Long = 0
runSuspendCatching {
sCache.edit(key) {
metadata.toFile().writeText(extension)
received = doSave(UniFile.fromFile(data.toFile())!!)
}
}.onFailure {
it.printStackTrace()
}.onSuccess {
return received == length
}
}
return false
}
fun saveToUniFile(index: Int, file: UniFile): Boolean {
@@ -100,6 +100,9 @@ class Image private constructor(private val src: CloseableSource) {
@JvmStatic
external fun rewriteGifSource(buffer: ByteBuffer)
@JvmStatic
external fun rewriteGifSource2(fd: Int)
}
interface CloseableSource : AutoCloseable {