what i got so far

This commit is contained in:
tom5079
2020-09-01 18:07:16 +09:00
parent c96d609803
commit 7704c96955
28 changed files with 611 additions and 444 deletions

View File

@@ -25,8 +25,6 @@ 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

View File

@@ -20,50 +20,58 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import android.util.Log
import android.util.SparseArray
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
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.io.util.*
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.formatDownloadFolder
import kotlin.io.deleteRecursively
import kotlin.io.writeText
@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] } })
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
companion object {
private val mutex = Mutex()
private val instances = SparseArray<Cache>()
fun getInstance(context: Context, galleryID: Int) =
instances[galleryID] ?: synchronized(this) {
instances[galleryID] ?: runBlocking { mutex.withLock {
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
}
} }
fun delete(galleryID: Int) { runBlocking { mutex.withLock {
instances[galleryID]?.galleryFolder?.deleteRecursively()
instances.delete(galleryID)
} } }
}
init {
galleryFolder.mkdirs()
}
private val mutex = Mutex()
var metadata = kotlin.runCatching {
findFile(".metadata")?.readText()?.let {
Json.decodeFromString<Metadata>(it)
@@ -76,7 +84,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
val cacheFolder: FileX
get() = FileX(this, cacheDir, "imageCache/$galleryID")
val cachedGallery: FileX
val galleryFolder: FileX
get() = DownloadFolderManager.getInstance(this).getDownloadFolder(galleryID)
?: FileX(this, cacheDir, "imageCache/$galleryID")
@@ -87,16 +95,19 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
if (it.exists()) it else null
} }
@Synchronized
fun setMetadata(change: (Metadata) -> Unit) {
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun setMetadata(change: (Metadata) -> Unit) { mutex.withLock {
change.invoke(metadata)
val file = cachedGallery.getChild(".metadata")
val file = galleryFolder.getChild(".metadata")
CoroutineScope(Dispatchers.IO).launch {
file.writeText(Json.encodeToString(Metadata))
kotlin.runCatching {
file.createNewFile()
file.writeText(Json.encodeToString(metadata))
}
}
}
} }
suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf(
@@ -123,59 +134,47 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}
}
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()
suspend fun getThumbnail(): ByteArray? =
findFile(".thumbnail")?.readBytes()
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(it)
.build()
val image = client.newCall(request).execute().body()?.use { it.bytes() }
kotlin.runCatching {
client.newCall(request).execute().body()?.use { it.bytes() }
}.getOrNull()?.also { kotlin.run {
galleryFolder.getChild(".thumbnail").writeBytes(it)
} }
} }
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('>')
suspend fun getReader(): Reader? {
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.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) }
).let {
if (mirrors.isNotEmpty())
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
else
it
}
return metadata.reader
?: withContext(Dispatchers.IO) {
var reader: Reader? = null
for (source in sources) {
reader = try { withTimeoutOrNull(1000) {
reader = try {
source.value.invoke()
} } catch (e: Exception) { null }
} catch (e: Exception) {
null
}
if (reader != null)
break
if (reader != null)
break
}
reader?.also {
@@ -193,13 +192,11 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
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()
}
suspend fun putImage(index: Int, fileName: String, data: ByteArray) {
val file = galleryFolder.getChild(fileName)
file.createNewFile()
file.writeBytes(data)
setMetadata { metadata -> metadata.imageList!![index] = fileName }
}
@@ -208,11 +205,12 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
if (downloadFolder == null)
DownloadFolderManager.getInstance(this@Cache).addDownloadFolder(galleryID, this@Cache.formatDownloadFolder())
metadata.imageList?.forEach {
it ?: return@forEach
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder!!.getChild(it)
val source = cacheFolder.getChild(it)
Log.i("PUPIL", downloadFolder?.uri.toString())
val target = downloadFolder!!.getChild(imageName)
val source = cacheFolder.getChild(imageName)
if (!source.exists())
return@forEach
@@ -223,6 +221,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}
}
Log.i("PUPIL", downloadFolder?.uri.toString())
val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder!!.getChild(".metadata")

View File

@@ -24,13 +24,18 @@ import android.webkit.URLUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Call
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.pupil.client
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences
class DownloadFolderManager private constructor(context: Context) : ContextWrapper(context) {
@@ -46,59 +51,70 @@ class DownloadFolderManager private constructor(context: Context) : ContextWrapp
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder = {
val uri: String = Preferences["download_directory"]
if (!URLUtil.isValidUrl(uri))
Preferences["download_directory"] = defaultDownloadFolder
val downloadFolder: FileX
get() = {
kotlin.runCatching {
FileX(this, Preferences.get<String>("download_folder"))
}.getOrElse {
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
defaultDownloadFolder
}
}.invoke()
private val downloadFolderMapMutex = Mutex()
private val downloadFolderMap: MutableMap<Int, String> = runBlocking { downloadFolderMapMutex.withLock {
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 {
downloadFolder.getChild(".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) }
fun isDownloading(galleryID: Int): Boolean {
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
@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))
} }
}
return downloadFolderMap.containsKey(galleryID)
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
}
@Synchronized
fun removeDownloadFolder(galleryID: Int) {
fun getDownloadFolder(galleryID: Int): FileX? = runBlocking { downloadFolderMapMutex.withLock {
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
} }
fun addDownloadFolder(galleryID: Int, name: String) { runBlocking { downloadFolderMapMutex.withLock {
if (downloadFolderMap.containsKey(galleryID))
return@withLock
val folder = downloadFolder.getChild(name)
if (!folder.exists())
folder.mkdirs()
downloadFolderMap[galleryID] = name
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
downloadFolder.getChild(".download").let {
it.createNewFile()
it.writeText(Json.encodeToString(downloadFolderMap))
}
} }
} } }
fun deleteDownloadFolder(galleryID: Int) { runBlocking { downloadFolderMapMutex.withLock {
if (!downloadFolderMap.containsKey(galleryID))
return
return@withLock
downloadFolderMap[galleryID]?.let {
if (FileX(this@DownloadFolderManager, downloadFolder, it).delete()) {
if (downloadFolder.getChild(it).delete()) {
downloadFolderMap.remove(galleryID)
CoroutineScope(Dispatchers.IO).launch { downloadFolderMapMutex.withLock {
FileX(this@DownloadFolderManager, downloadFolder, ".download").writeText(Json.encodeToString(downloadFolderMap))
downloadFolder.getChild(".download").let {
it.createNewFile()
it.writeText(Json.encodeToString(downloadFolderMap))
}
} }
}
}
}
} } }
}

View File

@@ -27,6 +27,7 @@ import java.io.FileOutputStream
import java.lang.reflect.Array
import java.net.URL
@Deprecated("Use downloader.Cache instead")
fun getCachedGallery(context: Context, galleryID: Int) =
File(getDownloadDirectory(context), galleryID.toString()).let {
if (it.exists())
@@ -35,6 +36,7 @@ fun getCachedGallery(context: Context, galleryID: Int) =
File(context.cacheDir, "imageCache/$galleryID")
}
@Deprecated("Use downloader.Cache instead")
fun getDownloadDirectory(context: Context) =
Preferences.get<String>("dl_location").let {
if (it.isNotEmpty() && !it.startsWith("content"))
@@ -43,81 +45,6 @@ fun getDownloadDirectory(context: Context) =
context.getExternalFilesDir(null)!!
}
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
if (to.parentFile?.exists() == false)
to.parentFile!!.mkdirs()
if (!to.exists())
to.createNewFile()
FileOutputStream(to).use { out ->
with(openConnection()) {
val fileSize = contentLength.toLong()
getInputStream().use {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = it.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
onDownloadProgress?.invoke(bytesCopied, fileSize)
bytes = it.read(buffer)
}
}
}
}
}
fun getExtSdCardPaths(context: Context) =
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
runCatching {
File(path).canonicalPath
}.getOrElse {
path
}
}
}
const val PRIMARY_VOLUME_NAME = "primary"
fun getVolumePath(context: Context, volumeID: String?): String? {
return runCatching {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
val getUUID = storageVolumeClass.getMethod("getUuid")
val getPath = storageVolumeClass.getMethod("getPath")
val isPrimary = storageVolumeClass.getMethod("isPrimary")
val result = getVolumeList.invoke(storageManager)!!
val length = Array.getLength(result)
for (i in 0 until length) {
val storageVolumeElement = Array.get(result, i)
val uuid = getUUID.invoke(storageVolumeElement) as? String
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
// primary volume?
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
return@runCatching getPath.invoke(storageVolumeElement) as? String
// other volumes?
if (volumeID == uuid) {
return@runCatching getPath.invoke(storageVolumeElement) as? String
}
}
return@runCatching null
}.getOrNull()
}
@Deprecated("Use FileX instead")
fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath)

View File

@@ -19,12 +19,23 @@
package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
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 okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.util.*
@@ -77,17 +88,47 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
}
val formatMap = mapOf<String, (Cache) -> (String)>(
"\$ID" to { runBlocking { it.getGalleryBlock()?.id.toString() } },
"\$TITLE" to { runBlocking { it.getGalleryBlock()?.title.toString() } },
"-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))
fun Cache.formatDownloadFolder(): String =
Preferences["download_folder_format", "-id-"].let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}
}
fun Context.startForegroundServiceCompat(service: Intent) {
if (Build.VERSION.SDK_INT >= 26)
startForegroundService(service)
else
startService(service)
}
val Reader.requestBuilders: List<Request.Builder>
get() {
val galleryID = this.galleryInfo.id ?: 0
val lowQuality = Preferences["low_quality", true]
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)
.header("User-Agent", user_agent)
.header("Cookie", cookie)
}
}
}
}