Migrated to Ktor-client

This commit is contained in:
tom5079
2021-07-12 08:44:43 +09:00
parent a9a07ddcfa
commit 2150d086e0
16 changed files with 199 additions and 277 deletions

View File

@@ -27,7 +27,6 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.internal.toImmutableMap
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import xyz.quaver.io.FileX
@@ -74,7 +73,7 @@ class DownloadManager constructor(context: Context) : ContextWrapper(context), D
}
val downloads: Map<String, String>
get() = downloadFolderMap.toImmutableMap()
get() = downloadFolderMap
@Synchronized
fun getDownloadFolder(source: String, itemID: String): FileX? =

View File

@@ -20,24 +20,32 @@ package xyz.quaver.pupil.util
import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.ExperimentalCoroutinesApi
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.coroutineScope
import okhttp3.*
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.pupil.Pupil
import java.io.File
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class ImageCache(context: Context) : DIAware {
override val di by closestDI(context)
private val client: OkHttpClient by instance()
private val applicationContext: Pupil by instance()
private val client: HttpClient by instance()
val cacheFolder = File(context.cacheDir, "imageCache")
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
@@ -45,6 +53,8 @@ class ImageCache(context: Context) : DIAware {
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
val channels = _channels as Map<String, Channel<Float>>
private val requests = mutableMapOf<String, Job>()
@Synchronized
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun cleanup() = coroutineScope {
@@ -62,66 +72,65 @@ class ImageCache(context: Context) : DIAware {
}
fun free(images: List<String>) {
client.dispatcher.let { it.queuedCalls() + it.runningCalls() }
.filter { it.request().url.toString() in images }
.forEach { it.cancel() }
images.forEach {
requests[it]?.cancel()
}
images.forEach { _channels.remove(it) }
}
@Synchronized
suspend fun clear() = coroutineScope {
client.dispatcher.queuedCalls().forEach { it.cancel() }
requests.values.forEach { it.cancel() }
cacheFolder.listFiles()?.forEach { it.delete() }
cache.clear()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun load(request: Request): File {
val key = request.url.toString()
fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File {
val request = HttpRequestBuilder().apply(requestBuilder)
val channel = if (_channels[key]?.isClosedForSend == false)
val key = request.url.buildString()
val progressChannel = if (_channels[key]?.isClosedForSend == false)
_channels[key]!!
else
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
return cache[key]?.let {
channel.close()
progressChannel.close()
File(it)
} ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
file.delete()
cache.remove(call.request().url.toString())
if (!file.exists())
file.createNewFile()
FirebaseCrashlytics.getInstance().recordException(e)
channel.close(e)
}
cache[key] = file.canonicalPath
override fun onResponse(call: Call, response: Response) {
if (response.code != 200) {
file.delete()
cache.remove(call.request().url.toString())
requests[key] = CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
client.get<HttpStatement>(request).execute { httpResponse ->
val responseChannel: ByteReadChannel = httpResponse.receive()
val contentLength = httpResponse.contentLength() ?: -1
var readBytes = 0F
channel.close(IOException("HTTP Response code is not 200"))
response.close()
return
}
response.body?.use { body ->
if (!file.exists())
file.createNewFile()
body.byteStream().copyTo(file.outputStream()) { bytes, _ ->
channel.trySendBlocking(bytes / body.contentLength().toFloat() * 100)
while (!responseChannel.isClosedForRead) {
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
val bytes = packet.readBytes()
file.appendBytes(bytes)
readBytes += bytes.size
progressChannel.trySend(readBytes / contentLength)
}
}
progressChannel.close()
}
channel.close()
}.onFailure {
file.delete()
cache.remove(key)
FirebaseCrashlytics.getInstance().recordException(it)
progressChannel.close(it)
}
})
}.also { cache[key] = it.canonicalPath }
}
}
}
}

View File

@@ -23,12 +23,10 @@ import android.view.MenuItem
import android.view.View
import androidx.lifecycle.MutableLiveData
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
import okhttp3.Request
import org.kodein.di.*
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import org.kodein.di.DIAware
import org.kodein.di.DirectDIAware
import org.kodein.di.direct
import org.kodein.di.instance
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.SourceEntries
import java.io.InputStream
@@ -74,13 +72,6 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
*/
fun Int.normalizeID() = this.and(0xFFFF)
fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
proxy(proxyInfo.proxy())
proxyInfo.authenticator()?.let {
proxyAuthenticator(it)
}
}
val formatMap = mapOf<String, ItemInfo.() -> (String)>(
"-id-" to { id },
"-title-" to { title },
@@ -120,7 +111,7 @@ fun <E> MutableLiveData<MutableList<E>>.notify() {
this.value = this.value
}
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Any): Long {
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Unit): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = read(buffer)

View File

@@ -22,8 +22,6 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Authenticator
import okhttp3.Credentials
import java.net.InetSocketAddress
import java.net.Proxy
@@ -42,15 +40,7 @@ data class ProxyInfo(
Proxy(type, InetSocketAddress.createUnresolved(host, port))
}
fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else
Authenticator { _, response ->
val credential = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
// TODO: Migrate to ktor-client and implement proxy authentication
}
fun getProxyInfo(): ProxyInfo =

View File

@@ -18,49 +18,39 @@
package xyz.quaver.pupil.util
import io.ktor.client.*
import io.ktor.client.request.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request
import xyz.quaver.pupil.client
import java.io.IOException
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.bindInstance
import org.kodein.di.instance
import java.util.*
private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags"
private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/"
var translations: Map<String, String> = run {
updateTranslations()
emptyMap()
}
private set
private var translations: Map<String, String> = emptyMap()
@Suppress("BlockingMethodInNonBlockingContext")
fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
fun updateTranslations(client: HttpClient) = CoroutineScope(Dispatchers.IO).launch {
translations = emptyMap()
kotlin.runCatching {
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
Request.Builder()
.url(contentURL + "${Preferences["hitomi.tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
.build()
).execute().also { if (it.code != 200) return@launch }.body?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() }
translations = client.get<Map<String, String>>("$contentURL${Preferences["hitomi.tag_translation", Locale.getDefault().language]}.json").filterValues { it.isNotEmpty() }
}
}
fun getAvailableLanguages(): List<String> {
fun getAvailableLanguages(client: HttpClient): List<String> {
val languages = Locale.getISOLanguages()
val json = Json.parseToJsonElement(client.newCall(
Request.Builder()
.url(filesURL)
.build()
).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.string() } ?: return emptyList())
val json = runCatching { runBlocking { Json.parseToJsonElement(client.get(filesURL)) } }.getOrNull()
return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull {
return listOf("en") + (json?.get("tree")?.jsonArray?.mapNotNull {
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
languages.firstOrNull { code -> code.equals(name, ignoreCase = true) }

View File

@@ -19,34 +19,18 @@
package xyz.quaver.pupil.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.*
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.di
import org.kodein.di.instance
import ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import java.io.File
import java.io.IOException
import java.net.URL
import java.util.*
@@ -178,48 +162,4 @@ fun checkUpdate(context: Context, force: Boolean = false) {
dialog.show()
}
}
}
fun restore(context: Context, url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Set<String>) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException())
return
}
val request = Request.Builder()
.url(url)
.get()
.build()
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
onFailure?.invoke(e)
}
override fun onResponse(call: Call, response: Response) {
val favorites = object: DIAware { override val di by di(context); val favorites: SavedSourceSet by instance(tag = "favorites") }
kotlin.runCatching {
Json.decodeFromString<Set<String>>(response.also { if (it.code != 200) throw IOException() }.body.use { it?.string() } ?: "[]").let {
favorites.favorites.addAll(mapOf("hitomi.la" to it))
onSuccess?.invoke(it)
}
}.onFailure { onFailure?.invoke(it) }
}
})
}
private var job: Job? = null
private val receiver = object: BroadcastReceiver() {
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
when (intent?.action) {
ACTION_CANCEL -> {
job?.cancel()
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
context.unregisterReceiver(this)
}
}
}
}