Rebuilding Downloader

This commit is contained in:
Pupil
2020-01-29 15:46:23 +09:00
parent 8a9ab6b36c
commit 9d80857a38
6 changed files with 237 additions and 89 deletions

View File

@@ -37,6 +37,7 @@ import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File import java.io.File
@@ -118,4 +119,24 @@ class ExampleInstrumentedTest {
updateOldReaderGalleries(context) updateOldReaderGalleries(context)
} }
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 515515
val worker = DownloadWorker.getInstance(context)
worker.queue.add(galleryID)
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { !it.isFinite() } == true)
break
}
Log.i("PUPILD", "DONE!!")
}
} }

View File

@@ -19,7 +19,6 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -130,8 +129,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
with(view.galleryblock_progressbar) { with(view.galleryblock_progressbar) {
progress = imageCache.invoke().list()?.size ?: 0 progress = imageCache.invoke().list()?.size ?: 0
Log.i("PUPILD", progress.toString())
if (!readerCache.invoke().exists()) { if (!readerCache.invoke().exists()) {
visibility = View.GONE visibility = View.GONE
max = 0 max = 0

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util.download
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Base64
import android.util.SparseArray import android.util.SparseArray
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -30,7 +31,10 @@ import kotlinx.serialization.parse
import kotlinx.serialization.stringify import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.isParentOf
import java.io.File import java.io.File
import java.net.URL
class Cache(context: Context) : ContextWrapper(context) { class Cache(context: Context) : ContextWrapper(context) {
@@ -86,6 +90,29 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
} }
suspend fun getThumbnail(galleryID: Int): String? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val thumbnail = if (metadata?.thumbnail == null)
withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
try {
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
} catch (e: Exception) {
null
}
}
else
metadata.thumbnail
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
)
return thumbnail
}
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? { suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID) val metadata = Cache(this).getCachedMetadata(galleryID)
@@ -102,7 +129,7 @@ class Cache(context: Context) : ContextWrapper(context) {
setCachedMetadata( setCachedMetadata(
galleryID, galleryID,
Metadata(metadata, galleryBlock = galleryBlock) Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
) )
return galleryBlock return galleryBlock
@@ -129,7 +156,7 @@ class Cache(context: Context) : ContextWrapper(context) {
if (readers.isNotEmpty()) if (readers.isNotEmpty())
setCachedMetadata( setCachedMetadata(
galleryID, galleryID,
Metadata(metadata, readers = readers) Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers)
) )
val mirrors = preference.getString("mirrors", "")!!.split('>') val mirrors = preference.getString("mirrors", "")!!.split('>')
@@ -140,22 +167,44 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
fun getImages(galleryID: Int): SparseArray<File>? { fun getImages(galleryID: Int): SparseArray<File>? {
val regex = Regex("[0-9]+")
val gallery = getCachedGallery(galleryID) ?: return null val gallery = getCachedGallery(galleryID) ?: return null
return SparseArray<File>().apply { return SparseArray<File>().apply {
gallery.listFiles { file -> gallery.listFiles { file ->
file.nameWithoutExtension.matches(regex) file.nameWithoutExtension.toIntOrNull() != null
}?.forEach { }?.forEach {
append(it.nameWithoutExtension.toInt(), it) append(it.nameWithoutExtension.toInt(), it)
} }
} }
} }
fun putImage(galleryID: Int, index: Int, data: ByteArray) { fun putImage(galleryID: Int, name: String, data: ByteArray) {
val cache = getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID") val cache = getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID")
File(cache, index.toString()).writeBytes(data) with(File(cache, name)) {
if (!parentFile!!.exists())
parentFile!!.mkdirs()
if (!exists())
createNewFile()
if (nameWithoutExtension.toIntOrNull() != null)
writeBytes(data)
else
IllegalArgumentException("File name is not a number")
}
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID")
val download = getDownloadDirectory(this)
if (!download.isParentOf(cache)) {
cache.copyRecursively(download)
cache.deleteRecursively()
}
} }
} }

View File

@@ -21,28 +21,39 @@ package xyz.quaver.pupil.util.download
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.SparseArray
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import okhttp3.* import okhttp3.*
import okio.* import okio.*
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import java.io.FileInputStream import xyz.quaver.hiyobi.user_agent
import java.io.IOException import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) { class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
//region ProgressListener //region ProgressListener
@Suppress("UNCHECKED_CAST")
private val progressListener = object: ProgressListener {
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
if (!done && progress[galleryID]!![index] != Float.POSITIVE_INFINITY)
progress[galleryID]!![index] = bytesRead * 100F / contentLength
}
}
interface ProgressListener { interface ProgressListener {
fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean) fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
} }
@@ -52,7 +63,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val responseBody: ResponseBody, val responseBody: ResponseBody,
val progressListener : ProgressListener val progressListener : ProgressListener
) : ResponseBody() { ) : ResponseBody() {
var bufferedSource : BufferedSource? = null private var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength() override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType() override fun contentType() = responseBody.contentType()
@@ -93,23 +104,46 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
//endregion //endregion
val queue = Channel<Int>() val queue = LinkedBlockingQueue<Int>()
/* VALUE
/*
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress
* null -> Loading / Gallery doesn't exist
* SECONDARY VALUE
* 0 <= value < 100 -> Download in progress * 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed * Float.POSITIVE_INFINITY -> Download completed
* Float.NaN -> Exception * Float.NaN -> Exception
*/ */
val progress = mutableMapOf<String, Float>() val progress = SparseArray<MutableList<Float>?>()
val result = mutableMapOf<String, ByteArray>() /*
val exception = mutableMapOf<String, Throwable>() * KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress / Loading
* null -> Gallery doesn't exist
* SECONDARY VALUE
* Throwable -> Exception
* null -> Download in progress / Loading
*/
val exception = SparseArray<MutableList<Throwable?>?>()
val client = OkHttpClient.Builder() private val loop = loop()
.addNetworkInterceptor { chain -> private val worker = SparseArray<Job?>()
@Volatile var nRunners = 0
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request() val request = chain.request()
var response = chain.proceed(request) var response = chain.proceed(request)
var retry = preferences.getInt("retry", 3) var retry = preferences.getInt("retry", 3)
while (!response.isSuccessful && retry > 0) { while (!response.isSuccessful && retry > 0) {
response.close()
response = chain.proceed(request) response = chain.proceed(request)
retry-- retry--
} }
@@ -117,72 +151,121 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
response.newBuilder() response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body!!, progressListener)) .body(ProgressResponseBody(request.tag(), response.body!!, progressListener))
.build() .build()
}.build()
val progressListener = object: ProgressListener {
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
if (tag !is String)
return
if (progress[tag] != Float.POSITIVE_INFINITY)
progress[tag] = bytesRead / contentLength.toFloat()
} }
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor()))
.build()
fun stop() {
loop.cancel()
for (i in 0..worker.size())
worker[worker.keyAt(i)]?.cancel()
client.dispatcher.cancelAll()
} }
init {
CoroutineScope(Dispatchers.Unconfined).launch {
while (!(queue.isEmpty && queue.isClosedForReceive)) {
val lowQuality = preferences.getBoolean("low_quality", false)
val galleryID = queue.receive()
launch(Dispatchers.IO) io@{ fun cancel(galleryID: Int) {
val reader = Cache(context).getReader(galleryID) ?: return@io worker[galleryID]?.cancel()
val cache = Cache(context).getImages(galleryID)
reader.galleryInfo.forEachIndexed { index, galleryInfo -> client.dispatcher.queuedCalls()
val tag = "$galleryID-$index" .filter { it.request().tag(Pair::class.java)?.first == galleryID }
val url = when(reader.code) { .forEach {
Reader.Code.HITOMI -> it.cancel()
urlFromUrlFromHash(galleryID, galleryInfo, if (lowQuality) "webp" else null) }
Reader.Code.HIYOBI -> }
createImgList(galleryID, reader, lowQuality)[index].path
else -> "" //Shouldn't be called anyways
}
//Cache exists :P private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
cache?.get(index)?.let { val cache = Cache(this@DownloadWorker).getImages(galleryID)
result[tag] = FileInputStream(it).readBytes() val lowQuality = preferences.getBoolean("low_quality", false)
progress[tag] = Float.POSITIVE_INFINITY
return@io //Cache exists :P
} cache?.get(index)?.let {
progress[galleryID]!![index] = Float.POSITIVE_INFINITY
val request = Request.Builder() return
.url(url) }
.tag(tag)
.build()
client.newCall(request).enqueue(object: Callback { val request = Request.Builder().apply {
override fun onFailure(call: Call, e: IOException) { when (reader.code) {
if (Fabric.isInitialized()) Reader.Code.HITOMI -> {
Crashlytics.logException(e) url(
urlFromUrlFromHash(
progress[tag] = Float.NaN galleryID,
exception[tag] = e reader.galleryInfo[index],
} if (lowQuality) "webp" else null
)
override fun onResponse(call: Call, response: Response) { )
response.use { addHeader("Referer", getReferer(galleryID))
val res = it.body!!.bytes() }
result[tag] = res Reader.Code.HIYOBI -> {
Cache(context).putImage(galleryID, index, res) url(createImgList(galleryID, reader, lowQuality)[index].path)
progress[tag] = Float.POSITIVE_INFINITY addHeader("User-Agent", user_agent)
} addHeader("Cookie", cookie)
} }
}) else -> {
} //shouldn't be called anyway
} }
} }
tag(galleryID to index)
}.build()
client.newCall(request).enqueue(callback)
}
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
val reader = Cache(this@DownloadWorker).getReader(galleryID)
//gallery doesn't exist
if (reader == null) {
progress.put(galleryID, null)
exception.put(galleryID, null)
nRunners--
return@launch
}
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
for (i in reader.galleryInfo.indices) {
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
if (Fabric.isInitialized())
Crashlytics.logException(e)
progress[galleryID]!![i] = Float.NaN
exception[galleryID]!![i] = e
if (progress[galleryID]!!.all { !it.isFinite() })
nRunners--
}
override fun onResponse(call: Call, response: Response) {
response.use {
val res = it.body!!.bytes()
val ext =
call.request().url.encodedPath.split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
progress[galleryID]!![i] = Float.POSITIVE_INFINITY
}
if (progress[galleryID]!!.all { !it.isFinite() })
nRunners--
}
}
queueDownload(galleryID, reader, i, callback)
}
}
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
continue
val galleryID = queue.poll() ?: continue
worker.put(galleryID, download(galleryID))
nRunners++
} }
} }

View File

@@ -62,4 +62,6 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
} }
} }
} }
fun File.isParentOf(file: File) = file.absolutePath.startsWith(this.absolutePath)

View File

@@ -26,20 +26,16 @@ package xyz.quaver.pupil
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
import android.util.SparseArray
import org.junit.Test import org.junit.Test
import xyz.quaver.pupil.util.download
import java.io.File
import java.net.URL
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
URL("https://github.com/tom5079/Pupil/releases/download/4.2-beta2-hotfix2/Pupil-v4.2-beta2-hotfix2.apk").download( val arr = SparseArray<Float>()
File(System.getenv("USERPROFILE"), "Pupil.apk")
) { downloaded, fileSize -> print(arr.indexOfKey(34))
println("%.1f%%".format(downloaded*100.0/fileSize))
}
} }
} }