Migrated to Ktor-client
This commit is contained in:
@@ -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? =
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user