This commit is contained in:
tom5079
2021-01-09 19:18:26 +09:00
parent c8aa26e2d9
commit 619730e2ab
32 changed files with 825 additions and 1385 deletions

View File

@@ -1,6 +1,6 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
* Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,35 +16,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util.downloader
package xyz.quaver.pupil.util
import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.kodein.di.DIAware
import org.kodein.di.android.di
import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.io.util.*
import xyz.quaver.pupil.sources.sources
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.formatDownloadFolder
import xyz.quaver.pupil.sources.AnySource
class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
companion object {
@Volatile private var instance: DownloadManager? = null
override val di by di(context)
fun getInstance(context: Context) =
instance ?: synchronized(this) {
instance ?: DownloadManager(context).also { instance = it }
}
}
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
private val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder: FileX
get() = {
@@ -58,7 +52,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
private var prevDownloadFolder: FileX? = null
private var downloadFolderMapInstance: MutableMap<String, String>? = null
val downloadFolderMap: MutableMap<String, String>
private val downloadFolderMap: MutableMap<String, String>
@Synchronized
get() {
if (prevDownloadFolder != downloadFolder) {
@@ -88,8 +82,12 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
@Synchronized
fun addDownloadFolder(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
val name = "A" // TODO
fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
val source: AnySource by instance(tag = source)
val info = async { source.info(itemID) }
val images = async { source.images(itemID) }
val name = info.await().formatDownloadFolder()
val folder = downloadFolder.getChild("$source/$name")
@@ -105,7 +103,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
}
@Synchronized
fun deleteDownloadFolder(source: String, itemID: String) {
fun delete(source: String, itemID: String) {
downloadFolderMap["$source/$itemID"]?.let {
kotlin.runCatching {
downloadFolder.getChild(it).deleteRecursively()

View File

@@ -0,0 +1,126 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
import okhttp3.*
import org.kodein.di.DIAware
import org.kodein.di.android.di
import org.kodein.di.instance
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 di(context)
private val client: OkHttpClient by instance()
val cacheFolder = File(context.cacheDir, "imageCache")
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
val channels = _channels as Map<String, Channel<Float>>
@Synchronized
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun cleanup() = coroutineScope {
val LIMIT = 100*1024*1024
cacheFolder.listFiles { it -> it.canonicalPath !in cache }?.forEach { it.delete() }
if (cacheFolder.size() > LIMIT)
do {
cache.entries.firstOrNull { !channels.containsKey(it.key) }?.let {
File(it.value).delete()
cache.remove(it.key)
}
} while (cacheFolder.size() > LIMIT / 2)
}
fun free(images: List<String>) {
client.dispatcher().let { it.queuedCalls() + it.runningCalls() }
.filter { it.request().url().toString() in images }
.forEach { it.cancel() }
images.forEach { _channels.remove(it) }
}
@Synchronized
suspend fun clear() = coroutineScope {
client.dispatcher().queuedCalls().forEach { it.cancel() }
cacheFolder.listFiles()?.forEach { it.delete() }
cache.clear()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun load(request: Request): File {
val key = request.url().toString()
val channel = if (_channels[key]?.isClosedForSend == false)
_channels[key]!!
else
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
return cache[key]?.let {
channel.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())
FirebaseCrashlytics.getInstance().recordException(e)
channel.close(e)
}
override fun onResponse(call: Call, response: Response) {
if (response.code() != 200) {
file.delete()
cache.remove(call.request().url().toString())
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.sendBlocking(bytes / body.contentLength().toFloat() * 100)
}
}
channel.close()
}
})
}.also { cache[key] = it.canonicalPath }
}
}

View File

@@ -0,0 +1,170 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import androidx.annotation.RequiresApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import java.io.File
class SavedSet <T: Any> (private val file: File, any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
val serializer: KSerializer<Set<T>> = SetSerializer(serializer(any::class.java) as KSerializer<T>)
init {
if (!file.exists()) {
file.parentFile?.mkdirs()
save()
}
load()
}
@Synchronized
fun load() {
set.clear()
kotlin.runCatching {
Json.decodeFromString(serializer, file.readText())
}.onSuccess {
set.addAll(it)
}
}
@Synchronized
fun save() {
if (!file.exists())
file.createNewFile()
file.writeText(Json.encodeToString(serializer, set))
}
@Synchronized
override fun add(element: T): Boolean {
set.remove(element)
return set.add(element).also {
save()
}
}
@Synchronized
override fun addAll(elements: Collection<T>): Boolean {
set.removeAll(elements)
return set.addAll(elements).also {
save()
}
}
@Synchronized
override fun remove(element: T): Boolean {
load()
return set.remove(element).also {
save()
}
}
@Synchronized
override fun clear() {
set.clear()
save()
}
}
class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V, private val map: MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
val serializer: KSerializer<Map<K, V>> = MapSerializer(serializer(anyKey::class.java) as KSerializer<K>, serializer(anyValue::class.java) as KSerializer<V>)
init {
if (!file.exists()) {
file.parentFile?.mkdirs()
save()
}
load()
}
@Synchronized
fun load() {
map.clear()
kotlin.runCatching {
Json.decodeFromString(serializer, file.readText())
}.onSuccess {
map.putAll(it)
}
}
@Synchronized
fun save() {
if (!file.exists())
file.createNewFile()
file.writeText(Json.encodeToString(serializer, map))
}
@Synchronized
override fun put(key: K, value: V): V? {
map.remove(key)
return map.put(key, value).also {
save()
}
}
@Synchronized
override fun putAll(from: Map<out K, V>) {
for (key in from.keys) {
map.remove(key)
}
map.putAll(from)
save()
}
@Synchronized
override fun remove(key: K): V? {
return map.remove(key).also {
save()
}
}
@Synchronized
@RequiresApi(24)
override fun remove(key: K, value: V): Boolean {
return map.remove(key, value).also {
save()
}
}
@Synchronized
override fun clear() {
map.clear()
save()
}
}

View File

@@ -1,95 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import java.io.File
import java.util.*
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
val serializer: KSerializer<List<T>>
get() = ListSerializer(serializer(any::class.java) as KSerializer<T>)
init {
if (!file.exists()) {
file.parentFile?.mkdirs()
save()
}
load()
}
@Synchronized
fun load() {
set.clear()
kotlin.runCatching {
Json.decodeFromString(serializer, file.readText())
}.onSuccess {
set.addAll(it)
}
}
@Synchronized
@OptIn(ExperimentalSerializationApi::class)
fun save() {
file.writeText(Json.encodeToString(serializer, set.toList()))
}
@Synchronized
override fun add(element: T): Boolean {
load()
set.remove(element)
return set.add(element).also {
save()
}
}
@Synchronized
override fun addAll(elements: Collection<T>): Boolean {
load()
set.removeAll(elements)
return set.addAll(elements).also {
save()
}
}
@Synchronized
override fun remove(element: T): Boolean {
load()
return set.remove(element).also {
save()
}
}
@Synchronized
override fun clear() {
set.clear()
save()
}
}

View File

@@ -1,117 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util.downloader
import android.content.Context
import android.content.ContextWrapper
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX
import xyz.quaver.io.util.deleteRecursively
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.outputStream
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.sources
import java.io.InputStream
import java.util.concurrent.ConcurrentHashMap
@Serializable
data class Metadata(
var itemInfo: ItemInfo? = null,
var imageList: MutableList<String?>? = null
) {
fun copy(): Metadata = Metadata(itemInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
class Cache private constructor(context: Context, source: String, private val itemID: String) : ContextWrapper(context) {
companion object {
val instances = ConcurrentHashMap<String, Cache>()
fun getInstance(context: Context, source: String, itemID: String): Cache {
val key = "$source/$itemID"
return instances[key] ?: synchronized(this) {
instances[key] ?: Cache(context, source, itemID).also { instances[key] = it }
}
}
@Synchronized
fun delete(source: String, itemID: String) {
val key = "$source/$itemID"
instances[key]?.cacheFolder?.deleteRecursively()
instances.remove("$source/$itemID")
}
}
val source = sources[source]!!
val downloadFolder: FileX?
get() = DownloadManager.getInstance(this).getDownloadFolder(source.name, itemID)
val cacheFolder: FileX
get() = FileX(this, cacheDir, "imageCache/$source/$itemID").also {
if (!it.exists())
it.mkdirs()
}
val metadata: Metadata = kotlin.runCatching {
Json.decodeFromString<Metadata>(findFile(".metadata")!!.readText())
}.getOrDefault(Metadata())
@Suppress("BlockingMethodInNonBlockingContext")
fun setMetadata(change: (Metadata) -> Unit) {
change.invoke(metadata)
val file = cacheFolder.getChild(".metadata")
kotlin.runCatching {
if (!file.exists()) {
file.createNewFile()
}
file.writeText(Json.encodeToString(metadata))
}
}
private fun findFile(fileName: String): FileX? =
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
if (it.exists()) it else null
} } ?: cacheFolder.getChild(fileName).let {
if (it.exists()) it else null
}
fun putImage(index: Int, name: String, `is`: InputStream) {
cacheFolder.getChild(name).also {
if (!it.exists())
it.createNewFile()
}.outputStream()?.use {
it.channel.truncate(0L)
`is`.copyTo(it)
}
setMetadata { metadata -> metadata.imageList!![index] = name }
}
fun getImage(index: Int): FileX? {
return metadata.imageList?.get(index)?.let { findFile(it) }
}
}

View File

@@ -1,300 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util.downloader
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.*
import okio.*
import xyz.quaver.pupil.PupilInterceptor
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.sources.sources
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.cleanCache
import xyz.quaver.pupil.util.normalizeID
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
private typealias ProgressListener = (Downloader.Tag, Long, Long, Boolean) -> Unit
class Downloader private constructor(private val context: Context) {
data class Tag(val source: String, val itemID: String, val index: Int)
companion object {
var instance: Downloader? = null
fun getInstance(context: Context): Downloader {
return instance ?: synchronized(this) {
instance ?: Downloader(context).also {
interceptors[Tag::class] = it.interceptor
instance = it
}
}
}
}
//region Notification
private val notificationManager by lazy {
NotificationManagerCompat.from(context)
}
private val serviceNotification by lazy {
NotificationCompat.Builder(context, "downloader")
.setContentTitle(context.getString(R.string.downloader_running))
.setProgress(0, 0, false)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
}
private val notification = ConcurrentHashMap<String, NotificationCompat.Builder?>()
private fun initNotification(source: String, itemID: String) {
val key = "$source-$itemID"
val intent = Intent(context, ReaderActivity::class.java)
.putExtra("source", source)
.putExtra("itemID", itemID)
val pendingIntent = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
getPendingIntent(itemID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
}
val action =
NotificationCompat.Action.Builder(0, context.getText(android.R.string.cancel),
PendingIntent.getService(
context,
R.id.notification_download_cancel_action.normalizeID(),
Intent(context, DownloadService::class.java)
.putExtra(DownloadService.KEY_COMMAND, DownloadService.COMMAND_CANCEL)
.putExtra(DownloadService.KEY_ID, itemID),
PendingIntent.FLAG_UPDATE_CURRENT),
).build()
notification[key] = NotificationCompat.Builder(context, "download").apply {
setContentTitle(context.getString(R.string.reader_loading))
setContentText(context.getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_notification)
setContentIntent(pendingIntent)
addAction(action)
setProgress(0, 0, true)
setOngoing(true)
}
notify(source, itemID)
}
@SuppressLint("RestrictedApi")
private fun notify(source: String, itemID: String) {
val key = "$source-$itemID"
val max = progress[key]?.size ?: 0
val progress = progress[key]?.count { it == Float.POSITIVE_INFINITY } ?: 0
val notification = notification[key] ?: return
if (isCompleted(source, itemID)) {
notification
.setContentText(context.getString(R.string.reader_notification_complete))
.setProgress(0, 0, false)
.setOngoing(false)
.mActions.clear()
notificationManager.cancel(key.hashCode())
} else
notification
.setProgress(max, progress, false)
.setContentText("$progress/$max")
}
//endregion
//region ProgressListener
@Suppress("UNCHECKED_CAST")
private val progressListener: ProgressListener = { (source, itemID, index), bytesRead, contentLength, done ->
if (!done && progress["$source-$itemID"]?.get(index)?.isFinite() == true)
progress["$source-$itemID"]?.set(index, bytesRead * 100F / contentLength)
}
private class ProgressResponseBody(
val tag: Any?,
val responseBody: ResponseBody,
val progressListener : ProgressListener
) : ResponseBody() {
private var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType()
override fun source(): BufferedSource {
if (bufferedSource == null)
bufferedSource = Okio.buffer(source(responseBody.source()))
return bufferedSource!!
}
private fun source(source: Source) = object: ForwardingSource(source) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
return bytesRead
}
}
}
private val interceptor: PupilInterceptor = { chain ->
val request = chain.request()
var response = chain.proceed(request)
var retry = 5
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder()
.body(response.body()?.let {
ProgressResponseBody(request.tag(), it, progressListener)
}).build()
}
//endregion
private val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
val (source, itemID, index) = call.request().tag() as Tag
FirebaseCrashlytics.getInstance().recordException(e)
progress["$source-$itemID"]?.set(index, Float.NEGATIVE_INFINITY)
}
override fun onResponse(call: Call, response: Response) {
val (source, itemID, index) = call.request().tag() as Tag
val ext = call.request().url().encodedPath().takeLastWhile { it != '.' }
if (response.code() != 200)
throw IOException()
response.body()?.use {
Cache.getInstance(context, source, itemID).putImage(index, "$index.$ext", it.byteStream())
}
progress["$source-$itemID"]?.set(index, Float.POSITIVE_INFINITY)
}
}
private val progress = ConcurrentHashMap<String, MutableList<Float>>()
fun getProgress(source: String, itemID: String): List<Float>? {
return progress["$source-$itemID"]
}
fun isCompleted(source: String, itemID: String) = progress["$source-$itemID"]?.all { it == Float.POSITIVE_INFINITY } == true
fun cancel() {
client.dispatcher().queuedCalls().filter {
it.request().tag() is Tag
}.forEach {
it.cancel()
}
client.dispatcher().runningCalls().filter {
it.request().tag() is Tag
}.forEach {
it.cancel()
}
progress.clear()
}
fun cancel(source: String, itemID: String) {
client.dispatcher().queuedCalls().filter {
(it.request().tag() as? Tag)?.let { tag ->
tag.source == source && tag.itemID == itemID
} == true
}.forEach {
it.cancel()
}
client.dispatcher().runningCalls().filter {
(it.request().tag() as? Tag)?.let { tag ->
tag.source == source && tag.itemID == itemID
} == true
}.forEach {
it.cancel()
}
progress.remove("$source-$itemID")
}
fun retry(source: String, itemID: String) {
cancel(source, itemID)
download(source, itemID)
}
var onImageListLoadedCallback: ((List<String>) -> Unit)? = null
fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
if (isDownloading(source, itemID))
return@launch
initNotification(source, itemID)
cleanCache(context)
val source = sources[source] ?: return@launch
val cache = Cache.getInstance(context, source.name, itemID)
source.images(itemID).also {
progress["${source.name}-$itemID"] = MutableList(it.size) { i ->
if (cache.metadata.imageList?.get(i) == null) 0F else Float.POSITIVE_INFINITY
}
if (cache.metadata.imageList == null)
cache.metadata.imageList = MutableList(it.size) { null }
onImageListLoadedCallback?.invoke(it)
}.forEachIndexed { index, url ->
client.newCall(
Request.Builder()
.tag(Tag(source.name, itemID, index))
.url(url)
.headers(Headers.of(source.getHeadersForImage(itemID, url)))
.build()
).enqueue(callback)
}
}
fun isDownloading(source: String, itemID: String): Boolean {
return (client.dispatcher().queuedCalls() + client.dispatcher().runningCalls()).any {
(it.request().tag() as? Tag)?.let { tag ->
tag.source == source && tag.itemID == itemID
} == true
}
}
}

View File

@@ -18,50 +18,7 @@
package xyz.quaver.pupil.util
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
import java.io.File
val mutex = Mutex()
fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
if (mutex.isLocked) return@launch
mutex.withLock {
val cacheFolder = File(context.cacheDir, "imageCache")
val downloadManager = DownloadManager.getInstance(context)
val limit = (Preferences.get<String>("cache_limit").toLongOrNull() ?: 0L)*1024*1024*1024
if (limit == 0L) return@withLock
val cacheSize = {
var size = 0L
cacheFolder.walk().forEach {
size += it.length()
}
size
}
if (cacheSize.invoke() > limit)
while (cacheSize.invoke() > limit/2) {
val caches = cacheFolder.list() ?: return@withLock
synchronized(histories) {
(histories.firstOrNull {
TODO()
} ?: return@withLock).let {
TODO()
}
}
}
}
}
fun File.size(): Long =
this.walk().fold(0L) { size, file -> size + file.length() }

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.view.MenuItem
import androidx.lifecycle.MutableLiveData
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -27,6 +28,8 @@ import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.pupil.sources.ItemInfo
import java.io.InputStream
import java.io.OutputStream
import java.util.*
import kotlin.collections.ArrayList
@@ -84,47 +87,13 @@ val formatMap = mapOf<String, ItemInfo.() -> (String)>(
/**
* Formats download folder name with given Metadata
*/
fun ItemInfo.formatDownloadFolder(): String =
Preferences["download_folder_name", "[-id-] -title-"].let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
fun ItemInfo.formatDownloadFolderTest(format: String): String =
fun ItemInfo.formatDownloadFolder(format: String = Preferences["download_folder_name", "[-id-] -title-"]): String =
format.let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
val GalleryInfo.requestBuilders: List<Request.Builder>
get() {
val galleryID = this.id ?: 0
val lowQuality = Preferences["low_quality", true]
return this.files.map {
Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality))
.header("Referer", getReferer(galleryID))
}
/*
return when(code) {
Code.HITOMI -> {
this.galleryInfo.files.map {
Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality))
.header("Referer", getReferer(galleryID))
}
}
Code.HIYOBI -> {
createImgList(galleryID, this, lowQuality).map {
Request.Builder()
.url(it.path)
}
}
}*/
}
fun String.ellipsize(n: Int): String =
if (this.length > n)
this.slice(0 until n) + ""
@@ -142,4 +111,21 @@ val JsonElement.content
fun List<MenuItem>.findMenu(itemID: Int): MenuItem {
return first { it.itemId == itemID }
}
fun <E> MutableLiveData<MutableList<E>>.notify() {
this.value = this.value
}
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Any): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
onCopy(bytesCopied, bytes)
bytes = read(buffer)
}
return bytesCopied
}

View File

@@ -45,21 +45,10 @@ import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.io.File
import java.io.IOException
import java.net.URL