Perform download cache renewal async

Don't block on cache renewals, but notify library on updates so that the badges show up when ready.

We skip the cache when checking if a chapter is downloaded for the reader assuming that it's a
relatively low cost to check for a single chapter.

(Probably) fixes #8254 / fixes #7847
This commit is contained in:
arkon
2022-10-21 15:00:41 -04:00
parent 93925a7286
commit 7e40680af0
6 changed files with 99 additions and 52 deletions

View File

@@ -6,12 +6,21 @@ import com.hippo.unifile.UniFile
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withTimeout
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
@@ -26,9 +35,15 @@ class DownloadCache(
private val context: Context,
private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
) {
// This is just a mechanism of notifying consumers of updates to the cache, the value itself
// is meaningless.
private val _state: MutableStateFlow<Long> = MutableStateFlow(0L)
val changes = _state.asStateFlow()
private val scope = CoroutineScope(Dispatchers.IO)
/**
@@ -41,6 +56,7 @@ class DownloadCache(
* The last time the cache was refreshed.
*/
private var lastRenew = 0L
private var renewalJob: Job? = null
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
@@ -134,6 +150,8 @@ class DownloadCache(
// Save the chapter directory
mangaDir.chapterDirs += chapterDirName
notifyChanges()
}
/**
@@ -151,6 +169,8 @@ class DownloadCache(
mangaDir.chapterDirs -= it
}
}
notifyChanges()
}
/**
@@ -170,6 +190,8 @@ class DownloadCache(
}
}
}
notifyChanges()
}
/**
@@ -184,6 +206,8 @@ class DownloadCache(
if (mangaDirName in sourceDir.mangaDirs) {
sourceDir.mangaDirs -= mangaDirName
}
notifyChanges()
}
@Synchronized
@@ -193,6 +217,8 @@ class DownloadCache(
sourceDir.delete()
rootDownloadsDir.sourceDirs -= source.id
}
notifyChanges()
}
/**
@@ -206,76 +232,83 @@ class DownloadCache(
/**
* Renews the downloads cache.
*/
@Synchronized
private fun renewCache() {
if (lastRenew + renewInterval >= System.currentTimeMillis()) {
// Avoid renewing cache if in the process nor too often
if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) {
return
}
val sources = sourceManager.getOnlineSources() + sourceManager.getStubSources()
renewalJob = scope.launchIO {
var sources = getSources()
// Ensure we try again later if no sources have been loaded
if (sources.isEmpty()) {
return
}
// Try to wait until extensions and sources have loaded
withTimeout(30000L) {
while (!extensionManager.isInitialized) {
delay(2000L)
}
val sourceDirs = rootDownloadsDir.dir.listFiles()
.orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
sources.find {
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
}?.id
while (sources.isEmpty()) {
delay(2000L)
sources = getSources()
}
}
rootDownloadsDir.sourceDirs = sourceDirs
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
sources.find {
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
}?.id
}
sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles()
.orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) }
rootDownloadsDir.sourceDirs = sourceDirs
sourceDir.mangaDirs = mangaDirs
sourceDirs.values
.map { sourceDir ->
async {
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
.filterNot { it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
.toMutableMap()
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles()
.orEmpty()
.mapNotNull { chapterDir ->
chapterDir.name
?.replace(".cbz", "")
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
sourceDir.mangaDirs = mangaDirs
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
.mapNotNull { chapterDir ->
chapterDir.name
?.replace(".cbz", "")
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
}
.toMutableSet()
mangaDir.chapterDirs = chapterDirs
}
}
.toHashSet()
}
.awaitAll()
mangaDir.chapterDirs = chapterDirs
}
lastRenew = System.currentTimeMillis()
notifyChanges()
}
}
lastRenew = System.currentTimeMillis()
private fun getSources(): List<Source> {
return sourceManager.getOnlineSources() + sourceManager.getStubSources()
}
private fun notifyChanges() {
_state.value += 1
}
/**
* Returns a new map containing only the key entries of [transform] that are not null.
*/
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): MutableMap<R, V> {
val destination = LinkedHashMap<R, V>()
forEach { element -> transform(element)?.let { destination[it] = element.value } }
return destination
}
/**
* Returns a map from a list containing only the key entries of [transform] that are not null.
*/
private inline fun <T, K, V> Array<T>.associateNotNullKeys(transform: (T) -> Pair<K?, V>): Map<K, V> {
val destination = LinkedHashMap<K, V>()
for (element in this) {
val (key, value) = transform(element)
if (key != null) {
destination[key] = value
}
}
return destination
}
}
/**
@@ -283,7 +316,7 @@ class DownloadCache(
*/
private class RootDirectory(
val dir: UniFile,
var sourceDirs: Map<Long, SourceDirectory> = hashMapOf(),
var sourceDirs: MutableMap<Long, SourceDirectory> = mutableMapOf(),
)
/**
@@ -291,7 +324,7 @@ private class RootDirectory(
*/
private class SourceDirectory(
val dir: UniFile,
var mangaDirs: Map<String, MangaDirectory> = hashMapOf(),
var mangaDirs: MutableMap<String, MangaDirectory> = mutableMapOf(),
)
/**
@@ -299,5 +332,5 @@ private class SourceDirectory(
*/
private class MangaDirectory(
val dir: UniFile,
var chapterDirs: Set<String> = hashSetOf(),
var chapterDirs: MutableSet<String> = mutableSetOf(),
)