Created new cache/downloader

This commit is contained in:
tom5079
2020-08-31 11:41:12 +09:00
parent aa0e5000ab
commit c96d609803
10 changed files with 388 additions and 23 deletions

View File

@@ -99,7 +99,7 @@ dependencies {
implementation ("xyz.quaver:libpupil:1.1") {
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
}
implementation "xyz.quaver:documentfilex:0.2"
implementation "xyz.quaver:documentfilex:0.2.2"
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.3.0'

View File

@@ -90,7 +90,7 @@ class Pupil : Application() {
}
try {
Preferences.get<String>("dl_location").also {
Preferences.get<String>("download_folder").also {
if (!File(it).canWrite())
throw Exception()
}

View File

@@ -19,32 +19,21 @@
package xyz.quaver.pupil.services
import android.app.Service
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Interceptor
import okhttp3.ResponseBody
import okio.*
import xyz.quaver.pupil.PupilInterceptor
import xyz.quaver.pupil.R
import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.util.downloader.Cache
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
class Cache(context: Context) : ContextWrapper(context) {
}
class DownloadService : Service() {
data class Tag(val galleryID: Int, val index: Int)
@@ -144,8 +133,17 @@ class DownloadService : Service() {
private val binder = Binder()
override fun onBind(p0: Intent?) = binder
val cache = SparseArray<Cache>()
fun load(galleryID: Int) {
if (progress.indexOfKey(galleryID) < 0)
progress.put(galleryID, mutableListOf())
if (cache.indexOfKey(galleryID) < 0)
cache.put(galleryID, Cache.getInstance(this, galleryID))
cache[galleryID].metadata.imageList?.forEach {
progress[galleryID]?.add(if (it == null) Float.POSITIVE_INFINITY else 0F)
}
}
fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {

View File

@@ -25,6 +25,8 @@ import android.util.SparseArray
import androidx.preference.PreferenceManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -41,6 +43,7 @@ import java.io.FileOutputStream
import java.io.InputStream
import java.net.URL
@Deprecated("Use downloader.Cache instead")
class Cache(context: Context) : ContextWrapper(context) {
companion object {

View File

@@ -48,6 +48,7 @@ import java.io.File
import java.io.IOException
import java.util.concurrent.LinkedBlockingQueue
@Deprecated("Use DownloadService instead")
@OptIn(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {

View File

@@ -22,12 +22,13 @@ import kotlinx.serialization.Serializable
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
@Deprecated("Use downloader.Cache.Metadata instead")
@Serializable
data class Metadata(
val thumbnail: String? = null,
val galleryBlock: GalleryBlock? = null,
val reader: Reader? = null,
val isDownloading: Boolean? = null
var thumbnail: String? = null,
var galleryBlock: GalleryBlock? = null,
var reader: Reader? = null,
var isDownloading: Boolean? = null
) {
constructor(
metadata: Metadata?,

View File

@@ -0,0 +1,239 @@
/*
* 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 android.util.Base64
import android.util.SparseArray
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getGallery
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readBytes
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.formatDownloadFolder
@Serializable
data class Metadata(
var galleryBlock: GalleryBlock? = null,
var gallery: Gallery? = null,
var thumbnail: String? = null,
var reader: Reader? = null,
var imageList: MutableList<String?>? = null
) {
fun copy(): Metadata = Metadata(galleryBlock, gallery, thumbnail, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
companion object {
private val instances = SparseArray<Cache>()
fun getInstance(context: Context, galleryID: Int) =
instances[galleryID] ?: synchronized(this) {
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
}
}
var metadata = kotlin.runCatching {
findFile(".metadata")?.readText()?.let {
Json.decodeFromString<Metadata>(it)
}
}.getOrNull() ?: Metadata()
val downloadFolder: FileX?
get() = DownloadFolderManager.getInstance(this).getDownloadFolder(galleryID)
val cacheFolder: FileX
get() = FileX(this, cacheDir, "imageCache/$galleryID")
val cachedGallery: FileX
get() = DownloadFolderManager.getInstance(this).getDownloadFolder(galleryID)
?: FileX(this, cacheDir, "imageCache/$galleryID")
fun findFile(fileName: String): FileX? =
cacheFolder.getChild(fileName).let {
if (it.exists()) it else null
} ?: downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
if (it.exists()) it else null
} }
@Synchronized
fun setMetadata(change: (Metadata) -> Unit) {
change.invoke(metadata)
val file = cachedGallery.getChild(".metadata")
CoroutineScope(Dispatchers.IO).launch {
file.writeText(Json.encodeToString(Metadata))
}
}
suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
return metadata.galleryBlock
?: withContext(Dispatchers.IO) {
var galleryBlock: GalleryBlock? = null
for (source in sources) {
galleryBlock = try {
source.invoke()
} catch (e: Exception) { null }
if (galleryBlock != null)
break
}
galleryBlock?.also {
launch { setMetadata { metadata -> metadata.galleryBlock = it } }
}
}
}
suspend fun getGallery(): Gallery? =
metadata.gallery
?: withContext(Dispatchers.IO) {
kotlin.runCatching {
getGallery(galleryID)
}.getOrNull()?.also {
launch { setMetadata { metadata ->
metadata.gallery = it
if (metadata.imageList == null)
metadata.imageList = MutableList(it.thumbnails.size) { null }
} }
}
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun getThumbnail(): String? =
metadata.thumbnail
?: withContext(Dispatchers.IO) {
getGalleryBlock()?.thumbnails?.firstOrNull()?.let { thumbnail ->
kotlin.runCatching {
val request = Request.Builder()
.url(thumbnail)
.build()
val image = client.newCall(request).execute().body()?.use { it.bytes() }
Base64.encodeToString(image, Base64.DEFAULT)
}.getOrNull()
}?.also {
launch { setMetadata { metadata -> metadata.thumbnail = it } }
}
}
suspend fun getReader(galleryID: Int): Reader? {
val mirrors = Preferences.get<String>("mirrors").split('>')
val sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).toSortedMap { o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
return metadata.reader
?: withContext(Dispatchers.IO) {
var reader: Reader? = null
for (source in sources) {
reader = try { withTimeoutOrNull(1000) {
source.value.invoke()
} } catch (e: Exception) { null }
if (reader != null)
break
}
reader?.also {
launch { setMetadata { metadata ->
metadata.reader = it
if (metadata.imageList == null)
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
} }
}
}
}
fun getImage(index: Int): FileX? =
metadata.imageList?.get(index)?.let { findFile(it) }
@Suppress("BlockingMethodInNonBlockingContext")
fun putImage(index: Int, fileName: String, data: ByteArray) = CoroutineScope(Dispatchers.IO).launch {
val file = FileX(this@Cache, cachedGallery, fileName).also {
it.createNewFile()
}
file.writeBytes(data)
setMetadata { metadata -> metadata.imageList!![index] = fileName }
}
@Suppress("BlockingMethodInNonBlockingContext")
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
if (downloadFolder == null)
DownloadFolderManager.getInstance(this@Cache).addDownloadFolder(galleryID, this@Cache.formatDownloadFolder())
metadata.imageList?.forEach {
it ?: return@forEach
val target = downloadFolder!!.getChild(it)
val source = cacheFolder.getChild(it)
if (!source.exists())
return@forEach
kotlin.runCatching {
target.createNewFile()
source.readBytes()?.let { target.writeBytes(it) }
}
}
val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder!!.getChild(".metadata")
if (cacheMetadata.exists()) {
kotlin.runCatching {
downloadMetadata.createNewFile()
cacheMetadata.readBytes()?.let { downloadMetadata.writeBytes(it) }
cacheMetadata.delete()
}
}
cacheFolder.delete()
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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 android.webkit.URLUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX
import xyz.quaver.io.util.readText
import xyz.quaver.pupil.util.Preferences
class DownloadFolderManager private constructor(context: Context) : ContextWrapper(context) {
companion object {
@Volatile private var instance: DownloadFolderManager? = null
fun getInstance(context: Context) =
instance ?: synchronized(this) {
instance ?: DownloadFolderManager(context).also { instance = it }
}
}
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder = {
val uri: String = Preferences["download_directory"]
if (!URLUtil.isValidUrl(uri))
Preferences["download_directory"] = defaultDownloadFolder
kotlin.runCatching {
FileX(this, uri)
}.getOrElse {
Preferences["download_directory"] = defaultDownloadFolder
FileX(this, defaultDownloadFolder)
}
}.invoke()
private val downloadFolderMap: MutableMap<Int, String> =
kotlin.runCatching {
FileX(this@DownloadFolderManager, downloadFolder, ".download").readText()?.let {
Json.decodeFromString<MutableMap<Int, String>>(it)
}
}.getOrNull() ?: mutableMapOf()
private val downloadFolderMapMutex = Mutex()
@Synchronized
fun getDownloadFolder(galleryID: Int): FileX? =
downloadFolderMap[galleryID]?.let { FileX(this, downloadFolder, it) }
@Synchronized
fun addDownloadFolder(galleryID: Int, name: String) {
if (downloadFolderMap.containsKey(galleryID))
return
if (FileX(this@DownloadFolderManager, downloadFolder, name).mkdir()) {
downloadFolderMap[galleryID] = name
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
FileX(this@DownloadFolderManager, downloadFolder, ".download").writeText(Json.encodeToString(downloadFolderMap))
} }
}
}
@Synchronized
fun removeDownloadFolder(galleryID: Int) {
if (!downloadFolderMap.containsKey(galleryID))
return
downloadFolderMap[galleryID]?.let {
if (FileX(this@DownloadFolderManager, downloadFolder, it).delete()) {
downloadFolderMap.remove(galleryID)
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
FileX(this@DownloadFolderManager, downloadFolder, ".download").writeText(Json.encodeToString(downloadFolderMap))
} }
}
}
}
}

View File

@@ -18,19 +18,15 @@
package xyz.quaver.pupil.util
import android.annotation.TargetApi
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import java.io.File
import java.io.FileOutputStream
import java.lang.reflect.Array
import java.net.URL
fun getCachedGallery(context: Context, galleryID: Int) =
File(getDownloadDirectory(context), galleryID.toString()).let {
if (it.exists())

View File

@@ -19,7 +19,14 @@
package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.util.*
import kotlin.collections.ArrayList
@@ -68,3 +75,19 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
proxyAuthenticator(it)
}
}
val formatMap = mapOf<String, (Cache) -> (String)>(
"\$ID" to { runBlocking { it.getGalleryBlock()?.id.toString() } },
"\$TITLE" to { runBlocking { it.getGalleryBlock()?.title.toString() } },
// TODO
)
/**
* Formats download folder name with given Metadata
*/
fun Cache.formatDownloadFolder(): String {
return Preferences["download_folder_format", "\$ID"].apply {
formatMap.entries.forEach { (key, lambda) ->
this.replace(key, lambda.invoke(this@formatDownloadFolder))
}
}
}